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,310 @@
1
+ import { EventEmitter } from 'events';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { type AgentRole, type AgentActivity, AGENT_CONFIG, PIPELINE_STAGES, getStageForRole, type WorkflowStatus } from './types.ts';
4
+ import { type PipelineState, createPipelineState } from './pipeline.ts';
5
+ import { AgentRunner } from './agent-runner.ts';
6
+ import { buildAgentPrompt, type AgentContext } from './context-builder.ts';
7
+ import { getSystemPrompt } from '../agents/prompts.ts';
8
+
9
+ export interface WorkflowEngineEvents {
10
+ 'workflow:created': (workflowId: string, title: string) => void;
11
+ 'workflow:completed': (workflowId: string) => void;
12
+ 'workflow:failed': (workflowId: string, error: string) => void;
13
+ 'workflow:paused': (workflowId: string) => void;
14
+ 'workflow:cancelled': (workflowId: string) => void;
15
+ 'step:started': (workflowId: string, stepId: string, role: AgentRole) => void;
16
+ 'step:stream': (workflowId: string, stepId: string, role: AgentRole, chunk: string) => void;
17
+ 'step:completed': (workflowId: string, stepId: string, role: AgentRole, output: string, durationMs: number, tokensIn?: number, tokensOut?: number) => void;
18
+ 'step:failed': (workflowId: string, stepId: string, role: AgentRole, error: string) => void;
19
+ 'step:activity': (workflowId: string, stepId: string, role: AgentRole, activity: AgentActivity) => void;
20
+ 'step:retry': (workflowId: string, stepId: string, role: AgentRole, attempt: number, maxRetries: number, reason: string) => void;
21
+ }
22
+
23
+ const MAX_RETRIES = 2;
24
+ const RETRY_DELAYS = [2000, 4000, 8000];
25
+
26
+ export class WorkflowEngine extends EventEmitter {
27
+ private pipelines: Map<string, PipelineState> = new Map();
28
+ private runners: Map<string, AgentRunner> = new Map();
29
+ private stepIds: Map<string, Map<AgentRole, string>> = new Map();
30
+ private stepOutputs: Map<string, Map<AgentRole, string>> = new Map();
31
+ private paused: Set<string> = new Set();
32
+
33
+ // Database operations injected from outside
34
+ private dbOps: {
35
+ createWorkflow: (id: string, title: string, userPrompt: string, projectPath: string) => void;
36
+ updateWorkflowStatus: (id: string, status: WorkflowStatus, currentStepIndex?: number) => void;
37
+ updateStepStatus: (id: string, updates: Record<string, unknown>) => void;
38
+ getStepsForWorkflow: (workflowId: string) => Array<{ id: string; role: AgentRole; output: string; status: string }>;
39
+ };
40
+
41
+ constructor(dbOps: WorkflowEngine['dbOps']) {
42
+ super();
43
+ this.dbOps = dbOps;
44
+ }
45
+
46
+ async startWorkflow(userPrompt: string, projectPath: string): Promise<string> {
47
+ const workflowId = uuidv4();
48
+ const title = userPrompt.slice(0, 80) + (userPrompt.length > 80 ? '...' : '');
49
+
50
+ this.dbOps.createWorkflow(workflowId, title, userPrompt, projectPath);
51
+
52
+ const pipeline = createPipelineState(workflowId);
53
+ pipeline.status = 'running';
54
+ this.pipelines.set(workflowId, pipeline);
55
+ this.stepOutputs.set(workflowId, new Map());
56
+
57
+ // Get step IDs from DB
58
+ const dbSteps = this.dbOps.getStepsForWorkflow(workflowId);
59
+ const stepIdMap = new Map<AgentRole, string>();
60
+ for (const step of dbSteps) {
61
+ stepIdMap.set(step.role, step.id);
62
+ }
63
+ this.stepIds.set(workflowId, stepIdMap);
64
+
65
+ this.dbOps.updateWorkflowStatus(workflowId, 'running');
66
+ this.emit('workflow:created', workflowId, title);
67
+
68
+ // Defer to macrotask so callers can subscribe (microtask) before first step:started fires
69
+ setTimeout(() => {
70
+ this.executePipeline(workflowId, userPrompt, projectPath).catch((err) => {
71
+ console.error(`Workflow ${workflowId} failed:`, err);
72
+ });
73
+ }, 0);
74
+
75
+ return workflowId;
76
+ }
77
+
78
+ private async executePipeline(workflowId: string, userPrompt: string, projectPath: string) {
79
+ const pipeline = this.pipelines.get(workflowId);
80
+ if (!pipeline) return;
81
+
82
+ for (const stage of PIPELINE_STAGES) {
83
+ // Check if cancelled
84
+ if ((pipeline.status as string) === 'cancelled') {
85
+ this.cleanupWorkflow(workflowId);
86
+ return;
87
+ }
88
+ // Wait while paused
89
+ while (this.paused.has(workflowId)) {
90
+ await new Promise((resolve) => setTimeout(resolve, 500));
91
+ if ((pipeline.status as string) === 'cancelled') {
92
+ this.cleanupWorkflow(workflowId);
93
+ return;
94
+ }
95
+ }
96
+
97
+ pipeline.currentStageIndex = stage.index;
98
+ this.dbOps.updateWorkflowStatus(workflowId, 'running', stage.index);
99
+
100
+ // Execute all roles in this stage in parallel
101
+ const results = await Promise.allSettled(
102
+ stage.roles.map(role => this.executeStep(workflowId, role, userPrompt, projectPath))
103
+ );
104
+
105
+ // Check for failures after all peers finish
106
+ const failedRoles: string[] = [];
107
+ for (let i = 0; i < results.length; i++) {
108
+ const result = results[i];
109
+ const role = stage.roles[i];
110
+ const stepState = pipeline.steps.find(s => s.role === role);
111
+
112
+ if (result.status === 'fulfilled' && result.value) {
113
+ if (stepState) stepState.status = 'completed';
114
+ } else {
115
+ failedRoles.push(AGENT_CONFIG[role].label);
116
+ }
117
+ }
118
+
119
+ if (failedRoles.length > 0) {
120
+ if ((pipeline.status as string) === 'cancelled') {
121
+ this.cleanupWorkflow(workflowId);
122
+ return;
123
+ }
124
+ pipeline.status = 'failed';
125
+ this.dbOps.updateWorkflowStatus(workflowId, 'failed');
126
+ this.emit('workflow:failed', workflowId, `Agent(s) ${failedRoles.join(', ')} failed`);
127
+ this.cleanupWorkflow(workflowId);
128
+ return;
129
+ }
130
+ }
131
+
132
+ pipeline.status = 'completed';
133
+ this.dbOps.updateWorkflowStatus(workflowId, 'completed');
134
+ this.emit('workflow:completed', workflowId);
135
+ this.cleanupWorkflow(workflowId);
136
+ }
137
+
138
+ private async executeStep(
139
+ workflowId: string,
140
+ role: AgentRole,
141
+ userPrompt: string,
142
+ projectPath: string
143
+ ): Promise<boolean> {
144
+ const stepIdMap = this.stepIds.get(workflowId)!;
145
+ const stepId = stepIdMap.get(role)!;
146
+ const outputsMap = this.stepOutputs.get(workflowId)!;
147
+
148
+ // Collect outputs from all prior stages (not peers in the same stage)
149
+ const currentStage = getStageForRole(role);
150
+ const previousOutputs: AgentContext[] = [];
151
+ for (const priorStage of PIPELINE_STAGES) {
152
+ if (priorStage.index >= currentStage.index) break;
153
+ for (const priorRole of priorStage.roles) {
154
+ const output = outputsMap.get(priorRole);
155
+ if (output) {
156
+ previousOutputs.push({ role: priorRole, output });
157
+ }
158
+ }
159
+ }
160
+
161
+ const prompt = buildAgentPrompt(role, userPrompt, previousOutputs, projectPath);
162
+ const systemPrompt = getSystemPrompt(role, projectPath);
163
+
164
+ let retries = 0;
165
+ while (retries <= MAX_RETRIES) {
166
+ // Bail out if workflow was cancelled during a retry
167
+ const pipeline = this.pipelines.get(workflowId);
168
+ if (pipeline && (pipeline.status as string) === 'cancelled') {
169
+ this.runners.delete(`${workflowId}:${role}`);
170
+ return false;
171
+ }
172
+
173
+ try {
174
+ this.dbOps.updateStepStatus(stepId, {
175
+ status: 'running',
176
+ prompt,
177
+ retryCount: retries,
178
+ startedAt: new Date().toISOString(),
179
+ });
180
+
181
+ this.emit('step:started', workflowId, stepId, role);
182
+ const startTime = Date.now();
183
+
184
+ const runner = new AgentRunner(role);
185
+ this.runners.set(`${workflowId}:${role}`, runner);
186
+
187
+ // Capture token usage from the runner's result event
188
+ let runnerTokensIn: number | undefined;
189
+ let runnerTokensOut: number | undefined;
190
+
191
+ runner.on('stream', (chunk: string) => {
192
+ this.emit('step:stream', workflowId, stepId, role, chunk);
193
+ });
194
+
195
+ runner.on('activity', (activity: AgentActivity) => {
196
+ this.emit('step:activity', workflowId, stepId, role, activity);
197
+ });
198
+
199
+ runner.on('result', (_output: string, tokensIn?: number, tokensOut?: number) => {
200
+ if (tokensIn !== undefined) runnerTokensIn = tokensIn;
201
+ if (tokensOut !== undefined) runnerTokensOut = tokensOut;
202
+ });
203
+
204
+ await runner.run(prompt, systemPrompt, projectPath);
205
+
206
+ const durationMs = Date.now() - startTime;
207
+ const output = runner.getOutput();
208
+ outputsMap.set(role, output);
209
+
210
+ this.dbOps.updateStepStatus(stepId, {
211
+ status: 'completed',
212
+ output,
213
+ durationMs,
214
+ tokensIn: runnerTokensIn ?? null,
215
+ tokensOut: runnerTokensOut ?? null,
216
+ completedAt: new Date().toISOString(),
217
+ });
218
+
219
+ this.emit('step:completed', workflowId, stepId, role, output, durationMs, runnerTokensIn, runnerTokensOut);
220
+ this.runners.delete(`${workflowId}:${role}`);
221
+ return true;
222
+ } catch (err: any) {
223
+ retries++;
224
+ const errMsg = err.message || String(err);
225
+ console.error(`[${AGENT_CONFIG[role].label}] Attempt ${retries} failed:`, errMsg);
226
+
227
+ if (retries > MAX_RETRIES) {
228
+ this.dbOps.updateStepStatus(stepId, {
229
+ status: 'failed',
230
+ error: errMsg,
231
+ completedAt: new Date().toISOString(),
232
+ });
233
+ this.emit('step:failed', workflowId, stepId, role, errMsg);
234
+ this.runners.delete(`${workflowId}:${role}`);
235
+ return false;
236
+ }
237
+
238
+ // Check cancellation before retrying
239
+ const pipelineState = this.pipelines.get(workflowId);
240
+ if (pipelineState && (pipelineState.status as string) === 'cancelled') {
241
+ this.runners.delete(`${workflowId}:${role}`);
242
+ return false;
243
+ }
244
+
245
+ // Notify about retry
246
+ this.emit('step:retry', workflowId, stepId, role, retries, MAX_RETRIES, errMsg);
247
+
248
+ // Exponential backoff
249
+ const delay = RETRY_DELAYS[retries - 1] || 8000;
250
+ await new Promise((resolve) => setTimeout(resolve, delay));
251
+ }
252
+ }
253
+
254
+ return false;
255
+ }
256
+
257
+ pauseWorkflow(workflowId: string) {
258
+ const pipeline = this.pipelines.get(workflowId);
259
+ if (pipeline && pipeline.status === 'running') {
260
+ this.paused.add(workflowId);
261
+ pipeline.status = 'paused';
262
+ this.dbOps.updateWorkflowStatus(workflowId, 'paused');
263
+ this.emit('workflow:paused', workflowId);
264
+ }
265
+ }
266
+
267
+ resumeWorkflow(workflowId: string) {
268
+ const pipeline = this.pipelines.get(workflowId);
269
+ if (pipeline && pipeline.status === 'paused') {
270
+ this.paused.delete(workflowId);
271
+ pipeline.status = 'running';
272
+ this.dbOps.updateWorkflowStatus(workflowId, 'running');
273
+ }
274
+ }
275
+
276
+ cancelWorkflow(workflowId: string) {
277
+ const pipeline = this.pipelines.get(workflowId);
278
+ if (!pipeline) return;
279
+
280
+ pipeline.status = 'cancelled';
281
+ this.paused.delete(workflowId);
282
+
283
+ // Kill running agents
284
+ for (const [key, runner] of this.runners.entries()) {
285
+ if (key.startsWith(workflowId)) {
286
+ runner.kill();
287
+ this.runners.delete(key);
288
+ }
289
+ }
290
+
291
+ this.dbOps.updateWorkflowStatus(workflowId, 'cancelled');
292
+ this.emit('workflow:cancelled', workflowId);
293
+ // Note: executePipeline loop will also call cleanupWorkflow when it detects cancelled.
294
+ // But if executePipeline already exited (e.g. stuck in pause polling), clean up here too.
295
+ // delete is idempotent so double-cleanup is safe.
296
+ this.stepIds.delete(workflowId);
297
+ this.stepOutputs.delete(workflowId);
298
+ }
299
+
300
+ private cleanupWorkflow(workflowId: string) {
301
+ this.pipelines.delete(workflowId);
302
+ this.stepIds.delete(workflowId);
303
+ this.stepOutputs.delete(workflowId);
304
+ this.paused.delete(workflowId);
305
+ }
306
+
307
+ getPipelineState(workflowId: string): PipelineState | undefined {
308
+ return this.pipelines.get(workflowId);
309
+ }
310
+ }
@@ -0,0 +1,28 @@
1
+ import { type AgentRole, AGENT_ORDER, type WorkflowStatus, type StepStatus } from './types.ts';
2
+
3
+ export interface PipelineState {
4
+ workflowId: string;
5
+ status: WorkflowStatus;
6
+ currentStageIndex: number;
7
+ steps: PipelineStepState[];
8
+ }
9
+
10
+ export interface PipelineStepState {
11
+ role: AgentRole;
12
+ status: StepStatus;
13
+ retryCount: number;
14
+ }
15
+
16
+ export function createPipelineState(workflowId: string): PipelineState {
17
+ return {
18
+ workflowId,
19
+ status: 'pending',
20
+ currentStageIndex: 0,
21
+ steps: AGENT_ORDER.map((role) => ({
22
+ role,
23
+ status: 'pending',
24
+ retryCount: 0,
25
+ })),
26
+ };
27
+ }
28
+
@@ -0,0 +1,111 @@
1
+ export type AgentRole = 'pm' | 'rd' | 'ui' | 'test' | 'sec';
2
+
3
+ export type WorkflowStatus =
4
+ | 'pending'
5
+ | 'running'
6
+ | 'paused'
7
+ | 'completed'
8
+ | 'failed'
9
+ | 'cancelled';
10
+
11
+ export type StepStatus =
12
+ | 'pending'
13
+ | 'running'
14
+ | 'completed'
15
+ | 'failed'
16
+ | 'skipped';
17
+
18
+ export interface Workflow {
19
+ id: string;
20
+ title: string;
21
+ userPrompt: string;
22
+ status: WorkflowStatus;
23
+ currentStepIndex: number;
24
+ projectPath: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ completedAt: string | null;
28
+ }
29
+
30
+ export interface AgentStep {
31
+ id: string;
32
+ workflowId: string;
33
+ role: AgentRole;
34
+ status: StepStatus;
35
+ prompt: string;
36
+ output: string;
37
+ error: string | null;
38
+ retryCount: number;
39
+ durationMs: number | null;
40
+ tokensIn: number | null;
41
+ tokensOut: number | null;
42
+ startedAt: string | null;
43
+ completedAt: string | null;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Pipeline stages — defines parallel execution groups
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface PipelineStage {
51
+ index: number;
52
+ roles: AgentRole[];
53
+ }
54
+
55
+ export const PIPELINE_STAGES: PipelineStage[] = [
56
+ { index: 0, roles: ['pm'] },
57
+ { index: 1, roles: ['rd', 'ui'] },
58
+ { index: 2, roles: ['test', 'sec'] },
59
+ ];
60
+
61
+ /** The fixed execution order for agents within a workflow (derived from stages). */
62
+ export const AGENT_ORDER: AgentRole[] = PIPELINE_STAGES.flatMap(s => s.roles);
63
+
64
+ /** Get the pipeline stage that contains the given role. */
65
+ export function getStageForRole(role: AgentRole): PipelineStage {
66
+ const stage = PIPELINE_STAGES.find(s => s.roles.includes(role));
67
+ if (!stage) throw new Error(`No stage found for role: ${role}`);
68
+ return stage;
69
+ }
70
+
71
+ export const AGENT_CONFIG: Record<
72
+ AgentRole,
73
+ { label: string; color: string; timeoutMs: number; tools: string[] }
74
+ > = {
75
+ pm: {
76
+ label: 'PM',
77
+ color: '#A855F7',
78
+ timeoutMs: 180_000,
79
+ tools: ['Read'],
80
+ },
81
+ rd: {
82
+ label: 'RD',
83
+ color: '#3B82F6',
84
+ timeoutMs: 600_000,
85
+ tools: ['Read', 'Edit', 'Bash'],
86
+ },
87
+ ui: {
88
+ label: 'UI',
89
+ color: '#22C55E',
90
+ timeoutMs: 600_000,
91
+ tools: ['Read', 'Edit', 'Bash'],
92
+ },
93
+ test: {
94
+ label: 'TEST',
95
+ color: '#F97316',
96
+ timeoutMs: 600_000,
97
+ tools: ['Read', 'Edit', 'Bash'],
98
+ },
99
+ sec: {
100
+ label: 'SEC',
101
+ color: '#EF4444',
102
+ timeoutMs: 300_000,
103
+ tools: ['Read', 'Bash'],
104
+ },
105
+ };
106
+
107
+ export type AgentActivity =
108
+ | { kind: 'idle' }
109
+ | { kind: 'thinking' }
110
+ | { kind: 'tool_use'; toolName: string }
111
+ | { kind: 'text' };
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import type { AgentRole, StepStatus, AgentActivity } from "@/lib/workflow/types";
5
+ import { AGENT_ORDER } from "@/lib/workflow/types";
6
+
7
+ export interface AgentState {
8
+ role: AgentRole;
9
+ status: StepStatus;
10
+ outputChunks: string[];
11
+ error: string | null;
12
+ startedAt: number | null;
13
+ completedAt: number | null;
14
+ durationMs: number | null;
15
+ tokensIn: number | null;
16
+ tokensOut: number | null;
17
+ activity: AgentActivity;
18
+ lastActivityAt: number | null;
19
+ retryCount: number;
20
+ }
21
+
22
+ interface AgentStoreState {
23
+ agents: Record<AgentRole, AgentState>;
24
+ appendChunks: (role: AgentRole, chunks: string[]) => void;
25
+ setAgentStatus: (role: AgentRole, status: StepStatus) => void;
26
+ setAgentStarted: (role: AgentRole) => void;
27
+ setAgentCompleted: (
28
+ role: AgentRole,
29
+ output: string,
30
+ durationMs: number,
31
+ tokensIn?: number,
32
+ tokensOut?: number
33
+ ) => void;
34
+ setAgentFailed: (role: AgentRole, error: string) => void;
35
+ setAgentActivity: (role: AgentRole, activity: AgentActivity) => void;
36
+ setAgentRetry: (role: AgentRole, attempt: number) => void;
37
+ resetAll: () => void;
38
+ }
39
+
40
+ function createInitialAgents(): Record<AgentRole, AgentState> {
41
+ const agents = {} as Record<AgentRole, AgentState>;
42
+ for (const role of AGENT_ORDER) {
43
+ agents[role] = {
44
+ role,
45
+ status: "pending",
46
+ outputChunks: [],
47
+ error: null,
48
+ startedAt: null,
49
+ completedAt: null,
50
+ durationMs: null,
51
+ tokensIn: null,
52
+ tokensOut: null,
53
+ activity: { kind: "idle" },
54
+ lastActivityAt: null,
55
+ retryCount: 0,
56
+ };
57
+ }
58
+ return agents;
59
+ }
60
+
61
+ export const useAgentStore = create<AgentStoreState>((set) => ({
62
+ agents: createInitialAgents(),
63
+
64
+ appendChunks: (role, chunks) =>
65
+ set((state) => ({
66
+ agents: {
67
+ ...state.agents,
68
+ [role]: {
69
+ ...state.agents[role],
70
+ outputChunks: [...state.agents[role].outputChunks, ...chunks],
71
+ },
72
+ },
73
+ })),
74
+
75
+ setAgentStatus: (role, status) =>
76
+ set((state) => ({
77
+ agents: {
78
+ ...state.agents,
79
+ [role]: { ...state.agents[role], status },
80
+ },
81
+ })),
82
+
83
+ setAgentStarted: (role) =>
84
+ set((state) => ({
85
+ agents: {
86
+ ...state.agents,
87
+ [role]: {
88
+ ...state.agents[role],
89
+ status: "running",
90
+ startedAt: Date.now(),
91
+ outputChunks: [],
92
+ error: null,
93
+ activity: { kind: "idle" },
94
+ lastActivityAt: null,
95
+ // retryCount is preserved — it's set by setAgentRetry before restart
96
+ },
97
+ },
98
+ })),
99
+
100
+ setAgentCompleted: (role, output, durationMs, tokensIn, tokensOut) =>
101
+ set((state) => ({
102
+ agents: {
103
+ ...state.agents,
104
+ [role]: {
105
+ ...state.agents[role],
106
+ status: "completed",
107
+ completedAt: Date.now(),
108
+ durationMs,
109
+ tokensIn: tokensIn ?? null,
110
+ tokensOut: tokensOut ?? null,
111
+ },
112
+ },
113
+ })),
114
+
115
+ setAgentFailed: (role, error) =>
116
+ set((state) => ({
117
+ agents: {
118
+ ...state.agents,
119
+ [role]: {
120
+ ...state.agents[role],
121
+ status: "failed",
122
+ error,
123
+ completedAt: Date.now(),
124
+ },
125
+ },
126
+ })),
127
+
128
+ setAgentActivity: (role, activity) =>
129
+ set((state) => ({
130
+ agents: {
131
+ ...state.agents,
132
+ [role]: {
133
+ ...state.agents[role],
134
+ activity,
135
+ lastActivityAt: Date.now(),
136
+ },
137
+ },
138
+ })),
139
+
140
+ setAgentRetry: (role, attempt) =>
141
+ set((state) => ({
142
+ agents: {
143
+ ...state.agents,
144
+ [role]: {
145
+ ...state.agents[role],
146
+ retryCount: attempt,
147
+ },
148
+ },
149
+ })),
150
+
151
+ resetAll: () => set({ agents: createInitialAgents() }),
152
+ }));
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import type { EventLogItem } from "@/types";
5
+
6
+ const MAX_EVENTS = 5000;
7
+
8
+ interface EventStoreState {
9
+ events: EventLogItem[];
10
+ addEvent: (event: Omit<EventLogItem, "id" | "timestamp">) => void;
11
+ clear: () => void;
12
+ }
13
+
14
+ let eventCounter = 0;
15
+
16
+ export const useEventStore = create<EventStoreState>((set) => ({
17
+ events: [],
18
+
19
+ addEvent: (event) =>
20
+ set((state) => {
21
+ const newEvent: EventLogItem = {
22
+ ...event,
23
+ id: `evt-${++eventCounter}`,
24
+ timestamp: new Date().toISOString(),
25
+ };
26
+ const events = [...state.events, newEvent];
27
+ // Trim if over max
28
+ if (events.length > MAX_EVENTS) {
29
+ return { events: events.slice(-MAX_EVENTS) };
30
+ }
31
+ return { events };
32
+ }),
33
+
34
+ clear: () => set({ events: [] }),
35
+ }));
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+
5
+ interface TerminalStoreState {
6
+ terminalId: string | null;
7
+ connected: boolean;
8
+ setTerminalId: (id: string | null) => void;
9
+ setConnected: (connected: boolean) => void;
10
+ reset: () => void;
11
+ }
12
+
13
+ export const useTerminalStore = create<TerminalStoreState>((set) => ({
14
+ terminalId: null,
15
+ connected: false,
16
+
17
+ setTerminalId: (id) => set({ terminalId: id }),
18
+ setConnected: (connected) => set({ connected }),
19
+ reset: () => set({ terminalId: null, connected: false }),
20
+ }));
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import { persist } from "zustand/middleware";
5
+
6
+ interface UiStoreState {
7
+ agentPanelHeight: number;
8
+ bottomPanelHeight: number;
9
+ terminalVisible: boolean;
10
+ eventLogVisible: boolean;
11
+ setAgentPanelHeight: (height: number) => void;
12
+ setBottomPanelHeight: (height: number) => void;
13
+ toggleTerminal: () => void;
14
+ toggleEventLog: () => void;
15
+ }
16
+
17
+ export const useUiStore = create<UiStoreState>()(
18
+ persist(
19
+ (set) => ({
20
+ agentPanelHeight: 400,
21
+ bottomPanelHeight: 300,
22
+ terminalVisible: true,
23
+ eventLogVisible: true,
24
+ setAgentPanelHeight: (height) => set({ agentPanelHeight: height }),
25
+ setBottomPanelHeight: (height) => set({ bottomPanelHeight: height }),
26
+ toggleTerminal: () =>
27
+ set((state) => ({ terminalVisible: !state.terminalVisible })),
28
+ toggleEventLog: () =>
29
+ set((state) => ({ eventLogVisible: !state.eventLogVisible })),
30
+ }),
31
+ {
32
+ name: "claude-dashboard-ui",
33
+ }
34
+ )
35
+ );