feat(workflows): drop the Tool Execution Flow sankey

The d3-sankey tool-transition chart was the last of the deep harness-internals
visualizations issue #6 set out to remove: a wall of bands showing that Read
tends to precede Edit, with nothing a reader could act on. It goes, along with
its i18n keys in both locales and the now-orphaned d3-sankey dependency.

The workflows API response is untouched — `toolFlow` still ships in the
aggregate and still feeds the Dashboard's top-tools list. Remaining sections
renumber 4-9.
This commit is contained in:
2026-08-26 10:54:53 +07:00
parent 348c645ef3
commit 79e56c3221
9 changed files with 26 additions and 1014 deletions
+7 -9
View File
@@ -660,13 +660,12 @@ graph TD
WF --> S3["SkillUsagePanel<br/>Skill + plugin call counts"] WF --> S3["SkillUsagePanel<br/>Skill + plugin call counts"]
WF --> S4["WorkflowRunsPanel<br/>Workflow-tool runs<br/>(on-disk journals)"] WF --> S4["WorkflowRunsPanel<br/>Workflow-tool runs<br/>(on-disk journals)"]
WF --> S5["OrchestrationDAG<br/>Horizontal DAG —<br/>Sessions → Main → Subagents → Outcomes"] WF --> S5["OrchestrationDAG<br/>Horizontal DAG —<br/>Sessions → Main → Subagents → Outcomes"]
WF --> S6["ToolExecutionFlow<br/>d3-sankey tool transitions"] WF --> S6["WorkflowPatterns<br/>Auto-detected sequences"]
WF --> S7["WorkflowPatterns<br/>Auto-detected sequences"] WF --> S7["ModelDelegationFlow<br/>Model → agent routing"]
WF --> S8["ModelDelegationFlow<br/>Model → agent routing"] WF --> S8["ErrorPropagationMap<br/>React horizontal bars +<br/>API/session error support"]
WF --> S9["ErrorPropagationMap<br/>React horizontal bars +<br/>API/session error support"] WF --> S9["SessionComplexityScatter<br/>D3 bubble chart"]
WF --> S10["SessionComplexityScatter<br/>D3 bubble chart"] WF --> S10["CompactionImpact<br/>Token compression analysis"]
WF --> S11["CompactionImpact<br/>Token compression analysis"] WF --> S11["SessionDrillIn<br/>Searchable session explorer<br/>(3 tabs: tree / timeline / events)"]
WF --> S12["SessionDrillIn<br/>Searchable session explorer<br/>(3 tabs: tree / timeline / events)"]
classDef root fill:#6366f1,stroke:#818cf8,color:#fff classDef root fill:#6366f1,stroke:#818cf8,color:#fff
``` ```
@@ -677,7 +676,6 @@ graph TD
| `SkillUsagePanel` | Which skills and plugins the sessions actually use | Plain React ranked bars from `GET /api/workflows/skill-usage`; plugin chips sum skill calls and the plugin's own MCP tool calls | | `SkillUsagePanel` | Which skills and plugins the sessions actually use | Plain React ranked bars from `GET /api/workflows/skill-usage`; plugin chips sum skill calls and the plugin's own MCP tool calls |
| `WorkflowRunsPanel` | Workflow-tool runs reconstructed from on-disk journals | Per-run phase chips, per-agent table, expandable results | | `WorkflowRunsPanel` | Workflow-tool runs reconstructed from on-disk journals | Per-run phase chips, per-agent table, expandable results |
| `OrchestrationDAG` | Horizontal DAG of aggregate spawning patterns | Custom DAG layout, capped at top 7 subagent types with overflow node | | `OrchestrationDAG` | Horizontal DAG of aggregate spawning patterns | Custom DAG layout, capped at top 7 subagent types with overflow node |
| `ToolExecutionFlow` | Tool-to-tool transition Sankey diagram | `d3-sankey` |
| `WorkflowPatterns` | Common orchestration sequences | Pattern detection from event data; clicking a row expands the full step chain, a stats grid, a deterministic narrative and a suggestion bucket, all i18n-driven (`workflows.patterns.detail.*`) | | `WorkflowPatterns` | Common orchestration sequences | Pattern detection from event data; clicking a row expands the full step chain, a stats grid, a deterministic narrative and a suggestion bucket, all i18n-driven (`workflows.patterns.detail.*`) |
| `ModelDelegationFlow` | Model routing through agent hierarchies | Hierarchical layout | | `ModelDelegationFlow` | Model routing through agent hierarchies | Hierarchical layout |
| `ErrorPropagationMap` | Error clustering by hierarchy depth with API/session event errors | Pure React horizontal bars, `eventErrors` support for API and session-level errors | | `ErrorPropagationMap` | Error clustering by hierarchy depth with API/session event errors | Pure React horizontal bars, `eventErrors` support for API and session-level errors |
@@ -2292,7 +2290,7 @@ For the extension source code, refer to the [vscode-extension/](./vscode-extensi
| **Network posture** | Server binds **`127.0.0.1` (loopback) by default** — not network-reachable out of the box (fix for GHSA-gr74-4xfh-6jw9; it previously bound `0.0.0.0` with no auth). Widen the bind via `DASHBOARD_HOST` (e.g. `0.0.0.0`), which logs a startup warning. A Host-header allowlist on HTTP + WebSocket upgrades blocks DNS-rebinding; extra LAN names go in `DASHBOARD_ALLOWED_HOSTS` (comma-separated) | | **Network posture** | Server binds **`127.0.0.1` (loopback) by default** — not network-reachable out of the box (fix for GHSA-gr74-4xfh-6jw9; it previously bound `0.0.0.0` with no auth). Widen the bind via `DASHBOARD_HOST` (e.g. `0.0.0.0`), which logs a startup warning. A Host-header allowlist on HTTP + WebSocket upgrades blocks DNS-rebinding; extra LAN names go in `DASHBOARD_ALLOWED_HOSTS` (comma-separated) |
| **Auth (optional)** | Off by default — the loopback bind is the trust boundary. Set `DASHBOARD_TOKEN` to gate every `/api/*` request **and** the WebSocket, presented as `Authorization: Bearer <token>`, an `x-dashboard-token` header, or `?token=`. Exempt even when set: `/api/health`, `/api/openapi.json`, `/api/docs`, and `/api/hooks` (local Claude Code hook ingestion) | | **Auth (optional)** | Off by default — the loopback bind is the trust boundary. Set `DASHBOARD_TOKEN` to gate every `/api/*` request **and** the WebSocket, presented as `Authorization: Bearer <token>`, an `x-dashboard-token` header, or `?token=`. Exempt even when set: `/api/health`, `/api/openapi.json`, `/api/docs`, and `/api/hooks` (local Claude Code hook ingestion) |
| **No secrets** | No API keys, tokens, or credentials stored or transmitted | | **No secrets** | No API keys, tokens, or credentials stored or transmitted |
| **Dependency surface** | Minimal: 5 runtime server deps, 6 runtime client deps (includes `d3` and `d3-sankey` for Workflows visualizations) | | **Dependency surface** | Minimal: 5 runtime server deps, 5 runtime client deps (includes `d3` for Workflows visualizations) |
--- ---
+1 -1
View File
@@ -178,7 +178,7 @@ client/
│ │ ├── Sidebar.tsx │ │ ├── Sidebar.tsx
│ │ ├── Layout.tsx │ │ ├── Layout.tsx
│ │ ├── RemoteSources.tsx # Remote Data Sources settings panel (SSH multi-machine collection) │ │ ├── RemoteSources.tsx # Remote Data Sources settings panel (SSH multi-machine collection)
│ │ └── workflows/ # Workflows page panels: own-pipeline stats + D3 analytics (12 files) │ │ └── workflows/ # Workflows page panels: own-pipeline stats + D3 analytics (11 files)
│ │ │ │
│ ├── pages/ # Route pages │ ├── pages/ # Route pages
│ │ ├── Dashboard.tsx # Monitor / Analytics / Health tabs │ │ ├── Dashboard.tsx # Monitor / Analytics / Health tabs
-69
View File
@@ -13,7 +13,6 @@
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"i18next": "^26.0.8", "i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
@@ -27,7 +26,6 @@
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.5",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
@@ -1814,33 +1812,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/d3-sankey": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.12.5.tgz",
"integrity": "sha512-/3RZSew0cLAtzGQ+C89hq/Rp3H20QJuVRSqFy6RKLe7E0B8kd2iOS1oBsodrgds4PcNVpqWhdUEng/SHvBcJ6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-shape": "^1"
}
},
"node_modules/@types/d3-sankey/node_modules/@types/d3-path": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-sankey/node_modules/@types/d3-shape": {
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-path": "^1"
}
},
"node_modules/@types/d3-scale": { "node_modules/@types/d3-scale": {
"version": "4.0.9", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
@@ -2802,46 +2773,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-sankey": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "1 - 2",
"d3-shape": "^1.2.0"
}
},
"node_modules/d3-sankey/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"license": "BSD-3-Clause",
"dependencies": {
"internmap": "^1.0.0"
}
},
"node_modules/d3-sankey/node_modules/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
"license": "BSD-3-Clause"
},
"node_modules/d3-sankey/node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "1"
}
},
"node_modules/d3-sankey/node_modules/internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
"license": "ISC"
},
"node_modules/d3-scale": { "node_modules/d3-scale": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
-2
View File
@@ -16,7 +16,6 @@
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"i18next": "^26.0.8", "i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
@@ -30,7 +29,6 @@
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.5",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
@@ -1,666 +0,0 @@
/**
* @file ToolExecutionFlow.tsx
* @description Defines the ToolExecutionFlow component that visualizes the flow of tool usage in agent workflows using a Sankey diagram. It processes the provided tool flow data, constructs a Sankey graph, and renders it using D3.js. The component also includes interactive tooltips for links and a legend for tool types. It handles responsiveness and edge cases such as empty data gracefully.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
* 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.
*
* ## Internal dependencies
* - `../../lib/types`
*
* ## Public surface
* - `ToolExecutionFlow` — 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.
* - `skills/file-headers/` — mandatory `@author` header policy.
* ============================================================================= */
/* -----------------------------------------------------------------------------
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
* -----------------------------------------------------------------------------
* **ToolExecutionFlow**
* 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 { useRef, useEffect, useState, useCallback } from "react";
import * as d3 from "d3";
import { sankey, sankeyLinkHorizontal } from "d3-sankey";
import type { SankeyGraph, SankeyNode, SankeyLink } from "d3-sankey";
import { useTranslation } from "react-i18next";
import type { ToolFlowData } from "../../lib/types";
// ── Constants ─────────────────────────────────────────────────────────────────
const MARGIN = { top: 24, right: 140, bottom: 24, left: 140 };
const NODE_WIDTH = 14;
const NODE_PADDING = 18;
const MIN_NODE_HEIGHT = 6;
const LINK_OPACITY_DEFAULT = 0.15;
const LINK_OPACITY_HOVER = 0.45;
const TOOL_COLORS: Record<string, string> = {
Read: "#3b82f6",
Write: "#22c55e",
Edit: "#eab308",
Bash: "#ef4444",
Grep: "#a855f7",
Glob: "#ec4899",
Agent: "#6366f1",
};
const COLOR_DEFAULT = "#64748b";
function toolColor(name: string): string {
// Strip the _source / _target suffix we add internally
const base = name.replace(/_(source|target)$/, "");
return TOOL_COLORS[base] ?? COLOR_DEFAULT;
}
function toolLabel(name: string): string {
return name.replace(/_(source|target)$/, "");
}
// ── Types ─────────────────────────────────────────────────────────────────────
interface NodeExtra {
id: string;
}
interface LinkExtra {
uid: string;
}
type SNode = SankeyNode<NodeExtra, LinkExtra>;
type SLink = SankeyLink<NodeExtra, LinkExtra>;
type SGraph = SankeyGraph<NodeExtra, LinkExtra>;
interface NodeTipPayload {
kind: "node";
rawName: string;
count: number;
shareOfTotal: number;
}
interface LinkTipPayload {
kind: "link";
source: string;
target: string;
count: number;
shareOfSource: number;
shareOfTarget: number;
}
type TipPayload = NodeTipPayload | LinkTipPayload;
// ── Props ─────────────────────────────────────────────────────────────────────
interface ToolExecutionFlowProps {
data: ToolFlowData;
filterAgentType?: string | null;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/**
* d3-sankey collapses self-loops and duplicate node references. To represent a
* tool appearing as both source and target we suffix the node id with
* `_source` or `_target` and deduplicate at the label layer.
*
* Strategy:
* - A node that ONLY appears as a source keeps its plain name.
* - A node that ONLY appears as a target keeps its plain name.
* - A node that appears on BOTH sides gets `_source` / `_target` copies.
*/
function buildSankeyInput(data: ToolFlowData): {
nodes: NodeExtra[];
links: Array<{ source: string; target: string; value: number; uid: string }>;
} {
const { transitions } = data;
if (transitions.length === 0) return { nodes: [], links: [] };
const sourcesSet = new Set(transitions.map((t) => t.source));
const targetsSet = new Set(transitions.map((t) => t.target));
// Nodes that appear on both sides need splitting
const bothSides = new Set<string>();
for (const s of sourcesSet) {
if (targetsSet.has(s)) bothSides.add(s);
}
const nodeIdSet = new Set<string>();
function sourceId(name: string): string {
return bothSides.has(name) ? `${name}_source` : name;
}
function targetId(name: string): string {
return bothSides.has(name) ? `${name}_target` : name;
}
const links = transitions.map((t, i) => ({
source: sourceId(t.source),
target: targetId(t.target),
value: Math.max(1, t.value),
uid: `link-${i}`,
}));
for (const l of links) {
nodeIdSet.add(l.source);
nodeIdSet.add(l.target);
}
const nodes: NodeExtra[] = Array.from(nodeIdSet).map((id) => ({ id }));
return { nodes, links };
}
// ── Component ─────────────────────────────────────────────────────────────────
export function ToolExecutionFlow({
data,
filterAgentType: _filterAgentType,
}: ToolExecutionFlowProps) {
const { t } = useTranslation("workflows");
const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const tipRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 700, height: 420 });
// Track container width for responsiveness
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) return;
const w = Math.floor(entry.contentRect.width);
if (w > 0) {
setDimensions((prev) => ({
...prev,
width: w,
}));
}
});
observer.observe(el);
return () => observer.disconnect();
}, []);
const isEmpty = data.transitions.length === 0 || data.transitions.every((t) => t.value === 0);
const totalUsage = data.toolCounts.reduce((s, c) => s + c.count, 0);
const hideTip = useCallback(() => {
const tip = tipRef.current;
if (tip) tip.style.opacity = "0";
}, []);
const localizeToolLabel = useCallback(
(name: string) => {
const lower = name.toLowerCase();
switch (lower) {
case "read":
case "write":
case "edit":
case "bash":
case "grep":
case "glob":
case "agent":
case "other":
return t(`errors:toolLegend.${lower}`);
default:
return name;
}
},
[t]
);
const showTip = useCallback(
(payload: TipPayload, anchorEl: SVGGraphicsElement) => {
const tip = tipRef.current;
if (!tip) return;
buildToolFlowTooltip(tip, payload, localizeToolLabel, t);
// Position
const r = anchorEl.getBoundingClientRect();
tip.style.opacity = "0";
tip.style.display = "block";
const tipW = tip.offsetWidth || 280;
const tipH = tip.offsetHeight || 160;
const margin = 8;
let left = r.left + r.width / 2 - tipW / 2;
if (left < margin) left = margin;
if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
let top = r.top - tipH - 10;
if (top < margin) top = r.bottom + 10;
tip.style.left = `${left}px`;
tip.style.top = `${top}px`;
tip.style.opacity = "1";
},
[localizeToolLabel, t]
);
useEffect(() => {
const svgEl = svgRef.current;
if (!svgEl || isEmpty) return;
const { width, height } = dimensions;
const innerW = width - MARGIN.left - MARGIN.right;
const innerH = height - MARGIN.top - MARGIN.bottom;
if (innerW <= 0 || innerH <= 0) return;
// Clear previous render
d3.select(svgEl).selectAll("*").remove();
const { nodes: rawNodes, links: rawLinks } = buildSankeyInput(data);
if (rawNodes.length === 0) return;
// Build sankey layout
const sankeyGen = sankey<NodeExtra, LinkExtra>()
.nodeId((d) => d.id)
.nodeWidth(NODE_WIDTH)
.nodePadding(NODE_PADDING)
.nodeSort(null) // preserve insertion order
.extent([
[0, 0],
[innerW, innerH],
]);
let graph: SGraph;
try {
graph = sankeyGen({
nodes: rawNodes.map((n) => ({ ...n })),
links: rawLinks.map((l) => ({ ...l })),
});
} catch {
// If layout fails (e.g., cycles), bail gracefully
return;
}
// Enforce minimum node height by adjusting y0/y1
for (const node of graph.nodes) {
const n = node as SNode;
if (n.y0 !== undefined && n.y1 !== undefined) {
const h = n.y1 - n.y0;
if (h < MIN_NODE_HEIGHT) {
const mid = (n.y0 + n.y1) / 2;
n.y0 = mid - MIN_NODE_HEIGHT / 2;
n.y1 = mid + MIN_NODE_HEIGHT / 2;
}
}
}
// Re-run update step so links follow the adjusted node positions
sankeyGen.update(graph);
const svg = d3
.select(svgEl)
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio", "xMidYMid meet");
const root = svg.append("g").attr("transform", `translate(${MARGIN.left},${MARGIN.top})`);
// ── Gradient defs ──────────────────────────────────────────────────────
const defs = svg.append("defs");
(graph.links as SLink[]).forEach((link, i) => {
const sourceNode = link.source as SNode;
const targetNode = link.target as SNode;
const gradId = `link-grad-${i}`;
const grad = defs
.append("linearGradient")
.attr("id", gradId)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", sourceNode.x1 ?? 0)
.attr("x2", targetNode.x0 ?? 0);
const srcColor = toolColor(sourceNode.id);
const tgtColor = toolColor(targetNode.id);
grad.append("stop").attr("offset", "0%").attr("stop-color", srcColor);
grad.append("stop").attr("offset", "100%").attr("stop-color", tgtColor);
(link as SLink & { _gradId: string })._gradId = gradId;
});
// ── Links ──────────────────────────────────────────────────────────────
const linkPath = sankeyLinkHorizontal();
const linkGroup = root.append("g").attr("class", "links");
// Pre-compute outgoing/incoming totals per node so we can show share-of-source
// and share-of-target percentages in the tooltip.
const outgoingByNode = new Map<string, number>();
const incomingByNode = new Map<string, number>();
for (const link of graph.links as SLink[]) {
const src = (link.source as SNode).id;
const tgt = (link.target as SNode).id;
outgoingByNode.set(src, (outgoingByNode.get(src) ?? 0) + (link.value ?? 0));
incomingByNode.set(tgt, (incomingByNode.get(tgt) ?? 0) + (link.value ?? 0));
}
linkGroup
.selectAll<SVGPathElement, SLink>("path")
.data(graph.links as SLink[])
.join("path")
.attr("d", (d) => linkPath(d) ?? "")
.attr("stroke", (d, i) => {
const gradId = (graph.links[i] as SLink & { _gradId?: string })._gradId;
return gradId ? `url(#${gradId})` : toolColor((d.source as SNode).id);
})
.attr("stroke-width", (d) => Math.max(1, d.width ?? 1))
.attr("fill", "none")
.attr("stroke-opacity", LINK_OPACITY_DEFAULT)
.style("cursor", "default")
.on("mouseenter", function (_event: MouseEvent, d: SLink) {
d3.select(this).attr("stroke-opacity", LINK_OPACITY_HOVER);
const src = (d.source as SNode).id;
const tgt = (d.target as SNode).id;
const srcOut = outgoingByNode.get(src) ?? 0;
const tgtIn = incomingByNode.get(tgt) ?? 0;
showTip(
{
kind: "link",
source: toolLabel(src),
target: toolLabel(tgt),
count: d.value ?? 0,
shareOfSource: srcOut > 0 ? (d.value ?? 0) / srcOut : 0,
shareOfTarget: tgtIn > 0 ? (d.value ?? 0) / tgtIn : 0,
},
this as SVGGraphicsElement
);
})
.on("mouseleave", function () {
d3.select(this).attr("stroke-opacity", LINK_OPACITY_DEFAULT);
hideTip();
});
// ── Nodes ──────────────────────────────────────────────────────────────
const nodeGroup = root.append("g").attr("class", "nodes");
const nodeGs = nodeGroup
.selectAll<SVGGElement, SNode>("g")
.data(graph.nodes as SNode[])
.join("g");
nodeGs
.append("rect")
.attr("x", (d) => d.x0 ?? 0)
.attr("y", (d) => d.y0 ?? 0)
.attr("width", (d) => (d.x1 ?? 0) - (d.x0 ?? 0))
.attr("height", (d) => Math.max(MIN_NODE_HEIGHT, (d.y1 ?? 0) - (d.y0 ?? 0)))
.attr("rx", 2)
.attr("ry", 2)
.attr("fill", (d) => toolColor(d.id))
.attr("stroke-width", 0)
.attr("fill-opacity", 0.9)
.style("cursor", "default")
.on("mouseenter", function (_event: MouseEvent, d: SNode) {
const rawName = toolLabel(d.id);
const countEntry = data.toolCounts.find((c) => c.tool_name === rawName);
const count = countEntry?.count ?? 0;
showTip(
{
kind: "node",
rawName,
count,
shareOfTotal: totalUsage > 0 ? count / totalUsage : 0,
},
this as SVGGraphicsElement
);
})
.on("mouseleave", function () {
hideTip();
});
// ── Node labels ────────────────────────────────────────────────────────
nodeGs.each(function (d: SNode) {
const g = d3.select(this);
const nodeX0 = d.x0 ?? 0;
const nodeX1 = d.x1 ?? 0;
const nodeY0 = d.y0 ?? 0;
const nodeY1 = d.y1 ?? 0;
const nodeH = nodeY1 - nodeY0;
const midY = nodeY0 + nodeH / 2;
const rawLabel = toolLabel(d.id);
const label = localizeToolLabel(rawLabel);
const isRightSide = (nodeX0 + nodeX1) / 2 > innerW / 2;
// Percentage of total
const countEntry = data.toolCounts.find((c) => c.tool_name === rawLabel);
const pct =
countEntry && totalUsage > 0
? ` ${((countEntry.count / totalUsage) * 100).toFixed(1)}%`
: "";
const textX = isRightSide ? nodeX1 + 8 : nodeX0 - 8;
const anchor = isRightSide ? "start" : "end";
const text = g
.append("text")
.attr("x", textX)
.attr("y", midY)
.attr("dy", "0.35em")
.attr("text-anchor", anchor)
.style("font-size", "12px")
.style("font-family", "Inter, -apple-system, sans-serif")
.style("fill", "#e2e8f0")
.style("pointer-events", "none")
.style("user-select", "none");
text.append("tspan").text(label).style("font-weight", "500");
if (pct) {
text.append("tspan").text(pct).style("fill", "#64748b").style("font-size", "11px");
}
});
// Hide any stale tooltip when the chart re-renders so it cannot get stuck.
hideTip();
}, [data, dimensions, isEmpty, localizeToolLabel, t, totalUsage, showTip, hideTip]);
// Adapt SVG height based on node count so tall graphs don't crush
useEffect(() => {
const nodeCount = new Set(data.transitions.flatMap((t) => [t.source, t.target])).size;
const estimatedH = Math.max(
320,
Math.min(600, nodeCount * (NODE_PADDING + 20) + MARGIN.top + MARGIN.bottom)
);
setDimensions((prev) => ({ ...prev, height: estimatedH }));
}, [data.transitions]);
return (
<div className="relative" ref={containerRef} onMouseLeave={hideTip}>
{isEmpty ? (
<div className="flex items-center justify-center" style={{ height: dimensions.height }}>
<span className="text-sm text-fg-muted">{t("toolFlow.noData")}</span>
</div>
) : (
<svg
ref={svgRef}
width={dimensions.width}
height={dimensions.height}
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
preserveAspectRatio="xMidYMid meet"
style={{ display: "block", width: "100%", height: dimensions.height }}
onMouseLeave={hideTip}
/>
)}
<Legend />
<div
ref={tipRef}
role="tooltip"
aria-hidden="true"
className="fixed z-50 px-3 py-2 rounded-lg shadow-2xl pointer-events-none"
style={{
display: "none",
opacity: 0,
left: 0,
top: 0,
background: "#12121f",
border: "1px solid #2a2a4a",
color: "#e2e8f0",
minWidth: 240,
maxWidth: 320,
transition: "opacity 120ms ease-out",
}}
/>
</div>
);
}
// ── Tooltip DOM builder ───────────────────────────────────────────────────────
function fmtPct(v: number): string {
if (v <= 0) return "-";
if (v < 0.01) return "<1%";
return `${(v * 100).toFixed(1)}%`;
}
function appendTipRow(parent: HTMLElement, label: string, value: string) {
const row = document.createElement("div");
row.style.cssText =
"display:flex;justify-content:space-between;gap:16px;font-size:11px;line-height:1.6";
const lbl = document.createElement("span");
lbl.style.color = "#64748b";
lbl.textContent = label;
const val = document.createElement("span");
val.style.cssText = "color:#cbd5e1;font-weight:500;font-variant-numeric:tabular-nums";
val.textContent = value;
row.appendChild(lbl);
row.appendChild(val);
parent.appendChild(row);
}
type TFn = (key: string, options?: Record<string, unknown>) => string;
function buildToolFlowTooltip(
el: HTMLDivElement,
payload: TipPayload,
localizeToolLabel: (name: string) => string,
t: TFn
) {
while (el.firstChild) el.removeChild(el.firstChild);
if (payload.kind === "node") {
const name = localizeToolLabel(payload.rawName);
const title = document.createElement("p");
title.style.cssText = "font-size:12px;font-weight:600;color:#e2e8f0;margin:0";
title.textContent = name;
el.appendChild(title);
const subtitle = document.createElement("p");
subtitle.style.cssText =
"font-size:10px;color:#64748b;margin:2px 0 8px;text-transform:uppercase;letter-spacing:0.05em";
subtitle.textContent = t("toolFlow.tooltip.node");
el.appendChild(subtitle);
appendTipRow(el, t("toolFlow.tooltip.totalCalls"), payload.count.toLocaleString());
appendTipRow(el, t("toolFlow.tooltip.shareOfAll"), fmtPct(payload.shareOfTotal));
const desc = document.createElement("p");
desc.style.cssText =
"font-size:11px;color:#94a3b8;line-height:1.45;border-top:1px solid #2a2a4a;padding-top:8px;margin:8px 0 0";
desc.textContent = t("toolFlow.tooltip.nodeDescFmt", { name });
el.appendChild(desc);
return;
}
// Link tooltip
const src = localizeToolLabel(payload.source);
const tgt = localizeToolLabel(payload.target);
const title = document.createElement("p");
title.style.cssText = "font-size:12px;font-weight:600;color:#e2e8f0;margin:0";
const tspanArrow = document.createElement("span");
tspanArrow.style.color = "#64748b";
tspanArrow.textContent = " → ";
title.appendChild(document.createTextNode(src));
title.appendChild(tspanArrow);
title.appendChild(document.createTextNode(tgt));
el.appendChild(title);
const subtitle = document.createElement("p");
subtitle.style.cssText =
"font-size:10px;color:#64748b;margin:2px 0 8px;text-transform:uppercase;letter-spacing:0.05em";
subtitle.textContent = t("toolFlow.tooltip.link");
el.appendChild(subtitle);
appendTipRow(el, t("toolFlow.tooltip.transitionsObserved"), payload.count.toLocaleString());
appendTipRow(
el,
t("toolFlow.tooltip.shareOfSourceFmt", { source: src }),
fmtPct(payload.shareOfSource)
);
appendTipRow(
el,
t("toolFlow.tooltip.shareOfTargetFmt", { target: tgt }),
fmtPct(payload.shareOfTarget)
);
const desc = document.createElement("p");
desc.style.cssText =
"font-size:11px;color:#94a3b8;line-height:1.45;border-top:1px solid #2a2a4a;padding-top:8px;margin:8px 0 0";
desc.textContent = t("toolFlow.tooltip.linkDescFmt", { source: src, target: tgt });
el.appendChild(desc);
}
// ── Legend ────────────────────────────────────────────────────────────────────
const LEGEND_ITEMS: Array<{ key: string; color: string }> = [
{ key: "read", color: "#3b82f6" },
{ key: "write", color: "#22c55e" },
{ key: "edit", color: "#eab308" },
{ key: "bash", color: "#ef4444" },
{ key: "grep", color: "#a855f7" },
{ key: "glob", color: "#ec4899" },
{ key: "agent", color: "#6366f1" },
{ key: "other", color: "#64748b" },
];
function Legend() {
const { t } = useTranslation("errors");
return (
<div className="flex flex-wrap gap-x-4 gap-y-1.5 mt-3 px-1">
{LEGEND_ITEMS.map(({ key, color }) => (
<div key={key} className="flex items-center gap-1.5">
<span
style={{ background: color, opacity: 0.9 }}
className="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
/>
<span className="text-xs text-fg-secondary">{t(`toolLegend.${key}`)}</span>
</div>
))}
</div>
);
}
-23
View File
@@ -58,11 +58,6 @@
"howToRead": "Each column is one layer. Box width is constant; the small badge is the count. Edges flow left-to-right; thicker edges = more frequent transitions. Hover any node for stats; click to filter the rest of the page by it.", "howToRead": "Each column is one layer. Box width is constant; the small badge is the count. Edges flow left-to-right; thicker edges = more frequent transitions. Hover any node for stats; click to filter the rest of the page by it.",
"why": "Use it to answer 'where does work go in my workflows?' - which subagent types dominate, where compaction kicks in, and which paths end in errors vs clean completions." "why": "Use it to answer 'where does work go in my workflows?' - which subagent types dominate, where compaction kicks in, and which paths end in errors vs clean completions."
}, },
"toolFlow": {
"what": "A Sankey-style chart of consecutive tool-call transitions across every session in scope. Each band is one (source → target) pair counted across all sessions.",
"howToRead": "Tools appear as colored bars on the left and right. Bands connect them: thickness = number of times the source ran immediately before the target. Hover a band for share-of-source and share-of-target percentages; hover a node for its total usage and share of all tool calls.",
"why": "Surfaces habitual tool chains (e.g. Read → Edit → Bash). Useful for spotting loops, wasted hops, and missing tool variety in agent behavior."
},
"drillIn": { "drillIn": {
"what": "Per-session detail view: full agent tree, tool-call timeline, and raw event sequence for whichever session you select.", "what": "Per-session detail view: full agent tree, tool-call timeline, and raw event sequence for whichever session you select.",
"howToRead": "Pick a session from the dropdown above or click a bubble in the scatter plot. The tabs let you switch between hierarchical (Agent Tree), temporal (Tool Timeline), and chronological (Event Sequence) views of the same session.", "howToRead": "Pick a session from the dropdown above or click a bubble in the scatter plot. The tabs let you switch between hierarchical (Agent Tree), temporal (Tool Timeline), and chronological (Event Sequence) views of the same session.",
@@ -221,24 +216,6 @@
} }
} }
}, },
"toolFlow": {
"title": "Tool Execution Flow",
"subtitle": "How tools chain together - Sankey-style directed flow",
"noData": "No tool flow data",
"transitionCount_one": "{{count}} transition",
"transitionCount_other": "{{count}} transitions",
"tooltip": {
"node": "Tool node",
"link": "Tool transition",
"totalCalls": "Total calls",
"shareOfAll": "Share of all tool use",
"transitionsObserved": "Transitions observed",
"shareOfSourceFmt": "Share of {{source}} → next",
"shareOfTargetFmt": "Share of prev → {{target}}",
"nodeDescFmt": "Bar height shows how often {{name}} appears across all sessions in scope. Bands flowing in on the left show what ran before {{name}}; bands flowing out on the right show what ran after.",
"linkDescFmt": "Across every session, this counts how often {{source}} ran immediately before {{target}}. Thicker bands = more common transitions; the percentages show how dominant this hop is among each tool's outgoing / incoming flow."
}
},
"drillIn": { "drillIn": {
"title": "Session Drill-In", "title": "Session Drill-In",
"subtitle": "Select a session from the scatter plot or use the dropdown to explore", "subtitle": "Select a session from the scatter plot or use the dropdown to explore",
-23
View File
@@ -58,11 +58,6 @@
"howToRead": "Mỗi cột là một lớp. Khung node có cùng kích thước; con số là số lần. Cạnh đi từ trái sang phải; cạnh càng dày = lượt chuyển càng phổ biến. Hover bất kỳ node để xem thống kê; bấm để lọc phần còn lại của trang theo node đó.", "howToRead": "Mỗi cột là một lớp. Khung node có cùng kích thước; con số là số lần. Cạnh đi từ trái sang phải; cạnh càng dày = lượt chuyển càng phổ biến. Hover bất kỳ node để xem thống kê; bấm để lọc phần còn lại của trang theo node đó.",
"why": "Dùng để trả lời 'công việc trong workflow của tôi đi đâu?' - loại Subagent nào chiếm ưu thế, nén ngữ cảnh xảy ra ở đâu, và đường nào kết thúc bằng lỗi so với hoàn tất sạch." "why": "Dùng để trả lời 'công việc trong workflow của tôi đi đâu?' - loại Subagent nào chiếm ưu thế, nén ngữ cảnh xảy ra ở đâu, và đường nào kết thúc bằng lỗi so với hoàn tất sạch."
}, },
"toolFlow": {
"what": "Biểu đồ kiểu Sankey về các lượt chuyển tool gọi liên tiếp trên mọi phiên trong phạm vi. Mỗi băng là một cặp (nguồn → đích) đếm trên toàn bộ phiên.",
"howToRead": "Tool xuất hiện thành các thanh màu ở hai bên. Băng nối chúng: độ dày = số lần nguồn chạy ngay trước đích. Hover một băng để xem tỷ trọng-trong-nguồn và tỷ trọng-trong-đích; hover một node để xem tổng lượt dùng và tỷ trọng trên tổng tool.",
"why": "Phơi bày những chuỗi tool quen thuộc (ví dụ Read → Edit → Bash). Hữu ích để phát hiện vòng lặp, bước thừa, và sự đơn điệu trong cách Agent dùng tool."
},
"drillIn": { "drillIn": {
"what": "Khung chi tiết theo phiên: cây Agent đầy đủ, dòng thời gian tool gọi và chuỗi sự kiện thô của phiên bạn chọn.", "what": "Khung chi tiết theo phiên: cây Agent đầy đủ, dòng thời gian tool gọi và chuỗi sự kiện thô của phiên bạn chọn.",
"howToRead": "Chọn phiên từ dropdown phía trên hoặc bấm một bong bóng trong biểu đồ phân tán. Các tab cho phép chuyển giữa góc nhìn phân cấp (Agent Tree), thời gian (Tool Timeline) và theo trình tự (Event Sequence) của cùng một phiên.", "howToRead": "Chọn phiên từ dropdown phía trên hoặc bấm một bong bóng trong biểu đồ phân tán. Các tab cho phép chuyển giữa góc nhìn phân cấp (Agent Tree), thời gian (Tool Timeline) và theo trình tự (Event Sequence) của cùng một phiên.",
@@ -221,24 +216,6 @@
} }
} }
}, },
"toolFlow": {
"title": "Luồng thực thi công cụ",
"subtitle": "Chuỗi liên kết giữa các công cụ - luồng có hướng kiểu Sankey",
"noData": "Chưa có dữ liệu luồng công cụ",
"transitionCount_one": "{{count}} lượt chuyển",
"transitionCount_other": "{{count}} lượt chuyển",
"tooltip": {
"node": "Node công cụ",
"link": "Lượt chuyển công cụ",
"totalCalls": "Tổng số lượt gọi",
"shareOfAll": "Tỷ trọng trên tổng dùng tool",
"transitionsObserved": "Số lượt chuyển quan sát",
"shareOfSourceFmt": "Tỷ trọng {{source}} → kế tiếp",
"shareOfTargetFmt": "Tỷ trọng trước → {{target}}",
"nodeDescFmt": "Chiều cao thanh thể hiện tần suất {{name}} xuất hiện trên mọi phiên trong phạm vi. Băng đi vào bên trái cho thấy tool chạy trước {{name}}; băng đi ra bên phải cho thấy tool chạy sau.",
"linkDescFmt": "Trên mọi phiên, đây là số lần {{source}} chạy ngay trước {{target}}. Băng càng dày = lượt chuyển càng phổ biến; phần trăm cho biết bước này chiếm tỷ trọng bao nhiêu trong luồng ra/vào của mỗi tool."
}
},
"drillIn": { "drillIn": {
"title": "Phân tích chi tiết phiên", "title": "Phân tích chi tiết phiên",
"subtitle": "Chọn phiên từ biểu đồ phân tán hoặc dùng danh sách để khám phá", "subtitle": "Chọn phiên từ biểu đồ phân tán hoặc dùng danh sách để khám phá",
+13 -25
View File
@@ -3,10 +3,10 @@
* @description Workflows page. Opens on this dashboard's OWN pipelines — per-node * @description Workflows page. Opens on this dashboard's OWN pipelines — per-node
* pass rates across every lane, and which skills/plugins actually get used — then * pass rates across every lane, and which skills/plugins actually get used — then
* the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the * the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the
* spawning DAG, tool flow, patterns, model delegation, error propagation, session * spawning DAG, patterns, model delegation, error propagation, session complexity,
* complexity, compaction impact and the per-session drill-in. Issue #6 dropped the * compaction impact and the per-session drill-in. Issue #6 dropped the agent
* agent collaboration network and the subagent effectiveness scorecards as too * collaboration network and the subagent effectiveness scorecards as too
* specialized to act on. * specialized to act on; the tool-execution Sankey followed for the same reason.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/ */
/* ============================================================================= /* =============================================================================
@@ -35,7 +35,6 @@
* - `../components/workflows/SkillUsagePanel` * - `../components/workflows/SkillUsagePanel`
* - `../components/workflows/WorkflowRunsPanel` * - `../components/workflows/WorkflowRunsPanel`
* - `../components/workflows/OrchestrationDAG` * - `../components/workflows/OrchestrationDAG`
* - `../components/workflows/ToolExecutionFlow`
* - `../components/workflows/WorkflowPatterns` * - `../components/workflows/WorkflowPatterns`
* - `../components/workflows/ModelDelegationFlow` * - `../components/workflows/ModelDelegationFlow`
* - `../components/workflows/ErrorPropagationMap` * - `../components/workflows/ErrorPropagationMap`
@@ -82,7 +81,6 @@ import type { WorkflowData, WSMessage } from "../lib/types";
import { WorkflowStats } from "../components/workflows/WorkflowStats"; import { WorkflowStats } from "../components/workflows/WorkflowStats";
import { OrchestrationDAG } from "../components/workflows/OrchestrationDAG"; import { OrchestrationDAG } from "../components/workflows/OrchestrationDAG";
import { ToolExecutionFlow } from "../components/workflows/ToolExecutionFlow";
import { WorkflowPatterns } from "../components/workflows/WorkflowPatterns"; import { WorkflowPatterns } from "../components/workflows/WorkflowPatterns";
import { ModelDelegationFlow } from "../components/workflows/ModelDelegationFlow"; import { ModelDelegationFlow } from "../components/workflows/ModelDelegationFlow";
import { ErrorPropagationMap } from "../components/workflows/ErrorPropagationMap"; import { ErrorPropagationMap } from "../components/workflows/ErrorPropagationMap";
@@ -267,19 +265,9 @@ export function Workflows() {
)} )}
</Section> </Section>
{/* Section 4: Tool Execution Flow */} {/* Section 4: Workflow Patterns */}
<Section <Section
number={4} number={4}
title={t("toolFlow.title")}
subtitle={t("toolFlow.subtitle")}
infoKey="toolFlow"
>
<ToolExecutionFlow data={data.toolFlow} filterAgentType={selectedNode} />
</Section>
{/* Section 5: Workflow Patterns */}
<Section
number={5}
title={t("patterns.title")} title={t("patterns.title")}
subtitle={t("patterns.subtitle")} subtitle={t("patterns.subtitle")}
infoKey="patterns" infoKey="patterns"
@@ -287,10 +275,10 @@ export function Workflows() {
<WorkflowPatterns data={data.patterns} onPatternClick={() => {}} /> <WorkflowPatterns data={data.patterns} onPatternClick={() => {}} />
</Section> </Section>
{/* Section 6 + 7: Two Column */} {/* Section 5 + 6: Two Column */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section <Section
number={6} number={5}
title={t("modelDelegation.title")} title={t("modelDelegation.title")}
subtitle={t("modelDelegation.subtitle")} subtitle={t("modelDelegation.subtitle")}
infoKey="modelDelegation" infoKey="modelDelegation"
@@ -299,7 +287,7 @@ export function Workflows() {
</Section> </Section>
<Section <Section
number={7} number={6}
title={t("errorPropagation.title")} title={t("errorPropagation.title")}
subtitle={t("errorPropagation.subtitle")} subtitle={t("errorPropagation.subtitle")}
infoKey="errorPropagation" infoKey="errorPropagation"
@@ -308,10 +296,10 @@ export function Workflows() {
</Section> </Section>
</div> </div>
{/* Section 8 + 9: Two Column */} {/* Section 7 + 8: Two Column */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section <Section
number={8} number={7}
title={t("complexity.title")} title={t("complexity.title")}
subtitle={t("complexity.subtitle")} subtitle={t("complexity.subtitle")}
infoKey="complexity" infoKey="complexity"
@@ -320,7 +308,7 @@ export function Workflows() {
</Section> </Section>
<Section <Section
number={9} number={8}
title={t("compaction.title")} title={t("compaction.title")}
subtitle={t("compaction.subtitle")} subtitle={t("compaction.subtitle")}
infoKey="compaction" infoKey="compaction"
@@ -329,9 +317,9 @@ export function Workflows() {
</Section> </Section>
</div> </div>
{/* Section 10: Session Drill-In */} {/* Section 9: Session Drill-In */}
<Section <Section
number={10} number={9}
title={t("drillIn.title")} title={t("drillIn.title")}
subtitle={t("drillIn.subtitle")} subtitle={t("drillIn.subtitle")}
infoKey="drillIn" infoKey="drillIn"
@@ -10715,197 +10715,6 @@ exports[`screen snapshots > Workflows 1`] = `
> >
4 4
</span> </span>
<h2
class="text-sm font-semibold text-fg-primary"
>
Tool Execution Flow
</h2>
<button
aria-expanded="false"
aria-label="What this shows"
class="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-fg-muted hover:text-fg-secondary transition-colors focus:outline-none focus:ring-1 focus:ring-accent/40"
type="button"
>
<svg
class="lucide lucide-info w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M12 16v-4"
/>
<path
d="M12 8h.01"
/>
</svg>
</button>
</div>
<span
class="hidden lg:block flex-shrink-0 max-w-[20rem] xl:max-w-sm truncate text-right text-[11px] text-fg-muted"
title="How tools chain together - Sankey-style directed flow"
>
How tools chain together - Sankey-style directed flow
</span>
</div>
<div
class="card p-4 flex-1"
>
<div
class="relative"
>
<div
class="flex items-center justify-center"
style="height: 320px;"
>
<span
class="text-sm text-fg-muted"
>
No tool flow data
</span>
</div>
<div
class="flex flex-wrap gap-x-4 gap-y-1.5 mt-3 px-1"
>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(59, 130, 246); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Read
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(34, 197, 94); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Write
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(234, 179, 8); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Edit
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(239, 68, 68); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Bash
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(168, 85, 247); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Grep
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(236, 72, 153); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Glob
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(99, 102, 241); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Agent
</span>
</div>
<div
class="flex items-center gap-1.5"
>
<span
class="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
style="background: rgb(100, 116, 139); opacity: 0.9;"
/>
<span
class="text-xs text-fg-secondary"
>
Other
</span>
</div>
</div>
<div
aria-hidden="true"
class="fixed z-50 px-3 py-2 rounded-lg shadow-2xl pointer-events-none"
role="tooltip"
style="display: none; opacity: 0; left: 0px; top: 0px; background: rgb(18, 18, 31); border: 1px solid rgb(42, 42, 74); color: rgb(226, 232, 240); min-width: 240px; max-width: 320px; transition: opacity 120ms ease-out;"
/>
</div>
</div>
</div>
<div
class="flex flex-col h-full"
>
<div
class="flex items-center justify-between gap-4 mb-3"
>
<div
class="flex items-center gap-2.5 min-w-0"
>
<span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
>
5
</span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"
> >
@@ -11013,7 +10822,7 @@ exports[`screen snapshots > Workflows 1`] = `
<span <span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0" class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
> >
6 5
</span> </span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"
@@ -11103,7 +10912,7 @@ exports[`screen snapshots > Workflows 1`] = `
<span <span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0" class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
> >
7 6
</span> </span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"
@@ -11205,7 +11014,7 @@ exports[`screen snapshots > Workflows 1`] = `
<span <span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0" class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
> >
8 7
</span> </span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"
@@ -11309,7 +11118,7 @@ exports[`screen snapshots > Workflows 1`] = `
<span <span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0" class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
> >
9 8
</span> </span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"
@@ -11404,7 +11213,7 @@ exports[`screen snapshots > Workflows 1`] = `
<span <span
class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0" class="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0"
> >
10 9
</span> </span>
<h2 <h2
class="text-sm font-semibold text-fg-primary" class="text-sm font-semibold text-fg-primary"