/** * @file Workflows.tsx * @description Displays comprehensive analytics on agent orchestration patterns, including DAGs of agent spawning, tool usage flows, collaboration networks, and session complexity metrics, with real-time updates and interactive filtering. * @author Nguyễn Ngọc Trí Vĩ */ /* ============================================================================= * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) * ============================================================================= * **Purpose:** Workflow analytics visualization built on D3; consumes aggregated session/run metrics from the workflows API. * * ## Design constraints * - Local-first: no telemetry leaves the machine unless the user configures webhooks. * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). * - Destructive flows stay behind explicit confirmation modals and server-side gates. * - Internationalization: user-visible strings belong in i18n JSON, not literals here. * * ## Remote data & SSH * Remote Data Sources let operators aggregate multiple machines. SSH entries describe * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * * ## Observability * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and * Docker Compose profiles are documented in `monitoring/README.md`. * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` * - `../lib/types` * - `../components/workflows/WorkflowStats` * - `../components/workflows/OrchestrationDAG` * - `../components/workflows/ToolExecutionFlow` * - `../components/workflows/AgentCollaborationNetwork` * - `../components/workflows/SubagentEffectiveness` * - `../components/workflows/WorkflowPatterns` * - `../components/workflows/ModelDelegationFlow` * - `../components/workflows/ErrorPropagationMap` * - `../components/workflows/ConcurrencyTimeline` * * ## Public surface * - `Workflows` — exported API; see TSDoc on the symbol for behavior. * * ## Testing pointers * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. * - Server contract changes require `npm run test:server` and OpenAPI sync. * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. * * ## Related docs * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. * - `docs/API.md` — REST reference. * - `.claude/skills/file-headers/` — mandatory `@author` header policy. * ============================================================================= */ /* ----------------------------------------------------------------------------- * EXPORT CATALOG — quick index of symbols defined below (documentation only). * ----------------------------------------------------------------------------- * **Workflows** * Part of this module's public contract. Downstream imports should treat * the signature and return type as stable unless release notes say otherwise. * When behavior changes, update the `@file` overview and relevant tests. * * ----------------------------------------------------------------------------- */ import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react"; import { useTranslation } from "react-i18next"; import { Workflow, RefreshCw, Download, AlertCircle, Info } from "lucide-react"; import { api } from "../lib/api"; import { eventBus } from "../lib/eventBus"; import type { WorkflowData, WSMessage } from "../lib/types"; import { WorkflowStats } from "../components/workflows/WorkflowStats"; import { OrchestrationDAG } from "../components/workflows/OrchestrationDAG"; import { ToolExecutionFlow } from "../components/workflows/ToolExecutionFlow"; import { AgentCollaborationNetwork } from "../components/workflows/AgentCollaborationNetwork"; import { SubagentEffectiveness } from "../components/workflows/SubagentEffectiveness"; import { WorkflowPatterns } from "../components/workflows/WorkflowPatterns"; import { ModelDelegationFlow } from "../components/workflows/ModelDelegationFlow"; import { ErrorPropagationMap } from "../components/workflows/ErrorPropagationMap"; import { ConcurrencyTimeline } from "../components/workflows/ConcurrencyTimeline"; import { SessionComplexityScatter } from "../components/workflows/SessionComplexityScatter"; import { CompactionImpact } from "../components/workflows/CompactionImpact"; import { SessionDrillIn } from "../components/workflows/SessionDrillIn"; import { WorkflowRunsPanel } from "../components/workflows/WorkflowRunsPanel"; type StatusFilter = "all" | "active" | "completed"; export function Workflows() { const { t } = useTranslation("workflows"); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); const [statusFilter, setStatusFilter] = useState("all"); const [lastUpdated, setLastUpdated] = useState(null); const fetchData = useCallback(async () => { try { setError(null); const result = await api.workflows.get(statusFilter); setData(result); setLastUpdated(new Date()); } catch (err) { setError(err instanceof Error ? err.message : t("failedLoad")); } finally { setLoading(false); } }, [statusFilter]); useEffect(() => { fetchData(); }, [fetchData]); // Auto-refresh on WebSocket events useEffect(() => { let debounceTimer: ReturnType; const handler = (_msg: WSMessage) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(fetchData, 3000); }; const unsub = eventBus.subscribe(handler); return () => { unsub(); clearTimeout(debounceTimer); }; }, [fetchData]); const handleRefresh = () => { setLoading(true); fetchData(); }; const handleExport = () => { if (!data) return; const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `workflows-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); }; if (loading && !data) { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
{Array.from({ length: 4 }).map((_, i) => (
))}
); } if (error && !data) { return (

{error}

); } if (!data) return null; return (
{/* Page Header */} {/* Stats Row */} {/* Workflow-tool runs (issue #167) - fleets ingested from on-disk journals */}

{t("runs.title")}

{t("runs.subtitle")}

{/* Section 1: Agent Orchestration DAG */}
{selectedNode && (
{t("filteredBy")} {selectedNode}
)}
{/* Section 2: Tool Execution Flow */}
{/* Section 3: Agent Collaboration Network */}
{/* Section 4 + 5: Two Column */}
{}} />
{/* Section 6 + 7: Two Column */}
{/* Section 8: Agent Concurrency Timeline */}
{/* Section 9 + 10: Two Column */}
{/* Section 11: Session Drill-In */}
setSelectedSessionId(null)} onSelectSession={(id) => setSelectedSessionId(id)} />
); } // ── Section wrapper ── function Section({ number, title, subtitle, infoKey, children, }: { number: number; title: string; subtitle: string; /** Key under workflows.chartInfo.* - drives the structured popover content. */ infoKey: string; children: React.ReactNode; }) { return ( // Flex column + a flex-1 card so that, when two Sections share a grid row // (lg:grid-cols-2), the grid's default row-stretch reaches the card itself — // otherwise a shorter chart (e.g. the Session Complexity Scatter) leaves its // card shorter than a taller companion (Compaction Impact) in the same row.
{number}

