feat(workflows): drop the patterns list and model delegation flow

Neither panel drove a decision: the detected-pattern ranking restated what
the orchestration DAG already shows, and the delegation flow's main→subagent
model pairing is visible in the per-session drill-in. Removes both components
and their i18n blocks, promotes the error-propagation map to full width, and
renumbers the remaining sections 4-7.
This commit is contained in:
2026-09-08 11:10:32 +07:00
parent 47a8726e27
commit fe45644b31
6 changed files with 46 additions and 1353 deletions
@@ -1,531 +0,0 @@
/**
* @file ModelDelegationFlow.tsx
* @description Defines the ModelDelegationFlow React component that visualizes the relationships between main models and subagent models in a flow diagram using D3.js. The component takes model delegation data as input and renders an SVG diagram that shows how different models are connected based on their usage in agents and sessions. It categorizes models into families (opus, sonnet, haiku, other) for color-coding and provides a clear visual representation of model delegation patterns.
* @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`
* - `../../lib/format`
*
* ## Public surface
* - `ModelDelegationFlowProps` — exported API; see TSDoc on the symbol for behavior.
* - `ModelDelegationFlow` — 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.
*
* ## 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).
* -----------------------------------------------------------------------------
* **ModelDelegationFlowProps**
* 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.
*
* **ModelDelegationFlow**
* 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, useCallback } from "react";
import { useTranslation } from "react-i18next";
import * as d3 from "d3";
import type { ModelDelegationData } from "../../lib/types";
import { formatModelName } from "../../lib/format";
// ── Helpers ───────────────────────────────────────────────────────────────────
function modelFamily(name: string): "opus" | "sonnet" | "haiku" | "other" {
const lower = name.toLowerCase();
if (lower.includes("opus")) return "opus";
if (lower.includes("sonnet")) return "sonnet";
if (lower.includes("haiku")) return "haiku";
return "other";
}
function fmtTokens(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
return String(n);
}
// Model formatting is handled by the shared formatModelName utility.
// ── Color palette per model family ───────────────────────────────────────────
const FAMILY_COLORS = {
opus: {
grad: ["#7c3aed", "#a855f7"] as [string, string],
stroke: "#a855f7",
text: "#e9d5ff",
badge: "rgba(168,85,247,0.15)",
},
sonnet: {
grad: ["#1d4ed8", "#3b82f6"] as [string, string],
stroke: "#3b82f6",
text: "#bfdbfe",
badge: "rgba(59,130,246,0.15)",
},
haiku: {
grad: ["#065f46", "#10b981"] as [string, string],
stroke: "#10b981",
text: "#a7f3d0",
badge: "rgba(16,185,129,0.15)",
},
other: {
grad: ["#374151", "#6b7280"] as [string, string],
stroke: "#6b7280",
text: "#d1d5db",
badge: "rgba(107,114,128,0.15)",
},
} as const;
// ── Types used internally ─────────────────────────────────────────────────────
interface NodeDatum {
id: string;
label: string;
family: "opus" | "sonnet" | "haiku" | "other";
agentCount: number;
sessionCount: number;
totalTokens: number;
side: "main" | "sub";
x: number;
y: number;
}
interface EdgeDatum {
sourceId: string;
targetId: string;
}
type ShowTipFn = (node: NodeDatum, anchor: SVGGraphicsElement) => void;
type HideTipFn = () => void;
// ── D3 chart renderer ─────────────────────────────────────────────────────────
const NODE_W = 160;
const NODE_H = 80;
const NODE_RX = 10;
const COL_GAP = 200;
const ROW_GAP = 108;
const PADDING = { top: 40, left: 24, right: 24, bottom: 24 };
function renderFlow(
svg: SVGSVGElement,
mainNodes: NodeDatum[],
subNodes: NodeDatum[],
edges: EdgeDatum[],
t: (key: string, options?: Record<string, unknown>) => string,
showTip: ShowTipFn,
hideTip: HideTipFn
): void {
const allNodes = [...mainNodes, ...subNodes];
const totalRows = Math.max(mainNodes.length, subNodes.length);
const chartH = totalRows * ROW_GAP + PADDING.top + PADDING.bottom;
const chartW = NODE_W * 2 + COL_GAP + PADDING.left + PADDING.right;
const root = d3.select(svg);
root.selectAll("*").remove();
root.attr("viewBox", `0 0 ${chartW} ${chartH}`).attr("preserveAspectRatio", "xMidYMid meet");
const defs = root.append("defs");
// Gradient defs per family
(["opus", "sonnet", "haiku", "other"] as const).forEach((fam) => {
const colors = FAMILY_COLORS[fam];
const grad = defs
.append("linearGradient")
.attr("id", `flow-grad-${fam}`)
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%");
grad.append("stop").attr("offset", "0%").attr("stop-color", colors.grad[0]);
grad.append("stop").attr("offset", "100%").attr("stop-color", colors.grad[1]);
});
const g = root.append("g");
// Column labels
const labelY = PADDING.top - 16;
g.append("text")
.attr("x", PADDING.left + NODE_W / 2)
.attr("y", labelY)
.attr("text-anchor", "middle")
.attr("fill", "#6b7280")
.attr("font-size", 11)
.attr("font-family", "Inter, sans-serif")
.attr("letter-spacing", "0.08em")
.text(t("modelDelegation.mainModels"));
g.append("text")
.attr("x", PADDING.left + NODE_W + COL_GAP + NODE_W / 2)
.attr("y", labelY)
.attr("text-anchor", "middle")
.attr("fill", "#6b7280")
.attr("font-size", 11)
.attr("font-family", "Inter, sans-serif")
.attr("letter-spacing", "0.08em")
.text(t("modelDelegation.subagentModels"));
// Build lookup for node positions
const nodeMap = new Map<string, NodeDatum>(allNodes.map((n) => [n.id, n]));
// Draw edges (cubic bezier curves)
edges.forEach(({ sourceId, targetId }) => {
const src = nodeMap.get(sourceId);
const tgt = nodeMap.get(targetId);
if (!src || !tgt) return;
const x1 = src.x + NODE_W;
const y1 = src.y + NODE_H / 2;
const x2 = tgt.x;
const y2 = tgt.y + NODE_H / 2;
const cx = (x1 + x2) / 2;
g.append("path")
.attr("d", `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`)
.attr("fill", "none")
.attr("stroke", "#2a2a3d")
.attr("stroke-width", 1.5)
.attr("opacity", 0.7);
});
// Draw nodes
allNodes.forEach((node) => {
const colors = FAMILY_COLORS[node.family];
const ng = g
.append("g")
.attr("transform", `translate(${node.x},${node.y})`)
.style("cursor", "default")
.on("mouseenter", function () {
showTip(node, this as SVGGraphicsElement);
})
.on("mouseleave", () => hideTip());
// Border glow rect (slightly larger)
ng.append("rect")
.attr("x", -1)
.attr("y", -1)
.attr("width", NODE_W + 2)
.attr("height", NODE_H + 2)
.attr("rx", NODE_RX + 1)
.attr("fill", "none")
.attr("stroke", colors.stroke)
.attr("stroke-width", 1)
.attr("opacity", 0.25);
// Main rect with gradient
ng.append("rect")
.attr("width", NODE_W)
.attr("height", NODE_H)
.attr("rx", NODE_RX)
.attr("fill", `url(#flow-grad-${node.family})`)
.attr("fill-opacity", 0.18)
.attr("stroke", colors.stroke)
.attr("stroke-width", 1);
// Model name
ng.append("text")
.attr("x", 12)
.attr("y", 22)
.attr("fill", colors.text)
.attr("font-size", 11)
.attr("font-weight", "600")
.attr("font-family", "Inter, sans-serif")
.text(formatModelName(node.label) ?? node.label);
// Agent count pill
ng.append("rect")
.attr("x", 10)
.attr("y", 32)
.attr("width", 64)
.attr("height", 16)
.attr("rx", 4)
.attr("fill", colors.badge);
ng.append("text")
.attr("x", 42)
.attr("y", 43.5)
.attr("text-anchor", "middle")
.attr("fill", colors.text)
.attr("font-size", 9.5)
.attr("font-family", "Inter, sans-serif")
.text(t("modelDelegation.agentsCount", { count: node.agentCount }));
// Token count
if (node.totalTokens > 0) {
ng.append("text")
.attr("x", 12)
.attr("y", 66)
.attr("fill", "#6b7280")
.attr("font-size", 9.5)
.attr("font-family", "Inter, sans-serif")
.text(t("modelDelegation.tokensCount", { tokens: fmtTokens(node.totalTokens) }));
}
// Session count (main nodes only)
if (node.side === "main" && node.sessionCount > 0) {
ng.append("text")
.attr("x", NODE_W - 10)
.attr("y", 66)
.attr("text-anchor", "end")
.attr("fill", "#6b7280")
.attr("font-size", 9.5)
.attr("font-family", "Inter, sans-serif")
.text(t("modelDelegation.sessionsCount", { count: node.sessionCount }));
}
});
}
// ── Component ─────────────────────────────────────────────────────────────────
export interface ModelDelegationFlowProps {
data: ModelDelegationData;
}
export function ModelDelegationFlow({ data }: ModelDelegationFlowProps) {
const { t } = useTranslation("workflows");
const svgRef = useRef<SVGSVGElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const tipRef = useRef<HTMLDivElement>(null);
const hasData = data.mainModels.length > 0 || data.subagentModels.length > 0;
const totalAgents = countTotalAgents(data);
const hideTip = useCallback(() => {
const tip = tipRef.current;
if (tip) tip.style.opacity = "0";
}, []);
const showTip = useCallback(
(node: NodeDatum, anchor: SVGGraphicsElement) => {
const tip = tipRef.current;
if (!tip) return;
buildModelDelegationTooltip(tip, node, totalAgents, t);
const r = anchor.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";
},
[totalAgents, t]
);
useEffect(() => {
if (!svgRef.current || !hasData) return;
const tokenMap = new Map<string, number>();
data.tokensByModel.forEach(({ model, input_tokens, output_tokens }) => {
tokenMap.set(model, (tokenMap.get(model) ?? 0) + input_tokens + output_tokens);
});
const mainNodes: NodeDatum[] = data.mainModels.map((m, i) => ({
id: `main-${m.model}`,
label: formatModelName(m.model) ?? m.model,
family: modelFamily(m.model),
agentCount: m.agent_count,
sessionCount: m.session_count,
totalTokens: tokenMap.get(m.model) ?? 0,
side: "main",
x: PADDING.left,
y: PADDING.top + i * ROW_GAP,
}));
const subNodes: NodeDatum[] = data.subagentModels.map((m, i) => ({
id: `sub-${m.model}`,
label: formatModelName(m.model) ?? m.model,
family: modelFamily(m.model),
agentCount: m.agent_count,
sessionCount: 0,
totalTokens: tokenMap.get(m.model) ?? 0,
side: "sub",
x: PADDING.left + NODE_W + COL_GAP,
y: PADDING.top + i * ROW_GAP,
}));
// Connect all main models to all subagent models that share a family, or all if no match
const edges: EdgeDatum[] = [];
mainNodes.forEach((mn) => {
subNodes.forEach((sn) => {
edges.push({ sourceId: mn.id, targetId: sn.id });
});
});
renderFlow(svgRef.current, mainNodes, subNodes, edges, t, showTip, hideTip);
hideTip();
}, [data, hasData, t, showTip, hideTip]);
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4M12 16h.01" />
</svg>
<span className="text-sm">{t("modelDelegation.noData")}</span>
</div>
);
}
return (
<div ref={containerRef} className="w-full overflow-x-auto relative" onMouseLeave={hideTip}>
<svg
ref={svgRef}
className="w-full"
style={{ minHeight: 120 }}
aria-label={t("modelDelegation.ariaLabel")}
role="img"
/>
<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 helpers ───────────────────────────────────────────────────────────
function countTotalAgents(data: ModelDelegationData): number {
const mainSum = data.mainModels.reduce((s, m) => s + m.agent_count, 0);
const subSum = data.subagentModels.reduce((s, m) => s + m.agent_count, 0);
return mainSum + subSum;
}
type TFn = (key: string, options?: Record<string, unknown>) => string;
function describeFamily(family: NodeDatum["family"], t: TFn): string {
switch (family) {
case "opus":
return t("modelDelegation.tooltip.family.opus");
case "sonnet":
return t("modelDelegation.tooltip.family.sonnet");
case "haiku":
return t("modelDelegation.tooltip.family.haiku");
case "other":
return t("modelDelegation.tooltip.family.other");
}
}
function buildModelDelegationTooltip(
el: HTMLDivElement,
node: NodeDatum,
totalAgents: number,
t: TFn
) {
while (el.firstChild) el.removeChild(el.firstChild);
const sharePct = totalAgents > 0 ? `${((node.agentCount / totalAgents) * 100).toFixed(1)}%` : "-";
const sideLabel =
node.side === "main"
? t("modelDelegation.tooltip.mainModel")
: t("modelDelegation.tooltip.subagentModel");
const title = document.createElement("p");
title.style.cssText = "font-size:12px;font-weight:600;color:#e2e8f0;margin:0";
title.textContent = node.label;
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 = `${sideLabel} · ${node.family}`;
el.appendChild(subtitle);
const addRow = (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);
el.appendChild(row);
};
addRow(t("modelDelegation.tooltip.agentRuns"), node.agentCount.toLocaleString());
addRow(t("modelDelegation.tooltip.shareOfAll"), sharePct);
if (node.side === "main") {
addRow(t("modelDelegation.tooltip.sessionsOnModel"), String(node.sessionCount));
}
addRow(t("modelDelegation.tooltip.totalTokens"), node.totalTokens.toLocaleString());
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 = describeFamily(node.family, t);
el.appendChild(desc);
const hint = document.createElement("p");
hint.style.cssText = "font-size:11px;color:#64748b;line-height:1.45;margin:6px 0 0";
hint.textContent =
node.side === "main"
? t("modelDelegation.tooltip.lines.main")
: t("modelDelegation.tooltip.lines.sub");
el.appendChild(hint);
}
@@ -1,394 +0,0 @@
/**
* @file WorkflowPatterns.tsx
* @description Defines the WorkflowPatterns React component that visualizes common workflow patterns detected from session data. It displays a ranked list of patterns based on their frequency, showing the sequence of agent steps in each pattern along with an icon representing the type of workflow. The component also handles cases where no patterns are detected and includes a special item for solo sessions without subagents. Users can click on a pattern to trigger a callback with the pattern's steps for further analysis or filtering.
* @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
* - `WorkflowPatterns` — 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.
*
* ## 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).
* -----------------------------------------------------------------------------
* **WorkflowPatterns**
* 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 { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronRight, Zap, Code2, Shield, Bug, FileText, Lightbulb, Info } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { WorkflowPattern, WorkflowPatternsData } from "../../lib/types";
type TFn = (key: string, options?: Record<string, unknown>) => string;
// ── Constants ─────────────────────────────────────────────────────────────────
const MAX_VISIBLE_STEPS = 4;
// ── Helpers ───────────────────────────────────────────────────────────────────
function patternIcon(steps: string[]): LucideIcon {
const joined = steps.join(" ").toLowerCase();
if (joined.includes("debug")) return Bug;
if (joined.includes("security") || joined.includes("audit")) return Shield;
if (joined.includes("code-review") || joined.includes("review")) return Code2;
if (joined.includes("doc") || joined.includes("text")) return FileText;
return Zap;
}
/**
* Find the first agent that appears more than once in the sequence (loop indicator).
* Returns null if every step is unique.
*/
function findRepeatedStep(steps: string[]): string | null {
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
if (s !== undefined && steps.indexOf(s) !== i) return s;
}
return null;
}
/**
* Build a deterministic, value-dependent narrative for a workflow pattern.
* Pure rule-based mapping - same input always yields the same output, so the
* UI never produces hallucinated descriptions for ambiguous patterns.
*/
function describePattern(pattern: WorkflowPattern, t: TFn): string {
const { steps, percentage } = pattern;
if (steps.length === 0) return "";
const first = steps[0] ?? "";
const last = steps[steps.length - 1] ?? first;
const repeated = findRepeatedStep(steps);
// Shape of the chain
let core: string;
if (steps.length === 1) {
core = t("patterns.detail.narrative.soloFmt", { first });
} else if (steps.length === 2) {
core = t("patterns.detail.narrative.twoStepFmt", { first, last });
} else if (steps.length <= 5) {
core = t("patterns.detail.narrative.shortFmt", { count: steps.length, first, last });
} else {
core = t("patterns.detail.narrative.longFmt", { count: steps.length, first, last });
}
if (repeated) {
core += t("patterns.detail.narrative.loopHintFmt", { agent: repeated });
}
// Frequency bucket
let freq: string;
if (percentage > 50) freq = t("patterns.detail.narrative.dominant");
else if (percentage > 25) freq = t("patterns.detail.narrative.common");
else if (percentage > 10) freq = t("patterns.detail.narrative.regular");
else freq = t("patterns.detail.narrative.niche");
return core + freq;
}
/**
* Pick a suggestion bucket based on chain length and whether a loop exists.
* Loop wins over length so the user is reminded to confirm intentional loops.
*/
function suggestionForPattern(pattern: WorkflowPattern, t: TFn): string {
const { steps } = pattern;
if (findRepeatedStep(steps)) return t("patterns.detail.suggestion.loop");
if (steps.length <= 1) return t("patterns.detail.suggestion.solo");
if (steps.length <= 3) return t("patterns.detail.suggestion.shortChain");
if (steps.length <= 6) return t("patterns.detail.suggestion.mediumChain");
return t("patterns.detail.suggestion.longChain");
}
// ── Sub-components ────────────────────────────────────────────────────────────
function StepPill({ label }: { label: string }) {
return (
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium bg-indigo-500/15 text-indigo-300 border border-indigo-500/20 whitespace-nowrap">
{label}
</span>
);
}
function StepFlow({ steps }: { steps: string[] }) {
const { t } = useTranslation("workflows");
const visible = steps.slice(0, MAX_VISIBLE_STEPS);
const overflow = steps.length - MAX_VISIBLE_STEPS;
return (
<div className="flex items-center flex-wrap gap-1 min-w-0">
{visible.map((step, idx) => (
<span key={idx} className="flex items-center gap-1">
<StepPill label={step} />
{(idx < visible.length - 1 || overflow > 0) && (
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-fg-muted" />
)}
</span>
))}
{overflow > 0 && (
<span className="inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium bg-surface-3/50 text-fg-secondary border border-border-light/20 whitespace-nowrap">
{t("common:plusMore", { count: overflow })}
</span>
)}
</div>
);
}
function PatternFrequency({ count, percentage }: { count: number; percentage: number }) {
const { t } = useTranslation("workflows");
return (
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-fg-primary">{count.toLocaleString()}</p>
<p className="text-xs text-fg-muted">
{percentage.toFixed(1)}% {t("common:ofSessions", { defaultValue: "of sessions" })}
</p>
</div>
);
}
interface PatternItemProps {
pattern: WorkflowPattern;
rank: number;
isSelected: boolean;
onClick: () => void;
}
function PatternItem({ pattern, rank, isSelected, onClick }: PatternItemProps) {
const { t } = useTranslation("workflows");
const Icon = patternIcon(pattern.steps);
return (
<div
className={[
"rounded-lg border transition-colors duration-150 overflow-hidden",
isSelected
? "bg-indigo-500/10 border-indigo-500/30"
: "bg-surface-2 border-transparent hover:bg-white/5 hover:border-white/10",
].join(" ")}
>
<button
type="button"
onClick={onClick}
aria-expanded={isSelected}
title={t("patterns.detail.clickHint")}
className="w-full flex items-center gap-3 px-4 py-3 text-left"
>
{/* Rank / icon */}
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center">
{rank <= 3 ? (
<span className="text-xs font-bold text-indigo-400">{rank}</span>
) : (
<Icon className="w-3.5 h-3.5 text-indigo-400" />
)}
</div>
{/* Step flow */}
<div className="flex-1 min-w-0 overflow-hidden">
<StepFlow steps={pattern.steps} />
</div>
{/* Frequency */}
<PatternFrequency count={pattern.count} percentage={pattern.percentage} />
{/* Click affordance - visible only when not yet expanded so users know the row is interactive. */}
{!isSelected && (
<Info className="hidden sm:block w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
)}
</button>
{isSelected && <PatternDetail pattern={pattern} />}
</div>
);
}
function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
const { t } = useTranslation("workflows");
const uniqueAgents = new Set(pattern.steps).size;
const narrative = describePattern(pattern, t);
const suggestion = suggestionForPattern(pattern, t);
return (
<div className="border-t border-indigo-500/20 bg-surface-1/40 px-4 py-3.5 space-y-3.5">
{/* Full step sequence (no truncation) */}
<div>
<p className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider mb-2">
{t("patterns.detail.stepsHeading")}
</p>
<div className="flex items-center flex-wrap gap-1.5">
{pattern.steps.map((step, i) => (
<span key={i} className="flex items-center gap-1.5">
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium bg-indigo-500/15 text-indigo-200 border border-indigo-500/25">
<span className="text-indigo-400/70 mr-1.5 text-[10px] font-bold">{i + 1}</span>
{step}
</span>
{i < pattern.steps.length - 1 && (
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-fg-muted" />
)}
</span>
))}
</div>
</div>
{/* Stats grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<DetailStat
label={t("patterns.detail.stepsCount", { count: pattern.steps.length })}
value={String(pattern.steps.length)}
/>
<DetailStat label={t("patterns.detail.uniqueAgents")} value={String(uniqueAgents)} />
<DetailStat
label={t("patterns.detail.occurrences")}
value={pattern.count.toLocaleString()}
/>
<DetailStat
label={t("patterns.detail.shareOfSessions")}
value={`${pattern.percentage.toFixed(1)}%`}
/>
</div>
{/* Narrative - what this means */}
<div>
<p className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
<Info className="w-3 h-3 text-indigo-400" />
{t("patterns.detail.narrativeHeading")}
</p>
<p className="text-xs text-fg-secondary leading-relaxed">{narrative}</p>
</div>
{/* Suggestion */}
<div className="bg-indigo-500/5 border border-indigo-500/15 rounded-md px-3 py-2.5">
<p className="text-[10px] font-semibold text-indigo-300 uppercase tracking-wider mb-1 flex items-center gap-1.5">
<Lightbulb className="w-3 h-3" />
{t("patterns.detail.suggestionHeading")}
</p>
<p className="text-xs text-fg-secondary leading-relaxed">{suggestion}</p>
</div>
</div>
);
}
function DetailStat({ label, value }: { label: string; value: string }) {
return (
<div className="bg-surface-2 border border-border rounded-md px-2.5 py-2">
<p className="text-sm font-semibold text-fg-primary tabular-nums">{value}</p>
<p className="text-[10px] text-fg-muted uppercase tracking-wider mt-0.5 truncate">{label}</p>
</div>
);
}
function SoloSessionItem({ count, percentage }: { count: number; percentage: number }) {
const { t } = useTranslation("workflows");
return (
<div className="flex items-center gap-3 px-4 py-3 rounded-lg border bg-yellow-500/5 border-yellow-500/20">
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-yellow-500/10 border border-yellow-500/20 flex items-center justify-center">
<Zap className="w-3.5 h-3.5 text-yellow-400" />
</div>
<div className="flex-1 min-w-0">
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium bg-yellow-500/15 text-yellow-300 border border-yellow-500/20">
{t("patterns.solo")}
</span>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-fg-primary">{count.toLocaleString()}</p>
<p className="text-xs text-fg-muted">
{percentage.toFixed(1)}% {t("common:ofSessions", { defaultValue: "of sessions" })}
</p>
</div>
</div>
);
}
function EmptyPatterns() {
const { t } = useTranslation("workflows");
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3">
<Zap className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-sm font-medium text-fg-secondary">{t("patterns.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("patterns.noDataDesc")}</p>
</div>
);
}
// ── Public component ──────────────────────────────────────────────────────────
interface WorkflowPatternsProps {
data: WorkflowPatternsData;
onPatternClick?: (steps: string[]) => void;
}
export function WorkflowPatterns({ data, onPatternClick }: WorkflowPatternsProps) {
const { t } = useTranslation("workflows");
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const handlePatternClick = (index: number, steps: string[]) => {
const next = selectedIndex === index ? null : index;
setSelectedIndex(next);
if (next !== null) {
onPatternClick?.(steps);
}
};
const hasContent = data.patterns.length > 0 || data.soloSessionCount > 0;
return (
<div className="card p-5">
<h2 className="text-sm font-semibold text-fg-secondary uppercase tracking-wider mb-4">
{t("patterns.label")}
</h2>
{!hasContent ? (
<EmptyPatterns />
) : (
<div className="flex flex-col gap-2">
{data.patterns.map((pattern, idx) => (
<PatternItem
key={idx}
pattern={pattern}
rank={idx + 1}
isSelected={selectedIndex === idx}
onClick={() => handlePatternClick(idx, pattern.steps)}
/>
))}
{data.soloSessionCount > 0 && (
<SoloSessionItem count={data.soloSessionCount} percentage={data.soloPercentage} />
)}
</div>
)}
</div>
);
}
-79
View File
@@ -68,16 +68,6 @@
"howToRead": "One row per node. The bar is scaled against the busiest node, split into green (passed with evidence), amber (passed but only claimed) and red (failed). The number on the right is passes / entries and the pass rate.",
"why": "A node with a long amber bar is a stage everyone declares and nobody proves; a node that is never reached is a stage your pipeline does not really use."
},
"patterns": {
"what": "A ranked list of recurring agent-orchestration sequences detected from session traces, plus a callout for solo sessions that ran without subagents.",
"howToRead": "Each row is a step sequence (main → sub → sub …). The right side shows how many sessions matched it and what percentage of all sessions that represents. Click a pattern to highlight it.",
"why": "Reveals the standard 'shapes' of work in your project. Frequent patterns are good candidates to formalize as named workflows or skills."
},
"modelDelegation": {
"what": "A two-column flow showing which main models delegate to which subagent models, plus per-node agent counts and total tokens.",
"howToRead": "Left column = main models for sessions; right column = models used inside subagents. Lines indicate observed main → subagent pairings. Hover a node for share-of-all-agents, sessions, total tokens, and a description of the model family (Opus / Sonnet / Haiku).",
"why": "Helps you confirm cost/latency tiering - e.g. main on Sonnet, subagents on Haiku - and catch unintended escalations to Opus."
},
"errorPropagation": {
"what": "Where errors cluster across the agent hierarchy: by depth (session/main vs direct subagent vs nested), by agent type, and by API/session-level error category.",
"howToRead": "The top banner gives the session-level error rate. The depth bars show how many errors occurred at each nesting level. The error-prone types list ranks subagent types by error count. The bottom block surfaces API/session error summaries.",
@@ -258,75 +248,6 @@
"empty": "No lane has recorded a stage yet. Create one with",
"emptyLane": "This lane has not recorded a stage yet."
},
"patterns": {
"title": "Detected Workflow Patterns",
"subtitle": "Common agent orchestration sequences",
"noData": "No patterns detected yet",
"noDataDesc": "Patterns appear once multiple sessions share the same agent sequence.",
"solo": "solo (no subagents)",
"label": "Workflow Patterns",
"detail": {
"clickHint": "Click for details",
"stepsHeading": "Full step sequence",
"statsHeading": "Stats",
"stepsCount_one": "{{count}} step",
"stepsCount_other": "{{count}} steps",
"uniqueAgents": "Unique agent types",
"occurrences": "Sessions matching this pattern",
"shareOfSessions": "Share of all sessions",
"narrativeHeading": "What this means",
"suggestionHeading": "Suggestion",
"narrative": {
"soloFmt": "Sessions that ran with only {{first}} - no delegation to other agent types.",
"twoStepFmt": "A two-step hand-off: {{first}} delegates straight to {{last}}, with no further branching.",
"shortFmt": "A {{count}}-step chain that starts at {{first}} and ends at {{last}}.",
"longFmt": "A long {{count}}-step chain - begins at {{first}}, threads through several agents, and lands at {{last}}.",
"loopHintFmt": " {{agent}} appears more than once in this sequence - typical of a verification or revision loop.",
"dominant": " This is the dominant workflow in your project - more than half of sessions follow it.",
"common": " This is a common workflow, accounting for roughly a quarter of sessions.",
"regular": " A recognised workflow, but not the majority pattern.",
"niche": " A niche pattern that only surfaces occasionally."
},
"suggestion": {
"solo": "Solo run - would a code-reviewer or test-runner subagent raise the quality of these sessions?",
"shortChain": "Short chain. If it keeps recurring, name it as a slash command, skill, or named workflow so it's easy to reproduce.",
"mediumChain": "Frequent enough to formalise. A named workflow or template would make this pattern reusable.",
"longChain": "Long chain - review whether every hop is necessary, or whether some agents could be merged or skipped.",
"loop": "Loop detected. Confirm the repeated step is intentional verification rather than a silent retry of a failing call."
}
}
},
"modelDelegation": {
"title": "Model Delegation Flow",
"subtitle": "How models route through agent hierarchies",
"mainModels": "MAIN MODELS",
"subagentModels": "SUBAGENT MODELS",
"noData": "No model delegation data available",
"agentsCount_one": "{{count}} agent",
"agentsCount_other": "{{count}} agents",
"tokensCount": "{{tokens}} tokens",
"sessionsCount_one": "{{count}} session",
"sessionsCount_other": "{{count}} sessions",
"ariaLabel": "Model delegation flow diagram",
"tooltip": {
"mainModel": "Main model",
"subagentModel": "Subagent model",
"agentRuns": "Agent runs",
"shareOfAll": "Share of all agents",
"sessionsOnModel": "Sessions on this model",
"totalTokens": "Total tokens",
"family": {
"opus": "Opus - highest-capability tier; best for complex reasoning, longest context, costliest per token.",
"sonnet": "Sonnet - balanced tier; good default for most coding and tool-use workflows.",
"haiku": "Haiku - fastest, cheapest tier; ideal for short, latency-sensitive subagents.",
"other": "Non-Anthropic or unrecognized model - falls outside the standard Opus/Sonnet/Haiku families."
},
"lines": {
"main": "Lines on the right show subagent models this main model delegated work to.",
"sub": "Lines on the left show main models that delegated work to this subagent model."
}
}
},
"errorPropagation": {
"title": "Error Propagation Map",
"subtitle": "Where errors cluster in agent hierarchy depth",
-79
View File
@@ -68,16 +68,6 @@
"howToRead": "Mỗi node một dòng. Thanh bar so tỉ lệ với node bận nhất, chia ba màu: xanh lá (pass có bằng chứng), vàng (pass nhưng chỉ khai báo), đỏ (thất bại). Số bên phải là pass / số lần vào và tỉ lệ pass.",
"why": "Node có dải vàng dài là giai đoạn ai cũng khai báo nhưng không ai chứng minh; node chưa bao giờ tới là giai đoạn pipeline của bạn thực tế không dùng."
},
"patterns": {
"what": "Danh sách xếp hạng các chuỗi điều phối Agent lặp lại được phát hiện từ trace phiên, kèm thẻ riêng cho phiên đơn lẻ không có Subagent.",
"howToRead": "Mỗi dòng là một chuỗi bước (main → sub → sub …). Bên phải hiển thị số phiên khớp và tỷ lệ trên tổng phiên. Bấm vào một mẫu để làm nổi bật.",
"why": "Cho thấy các 'hình dáng' công việc tiêu chuẩn trong dự án. Mẫu phổ biến là ứng viên tốt để chuẩn hóa thành workflow hoặc skill có tên gọi."
},
"modelDelegation": {
"what": "Sơ đồ luồng hai cột cho thấy mô hình chính nào ủy quyền cho mô hình Subagent nào, kèm số Agent và tổng token theo node.",
"howToRead": "Cột trái = mô hình chính của phiên; cột phải = mô hình dùng bên trong Subagent. Đường nối chỉ các cặp main → Subagent quan sát được. Hover một node để xem tỷ trọng trên tổng Agent, số phiên, tổng token và mô tả họ mô hình (Opus / Sonnet / Haiku).",
"why": "Giúp xác nhận phân tầng chi phí/độ trễ - ví dụ main trên Sonnet, Subagent trên Haiku - và phát hiện các trường hợp bị đẩy lên Opus ngoài ý muốn."
},
"errorPropagation": {
"what": "Lỗi tập trung ở đâu trong phân cấp Agent: theo độ sâu (phiên/main vs Subagent trực tiếp vs lồng nhau), theo loại Agent, và theo nhóm lỗi API / phiên.",
"howToRead": "Banner trên đầu cho biết tỷ lệ lỗi cấp phiên. Các thanh độ sâu cho biết bao nhiêu lỗi xảy ra ở từng cấp lồng. Danh sách 'loại dễ lỗi' xếp Subagent theo số lỗi. Khối dưới hiển thị tóm tắt lỗi API/phiên.",
@@ -258,75 +248,6 @@
"empty": "Chưa lane nào ghi nhận giai đoạn. Tạo lane bằng",
"emptyLane": "Lane này chưa ghi nhận giai đoạn nào."
},
"patterns": {
"title": "Mẫu quy trình phát hiện được",
"subtitle": "Chuỗi điều phối Agent thường gặp",
"noData": "Chưa phát hiện mẫu nào",
"noDataDesc": "Mẫu sẽ xuất hiện khi nhiều phiên có cùng chuỗi Agent.",
"solo": "đơn lẻ (không có Subagent)",
"label": "Mẫu quy trình",
"detail": {
"clickHint": "Bấm để xem chi tiết",
"stepsHeading": "Chuỗi bước đầy đủ",
"statsHeading": "Thống kê",
"stepsCount_one": "{{count}} bước",
"stepsCount_other": "{{count}} bước",
"uniqueAgents": "Số loại Agent khác nhau",
"occurrences": "Số phiên khớp mẫu này",
"shareOfSessions": "Tỷ trọng trên tổng phiên",
"narrativeHeading": "Ý nghĩa của mẫu này",
"suggestionHeading": "Gợi ý",
"narrative": {
"soloFmt": "Phiên chỉ chạy {{first}} - không ủy quyền cho loại Agent nào khác.",
"twoStepFmt": "Hand-off hai bước: {{first}} ủy quyền thẳng cho {{last}}, không có nhánh nào khác.",
"shortFmt": "Chuỗi {{count}} bước, bắt đầu ở {{first}} và kết thúc ở {{last}}.",
"longFmt": "Chuỗi dài {{count}} bước - bắt đầu ở {{first}}, đi qua nhiều Agent, rồi dừng ở {{last}}.",
"loopHintFmt": " {{agent}} xuất hiện nhiều lần trong chuỗi này - điển hình cho vòng lặp kiểm tra hoặc sửa lại.",
"dominant": " Đây là quy trình chủ đạo trong dự án - hơn một nửa số phiên đi theo.",
"common": " Là quy trình phổ biến, chiếm khoảng một phần tư số phiên.",
"regular": " Là quy trình được nhận diện, nhưng chưa phải đa số.",
"niche": " Là mẫu hiếm, chỉ thi thoảng mới xuất hiện."
},
"suggestion": {
"solo": "Phiên đơn lẻ - liệu thêm code-reviewer hay test-runner Subagent có cải thiện chất lượng không?",
"shortChain": "Chuỗi ngắn. Nếu lặp lại nhiều, hãy đặt tên thành slash command, skill hoặc workflow để dễ tái sử dụng.",
"mediumChain": "Đủ phổ biến để chuẩn hóa. Một workflow/template có tên sẽ giúp tái sử dụng dễ dàng.",
"longChain": "Chuỗi dài - xem xét liệu mọi bước có thực sự cần thiết, hay có thể gộp/bỏ bớt một số Agent.",
"loop": "Phát hiện vòng lặp. Hãy xác nhận bước lặp là kiểm tra có chủ đích, không phải retry âm thầm một lệnh đang lỗi."
}
}
},
"modelDelegation": {
"title": "Luồng ủy quyền mô hình",
"subtitle": "Cách các mô hình đi qua phân cấp Agent",
"mainModels": "MÔ HÌNH CHÍNH",
"subagentModels": "MÔ HÌNH TÁC TỬ PHỤ",
"noData": "Chưa có dữ liệu ủy quyền mô hình",
"agentsCount_one": "{{count}} Agent",
"agentsCount_other": "{{count}} Agent",
"tokensCount": "{{tokens}} token",
"sessionsCount_one": "{{count}} phiên",
"sessionsCount_other": "{{count}} phiên",
"ariaLabel": "Sơ đồ luồng ủy quyền mô hình",
"tooltip": {
"mainModel": "Mô hình chính",
"subagentModel": "Mô hình Subagent",
"agentRuns": "Lượt chạy Agent",
"shareOfAll": "Tỷ trọng trên tổng Agent",
"sessionsOnModel": "Phiên dùng mô hình này",
"totalTokens": "Tổng token",
"family": {
"opus": "Opus - bậc khả năng cao nhất; tốt cho lý luận phức tạp, ngữ cảnh dài nhất, đắt nhất theo token.",
"sonnet": "Sonnet - bậc cân bằng; mặc định tốt cho hầu hết quy trình code và dùng tool.",
"haiku": "Haiku - bậc nhanh nhất, rẻ nhất; phù hợp cho Subagent ngắn, nhạy cảm độ trễ.",
"other": "Mô hình ngoài Anthropic hoặc không nhận dạng được - không thuộc bộ Opus/Sonnet/Haiku tiêu chuẩn."
},
"lines": {
"main": "Các đường bên phải cho thấy mô hình Subagent mà mô hình chính này đã ủy quyền.",
"sub": "Các đường bên trái cho thấy mô hình chính nào đã ủy quyền cho mô hình Subagent này."
}
}
},
"errorPropagation": {
"title": "Bản đồ lan truyền lỗi",
"subtitle": "Điểm tập trung lỗi theo độ sâu phân cấp Agent",
+13 -37
View File
@@ -3,10 +3,11 @@
* @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
* the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the
* spawning DAG, patterns, model delegation, error propagation, session complexity,
* compaction impact and the per-session drill-in. Issue #6 dropped the agent
* collaboration network and the subagent effectiveness scorecards as too
* specialized to act on; the tool-execution Sankey followed for the same reason.
* spawning DAG, error propagation, session complexity, compaction impact and the
* per-session drill-in. Issue #6 dropped the agent collaboration network and the
* subagent effectiveness scorecards as too specialized to act on; the
* tool-execution Sankey, the detected-patterns list and the model delegation flow
* followed for the same reason.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
@@ -35,8 +36,6 @@
* - `../components/workflows/SkillUsagePanel`
* - `../components/workflows/WorkflowRunsPanel`
* - `../components/workflows/OrchestrationDAG`
* - `../components/workflows/WorkflowPatterns`
* - `../components/workflows/ModelDelegationFlow`
* - `../components/workflows/ErrorPropagationMap`
* - `../components/workflows/SessionComplexityScatter`
* - `../components/workflows/CompactionImpact`
@@ -80,8 +79,6 @@ import type { WorkflowData, WSMessage } from "../lib/types";
import { WorkflowStats } from "../components/workflows/WorkflowStats";
import { OrchestrationDAG } from "../components/workflows/OrchestrationDAG";
import { WorkflowPatterns } from "../components/workflows/WorkflowPatterns";
import { ModelDelegationFlow } from "../components/workflows/ModelDelegationFlow";
import { ErrorPropagationMap } from "../components/workflows/ErrorPropagationMap";
import { SessionComplexityScatter } from "../components/workflows/SessionComplexityScatter";
import { CompactionImpact } from "../components/workflows/CompactionImpact";
@@ -264,41 +261,20 @@ export function Workflows() {
)}
</Section>
{/* Section 4: Workflow Patterns */}
{/* Section 4: Error Propagation */}
<Section
number={4}
title={t("patterns.title")}
subtitle={t("patterns.subtitle")}
infoKey="patterns"
title={t("errorPropagation.title")}
subtitle={t("errorPropagation.subtitle")}
infoKey="errorPropagation"
>
<WorkflowPatterns data={data.patterns} onPatternClick={() => {}} />
<ErrorPropagationMap data={data.errorPropagation} />
</Section>
{/* Section 5 + 6: Two Column */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section
number={5}
title={t("modelDelegation.title")}
subtitle={t("modelDelegation.subtitle")}
infoKey="modelDelegation"
>
<ModelDelegationFlow data={data.modelDelegation} />
</Section>
<Section
number={6}
title={t("errorPropagation.title")}
subtitle={t("errorPropagation.subtitle")}
infoKey="errorPropagation"
>
<ErrorPropagationMap data={data.errorPropagation} />
</Section>
</div>
{/* Section 7 + 8: Two Column */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section
number={7}
title={t("complexity.title")}
subtitle={t("complexity.subtitle")}
infoKey="complexity"
@@ -307,7 +283,7 @@ export function Workflows() {
</Section>
<Section
number={8}
number={6}
title={t("compaction.title")}
subtitle={t("compaction.subtitle")}
infoKey="compaction"
@@ -316,9 +292,9 @@ export function Workflows() {
</Section>
</div>
{/* Section 9: Session Drill-In */}
{/* Section 7: Session Drill-In */}
<Section
number={9}
number={7}
title={t("drillIn.title")}
subtitle={t("drillIn.subtitle")}
infoKey="drillIn"
@@ -10808,7 +10808,7 @@ exports[`screen snapshots > Workflows 1`] = `
<h2
class="text-sm font-semibold text-fg-primary"
>
Detected Workflow Patterns
Error Propagation Map
</h2>
<button
aria-expanded="false"
@@ -10844,56 +10844,48 @@ exports[`screen snapshots > Workflows 1`] = `
</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="Common agent orchestration sequences"
title="Where errors cluster in agent hierarchy depth"
>
Common agent orchestration sequences
Where errors cluster in agent hierarchy depth
</span>
</div>
<div
class="card p-4 flex-1"
>
<div
class="card p-5"
class="flex flex-col items-center justify-center py-16 gap-3"
>
<h2
class="text-sm font-semibold text-fg-secondary uppercase tracking-wider mb-4"
>
Workflow Patterns
</h2>
<div
class="flex flex-col items-center justify-center py-12 text-center"
class="w-12 h-12 rounded-full bg-status-success/10 border border-status-success/20 flex items-center justify-center"
>
<div
class="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3"
<svg
fill="none"
height="24"
stroke="#10b981"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
>
<svg
class="lucide lucide-zap w-5 h-5 text-fg-muted"
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"
>
<path
d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"
/>
</svg>
</div>
<p
class="text-sm font-medium text-fg-secondary"
>
No patterns detected yet
</p>
<p
class="text-xs text-fg-muted mt-1"
>
Patterns appear once multiple sessions share the same agent sequence.
</p>
<path
d="M22 11.08V12a10 10 0 1 1-5.93-9.14"
/>
<polyline
points="22 4 12 14.01 9 11.01"
/>
</svg>
</div>
<span
class="text-sm text-status-success font-medium"
>
No errors recorded
</span>
<span
class="text-xs text-fg-muted"
>
All sessions completed successfully
</span>
</div>
</div>
</div>
@@ -10914,198 +10906,6 @@ exports[`screen snapshots > Workflows 1`] = `
>
5
</span>
<h2
class="text-sm font-semibold text-fg-primary"
>
Model Delegation 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 models route through agent hierarchies"
>
How models route through agent hierarchies
</span>
</div>
<div
class="card p-4 flex-1"
>
<div
class="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted"
>
<svg
aria-hidden="true"
fill="none"
height="40"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
width="40"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M12 8v4M12 16h.01"
/>
</svg>
<span
class="text-sm"
>
No model delegation data available
</span>
</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"
>
6
</span>
<h2
class="text-sm font-semibold text-fg-primary"
>
Error Propagation Map
</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="Where errors cluster in agent hierarchy depth"
>
Where errors cluster in agent hierarchy depth
</span>
</div>
<div
class="card p-4 flex-1"
>
<div
class="flex flex-col items-center justify-center py-16 gap-3"
>
<div
class="w-12 h-12 rounded-full bg-status-success/10 border border-status-success/20 flex items-center justify-center"
>
<svg
fill="none"
height="24"
stroke="#10b981"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
>
<path
d="M22 11.08V12a10 10 0 1 1-5.93-9.14"
/>
<polyline
points="22 4 12 14.01 9 11.01"
/>
</svg>
</div>
<span
class="text-sm text-status-success font-medium"
>
No errors recorded
</span>
<span
class="text-xs text-fg-muted"
>
All sessions completed successfully
</span>
</div>
</div>
</div>
</div>
<div
class="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<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"
>
7
</span>
<h2
class="text-sm font-semibold text-fg-primary"
>
@@ -11208,7 +11008,7 @@ exports[`screen snapshots > Workflows 1`] = `
<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"
>
8
6
</span>
<h2
class="text-sm font-semibold text-fg-primary"
@@ -11303,7 +11103,7 @@ exports[`screen snapshots > Workflows 1`] = `
<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"
>
9
7
</span>
<h2
class="text-sm font-semibold text-fg-primary"