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,257 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { getDb } from './connection.ts';
3
+ import {
4
+ AGENT_ORDER,
5
+ type AgentRole,
6
+ type AgentStep,
7
+ type StepStatus,
8
+ type Workflow,
9
+ type WorkflowStatus,
10
+ } from '../workflow/types.ts';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Internal helpers: snake_case <-> camelCase row mapping
14
+ // ---------------------------------------------------------------------------
15
+
16
+ interface WorkflowRow {
17
+ id: string;
18
+ title: string;
19
+ user_prompt: string;
20
+ status: string;
21
+ current_step_index: number;
22
+ project_path: string;
23
+ created_at: string;
24
+ updated_at: string;
25
+ completed_at: string | null;
26
+ }
27
+
28
+ interface AgentStepRow {
29
+ id: string;
30
+ workflow_id: string;
31
+ role: string;
32
+ status: string;
33
+ prompt: string;
34
+ output: string;
35
+ error: string | null;
36
+ retry_count: number;
37
+ duration_ms: number | null;
38
+ tokens_in: number | null;
39
+ tokens_out: number | null;
40
+ started_at: string | null;
41
+ completed_at: string | null;
42
+ }
43
+
44
+ function rowToWorkflow(row: WorkflowRow): Workflow {
45
+ return {
46
+ id: row.id,
47
+ title: row.title,
48
+ userPrompt: row.user_prompt,
49
+ status: row.status as WorkflowStatus,
50
+ currentStepIndex: row.current_step_index,
51
+ projectPath: row.project_path,
52
+ createdAt: row.created_at,
53
+ updatedAt: row.updated_at,
54
+ completedAt: row.completed_at,
55
+ };
56
+ }
57
+
58
+ function rowToStep(row: AgentStepRow): AgentStep {
59
+ return {
60
+ id: row.id,
61
+ workflowId: row.workflow_id,
62
+ role: row.role as AgentRole,
63
+ status: row.status as StepStatus,
64
+ prompt: row.prompt,
65
+ output: row.output,
66
+ error: row.error,
67
+ retryCount: row.retry_count,
68
+ durationMs: row.duration_ms,
69
+ tokensIn: row.tokens_in,
70
+ tokensOut: row.tokens_out,
71
+ startedAt: row.started_at,
72
+ completedAt: row.completed_at,
73
+ };
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Workflow CRUD
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Create a new workflow together with its five agent-step rows (one per role
82
+ * in `AGENT_ORDER`). Everything runs inside a single transaction so that the
83
+ * workflow and its steps are always created atomically.
84
+ */
85
+ export function createWorkflow(
86
+ id: string,
87
+ title: string,
88
+ userPrompt: string,
89
+ projectPath: string,
90
+ ): Workflow {
91
+ const db = getDb();
92
+
93
+ const insertWorkflow = db.prepare(`
94
+ INSERT INTO workflows (id, title, user_prompt, status, current_step_index, project_path)
95
+ VALUES (@id, @title, @userPrompt, 'pending', 0, @projectPath)
96
+ `);
97
+
98
+ const insertStep = db.prepare(`
99
+ INSERT INTO agent_steps (id, workflow_id, role, status)
100
+ VALUES (@id, @workflowId, @role, 'pending')
101
+ `);
102
+
103
+ const txn = db.transaction(() => {
104
+ insertWorkflow.run({ id, title, userPrompt, projectPath });
105
+
106
+ for (const role of AGENT_ORDER) {
107
+ insertStep.run({
108
+ id: uuidv4(),
109
+ workflowId: id,
110
+ role,
111
+ });
112
+ }
113
+ });
114
+
115
+ txn();
116
+
117
+ return getWorkflow(id)!;
118
+ }
119
+
120
+ /**
121
+ * Retrieve a single workflow by id, or `null` if not found.
122
+ */
123
+ export function getWorkflow(id: string): Workflow | null {
124
+ const db = getDb();
125
+ const stmt = db.prepare('SELECT * FROM workflows WHERE id = ?');
126
+ const row = stmt.get(id) as WorkflowRow | undefined;
127
+ return row ? rowToWorkflow(row) : null;
128
+ }
129
+
130
+ /**
131
+ * List workflows ordered by creation time (newest first).
132
+ */
133
+ export function listWorkflows(limit = 50, offset = 0): Workflow[] {
134
+ const db = getDb();
135
+ const stmt = db.prepare(
136
+ 'SELECT * FROM workflows ORDER BY created_at DESC LIMIT ? OFFSET ?',
137
+ );
138
+ const rows = stmt.all(limit, offset) as WorkflowRow[];
139
+ return rows.map(rowToWorkflow);
140
+ }
141
+
142
+ /**
143
+ * Update a workflow's status (and optionally the current step index).
144
+ *
145
+ * `updated_at` is always bumped. When the status is a terminal state
146
+ * (`completed`, `failed`, `cancelled`) `completed_at` is also set.
147
+ */
148
+ export function updateWorkflowStatus(
149
+ id: string,
150
+ status: WorkflowStatus,
151
+ currentStepIndex?: number,
152
+ ): void {
153
+ const db = getDb();
154
+
155
+ const isTerminal =
156
+ status === 'completed' || status === 'failed' || status === 'cancelled';
157
+
158
+ if (currentStepIndex !== undefined) {
159
+ const stmt = db.prepare(`
160
+ UPDATE workflows
161
+ SET status = @status,
162
+ current_step_index = @currentStepIndex,
163
+ updated_at = datetime('now'),
164
+ completed_at = CASE WHEN @isTerminal THEN datetime('now') ELSE completed_at END
165
+ WHERE id = @id
166
+ `);
167
+ stmt.run({ id, status, currentStepIndex, isTerminal: isTerminal ? 1 : 0 });
168
+ } else {
169
+ const stmt = db.prepare(`
170
+ UPDATE workflows
171
+ SET status = @status,
172
+ updated_at = datetime('now'),
173
+ completed_at = CASE WHEN @isTerminal THEN datetime('now') ELSE completed_at END
174
+ WHERE id = @id
175
+ `);
176
+ stmt.run({ id, status, isTerminal: isTerminal ? 1 : 0 });
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // AgentStep CRUD
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /**
185
+ * Return all steps for a given workflow, ordered by the canonical agent order.
186
+ *
187
+ * We use a CASE expression so the rows always come back in the same sequence
188
+ * as `AGENT_ORDER` regardless of insertion order.
189
+ */
190
+ export function getStepsForWorkflow(workflowId: string): AgentStep[] {
191
+ const db = getDb();
192
+ const stmt = db.prepare(`
193
+ SELECT *
194
+ FROM agent_steps
195
+ WHERE workflow_id = ?
196
+ ORDER BY CASE role
197
+ WHEN 'pm' THEN 0
198
+ WHEN 'rd' THEN 1
199
+ WHEN 'ui' THEN 2
200
+ WHEN 'test' THEN 3
201
+ WHEN 'sec' THEN 4
202
+ ELSE 5
203
+ END
204
+ `);
205
+ const rows = stmt.all(workflowId) as AgentStepRow[];
206
+ return rows.map(rowToStep);
207
+ }
208
+
209
+ /**
210
+ * Partial update for an agent step. Only the fields present in `updates`
211
+ * will be written; all others are left untouched.
212
+ */
213
+ export interface StepUpdateFields {
214
+ status?: StepStatus;
215
+ prompt?: string;
216
+ output?: string;
217
+ error?: string | null;
218
+ retryCount?: number;
219
+ durationMs?: number | null;
220
+ tokensIn?: number | null;
221
+ tokensOut?: number | null;
222
+ startedAt?: string | null;
223
+ completedAt?: string | null;
224
+ }
225
+
226
+ export function updateStepStatus(id: string, updates: StepUpdateFields): void {
227
+ const db = getDb();
228
+
229
+ // Build SET clause dynamically based on provided keys.
230
+ const columnMap: Record<keyof StepUpdateFields, string> = {
231
+ status: 'status',
232
+ prompt: 'prompt',
233
+ output: 'output',
234
+ error: 'error',
235
+ retryCount: 'retry_count',
236
+ durationMs: 'duration_ms',
237
+ tokensIn: 'tokens_in',
238
+ tokensOut: 'tokens_out',
239
+ startedAt: 'started_at',
240
+ completedAt: 'completed_at',
241
+ };
242
+
243
+ const setClauses: string[] = [];
244
+ const params: Record<string, unknown> = { id };
245
+
246
+ for (const [key, column] of Object.entries(columnMap)) {
247
+ if (key in updates) {
248
+ setClauses.push(`${column} = @${key}`);
249
+ params[key] = (updates as Record<string, unknown>)[key] ?? null;
250
+ }
251
+ }
252
+
253
+ if (setClauses.length === 0) return;
254
+
255
+ const sql = `UPDATE agent_steps SET ${setClauses.join(', ')} WHERE id = @id`;
256
+ db.prepare(sql).run(params);
257
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * SQLite schema for the Claude Dashboard.
3
+ *
4
+ * Tables:
5
+ * - workflows : top-level workflow records
6
+ * - agent_steps : one row per agent role per workflow (5 per workflow)
7
+ */
8
+ export const SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS workflows (
10
+ id TEXT PRIMARY KEY,
11
+ title TEXT NOT NULL,
12
+ user_prompt TEXT NOT NULL,
13
+ status TEXT NOT NULL DEFAULT 'pending',
14
+ current_step_index INTEGER NOT NULL DEFAULT 0,
15
+ project_path TEXT NOT NULL,
16
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
17
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ completed_at TEXT
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS agent_steps (
22
+ id TEXT PRIMARY KEY,
23
+ workflow_id TEXT NOT NULL,
24
+ role TEXT NOT NULL,
25
+ status TEXT NOT NULL DEFAULT 'pending',
26
+ prompt TEXT NOT NULL DEFAULT '',
27
+ output TEXT NOT NULL DEFAULT '',
28
+ error TEXT,
29
+ retry_count INTEGER NOT NULL DEFAULT 0,
30
+ duration_ms INTEGER,
31
+ tokens_in INTEGER,
32
+ tokens_out INTEGER,
33
+ started_at TEXT,
34
+ completed_at TEXT,
35
+ FOREIGN KEY (workflow_id) REFERENCES workflows(id)
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_agent_steps_workflow ON agent_steps(workflow_id);
39
+ `;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * OutputBuffer: Batches high-frequency token streams into 50ms flushes
3
+ */
4
+ export class OutputBuffer {
5
+ private buffer: string[] = [];
6
+ private timer: ReturnType<typeof setTimeout> | null = null;
7
+ private readonly flushIntervalMs: number;
8
+ private readonly onFlush: (chunks: string[]) => void;
9
+
10
+ constructor(onFlush: (chunks: string[]) => void, flushIntervalMs = 50) {
11
+ this.onFlush = onFlush;
12
+ this.flushIntervalMs = flushIntervalMs;
13
+ }
14
+
15
+ push(chunk: string) {
16
+ this.buffer.push(chunk);
17
+ if (!this.timer) {
18
+ this.timer = setTimeout(() => this.flush(), this.flushIntervalMs);
19
+ }
20
+ }
21
+
22
+ flush() {
23
+ if (this.timer) {
24
+ clearTimeout(this.timer);
25
+ this.timer = null;
26
+ }
27
+ if (this.buffer.length > 0) {
28
+ const chunks = this.buffer;
29
+ this.buffer = [];
30
+ this.onFlush(chunks);
31
+ }
32
+ }
33
+
34
+ destroy() {
35
+ if (this.timer) {
36
+ clearTimeout(this.timer);
37
+ this.timer = null;
38
+ }
39
+ this.buffer = [];
40
+ }
41
+ }
@@ -0,0 +1,106 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { existsSync, chmodSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { createRequire } from 'module';
5
+ const require = createRequire(import.meta.url);
6
+
7
+ // node-pty is optional - gracefully handle if not available
8
+ let pty: any;
9
+ try {
10
+ pty = require('node-pty');
11
+ fixSpawnHelperPermissions();
12
+ } catch {
13
+ console.warn('[PTY] node-pty not available, terminal features disabled');
14
+ }
15
+
16
+ function fixSpawnHelperPermissions() {
17
+ if (process.platform === 'win32') return;
18
+ try {
19
+ const ptyPath = require.resolve('node-pty');
20
+ const baseDir = join(dirname(ptyPath), '..');
21
+ const arch = process.arch;
22
+ const helperPath = join(baseDir, 'prebuilds', `${process.platform}-${arch}`, 'spawn-helper');
23
+ if (existsSync(helperPath)) {
24
+ chmodSync(helperPath, 0o755);
25
+ }
26
+ } catch {}
27
+ }
28
+
29
+ function getShellPath(): string {
30
+ if (process.platform === 'win32') return 'powershell.exe';
31
+
32
+ const preferred = process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash';
33
+ if (existsSync(preferred)) return preferred;
34
+ return '/bin/sh';
35
+ }
36
+
37
+ interface PtySession {
38
+ id: string;
39
+ process: any; // IPty
40
+ onData: (data: string) => void;
41
+ }
42
+
43
+ export class PtyManager {
44
+ private sessions: Map<string, PtySession> = new Map();
45
+
46
+ create(cwd: string, onData: (data: string) => void): string {
47
+ if (!pty) {
48
+ throw new Error('node-pty is not available');
49
+ }
50
+
51
+ const id = uuidv4();
52
+ const shell = getShellPath();
53
+
54
+ const proc = pty.spawn(shell, [], {
55
+ name: 'xterm-256color',
56
+ cols: 80,
57
+ rows: 24,
58
+ cwd,
59
+ env: { ...process.env },
60
+ });
61
+
62
+ proc.onData((data: string) => {
63
+ onData(data);
64
+ });
65
+
66
+ proc.onExit(() => {
67
+ this.sessions.delete(id);
68
+ });
69
+
70
+ this.sessions.set(id, { id, process: proc, onData });
71
+ return id;
72
+ }
73
+
74
+ write(id: string, data: string) {
75
+ const session = this.sessions.get(id);
76
+ if (session) {
77
+ session.process.write(data);
78
+ }
79
+ }
80
+
81
+ resize(id: string, cols: number, rows: number) {
82
+ const session = this.sessions.get(id);
83
+ if (session) {
84
+ session.process.resize(cols, rows);
85
+ }
86
+ }
87
+
88
+ kill(id: string) {
89
+ const session = this.sessions.get(id);
90
+ if (session) {
91
+ session.process.kill();
92
+ this.sessions.delete(id);
93
+ }
94
+ }
95
+
96
+ killAll() {
97
+ for (const session of this.sessions.values()) {
98
+ session.process.kill();
99
+ }
100
+ this.sessions.clear();
101
+ }
102
+
103
+ getSessionCount(): number {
104
+ return this.sessions.size;
105
+ }
106
+ }
@@ -0,0 +1,48 @@
1
+ import { execSync } from "child_process";
2
+ import { readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import path from "path";
5
+
6
+ interface ClaudeCredentials {
7
+ claudeAiOauth: {
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ };
11
+ }
12
+
13
+ /**
14
+ * macOS: 從 Keychain 讀取
15
+ */
16
+ function readFromKeychain(): string {
17
+ const raw = execSync(
18
+ 'security find-generic-password -s "Claude Code-credentials" -w',
19
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
20
+ ).trim();
21
+ return raw;
22
+ }
23
+
24
+ /**
25
+ * Windows / Linux: 從 ~/.claude/.credentials.json 讀取
26
+ */
27
+ function readFromCredentialsFile(): string {
28
+ const filePath = path.join(homedir(), ".claude", ".credentials.json");
29
+ return readFileSync(filePath, "utf-8");
30
+ }
31
+
32
+ export function getClaudeOAuthToken(): string {
33
+ try {
34
+ const raw = process.platform === "darwin"
35
+ ? readFromKeychain()
36
+ : readFromCredentialsFile();
37
+
38
+ const credentials: ClaudeCredentials = JSON.parse(raw);
39
+ const token = credentials?.claudeAiOauth?.accessToken;
40
+ if (!token) {
41
+ throw new Error("accessToken not found in credential JSON");
42
+ }
43
+ return token;
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : "Unknown error";
46
+ throw new Error(`Failed to read Claude Code OAuth token: ${message}`);
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,71 @@
1
+ import { WebSocket } from 'ws';
2
+
3
+ export interface ClientConnection {
4
+ ws: WebSocket;
5
+ id: string;
6
+ subscribedWorkflows: Set<string>;
7
+ }
8
+
9
+ export class ConnectionManager {
10
+ private clients: Map<string, ClientConnection> = new Map();
11
+ private clientCounter = 0;
12
+
13
+ addClient(ws: WebSocket): string {
14
+ const id = `client-${++this.clientCounter}`;
15
+ this.clients.set(id, {
16
+ ws,
17
+ id,
18
+ subscribedWorkflows: new Set(),
19
+ });
20
+ return id;
21
+ }
22
+
23
+ removeClient(id: string) {
24
+ this.clients.delete(id);
25
+ }
26
+
27
+ subscribeToWorkflow(clientId: string, workflowId: string) {
28
+ const client = this.clients.get(clientId);
29
+ if (client) {
30
+ client.subscribedWorkflows.add(workflowId);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Send to all clients subscribed to a workflow
36
+ */
37
+ broadcastToWorkflow(workflowId: string, message: object) {
38
+ const data = JSON.stringify(message);
39
+ for (const client of this.clients.values()) {
40
+ if (client.subscribedWorkflows.has(workflowId) && client.ws.readyState === WebSocket.OPEN) {
41
+ client.ws.send(data);
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Send to all connected clients
48
+ */
49
+ broadcastAll(message: object) {
50
+ const data = JSON.stringify(message);
51
+ for (const client of this.clients.values()) {
52
+ if (client.ws.readyState === WebSocket.OPEN) {
53
+ client.ws.send(data);
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Send to a specific client
60
+ */
61
+ sendTo(clientId: string, message: object) {
62
+ const client = this.clients.get(clientId);
63
+ if (client && client.ws.readyState === WebSocket.OPEN) {
64
+ client.ws.send(JSON.stringify(message));
65
+ }
66
+ }
67
+
68
+ getClientCount(): number {
69
+ return this.clients.size;
70
+ }
71
+ }
@@ -0,0 +1,90 @@
1
+ import type { AgentRole, AgentActivity } from '../workflow/types.ts';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Client -> Server messages
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type ClientMessage =
8
+ | { type: 'workflow:start'; payload: { prompt: string; projectPath: string } }
9
+ | { type: 'workflow:pause'; payload: { workflowId: string } }
10
+ | { type: 'workflow:resume'; payload: { workflowId: string } }
11
+ | { type: 'workflow:cancel'; payload: { workflowId: string } }
12
+ | { type: 'workflow:subscribe'; payload: { workflowId: string } }
13
+ | { type: 'terminal:create'; payload: { projectPath: string } }
14
+ | { type: 'terminal:input'; payload: { terminalId: string; data: string } }
15
+ | {
16
+ type: 'terminal:resize';
17
+ payload: { terminalId: string; cols: number; rows: number };
18
+ }
19
+ | { type: 'terminal:close'; payload: { terminalId: string } }
20
+ | { type: 'ping' };
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Server -> Client messages
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export type ServerMessage =
27
+ | { type: 'workflow:created'; payload: { workflowId: string; title: string } }
28
+ | { type: 'workflow:completed'; payload: { workflowId: string } }
29
+ | { type: 'workflow:failed'; payload: { workflowId: string; error: string } }
30
+ | { type: 'workflow:paused'; payload: { workflowId: string } }
31
+ | { type: 'workflow:cancelled'; payload: { workflowId: string } }
32
+ | {
33
+ type: 'step:started';
34
+ payload: { workflowId: string; stepId: string; role: AgentRole };
35
+ }
36
+ | {
37
+ type: 'step:stream';
38
+ payload: {
39
+ workflowId: string;
40
+ stepId: string;
41
+ role: AgentRole;
42
+ chunk: string;
43
+ };
44
+ }
45
+ | {
46
+ type: 'step:completed';
47
+ payload: {
48
+ workflowId: string;
49
+ stepId: string;
50
+ role: AgentRole;
51
+ output: string;
52
+ durationMs: number;
53
+ tokensIn?: number;
54
+ tokensOut?: number;
55
+ };
56
+ }
57
+ | {
58
+ type: 'step:failed';
59
+ payload: {
60
+ workflowId: string;
61
+ stepId: string;
62
+ role: AgentRole;
63
+ error: string;
64
+ };
65
+ }
66
+ | {
67
+ type: 'step:activity';
68
+ payload: {
69
+ workflowId: string;
70
+ stepId: string;
71
+ role: AgentRole;
72
+ activity: AgentActivity;
73
+ };
74
+ }
75
+ | {
76
+ type: 'step:retry';
77
+ payload: {
78
+ workflowId: string;
79
+ stepId: string;
80
+ role: AgentRole;
81
+ attempt: number;
82
+ maxRetries: number;
83
+ reason: string;
84
+ };
85
+ }
86
+ | { type: 'terminal:created'; payload: { terminalId: string } }
87
+ | { type: 'terminal:output'; payload: { terminalId: string; data: string } }
88
+ | { type: 'terminal:error'; payload: { error: string } }
89
+ | { type: 'terminal:closed'; payload: { terminalId: string } }
90
+ | { type: 'pong' };