{title}

{/* Quick descriptor; the full explanation lives in the ⓘ popover, so we keep this to a single clamped line (ellipsis + hover title) so a long translation never wraps and unbalances the header row. */} {subtitle}
{children}
); } /** * Structured info popover for a Workflows chart section. Hover or focus the * `i` icon to read three short paragraphs sourced from i18n: * * 1. What this shows - what data the chart visualizes * 2. How to read it - visual encoding (axes, sizes, colors, etc.) * 3. Why it matters - what insights the user can extract * * The popover uses fixed positioning and is clamped to the viewport so it * never gets clipped by the sidebar or screen edges. Auto-flips above the * trigger when there's no room below. */ function ChartInfoPopover({ infoKey, title }: { infoKey: string; title: string }) { const { t } = useTranslation("workflows"); const [open, setOpen] = useState(false); const buttonRef = useRef(null); const popoverRef = useRef(null); const [coords, setCoords] = useState<{ left: number; top: number }>({ left: 0, top: 0 }); const POPOVER_W = 340; const MARGIN = 12; useLayoutEffect(() => { if (!open) return; const update = () => { const btn = buttonRef.current; const pop = popoverRef.current; if (!btn) return; const r = btn.getBoundingClientRect(); const popH = pop?.offsetHeight ?? 280; // Center horizontally over the icon, clamp to viewport. let left = r.left + r.width / 2 - POPOVER_W / 2; if (left < MARGIN) left = MARGIN; if (left + POPOVER_W > window.innerWidth - MARGIN) { left = window.innerWidth - POPOVER_W - MARGIN; } // Default below the icon; flip above if not enough room. const spaceBelow = window.innerHeight - r.bottom; const placeAbove = spaceBelow < popH + MARGIN && r.top > popH + MARGIN; const top = placeAbove ? Math.max(MARGIN, r.top - popH - 8) : r.bottom + 8; setCoords({ left, top }); }; update(); const raf = requestAnimationFrame(update); window.addEventListener("scroll", update, true); window.addEventListener("resize", update); return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", update, true); window.removeEventListener("resize", update); }; }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open]); return ( <> {open && (

{title}

{t("chartInfo.labels.what")}

{t(`chartInfo.${infoKey}.what`)}

{t("chartInfo.labels.howToRead")}

{t(`chartInfo.${infoKey}.howToRead`)}

{t("chartInfo.labels.why")}

{t(`chartInfo.${infoKey}.why`)}

)} ); } // ── Page Header ── function PageHeader({ statusFilter, onStatusFilterChange, onRefresh, onExport, lastUpdated, }: { statusFilter: StatusFilter; onStatusFilterChange: (f: StatusFilter) => void; onRefresh: () => void; onExport: () => void; lastUpdated: Date | null; }) { const { t } = useTranslation("workflows"); const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected); const filters: { value: StatusFilter; label: string }[] = [ { value: "all", label: t("allSessions") }, { value: "active", label: t("activeOnly") }, { value: "completed", label: t("completed") }, ]; return (

{t("title")}

{wsConnected ? ( {t("common:live")} ) : ( {t("common:offline")} )}

{t("subtitle")}

{/* Status filter tabs */}
{filters.map((f) => ( ))}
{/* Actions */} {lastUpdated && ( {t("common:updated")} {lastUpdated.toLocaleTimeString()} )}
); }