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.
- package/.claude/settings.local.json +10 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/README.zh-TW.md +99 -0
- package/bin/cdb.ts +60 -0
- package/bun.lock +1612 -0
- package/bunfig.toml +4 -0
- package/components.json +20 -0
- package/next.config.ts +19 -0
- package/package.json +62 -0
- package/postcss.config.mjs +9 -0
- package/prompts/pm-system.md +61 -0
- package/prompts/rd-system.md +68 -0
- package/prompts/sec-system.md +93 -0
- package/prompts/test-system.md +71 -0
- package/prompts/ui-system.md +72 -0
- package/server.ts +118 -0
- package/sql.js.d.ts +33 -0
- package/src/__tests__/api/usage/route.test.ts +193 -0
- package/src/__tests__/components/layout/TopNav.test.tsx +155 -0
- package/src/__tests__/components/layout/UsageIndicator.test.tsx +503 -0
- package/src/__tests__/hooks/useUsage.test.tsx +174 -0
- package/src/__tests__/lib/usage/get-token.test.ts +117 -0
- package/src/__tests__/react-sanity.test.tsx +14 -0
- package/src/__tests__/sanity.test.ts +7 -0
- package/src/__tests__/setup.ts +1 -0
- package/src/app/api/health/route.ts +8 -0
- package/src/app/api/usage/route.ts +86 -0
- package/src/app/api/workflows/[id]/route.ts +17 -0
- package/src/app/api/workflows/route.ts +14 -0
- package/src/app/globals.css +74 -0
- package/src/app/history/page.tsx +15 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +112 -0
- package/src/components/agent/AgentCard.tsx +117 -0
- package/src/components/agent/AgentCardGrid.tsx +14 -0
- package/src/components/agent/AgentOutput.tsx +87 -0
- package/src/components/agent/AgentStatusBadge.tsx +20 -0
- package/src/components/events/EventLog.tsx +65 -0
- package/src/components/events/EventLogItem.tsx +39 -0
- package/src/components/history/HistoryTable.tsx +105 -0
- package/src/components/layout/DashboardShell.tsx +12 -0
- package/src/components/layout/TopNav.tsx +86 -0
- package/src/components/layout/UsageIndicator.tsx +163 -0
- package/src/components/pipeline/PipelineBar.tsx +59 -0
- package/src/components/pipeline/PipelineNode.tsx +55 -0
- package/src/components/terminal/TerminalPanel.tsx +138 -0
- package/src/components/terminal/XTermRenderer.tsx +129 -0
- package/src/components/ui/badge.tsx +37 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +80 -0
- package/src/components/ui/input.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/tooltip.tsx +73 -0
- package/src/components/workflow/WorkflowLauncher.tsx +102 -0
- package/src/hooks/useAgentStream.ts +27 -0
- package/src/hooks/useAutoScroll.ts +24 -0
- package/src/hooks/useUsage.ts +66 -0
- package/src/hooks/useWebSocket.ts +289 -0
- package/src/lib/agents/prompts.ts +341 -0
- package/src/lib/db/connection.ts +263 -0
- package/src/lib/db/queries.ts +257 -0
- package/src/lib/db/schema.ts +39 -0
- package/src/lib/output-buffer.ts +41 -0
- package/src/lib/terminal/pty-manager.ts +106 -0
- package/src/lib/usage/get-token.ts +48 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/websocket/connection-manager.ts +71 -0
- package/src/lib/websocket/protocol.ts +90 -0
- package/src/lib/websocket/server.ts +231 -0
- package/src/lib/workflow/agent-runner.ts +254 -0
- package/src/lib/workflow/context-builder.ts +62 -0
- package/src/lib/workflow/engine.ts +310 -0
- package/src/lib/workflow/pipeline.ts +28 -0
- package/src/lib/workflow/types.ts +111 -0
- package/src/stores/agentStore.ts +152 -0
- package/src/stores/eventStore.ts +35 -0
- package/src/stores/terminalStore.ts +20 -0
- package/src/stores/uiStore.ts +35 -0
- package/src/stores/workflowStore.ts +57 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/index.ts +12 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +25 -0
- package/tsconfig.server.json +21 -0
- 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
|
+
}
|