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,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
|
+
);
|