claude-dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +99 -0
  4. package/README.zh-TW.md +99 -0
  5. package/bin/cdb.ts +60 -0
  6. package/bun.lock +1612 -0
  7. package/bunfig.toml +4 -0
  8. package/components.json +20 -0
  9. package/next.config.ts +19 -0
  10. package/package.json +62 -0
  11. package/postcss.config.mjs +9 -0
  12. package/prompts/pm-system.md +61 -0
  13. package/prompts/rd-system.md +68 -0
  14. package/prompts/sec-system.md +93 -0
  15. package/prompts/test-system.md +71 -0
  16. package/prompts/ui-system.md +72 -0
  17. package/server.ts +118 -0
  18. package/sql.js.d.ts +33 -0
  19. package/src/__tests__/api/usage/route.test.ts +193 -0
  20. package/src/__tests__/components/layout/TopNav.test.tsx +155 -0
  21. package/src/__tests__/components/layout/UsageIndicator.test.tsx +503 -0
  22. package/src/__tests__/hooks/useUsage.test.tsx +174 -0
  23. package/src/__tests__/lib/usage/get-token.test.ts +117 -0
  24. package/src/__tests__/react-sanity.test.tsx +14 -0
  25. package/src/__tests__/sanity.test.ts +7 -0
  26. package/src/__tests__/setup.ts +1 -0
  27. package/src/app/api/health/route.ts +8 -0
  28. package/src/app/api/usage/route.ts +86 -0
  29. package/src/app/api/workflows/[id]/route.ts +17 -0
  30. package/src/app/api/workflows/route.ts +14 -0
  31. package/src/app/globals.css +74 -0
  32. package/src/app/history/page.tsx +15 -0
  33. package/src/app/layout.tsx +24 -0
  34. package/src/app/page.tsx +112 -0
  35. package/src/components/agent/AgentCard.tsx +117 -0
  36. package/src/components/agent/AgentCardGrid.tsx +14 -0
  37. package/src/components/agent/AgentOutput.tsx +87 -0
  38. package/src/components/agent/AgentStatusBadge.tsx +20 -0
  39. package/src/components/events/EventLog.tsx +65 -0
  40. package/src/components/events/EventLogItem.tsx +39 -0
  41. package/src/components/history/HistoryTable.tsx +105 -0
  42. package/src/components/layout/DashboardShell.tsx +12 -0
  43. package/src/components/layout/TopNav.tsx +86 -0
  44. package/src/components/layout/UsageIndicator.tsx +163 -0
  45. package/src/components/pipeline/PipelineBar.tsx +59 -0
  46. package/src/components/pipeline/PipelineNode.tsx +55 -0
  47. package/src/components/terminal/TerminalPanel.tsx +138 -0
  48. package/src/components/terminal/XTermRenderer.tsx +129 -0
  49. package/src/components/ui/badge.tsx +37 -0
  50. package/src/components/ui/button.tsx +55 -0
  51. package/src/components/ui/card.tsx +80 -0
  52. package/src/components/ui/input.tsx +26 -0
  53. package/src/components/ui/scroll-area.tsx +52 -0
  54. package/src/components/ui/separator.tsx +31 -0
  55. package/src/components/ui/textarea.tsx +25 -0
  56. package/src/components/ui/tooltip.tsx +73 -0
  57. package/src/components/workflow/WorkflowLauncher.tsx +102 -0
  58. package/src/hooks/useAgentStream.ts +27 -0
  59. package/src/hooks/useAutoScroll.ts +24 -0
  60. package/src/hooks/useUsage.ts +66 -0
  61. package/src/hooks/useWebSocket.ts +289 -0
  62. package/src/lib/agents/prompts.ts +341 -0
  63. package/src/lib/db/connection.ts +263 -0
  64. package/src/lib/db/queries.ts +257 -0
  65. package/src/lib/db/schema.ts +39 -0
  66. package/src/lib/output-buffer.ts +41 -0
  67. package/src/lib/terminal/pty-manager.ts +106 -0
  68. package/src/lib/usage/get-token.ts +48 -0
  69. package/src/lib/utils.ts +6 -0
  70. package/src/lib/websocket/connection-manager.ts +71 -0
  71. package/src/lib/websocket/protocol.ts +90 -0
  72. package/src/lib/websocket/server.ts +231 -0
  73. package/src/lib/workflow/agent-runner.ts +254 -0
  74. package/src/lib/workflow/context-builder.ts +62 -0
  75. package/src/lib/workflow/engine.ts +310 -0
  76. package/src/lib/workflow/pipeline.ts +28 -0
  77. package/src/lib/workflow/types.ts +111 -0
  78. package/src/stores/agentStore.ts +152 -0
  79. package/src/stores/eventStore.ts +35 -0
  80. package/src/stores/terminalStore.ts +20 -0
  81. package/src/stores/uiStore.ts +35 -0
  82. package/src/stores/workflowStore.ts +57 -0
  83. package/src/types/css.d.ts +4 -0
  84. package/src/types/index.ts +12 -0
  85. package/tailwind.config.ts +65 -0
  86. package/tsconfig.json +25 -0
  87. package/tsconfig.server.json +21 -0
  88. package/vitest.config.ts +25 -0
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+ import { useAutoScroll } from "@/hooks/useAutoScroll";
6
+ import type { AgentActivity } from "@/lib/workflow/types";
7
+
8
+ function ActivityIndicator({ activity }: { activity: AgentActivity }) {
9
+ switch (activity.kind) {
10
+ case "thinking":
11
+ return (
12
+ <span className="flex items-center gap-2">
13
+ <span className="w-1.5 h-1.5 bg-amber-400 rounded-full animate-pulse" />
14
+ Thinking...
15
+ </span>
16
+ );
17
+ case "tool_use":
18
+ return (
19
+ <span className="flex items-center gap-2">
20
+ <span className="relative flex h-2 w-2">
21
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
22
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
23
+ </span>
24
+ Using {activity.toolName}...
25
+ </span>
26
+ );
27
+ case "text":
28
+ return (
29
+ <span className="flex items-center gap-2">
30
+ <span className="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-pulse" />
31
+ Writing response...
32
+ </span>
33
+ );
34
+ case "idle":
35
+ default:
36
+ return (
37
+ <span className="flex items-center gap-2">
38
+ <span className="relative flex h-2 w-2">
39
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
40
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
41
+ </span>
42
+ Waiting for output...
43
+ </span>
44
+ );
45
+ }
46
+ }
47
+
48
+ interface AgentOutputProps {
49
+ output: string;
50
+ isStreaming: boolean;
51
+ activity?: AgentActivity;
52
+ }
53
+
54
+ export function AgentOutput({
55
+ output,
56
+ isStreaming,
57
+ activity,
58
+ }: AgentOutputProps) {
59
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>([output]);
60
+
61
+ if (!output) {
62
+ return (
63
+ <div className="flex flex-col items-center justify-center h-full gap-2 text-xs text-muted-foreground">
64
+ {isStreaming ? (
65
+ <ActivityIndicator activity={activity ?? { kind: "idle" }} />
66
+ ) : (
67
+ "No output yet"
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div
75
+ ref={ref}
76
+ onScroll={handleScroll}
77
+ className="agent-output overflow-y-auto h-full px-2 py-1"
78
+ >
79
+ <div className="prose prose-invert prose-sm max-w-none">
80
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{output}</ReactMarkdown>
81
+ </div>
82
+ {isStreaming && (
83
+ <span className="inline-block w-1.5 h-3 bg-emerald-400 animate-pulse ml-0.5" />
84
+ )}
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { StepStatus } from "@/lib/workflow/types";
4
+
5
+ const statusConfig: Record<StepStatus, { label: string; className: string }> = {
6
+ pending: { label: "Pending", className: "bg-gray-500/20 text-gray-400" },
7
+ running: { label: "Running", className: "bg-emerald-500/20 text-emerald-400 animate-pulse" },
8
+ completed: { label: "Done", className: "bg-blue-500/20 text-blue-400" },
9
+ failed: { label: "Failed", className: "bg-red-500/20 text-red-400" },
10
+ skipped: { label: "Skipped", className: "bg-gray-500/20 text-gray-500" },
11
+ };
12
+
13
+ export function AgentStatusBadge({ status }: { status: StepStatus }) {
14
+ const config = statusConfig[status];
15
+ return (
16
+ <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${config.className}`}>
17
+ {config.label}
18
+ </span>
19
+ );
20
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useRef } from "react";
4
+ import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { useEventStore } from "@/stores/eventStore";
6
+ import { useAutoScroll } from "@/hooks/useAutoScroll";
7
+ import { EventLogItem } from "./EventLogItem";
8
+
9
+ export function EventLog() {
10
+ const events = useEventStore((s) => s.events);
11
+ const parentRef = useRef<HTMLDivElement>(null);
12
+ const { ref: scrollRef, handleScroll } = useAutoScroll<HTMLDivElement>([
13
+ events.length,
14
+ ]);
15
+
16
+ const virtualizer = useVirtualizer({
17
+ count: events.length,
18
+ getScrollElement: () => parentRef.current,
19
+ estimateSize: () => 28,
20
+ overscan: 20,
21
+ });
22
+
23
+ return (
24
+ <div className="flex flex-col h-full">
25
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
26
+ <span className="text-xs font-medium">Event Log</span>
27
+ <span className="text-[10px] text-muted-foreground">
28
+ {events.length} events
29
+ </span>
30
+ </div>
31
+ <div
32
+ ref={(el) => {
33
+ (parentRef as any).current = el;
34
+ (scrollRef as any).current = el;
35
+ }}
36
+ onScroll={handleScroll}
37
+ className="flex-1 overflow-auto"
38
+ >
39
+ <div
40
+ style={{
41
+ height: `${virtualizer.getTotalSize()}px`,
42
+ width: "100%",
43
+ position: "relative",
44
+ }}
45
+ >
46
+ {virtualizer.getVirtualItems().map((virtualItem) => (
47
+ <div
48
+ key={virtualItem.key}
49
+ style={{
50
+ position: "absolute",
51
+ top: 0,
52
+ left: 0,
53
+ width: "100%",
54
+ height: `${virtualItem.size}px`,
55
+ transform: `translateY(${virtualItem.start}px)`,
56
+ }}
57
+ >
58
+ <EventLogItem event={events[virtualItem.index]} />
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import type { EventLogItem as EventLogItemType } from "@/types";
4
+ import { AGENT_CONFIG } from "@/lib/workflow/types";
5
+
6
+ const typeStyles: Record<string, string> = {
7
+ info: "text-gray-400",
8
+ warning: "text-yellow-400",
9
+ error: "text-red-400",
10
+ success: "text-emerald-400",
11
+ };
12
+
13
+ export function EventLogItem({ event }: { event: EventLogItemType }) {
14
+ const time = new Date(event.timestamp).toLocaleTimeString("en-US", {
15
+ hour12: false,
16
+ hour: "2-digit",
17
+ minute: "2-digit",
18
+ second: "2-digit",
19
+ });
20
+
21
+ const agentColor = event.role ? AGENT_CONFIG[event.role]?.color : undefined;
22
+
23
+ return (
24
+ <div className="flex items-start gap-2 px-3 py-1 text-xs hover:bg-white/5">
25
+ <span className="text-muted-foreground shrink-0 font-mono">{time}</span>
26
+ {event.role && (
27
+ <span
28
+ className="shrink-0 font-medium"
29
+ style={{ color: agentColor }}
30
+ >
31
+ [{AGENT_CONFIG[event.role]?.label}]
32
+ </span>
33
+ )}
34
+ <span className={typeStyles[event.type] || "text-gray-400"}>
35
+ {event.message}
36
+ </span>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import type { Workflow } from "@/lib/workflow/types";
5
+
6
+ const statusColors: Record<string, string> = {
7
+ pending: "text-gray-400",
8
+ running: "text-emerald-400",
9
+ paused: "text-yellow-400",
10
+ completed: "text-blue-400",
11
+ failed: "text-red-400",
12
+ cancelled: "text-gray-500",
13
+ };
14
+
15
+ export function HistoryTable() {
16
+ const [workflows, setWorkflows] = useState<Workflow[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ useEffect(() => {
21
+ fetch("/api/workflows")
22
+ .then((r) => {
23
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
24
+ return r.json();
25
+ })
26
+ .then((data) => {
27
+ setWorkflows(data.workflows || []);
28
+ setLoading(false);
29
+ })
30
+ .catch((err) => {
31
+ setError(err.message || "Failed to load workflows");
32
+ setLoading(false);
33
+ });
34
+ }, []);
35
+
36
+ if (loading) {
37
+ return (
38
+ <div className="flex items-center justify-center p-8 text-sm text-muted-foreground">
39
+ Loading...
40
+ </div>
41
+ );
42
+ }
43
+
44
+ if (error) {
45
+ return (
46
+ <div className="flex items-center justify-center p-8 text-sm text-red-400">
47
+ Failed to load workflows: {error}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ if (workflows.length === 0) {
53
+ return (
54
+ <div className="flex items-center justify-center p-8 text-sm text-muted-foreground">
55
+ No workflows yet. Start one from the Dashboard.
56
+ </div>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <div className="overflow-auto">
62
+ <table className="w-full text-sm">
63
+ <thead>
64
+ <tr className="border-b border-border text-muted-foreground">
65
+ <th className="text-left py-2 px-3 font-medium">Title</th>
66
+ <th className="text-left py-2 px-3 font-medium">Status</th>
67
+ <th className="text-left py-2 px-3 font-medium">Created</th>
68
+ <th className="text-left py-2 px-3 font-medium">Duration</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody>
72
+ {workflows.map((w) => (
73
+ <tr
74
+ key={w.id}
75
+ className="border-b border-border/50 hover:bg-white/5 cursor-pointer"
76
+ >
77
+ <td className="py-2 px-3 truncate max-w-[400px]">{w.title}</td>
78
+ <td className={`py-2 px-3 ${statusColors[w.status] || ""}`}>
79
+ {w.status}
80
+ </td>
81
+ <td className="py-2 px-3 text-muted-foreground">
82
+ {new Date(w.createdAt).toLocaleString()}
83
+ </td>
84
+ <td className="py-2 px-3 text-muted-foreground">
85
+ {w.completedAt
86
+ ? formatDuration(
87
+ new Date(w.completedAt).getTime() -
88
+ new Date(w.createdAt).getTime()
89
+ )
90
+ : "-"}
91
+ </td>
92
+ </tr>
93
+ ))}
94
+ </tbody>
95
+ </table>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ function formatDuration(ms: number): string {
101
+ const s = Math.floor(ms / 1000);
102
+ const m = Math.floor(s / 60);
103
+ if (m > 0) return `${m}m ${s % 60}s`;
104
+ return `${s}s`;
105
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { TopNav } from "./TopNav";
4
+
5
+ export function DashboardShell({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <div className="h-screen flex flex-col overflow-hidden">
8
+ <TopNav />
9
+ <main className="flex-1 overflow-hidden flex flex-col">{children}</main>
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useWorkflowStore } from "@/stores/workflowStore";
5
+ import { useAgentStore } from "@/stores/agentStore";
6
+ import { AGENT_ORDER } from "@/lib/workflow/types";
7
+ import { UsageIndicator } from "@/components/layout/UsageIndicator";
8
+
9
+ function formatDuration(ms: number): string {
10
+ const s = Math.floor(ms / 1000);
11
+ const m = Math.floor(s / 60);
12
+ if (m > 0) return `${m}m ${s % 60}s`;
13
+ return `${s}s`;
14
+ }
15
+
16
+ function LiveElapsed({ startedAt }: { startedAt: number }) {
17
+ const [now, setNow] = useState(Date.now());
18
+
19
+ useEffect(() => {
20
+ const timer = setInterval(() => setNow(Date.now()), 1000);
21
+ return () => clearInterval(timer);
22
+ }, [startedAt]);
23
+
24
+ return (
25
+ <span className="text-xs text-muted-foreground tabular-nums">
26
+ {formatDuration(now - startedAt)}
27
+ </span>
28
+ );
29
+ }
30
+
31
+ export function TopNav() {
32
+ const { status, title, startedAt, completedAt } = useWorkflowStore();
33
+ const agents = useAgentStore((s) => s.agents);
34
+
35
+ const completedCount = AGENT_ORDER.filter(
36
+ (r) => agents[r].status === "completed"
37
+ ).length;
38
+
39
+ const statusColors: Record<string, string> = {
40
+ pending: "bg-gray-500/20 text-gray-400",
41
+ running: "bg-emerald-500/20 text-emerald-400",
42
+ paused: "bg-yellow-500/20 text-yellow-400",
43
+ completed: "bg-blue-500/20 text-blue-400",
44
+ failed: "bg-red-500/20 text-red-400",
45
+ cancelled: "bg-gray-500/20 text-gray-400",
46
+ };
47
+
48
+ return (
49
+ <header className="h-12 border-b border-border bg-card flex items-center justify-between px-4">
50
+ <div className="flex items-center gap-3 min-w-0">
51
+ <h1 className="text-sm font-semibold tracking-tight whitespace-nowrap">
52
+ Claude Dashboard
53
+ </h1>
54
+ {title && (
55
+ <span className="text-xs text-muted-foreground truncate max-w-[300px]">
56
+ {title}
57
+ </span>
58
+ )}
59
+ <div className="hidden md:block h-4 w-px bg-border" />
60
+ <UsageIndicator />
61
+ </div>
62
+
63
+ <div className="flex items-center gap-3">
64
+ {status !== "pending" && (
65
+ <>
66
+ <span
67
+ className={`text-xs px-2 py-0.5 rounded-full font-medium ${statusColors[status] || ""}`}
68
+ >
69
+ {status.toUpperCase()}
70
+ </span>
71
+ <span className="text-xs text-muted-foreground">
72
+ {completedCount}/{AGENT_ORDER.length} agents
73
+ </span>
74
+ {startedAt && (
75
+ status === "running" || status === "paused"
76
+ ? <LiveElapsed startedAt={startedAt} />
77
+ : <span className="text-xs text-muted-foreground tabular-nums">
78
+ {formatDuration((completedAt ?? Date.now()) - startedAt)}
79
+ </span>
80
+ )}
81
+ </>
82
+ )}
83
+ </div>
84
+ </header>
85
+ );
86
+ }
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ import { useUsage } from "@/hooks/useUsage";
4
+ import { Tooltip } from "@/components/ui/tooltip";
5
+
6
+ /**
7
+ * Returns Tailwind color classes based on usage percentage thresholds.
8
+ * - < 50% → emerald (normal)
9
+ * - 50-80% → yellow (warning)
10
+ * - > 80% → red (danger)
11
+ */
12
+ function getUsageColor(utilization: number | null | undefined): string {
13
+ if (utilization == null) return "text-muted-foreground";
14
+ if (utilization > 80) return "text-red-400";
15
+ if (utilization >= 50) return "text-yellow-400";
16
+ return "text-emerald-400";
17
+ }
18
+
19
+ /**
20
+ * Formats a utilization number to a display string.
21
+ * Returns "--" if the value is unavailable.
22
+ */
23
+ function formatPercent(utilization: number | null | undefined): string {
24
+ if (utilization == null) return "--";
25
+ return `${Math.round(utilization)}%`;
26
+ }
27
+
28
+ /**
29
+ * Formats an ISO 8601 date string into a localized reset time string.
30
+ * Example: "2026-02-13 14:00"
31
+ */
32
+ function formatResetTime(resetsAt: string | null | undefined): string {
33
+ if (!resetsAt) return "N/A";
34
+ try {
35
+ const date = new Date(resetsAt);
36
+ return date.toLocaleString("zh-TW", {
37
+ month: "2-digit",
38
+ day: "2-digit",
39
+ hour: "2-digit",
40
+ minute: "2-digit",
41
+ hour12: false,
42
+ });
43
+ } catch {
44
+ return "N/A";
45
+ }
46
+ }
47
+
48
+ /** Single usage metric item props */
49
+ interface UsageItemProps {
50
+ label: string;
51
+ utilization: number | null | undefined;
52
+ resetsAt: string | null | undefined;
53
+ tooltipLabel: string;
54
+ }
55
+
56
+ /**
57
+ * A single usage metric with tooltip showing reset time.
58
+ */
59
+ function UsageItem({ label, utilization, resetsAt, tooltipLabel }: UsageItemProps) {
60
+ const tooltipContent = (
61
+ <div className="text-xs">
62
+ <div className="font-medium mb-0.5">{tooltipLabel}</div>
63
+ <div className="text-muted-foreground">
64
+ Usage: {formatPercent(utilization)}
65
+ </div>
66
+ <div className="text-muted-foreground">
67
+ Resets: {formatResetTime(resetsAt)}
68
+ </div>
69
+ </div>
70
+ );
71
+
72
+ return (
73
+ <Tooltip content={tooltipContent} side="bottom" delayMs={150}>
74
+ <span
75
+ className={`${getUsageColor(utilization)} cursor-default`}
76
+ aria-label={`${tooltipLabel}: ${formatPercent(utilization)}`}
77
+ >
78
+ {label}: {formatPercent(utilization)}
79
+ </span>
80
+ </Tooltip>
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Compact usage indicator showing three Claude Code usage metrics
86
+ * inline in the header. Designed to fit within h-12 without increasing height.
87
+ *
88
+ * Displays: Session | Week | Sonnet with color-coded percentages
89
+ * and tooltips showing reset times on hover.
90
+ */
91
+ export function UsageIndicator() {
92
+ const { data, loading } = useUsage();
93
+
94
+ // Show subtle loading skeleton while fetching initial data
95
+ if (loading) {
96
+ return (
97
+ <div
98
+ className="hidden md:flex items-center gap-2 text-xs text-muted-foreground"
99
+ aria-label="Loading usage data"
100
+ >
101
+ <span className="animate-pulse opacity-50">Session: --</span>
102
+ <span className="text-border opacity-30">|</span>
103
+ <span className="animate-pulse opacity-50">Week: --</span>
104
+ <span className="text-border opacity-30">|</span>
105
+ <span className="animate-pulse opacity-50">Sonnet: --</span>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ const session = data?.five_hour?.utilization ?? null;
111
+ const week = data?.seven_day?.utilization ?? null;
112
+ const sonnet = data?.seven_day_sonnet?.utilization ?? null;
113
+
114
+ // If there's an error and no data at all, show a subtle indicator
115
+ if (data?.error && session == null && week == null && sonnet == null) {
116
+ return (
117
+ <Tooltip
118
+ content={
119
+ <div className="text-xs text-muted-foreground">
120
+ {data.error}
121
+ </div>
122
+ }
123
+ side="bottom"
124
+ >
125
+ <div
126
+ className="hidden md:flex items-center gap-2 text-xs text-muted-foreground"
127
+ aria-label="Usage data unavailable"
128
+ >
129
+ <span className="opacity-50">Usage: --</span>
130
+ </div>
131
+ </Tooltip>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div
137
+ className="hidden md:flex items-center gap-2 text-xs tabular-nums"
138
+ role="status"
139
+ aria-label="Claude Code usage metrics"
140
+ >
141
+ <UsageItem
142
+ label="Session"
143
+ utilization={session}
144
+ resetsAt={data?.five_hour?.resets_at}
145
+ tooltipLabel="Current Session (5h window)"
146
+ />
147
+ <span className="text-border select-none" aria-hidden="true">|</span>
148
+ <UsageItem
149
+ label="Week"
150
+ utilization={week}
151
+ resetsAt={data?.seven_day?.resets_at}
152
+ tooltipLabel="Weekly All Models (7 days)"
153
+ />
154
+ <span className="text-border select-none" aria-hidden="true">|</span>
155
+ <UsageItem
156
+ label="Sonnet"
157
+ utilization={sonnet}
158
+ resetsAt={data?.seven_day_sonnet?.resets_at}
159
+ tooltipLabel="Weekly Sonnet (7 days)"
160
+ />
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useWorkflowStore } from "@/stores/workflowStore";
4
+ import { useAgentStore } from "@/stores/agentStore";
5
+ import { PIPELINE_STAGES } from "@/lib/workflow/types";
6
+ import { cn } from "@/lib/utils";
7
+ import { PipelineNode } from "./PipelineNode";
8
+
9
+ export function PipelineBar() {
10
+ const { currentStageIndex, status } = useWorkflowStore();
11
+ const agents = useAgentStore((s) => s.agents);
12
+
13
+ return (
14
+ <div className="h-10 border-b border-border bg-card/50 flex items-center px-4 gap-1">
15
+ {PIPELINE_STAGES.map((stage, stageIdx) => (
16
+ <div key={stage.index} className="flex items-center">
17
+ {stageIdx > 0 && (
18
+ <div className="w-6 h-px bg-border mx-1" />
19
+ )}
20
+ {stage.roles.length > 1 ? (
21
+ /* Multi-agent stage: group with a border */
22
+ <div className="flex items-center border border-border/50 rounded-lg px-1 gap-0.5">
23
+ {stage.roles.map((role, roleIdx) => (
24
+ <div key={role} className="flex items-center">
25
+ {roleIdx > 0 && (
26
+ <div className="w-px h-4 bg-border mx-0.5" />
27
+ )}
28
+ <PipelineNode
29
+ role={role}
30
+ status={agents[role].status}
31
+ isCurrent={stageIdx === currentStageIndex && status === "running"}
32
+ />
33
+ </div>
34
+ ))}
35
+ </div>
36
+ ) : (
37
+ /* Single-agent stage */
38
+ <PipelineNode
39
+ role={stage.roles[0]}
40
+ status={agents[stage.roles[0]].status}
41
+ isCurrent={stageIdx === currentStageIndex && status === "running"}
42
+ />
43
+ )}
44
+ </div>
45
+ ))}
46
+ <div className="w-6 h-px bg-border mx-1" />
47
+ <div
48
+ className={cn(
49
+ "px-3 py-1.5 rounded-md border text-xs font-medium",
50
+ status === "completed"
51
+ ? "border-emerald-500 bg-emerald-500/10 text-emerald-400"
52
+ : "border-gray-600 bg-gray-800/50 text-gray-500"
53
+ )}
54
+ >
55
+ Done
56
+ </div>
57
+ </div>
58
+ );
59
+ }