feat(theme): dark/light mode with a Radix Colors-based palette

Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.

- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
  tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
  fg.primary/secondary/muted, status.success/danger/warning. One class flip
  on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
  prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
  scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
  every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
  client/src onto the new tokens, so every badge/button/component pulls the
  same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
  constants — slate/blue/green/red/amber steps 1-12 — adopted after three
  rounds of hand-picked values that kept overshooting (flat, then too dark,
  then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
  dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
  visual language (border + text + translucent wash of the same status
  color); `current` alone stays a solid accent fill, the one state that
  gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
  Workspace lane-detail header already showing them.

Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
2026-07-31 10:54:31 +07:00
parent 4905d63b97
commit b673363351
82 changed files with 3776 additions and 2578 deletions
@@ -581,8 +581,8 @@ export function AgentCollaborationNetwork({
if (isEmpty) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<p className="text-sm font-medium text-gray-400">{t("pipeline.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("pipeline.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("pipeline.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("pipeline.noDataDesc")}</p>
</div>
);
}
@@ -615,7 +615,7 @@ export function AgentCollaborationNetwork({
}}
/>
<div className="flex flex-wrap items-center gap-3 mt-3 px-1">
<span className="text-[10px] text-gray-600 uppercase tracking-widest font-medium">
<span className="text-[10px] text-fg-muted uppercase tracking-widest font-medium">
{t("pipeline.legend")}
</span>
{nodes.map((n) => (
@@ -627,7 +627,7 @@ export function AgentCollaborationNetwork({
border: `1.5px solid ${STROKE_PALETTE[n.colorIndex] ?? STROKE_PALETTE[0]}`,
}}
/>
<span className="text-[11px] text-gray-500">{n.id}</span>
<span className="text-[11px] text-fg-muted">{n.id}</span>
</div>
))}
<div className="flex items-center gap-1.5 ml-2">
@@ -635,7 +635,7 @@ export function AgentCollaborationNetwork({
<line x1="0" y1="4" x2="14" y2="4" stroke="#64748b" strokeWidth="1.5" />
<polygon points="14,1 20,4 14,7" fill="#64748b" />
</svg>
<span className="text-[11px] text-gray-500">{t("pipeline.legendDesc")}</span>
<span className="text-[11px] text-fg-muted">{t("pipeline.legendDesc")}</span>
</div>
</div>
</div>
@@ -284,10 +284,10 @@ function StatBox({ label, value, sub, accent = "text-accent" }: StatBoxProps) {
return (
<div className="flex flex-col gap-1 bg-surface-3 border border-border rounded-xl px-4 py-3.5 flex-1 min-w-0">
<span className={`text-2xl font-semibold tabular-nums ${accent}`}>{value}</span>
<span className="text-[11px] font-medium text-gray-500 uppercase tracking-wider leading-tight">
<span className="text-[11px] font-medium text-fg-muted uppercase tracking-wider leading-tight">
{label}
</span>
{sub && <span className="text-[11px] text-gray-600 tabular-nums">{sub}</span>}
{sub && <span className="text-[11px] text-fg-muted tabular-nums">{sub}</span>}
</div>
);
}
@@ -334,7 +334,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-gray-500">
<div className="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted">
<svg
width="40"
height="40"
@@ -357,7 +357,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
return (
<div className="flex flex-col gap-5">
{/* What compaction is - one line so the numbers below make sense */}
<p className="text-xs text-gray-500 leading-relaxed">{t("compaction.help")}</p>
<p className="text-xs text-fg-muted leading-relaxed">{t("compaction.help")}</p>
{/* Stat tiles */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
@@ -375,18 +375,18 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
<StatBox
label={t("compaction.avgPerSession")}
value={avgPerSession.toFixed(1)}
accent="text-blue-300"
accent="text-blue-400"
/>
<StatBox
label={t("compaction.peakSession")}
value={peak.toLocaleString()}
accent="text-emerald-400"
accent="text-status-success"
/>
</div>
{/* Histogram: sessions by compaction count */}
<div className="w-full overflow-hidden">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-2">
<p className="text-xs font-medium text-fg-muted uppercase tracking-wider mb-2">
{t("compaction.distribution")}
</p>
<svg
@@ -399,7 +399,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
</div>
{/* Plain-English summary + (when present) tokens freed */}
<p className="text-xs text-gray-500 leading-relaxed">
<p className="text-xs text-fg-muted leading-relaxed">
{t("compaction.summary", {
affected: affected.toLocaleString(),
total: data.totalSessions.toLocaleString(),
@@ -420,8 +420,8 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
transform: tip.x > window.innerWidth - 220 ? "translateX(-100%)" : undefined,
}}
>
<div className="font-medium text-gray-100">{tip.title}</div>
<div className="mt-0.5 text-gray-400">{tip.detail}</div>
<div className="font-medium text-fg-primary">{tip.title}</div>
<div className="mt-0.5 text-fg-secondary">{tip.detail}</div>
</div>
)}
</div>
@@ -121,7 +121,7 @@ function LaneRow({ lane, color, maxCount, onShowTip, onHideTip }: LaneRowProps)
<div className="flex items-center gap-3 py-1.5 group">
{/* Label column */}
<div className="flex-shrink-0 w-[140px] text-right" title={displayName}>
<span className="text-xs font-medium text-gray-400 truncate block group-hover:text-gray-200 transition-colors">
<span className="text-xs font-medium text-fg-secondary truncate block group-hover:text-fg-secondary transition-colors">
{displayName}
</span>
</div>
@@ -154,7 +154,7 @@ function LaneRow({ lane, color, maxCount, onShowTip, onHideTip }: LaneRowProps)
</div>
{/* Timing range */}
<div className="flex-shrink-0 w-[72px] text-[11px] text-gray-600 tabular-nums">
<div className="flex-shrink-0 w-[72px] text-[11px] text-fg-muted tabular-nums">
{startPct}%&ndash;{endPct}%
</div>
</div>
@@ -229,7 +229,7 @@ function EmptyState() {
<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">
<svg
className="w-5 h-5 text-gray-600"
className="w-5 h-5 text-fg-muted"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -240,8 +240,8 @@ function EmptyState() {
<rect x="3" y="16" width="15" height="4" rx="1" />
</svg>
</div>
<p className="text-sm font-medium text-gray-400">{t("concurrency.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("concurrency.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("concurrency.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("concurrency.noDataDesc")}</p>
</div>
);
}
@@ -314,15 +314,15 @@ export function ConcurrencyTimeline({ data }: ConcurrencyTimelineProps) {
<div className="flex items-center gap-3 mb-2">
<div className="flex-shrink-0 w-[140px]" />
<div className="flex-1 flex items-center justify-between">
<span className="text-[10px] text-gray-600 uppercase tracking-wider">
<span className="text-[10px] text-fg-muted uppercase tracking-wider">
{t("concurrency.sessions")}
</span>
<span className="text-[10px] text-gray-600 tabular-nums">
<span className="text-[10px] text-fg-muted tabular-nums">
{maxCount}
{t("concurrency.max")}
</span>
</div>
<div className="flex-shrink-0 w-[72px] text-[10px] text-gray-600 uppercase tracking-wider">
<div className="flex-shrink-0 w-[72px] text-[10px] text-fg-muted uppercase tracking-wider">
{t("concurrency.timing")}
</div>
</div>
@@ -92,7 +92,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
if (!hasErrors) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<svg
width="24"
height="24"
@@ -107,10 +107,10 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<span className="text-sm text-emerald-400 font-medium">
<span className="text-sm text-status-success font-medium">
{t("errorPropagation.noErrors")}
</span>
<span className="text-xs text-gray-600">{t("errorPropagation.allSuccess")}</span>
<span className="text-xs text-fg-muted">{t("errorPropagation.allSuccess")}</span>
</div>
);
}
@@ -124,20 +124,20 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
return (
<div className="flex flex-col gap-4">
{/* Error rate summary bar */}
<div className="flex items-center gap-3 p-3 rounded-xl bg-red-500/5 border border-red-500/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-red-400 tabular-nums whitespace-nowrap">
<div className="flex items-center gap-3 p-3 rounded-xl bg-status-danger/5 border border-status-danger/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-status-danger tabular-nums whitespace-nowrap">
{errorRatePct}%
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-red-300">
<p className="text-xs font-medium text-status-danger">
{t("errorPropagation.sessionsErrorSummary", {
errorSessions: data.sessionsWithErrors,
totalSessions: data.totalSessions,
})}
</p>
<p className="text-[11px] text-gray-500 mt-0.5">
<p className="text-[11px] text-fg-muted mt-0.5">
{totalErrors > 0
? `${totalErrors}${t("errorPropagation.agentErrors")}`
: t("errorPropagation.sessionErrorsOnly")}
@@ -148,7 +148,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Errors by depth - horizontal bars */}
{hasDepthData && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorsByDepth")}
</p>
<div className="flex flex-col gap-1.5">
@@ -165,7 +165,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
onMouseEnter={() => setHoveredDepth(d.depth)}
onMouseLeave={() => setHoveredDepth(null)}
>
<span className="text-[11px] text-gray-500 w-24 flex-shrink-0 text-right truncate">
<span className="text-[11px] text-fg-muted w-24 flex-shrink-0 text-right truncate">
{depthLabel(d.depth)}
</span>
<div className="flex-1 h-5 bg-surface-3 rounded overflow-hidden relative">
@@ -194,7 +194,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Error-prone agent types */}
{topTypes.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorProneTypes")}
</p>
<div className="flex flex-col gap-1">
@@ -210,16 +210,16 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: DEPTH_COLORS[Math.min(i, DEPTH_COLORS.length - 1)] }}
/>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0">
<span className="text-xs text-fg-secondary truncate flex-1 min-w-0">
{t.subagent_type}
</span>
<div className="w-16 h-1.5 bg-surface-4 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full bg-red-400/60"
className="h-full rounded-full bg-status-danger/60"
style={{ width: `${Math.max(pct, 8)}%` }}
/>
</div>
<span className="text-[11px] font-semibold text-red-300 tabular-nums w-5 text-right flex-shrink-0">
<span className="text-[11px] font-semibold text-status-danger tabular-nums w-5 text-right flex-shrink-0">
{t.count}
</span>
</div>
@@ -232,14 +232,14 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* API & session errors */}
{data.eventErrors && data.eventErrors.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.apiSessionErrors")}
</p>
<div className="flex flex-col gap-1">
{data.eventErrors.map((e) => (
<div
key={e.summary}
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-amber-500/5 border border-amber-500/10 hover:border-amber-500/20 transition-colors"
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-status-warning/5 border border-status-warning/10 hover:border-status-warning/20 transition-colors"
>
<svg
width="14"
@@ -256,10 +256,13 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0" title={e.summary}>
<span
className="text-xs text-fg-secondary truncate flex-1 min-w-0"
title={e.summary}
>
{e.summary}
</span>
<span className="flex-shrink-0 text-[11px] font-semibold text-amber-400 tabular-nums">
<span className="flex-shrink-0 text-[11px] font-semibold text-status-warning tabular-nums">
{e.count}x
</span>
</div>
@@ -402,7 +402,7 @@ export function ModelDelegationFlow({ data }: ModelDelegationFlowProps) {
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-gray-500">
<div className="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted">
<svg
width="40"
height="40"
@@ -787,7 +787,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
fill="none"
stroke="currentColor"
strokeWidth={1.5}
className="w-6 h-6 text-gray-500"
className="w-6 h-6 text-fg-muted"
>
<circle cx="6" cy="12" r="2" />
<circle cx="18" cy="6" r="2" />
@@ -796,8 +796,10 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
<line x1="8" y1="13" x2="16" y2="17" />
</svg>
</div>
<h3 className="text-base font-medium text-gray-300 mb-2">{t("orchestration.noData")}</h3>
<p className="text-sm text-gray-500 max-w-sm">{t("orchestration.noDataDesc")}</p>
<h3 className="text-base font-medium text-fg-secondary mb-2">
{t("orchestration.noData")}
</h3>
<p className="text-sm text-fg-muted max-w-sm">{t("orchestration.noDataDesc")}</p>
</div>
);
}
@@ -831,7 +833,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
{/* Legend */}
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 px-1 mt-4">
<span className="text-[10px] text-gray-600 uppercase tracking-widest font-medium mr-1">
<span className="text-[10px] text-fg-muted uppercase tracking-widest font-medium mr-1">
{t("orchestration.legend")}
</span>
{LEGEND_ITEMS.map((item) => (
@@ -840,7 +842,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
className="inline-block w-3 h-3 rounded-sm flex-shrink-0"
style={{ background: item.color, border: `1px solid ${item.border}` }}
/>
<span className="text-[11px] text-gray-500">{item.label}</span>
<span className="text-[11px] text-fg-muted">{item.label}</span>
</div>
))}
<div className="flex items-center gap-1.5 ml-2">
@@ -848,7 +850,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
className="inline-block h-[2px] w-8 rounded flex-shrink-0"
style={{ background: "linear-gradient(to right, #312e81, #4f46e5)" }}
/>
<span className="text-[11px] text-gray-500">{t("orchestration.edgeWeight")}</span>
<span className="text-[11px] text-fg-muted">{t("orchestration.edgeWeight")}</span>
</div>
</div>
@@ -882,12 +884,12 @@ function buildDAGTooltipContent(el: HTMLDivElement, node: DAGNode, t: TFn) {
const meta = describeNode(node, t);
const title = document.createElement("p");
title.className = "text-xs font-semibold text-gray-200";
title.className = "text-xs font-semibold text-fg-secondary";
title.textContent = node.label;
el.appendChild(title);
const subtitle = document.createElement("p");
subtitle.className = "text-[10px] uppercase tracking-wider text-gray-600 mt-0.5 mb-2";
subtitle.className = "text-[10px] uppercase tracking-wider text-fg-muted mt-0.5 mb-2";
subtitle.textContent = meta.layer;
el.appendChild(subtitle);
@@ -121,17 +121,17 @@ function Tooltip({ state }: { state: TooltipState }) {
return (
<div
className="fixed z-50 px-3 py-2 text-xs bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl text-gray-200 pointer-events-none whitespace-nowrap"
className="fixed z-50 px-3 py-2 text-xs bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl text-fg-secondary pointer-events-none whitespace-nowrap"
style={{
left: nearRight ? state.x - 12 : state.x + 12,
top: state.y - 10,
transform: nearRight ? "translateX(-100%)" : undefined,
}}
>
<p className="font-semibold text-gray-100 mb-1 truncate max-w-[180px]">
<p className="font-semibold text-fg-primary mb-1 truncate max-w-[180px]">
{state.item.name ?? state.item.id.slice(0, 12)}
</p>
<div className="flex flex-col gap-0.5 text-gray-400">
<div className="flex flex-col gap-0.5 text-fg-secondary">
<span>
{t("complexity.tooltip.duration")} {formatDurationSec(state.item.duration)}
</span>
@@ -173,7 +173,7 @@ function Legend() {
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: statusColor(s) }}
/>
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-muted">
{t(`common:status.${s}`, { defaultValue: s })}
</span>
</div>
@@ -190,7 +190,7 @@ function EmptyState() {
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3">
<svg
className="w-5 h-5 text-gray-600"
className="w-5 h-5 text-fg-muted"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -201,8 +201,8 @@ function EmptyState() {
<circle cx="14" cy="17" r="4" />
</svg>
</div>
<p className="text-sm font-medium text-gray-400">{t("complexity.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("complexity.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("complexity.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("complexity.noDataDesc")}</p>
</div>
);
}
@@ -83,15 +83,15 @@ function statusColor(status: string): string {
case "completed":
return "text-violet-400 bg-violet-500/10 border-violet-500/20";
case "working":
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
return "text-status-success bg-status-success/10 border-status-success/20";
case "error":
return "text-red-400 bg-red-500/10 border-red-500/20";
return "text-status-danger bg-status-danger/10 border-status-danger/20";
case "active":
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
return "text-status-success bg-status-success/10 border-status-success/20";
case "waiting":
return "text-yellow-400 bg-yellow-500/10 border-yellow-500/20";
default:
return "text-gray-400 bg-gray-500/10 border-gray-500/20";
return "text-fg-secondary bg-surface-4/10 border-border-light/20";
}
}
@@ -140,8 +140,8 @@ function TabBar({ active, onChange }: TabBarProps) {
className={[
"flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-150",
active === tab.id
? "bg-surface-5 text-gray-100 shadow-sm"
: "text-gray-500 hover:text-gray-300",
? "bg-surface-5 text-fg-primary shadow-sm"
: "text-fg-muted hover:text-fg-secondary",
].join(" ")}
>
{tab.icon}
@@ -192,20 +192,20 @@ function TreeNode({ node, depth }: TreeNodeProps) {
{/* Name */}
<span
className={`text-sm font-medium truncate ${isMain ? "text-indigo-300" : "text-gray-200"}`}
className={`text-sm font-medium truncate ${isMain ? "text-indigo-300" : "text-fg-secondary"}`}
>
{node.name}
</span>
{/* Subagent type */}
{node.subagent_type && (
<span className="text-xs text-gray-500 truncate flex-shrink-0">
<span className="text-xs text-fg-muted truncate flex-shrink-0">
[{node.subagent_type}]
</span>
)}
{/* Duration */}
<span className="ml-auto flex-shrink-0 text-xs text-gray-600 tabular-nums">{dur}</span>
<span className="ml-auto flex-shrink-0 text-xs text-fg-muted tabular-nums">{dur}</span>
</div>
{node.children.length > 0 && (
@@ -226,7 +226,7 @@ interface AgentTreeProps {
function AgentTree({ tree }: AgentTreeProps) {
const { t } = useTranslation("workflows");
if (tree.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noAgentTree")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noAgentTree")}</p>;
}
return (
@@ -249,7 +249,7 @@ interface ToolTimelineProps {
function ToolTimeline({ events }: ToolTimelineProps) {
const { t } = useTranslation("workflows");
if (events.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noToolEvents")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noToolEvents")}</p>;
}
return (
@@ -267,11 +267,13 @@ function ToolTimeline({ events }: ToolTimelineProps) {
{/* Summary */}
{ev.summary && (
<span className="text-xs text-gray-400 truncate flex-1 min-w-0">{ev.summary}</span>
<span className="text-xs text-fg-secondary truncate flex-1 min-w-0">
{ev.summary}
</span>
)}
{/* Timestamp */}
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums ml-auto">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums ml-auto">
{safeTimestamp(ev.created_at)}
</span>
</div>
@@ -288,22 +290,22 @@ interface EventSequenceProps {
}
const EVENT_TYPE_COLOR: Record<string, string> = {
tool_use: "text-blue-400",
tool_result: "text-emerald-400",
tool_use: "text-blue-500",
tool_result: "text-status-success",
agent_start: "text-indigo-400",
agent_stop: "text-violet-400",
compaction: "text-amber-400",
error: "text-red-400",
compaction: "text-status-warning",
error: "text-status-danger",
};
function eventTypeColor(type: string): string {
return EVENT_TYPE_COLOR[type] ?? "text-gray-400";
return EVENT_TYPE_COLOR[type] ?? "text-fg-secondary";
}
function EventSequence({ events }: EventSequenceProps) {
const { t } = useTranslation("workflows");
if (events.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noEvents")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noEvents")}</p>;
}
const recent = events.slice(0, 100);
@@ -325,18 +327,18 @@ function EventSequence({ events }: EventSequenceProps) {
</span>
{/* Summary */}
<span className="text-xs text-gray-400 flex-1 min-w-0 truncate">
<span className="text-xs text-fg-secondary flex-1 min-w-0 truncate">
{ev.summary ?? ev.tool_name ?? "-"}
</span>
{/* Timestamp */}
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums opacity-0 group-hover:opacity-100 transition-opacity">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums opacity-0 group-hover:opacity-100 transition-opacity">
{safeTimestamp(ev.created_at)}
</span>
</div>
))}
{events.length > 100 && (
<p className="text-xs text-gray-600 text-center py-2">
<p className="text-xs text-fg-muted text-center py-2">
{t("drillIn.showingOf", { total: events.length })}
</p>
)}
@@ -365,11 +367,11 @@ function ErrorState({ message }: ErrorStateProps) {
const { t } = useTranslation("workflows");
return (
<div className="flex flex-col items-center justify-center py-10 text-center px-4">
<div className="w-9 h-9 rounded-xl bg-red-500/10 border border-red-500/20 flex items-center justify-center mb-3">
<X className="w-4 h-4 text-red-400" />
<div className="w-9 h-9 rounded-xl bg-status-danger/10 border border-status-danger/20 flex items-center justify-center mb-3">
<X className="w-4 h-4 text-status-danger" />
</div>
<p className="text-sm font-medium text-red-400">{t("drillIn.failedLoad")}</p>
<p className="text-xs text-gray-600 mt-1 max-w-xs">{message}</p>
<p className="text-sm font-medium text-status-danger">{t("drillIn.failedLoad")}</p>
<p className="text-xs text-fg-muted mt-1 max-w-xs">{message}</p>
</div>
);
}
@@ -405,17 +407,19 @@ function NoSessionState({ onSelectSession }: NoSessionStateProps) {
<div className="flex flex-col items-center text-center mt-2">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-4">
<GitFork className="w-5 h-5 text-gray-600" />
<GitFork className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-sm font-medium text-gray-400 mb-1">{t("drillIn.noSessionSelected")}</p>
<p className="text-xs text-gray-600 max-w-xs">{t("drillIn.noSessionDesc")}</p>
<p className="text-sm font-medium text-fg-secondary mb-1">
{t("drillIn.noSessionSelected")}
</p>
<p className="text-xs text-fg-muted max-w-xs">{t("drillIn.noSessionDesc")}</p>
{/* Preview tab pills */}
<div className="flex gap-2 mt-5">
{tabs.map((tab) => (
<div
key={tab.id}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-gray-600 bg-surface-3 border border-border"
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-fg-muted bg-surface-3 border border-border"
>
{tab.icon}
{tab.label}
@@ -444,10 +448,10 @@ function SessionHeader({ drillIn, onClose, activeTab, onTabChange }: SessionHead
<div className="flex flex-col gap-3 mb-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-gray-100 truncate">
<p className="text-sm font-semibold text-fg-primary truncate">
{session.name ?? session.id}
</p>
<p className="text-xs text-gray-500 mt-0.5 truncate">
<p className="text-xs text-fg-muted mt-0.5 truncate">
{formatModelName(session.model) ?? t("drillIn.unknownModel")} &middot;{" "}
{t(`common:status.${session.status}`, { defaultValue: session.status })}
{session.started_at && ` \u00b7 ${safeTimestamp(session.started_at)}`}
@@ -456,7 +460,7 @@ function SessionHeader({ drillIn, onClose, activeTab, onTabChange }: SessionHead
<button
type="button"
onClick={onClose}
className="flex-shrink-0 w-7 h-7 rounded-md flex items-center justify-center text-gray-500 hover:text-gray-200 hover:bg-white/10 transition-colors"
className="flex-shrink-0 w-7 h-7 rounded-md flex items-center justify-center text-fg-muted hover:text-fg-secondary hover:bg-white/10 transition-colors"
aria-label={t("drillIn.closePanel")}
>
<X className="w-4 h-4" />
@@ -569,13 +573,13 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
inputRef.current?.focus();
}}
>
<Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<Search className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<input
ref={inputRef}
type="text"
value={search}
placeholder={t("drillIn.searchPlaceholder")}
className="flex-1 bg-transparent text-xs text-gray-200 placeholder-gray-600 outline-none min-w-0"
className="flex-1 bg-transparent text-xs text-fg-secondary placeholder-fg-muted outline-none min-w-0"
onFocus={() => setOpen(true)}
onChange={(e) => {
setSearch(e.target.value);
@@ -584,7 +588,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
/>
<ChevronDown
className={[
"w-3.5 h-3.5 text-gray-600 flex-shrink-0 transition-transform duration-150",
"w-3.5 h-3.5 text-fg-muted flex-shrink-0 transition-transform duration-150",
open ? "rotate-180" : "",
].join(" ")}
/>
@@ -605,7 +609,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
))}
</div>
) : filtered.length === 0 ? (
<p className="text-xs text-gray-600 text-center py-6 px-3">
<p className="text-xs text-fg-muted text-center py-6 px-3">
{search.trim() ? t("drillIn.noMatch") : t("drillIn.notFound")}
</p>
) : (
@@ -625,22 +629,22 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
{s.status}
</span>
<span className="flex-1 min-w-0">
<span className="block text-xs font-medium text-gray-200 truncate">
<span className="block text-xs font-medium text-fg-secondary truncate">
{s.name ?? s.id}
</span>
{s.name && (
<span className="block text-[10px] text-gray-600 font-mono truncate">
<span className="block text-[10px] text-fg-muted font-mono truncate">
{s.id}
</span>
)}
</span>
{s.model && (
<span className="flex-shrink-0 text-[10px] text-gray-500 truncate max-w-[80px]">
<span className="flex-shrink-0 text-[10px] text-fg-muted truncate max-w-[80px]">
{formatModelName(s.model)}
</span>
)}
{s.started_at && (
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums">
{safeTimestamp(s.started_at)}
</span>
)}
@@ -656,7 +660,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
type="button"
onClick={handleLoadMore}
disabled={loading}
className="w-full px-3 py-2 text-xs text-gray-500 hover:text-gray-300 hover:bg-white/5 transition-colors border-t border-border/50 disabled:opacity-50"
className="w-full px-3 py-2 text-xs text-fg-muted hover:text-fg-secondary hover:bg-white/5 transition-colors border-t border-border/50 disabled:opacity-50"
>
{loading ? t("drillIn.loading") : t("drillIn.loadMore")}
</button>
@@ -737,11 +741,11 @@ export function SessionDrillIn({ sessionId, onClose, onSelectSession }: SessionD
<div className="bg-surface-2 border border-border rounded-xl p-4">
<SessionSelector onSelectSession={onSelectSession} />
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-gray-600 font-mono truncate">{sessionId}</p>
<p className="text-xs text-fg-muted font-mono truncate">{sessionId}</p>
<button
type="button"
onClick={onClose}
className="w-6 h-6 flex items-center justify-center rounded text-gray-600 hover:text-gray-300 hover:bg-white/10 transition-colors"
className="w-6 h-6 flex items-center justify-center rounded text-fg-muted hover:text-fg-secondary hover:bg-white/10 transition-colors"
aria-label={t("drillIn.close")}
>
<X className="w-3.5 h-3.5" />
@@ -146,7 +146,7 @@ function SuccessRing({ rate, color }: SuccessRingProps) {
{clampedRate.toFixed(0)}%
</text>
</svg>
<span className="text-[10px] font-medium text-gray-500 uppercase tracking-wider">
<span className="text-[10px] font-medium text-fg-muted uppercase tracking-wider">
{t("effectiveness.success")}
</span>
</div>
@@ -213,7 +213,7 @@ function Sparkline({ data, color }: SparklineProps) {
{bars.map((_, i) => (
<span
key={i}
className="flex-1 text-center text-[8px] text-gray-600 leading-none select-none"
className="flex-1 text-center text-[8px] text-fg-muted leading-none select-none"
>
{dayLabels[i % dayLabels.length] ?? ""}
</span>
@@ -281,11 +281,11 @@ function SparklineTooltip({
<div
ref={ref}
role="tooltip"
className="fixed z-[60] px-2 py-1 bg-[#12121f] border border-[#2a2a4a] rounded-md shadow-xl text-[10px] text-gray-200 whitespace-nowrap pointer-events-none"
className="fixed z-[60] px-2 py-1 bg-[#12121f] border border-[#2a2a4a] rounded-md shadow-xl text-[10px] text-fg-secondary whitespace-nowrap pointer-events-none"
style={{ left: pos.left, top: pos.top }}
>
<span className="font-medium">{label}</span>
<span className="text-gray-400 mx-1">·</span>
<span className="text-fg-secondary mx-1">·</span>
<span className="tabular-nums" style={{ color }}>
{t("effectiveness.sessionCount", { count: value })}
</span>
@@ -302,10 +302,10 @@ interface MetricBoxProps {
function MetricBox({ label, value }: MetricBoxProps) {
return (
<div className="flex flex-col items-center gap-0.5 bg-surface-3 rounded-lg px-2 py-2 flex-1 min-w-0 overflow-hidden">
<span className="text-xs font-semibold text-gray-200 tabular-nums truncate w-full text-center">
<span className="text-xs font-semibold text-fg-secondary tabular-nums truncate w-full text-center">
{value}
</span>
<span className="text-[9px] text-gray-500 uppercase tracking-wider truncate w-full text-center">
<span className="text-[9px] text-fg-muted uppercase tracking-wider truncate w-full text-center">
{label}
</span>
</div>
@@ -337,7 +337,7 @@ function ScoreCard({ item, colorIndex }: ScoreCardProps) {
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<span className="text-sm font-medium text-gray-200 truncate" title={item.subagent_type}>
<span className="text-sm font-medium text-fg-secondary truncate" title={item.subagent_type}>
{item.subagent_type}
</span>
</div>
@@ -358,7 +358,7 @@ function ScoreCard({ item, colorIndex }: ScoreCardProps) {
{/* Sparkline */}
<div className="flex flex-col gap-1">
<span className="text-[10px] text-gray-500 uppercase tracking-wider">
<span className="text-[10px] text-fg-muted uppercase tracking-wider">
{t("effectiveness.weeklyActivity")}
</span>
<Sparkline data={item.trend} color={color} />
@@ -375,7 +375,7 @@ export function SubagentEffectiveness({ data }: SubagentEffectivenessProps) {
const { t } = useTranslation("workflows");
if (data.length === 0) {
return (
<div className="flex items-center justify-center py-16 text-gray-500 text-sm">
<div className="flex items-center justify-center py-16 text-fg-muted text-sm">
{t("effectiveness.noData")}
</div>
);
@@ -507,7 +507,7 @@ export function ToolExecutionFlow({
<div className="relative" ref={containerRef} onMouseLeave={hideTip}>
{isEmpty ? (
<div className="flex items-center justify-center" style={{ height: dimensions.height }}>
<span className="text-sm text-gray-500">{t("toolFlow.noData")}</span>
<span className="text-sm text-fg-muted">{t("toolFlow.noData")}</span>
</div>
) : (
<svg
@@ -663,7 +663,7 @@ function Legend() {
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-gray-400">{t(`toolLegend.${key}`)}</span>
<span className="text-xs text-fg-secondary">{t(`toolLegend.${key}`)}</span>
</div>
))}
</div>
@@ -159,12 +159,12 @@ function StepFlow({ steps }: { steps: string[] }) {
<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-gray-600" />
<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-gray-700/50 text-gray-400 border border-gray-600/20 whitespace-nowrap">
<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>
)}
@@ -176,8 +176,8 @@ function PatternFrequency({ count, percentage }: { count: number; percentage: nu
const { t } = useTranslation("workflows");
return (
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-gray-100">{count.toLocaleString()}</p>
<p className="text-xs text-gray-500">
<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>
@@ -230,7 +230,7 @@ function PatternItem({ pattern, rank, isSelected, onClick }: PatternItemProps) {
{/* 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-gray-600 flex-shrink-0" />
<Info className="hidden sm:block w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
)}
</button>
@@ -249,7 +249,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
<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-gray-500 uppercase tracking-wider mb-2">
<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">
@@ -260,7 +260,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
{step}
</span>
{i < pattern.steps.length - 1 && (
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-gray-600" />
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-fg-muted" />
)}
</span>
))}
@@ -286,11 +286,11 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
{/* Narrative - what this means */}
<div>
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
<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-gray-300 leading-relaxed">{narrative}</p>
<p className="text-xs text-fg-secondary leading-relaxed">{narrative}</p>
</div>
{/* Suggestion */}
@@ -299,7 +299,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
<Lightbulb className="w-3 h-3" />
{t("patterns.detail.suggestionHeading")}
</p>
<p className="text-xs text-gray-300 leading-relaxed">{suggestion}</p>
<p className="text-xs text-fg-secondary leading-relaxed">{suggestion}</p>
</div>
</div>
);
@@ -308,8 +308,8 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
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-gray-100 tabular-nums">{value}</p>
<p className="text-[10px] text-gray-500 uppercase tracking-wider mt-0.5 truncate">{label}</p>
<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>
);
}
@@ -327,8 +327,8 @@ function SoloSessionItem({ count, percentage }: { count: number; percentage: num
</span>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-gray-100">{count.toLocaleString()}</p>
<p className="text-xs text-gray-500">
<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>
@@ -341,10 +341,10 @@ function EmptyPatterns() {
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-gray-600" />
<Zap className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-sm font-medium text-gray-400">{t("patterns.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("patterns.noDataDesc")}</p>
<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>
);
}
@@ -372,7 +372,7 @@ export function WorkflowPatterns({ data, onPatternClick }: WorkflowPatternsProps
return (
<div className="card p-5">
<h2 className="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-4">
<h2 className="text-sm font-semibold text-fg-secondary uppercase tracking-wider mb-4">
{t("patterns.label")}
</h2>
@@ -109,18 +109,18 @@ interface Props {
}
const STATUS_STYLES: Record<string, string> = {
running: "bg-amber-500/15 text-amber-400 border-amber-500/30",
working: "bg-amber-500/15 text-amber-400 border-amber-500/30",
queued: "bg-gray-500/15 text-gray-400 border-gray-500/30",
completed: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
done: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
error: "bg-red-500/15 text-red-400 border-red-500/30",
failed: "bg-red-500/15 text-red-400 border-red-500/30",
running: "bg-status-warning/15 text-status-warning border-status-warning/30",
working: "bg-status-warning/15 text-status-warning border-status-warning/30",
queued: "bg-surface-4/15 text-fg-secondary border-border-light/30",
completed: "bg-status-success/15 text-status-success border-status-success/30",
done: "bg-status-success/15 text-status-success border-status-success/30",
success: "bg-status-success/15 text-status-success border-status-success/30",
error: "bg-status-danger/15 text-status-danger border-status-danger/30",
failed: "bg-status-danger/15 text-status-danger border-status-danger/30",
};
function statusClass(status: string): string {
return STATUS_STYLES[status] || "bg-gray-500/15 text-gray-400 border-gray-500/30";
return STATUS_STYLES[status] || "bg-surface-4/15 text-fg-secondary border-border-light/30";
}
// Distinct per-phase chip colors, cycled by phase index so every phase
@@ -129,15 +129,15 @@ function statusClass(status: string): string {
const PHASE_PALETTE = [
"bg-violet-500/15 text-violet-300 border-violet-500/40",
"bg-sky-500/15 text-sky-300 border-sky-500/40",
"bg-amber-500/15 text-amber-300 border-amber-500/40",
"bg-emerald-500/15 text-emerald-300 border-emerald-500/40",
"bg-status-warning/15 text-status-warning border-status-warning/40",
"bg-status-success/15 text-status-success border-status-success/40",
"bg-rose-500/15 text-rose-300 border-rose-500/40",
"bg-cyan-500/15 text-cyan-300 border-cyan-500/40",
"bg-fuchsia-500/15 text-fuchsia-300 border-fuchsia-500/40",
];
function phaseColor(phaseTitles: string[], title: string | null | undefined): string {
if (!title) return "bg-gray-500/15 text-gray-300 border-gray-500/40";
if (!title) return "bg-surface-4/15 text-fg-secondary border-border-light/40";
const i = phaseTitles.indexOf(title);
const idx = i >= 0 ? i : Math.abs(hashStr(title)) % PHASE_PALETTE.length;
return PHASE_PALETTE[idx % PHASE_PALETTE.length] as string;
@@ -326,7 +326,7 @@ export function WorkflowRunsPanel({
if (!controlled && loading) {
return (
<div className="flex items-center justify-center gap-2 py-8 text-sm text-gray-500">
<div className="flex items-center justify-center gap-2 py-8 text-sm text-fg-muted">
<Loader2 className="w-4 h-4 animate-spin text-violet-400" />
<span className="animate-pulse">{t("runs.loading")}</span>
</div>
@@ -334,8 +334,8 @@ export function WorkflowRunsPanel({
}
if (runs.length === 0) {
return (
<div className="text-sm text-gray-500 flex items-center gap-2">
<Workflow className="w-4 h-4 text-gray-600" />
<div className="text-sm text-fg-muted flex items-center gap-2">
<Workflow className="w-4 h-4 text-fg-muted" />
{t("runs.empty")}
</div>
);
@@ -356,36 +356,36 @@ export function WorkflowRunsPanel({
return (
<div
key={run.run_id}
className="rounded-lg border border-gray-800 bg-card/40 overflow-hidden"
className="rounded-lg border border-border bg-card/40 overflow-hidden"
>
<button
onClick={() => toggle(run.run_id)}
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-gray-800/30 transition-colors"
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-surface-2/30 transition-colors"
aria-expanded={isOpen}
>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
<ChevronDown className="w-4 h-4 text-fg-muted flex-shrink-0" />
) : (
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
<ChevronRight className="w-4 h-4 text-fg-muted flex-shrink-0" />
)}
{running ? (
<Loader2 className="w-4 h-4 text-amber-400 flex-shrink-0 animate-spin" />
<Loader2 className="w-4 h-4 text-status-warning flex-shrink-0 animate-spin" />
) : (
<Workflow className="w-4 h-4 text-violet-400 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-gray-200 truncate">
<span className="text-sm font-medium text-fg-secondary truncate">
{run.name || run.run_id}
</span>
<span className={`badge text-[10px] border ${statusClass(run.status)}`}>
{t(`runs.status.${run.status}`, run.status)}
</span>
{run.default_model && (
<span className="text-[10px] font-mono text-gray-500">{run.default_model}</span>
<span className="text-[10px] font-mono text-fg-muted">{run.default_model}</span>
)}
</div>
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-gray-500 flex-wrap">
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-fg-muted flex-wrap">
<span>{t("runs.agents", { count: run.agent_count })}</span>
<span>{t("runs.tools", { count: run.total_tool_calls })}</span>
<span>
@@ -399,7 +399,7 @@ export function WorkflowRunsPanel({
<Link
to={`/sessions/${encodeURIComponent(run.session_id)}`}
onClick={(e) => e.stopPropagation()}
className="text-gray-500 hover:text-violet-400 transition-colors flex-shrink-0"
className="text-fg-muted hover:text-violet-400 transition-colors flex-shrink-0"
title={t("runs.openSession")}
>
<ExternalLink className="w-3.5 h-3.5" />
@@ -408,11 +408,11 @@ export function WorkflowRunsPanel({
</button>
{isOpen && (
<div className="px-3 pb-3 pt-3 border-t border-gray-800/60 space-y-3">
<div className="px-3 pb-3 pt-3 border-t border-border/60 space-y-3">
{/* Clickable, colored phase filters */}
{phaseTitles.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap">
<Layers className="w-3.5 h-3.5 text-gray-500" />
<Layers className="w-3.5 h-3.5 text-fg-muted" />
{phaseTitles.map((title, i) => {
const active = sel === title;
return (
@@ -435,7 +435,7 @@ export function WorkflowRunsPanel({
{sel && (
<button
onClick={() => setPhase(run.run_id, sel)}
className="text-[10px] text-gray-500 hover:text-gray-300 underline"
className="text-[10px] text-fg-muted hover:text-fg-secondary underline"
>
{t("runs.clearFilter")}
</button>
@@ -447,7 +447,7 @@ export function WorkflowRunsPanel({
<div className="overflow-x-auto">
<table className="w-full text-[11px]">
<thead>
<tr className="text-gray-500 text-left border-b border-gray-800">
<tr className="text-fg-muted text-left border-b border-border">
<th className="py-1 pr-3 font-medium">{t("runs.col.agent")}</th>
<th className="py-1 pr-3 font-medium">{t("runs.col.phase")}</th>
<th className="py-1 pr-3 font-medium">{t("runs.col.state")}</th>
@@ -464,11 +464,11 @@ export function WorkflowRunsPanel({
</thead>
<tbody>
{shown.map((a: WorkflowProgressEntry, i) => (
<tr key={a.agentId || i} className="border-b border-gray-800/40">
<td className="py-1 pr-3 text-gray-300">
<tr key={a.agentId || i} className="border-b border-border/40">
<td className="py-1 pr-3 text-fg-secondary">
{a.label || a.agentType || a.agentId}
{a.lastToolName && (
<span className="text-gray-600 font-mono ml-1">
<span className="text-fg-muted font-mono ml-1">
· {a.lastToolName}
</span>
)}
@@ -487,13 +487,13 @@ export function WorkflowRunsPanel({
{t(`runs.status.${a.state}`, String(a.state || "-"))}
</span>
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{fmt(a.tokens || 0)}
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{a.toolCalls || 0}
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{a.durationMs != null ? formatMs(a.durationMs) : "-"}
</td>
</tr>
@@ -502,15 +502,15 @@ export function WorkflowRunsPanel({
</table>
</div>
) : (
<p className="text-[11px] text-gray-600">{t("runs.noAgents")}</p>
<p className="text-[11px] text-fg-muted">{t("runs.noAgents")}</p>
)}
{/* Clickable, colored, expandable results - full content on click */}
{resultRows.length > 0 && (
<div className="space-y-1.5">
<div className="text-[10px] font-medium uppercase tracking-wider text-gray-600">
<div className="text-[10px] font-medium uppercase tracking-wider text-fg-muted">
{t("runs.resultsLabel")}
<span className="ml-1 text-gray-700">· {resultRows.length}</span>
<span className="ml-1 text-fg-muted">· {resultRows.length}</span>
</div>
{resultRows.map((a, i) => {
const key = `${run.run_id}::${a.agentId || i}`;
@@ -528,7 +528,7 @@ export function WorkflowRunsPanel({
return (
<div
key={key}
className="rounded border border-gray-800/70 bg-gray-900/30 overflow-hidden"
className="rounded border border-border/70 bg-surface-0/30 overflow-hidden"
>
<button
onClick={() => {
@@ -537,13 +537,13 @@ export function WorkflowRunsPanel({
}
toggleResult(key);
}}
className="w-full flex items-center gap-2 px-2 py-1.5 text-left hover:bg-gray-800/40 transition-colors"
className="w-full flex items-center gap-2 px-2 py-1.5 text-left hover:bg-surface-2/40 transition-colors"
aria-expanded={open}
>
{open ? (
<ChevronDown className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<ChevronDown className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
) : (
<ChevronRight className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<ChevronRight className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
)}
<span
className={`badge text-[10px] border flex-shrink-0 ${phaseColor(phaseTitles, a.phaseTitle)}`}
@@ -551,14 +551,14 @@ export function WorkflowRunsPanel({
{a.label || a.agentType || a.agentId}
</span>
{!open && (
<span className="text-[11px] text-gray-500 leading-snug min-w-0">
<span className="text-[11px] text-fg-muted leading-snug min-w-0">
{truncate(friendlyPreview(a.resultPreview), 160)}
</span>
)}
</button>
{open && (
<div className="px-2.5 pb-2.5 pt-0.5 space-y-2">
<div className="flex flex-wrap items-center gap-2 text-[10px] text-gray-500">
<div className="flex flex-wrap items-center gap-2 text-[10px] text-fg-muted">
{a.model && <span className="font-mono">{a.model}</span>}
<span
className={`badge border ${statusClass(String(a.state || ""))}`}
@@ -579,19 +579,19 @@ export function WorkflowRunsPanel({
</div>
{fullPrompt && (
<div>
<div className="text-[10px] uppercase tracking-wider text-gray-600 mb-0.5">
<div className="text-[10px] uppercase tracking-wider text-fg-muted mb-0.5">
{t("runs.promptLabel")}
</div>
<pre className="text-[11px] text-gray-400 whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-48 overflow-auto">
<pre className="text-[11px] text-fg-secondary whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-48 overflow-auto">
{fullPrompt}
</pre>
</div>
)}
<div>
<div className="text-[10px] uppercase tracking-wider text-gray-600 mb-0.5">
<div className="text-[10px] uppercase tracking-wider text-fg-muted mb-0.5">
{t("runs.resultLabel")}
</div>
<pre className="text-[11px] text-gray-300 whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-96 overflow-auto">
<pre className="text-[11px] text-fg-secondary whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-96 overflow-auto">
{fullResult}
</pre>
</div>
@@ -76,9 +76,9 @@ function formatDurationSec(sec: number): string {
}
function successRateColor(rate: number): string {
if (rate > 90) return "text-emerald-400";
if (rate > 90) return "text-status-success";
if (rate > 70) return "text-yellow-400";
return "text-red-400";
return "text-status-danger";
}
// ── Deterministic interpreters - return an i18n key + params ─────────────────
@@ -228,7 +228,7 @@ function InfoPopover({ calculationKey, interp, valueDisplay, metricPhraseKey }:
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-gray-600 hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-accent/40"
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-fg-muted hover:text-fg-secondary focus:outline-none focus:ring-1 focus:ring-accent/40"
>
<Info className="w-4 h-4" />
</button>
@@ -236,27 +236,27 @@ function InfoPopover({ calculationKey, interp, valueDisplay, metricPhraseKey }:
<div
ref={popoverRef}
role="tooltip"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-gray-300 pointer-events-none"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-fg-secondary pointer-events-none"
style={{ left: coords.left, top: coords.top, width: POPOVER_W }}
>
<div className="flex items-baseline gap-2 mb-2 pb-2 border-b border-[#2a2a4a]">
<span className="text-base font-semibold text-gray-100 tabular-nums">
<span className="text-base font-semibold text-fg-primary tabular-nums">
{valueDisplay}
</span>
<span className="text-[10px] uppercase tracking-wider text-gray-500">
<span className="text-[10px] uppercase tracking-wider text-fg-muted">
{metricPhrase}
</span>
</div>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("stats.tooltip.howCalc")}
</p>
<p className="text-gray-400 leading-snug mb-2.5">{t(calculationKey)}</p>
<p className="text-fg-secondary leading-snug mb-2.5">{t(calculationKey)}</p>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("stats.tooltip.whatItMeans")}
</p>
<p className="text-gray-400 leading-snug">{valueMeans}</p>
<p className="text-fg-secondary leading-snug">{valueMeans}</p>
</div>
)}
</>
@@ -287,7 +287,7 @@ function StatCard({
return (
<div className="bg-surface-2 border border-border rounded-xl p-4 flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider leading-none">
<span className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider leading-none">
{label}
</span>
<Icon className={`w-4 h-4 flex-shrink-0 ${accentClass}`} />
@@ -340,7 +340,7 @@ export function WorkflowStats({ stats }: WorkflowStatsProps) {
label={t("stats.avgSubagentsPerSession")}
value={stats.avgSubagents.toFixed(1)}
icon={Users}
accentClass="text-blue-400"
accentClass="text-blue-500"
calculationKey="stats.tooltip.calc.subagents"
interp={interpAvgSubagents(stats.avgSubagents)}
metricPhraseKey="stats.tooltip.phrase.subagents"
@@ -376,7 +376,7 @@ export function WorkflowStats({ stats }: WorkflowStatsProps) {
label={t("stats.avgDuration")}
value={formatDurationSec(stats.avgDurationSec)}
icon={Clock}
accentClass="text-amber-400"
accentClass="text-status-warning"
calculationKey="stats.tooltip.calc.duration"
interp={interpAvgDuration(stats.avgDurationSec)}
metricPhraseKey="stats.tooltip.phrase.duration"