claude-ws 0.3.97 → 0.3.99

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 (86) hide show
  1. package/locales/de.json +374 -12
  2. package/locales/en.json +374 -12
  3. package/locales/es.json +398 -11
  4. package/locales/fr.json +398 -11
  5. package/locales/ja.json +398 -11
  6. package/locales/ko.json +398 -11
  7. package/locales/vi.json +374 -12
  8. package/locales/zh.json +398 -11
  9. package/package.json +1 -1
  10. package/server.ts +283 -6
  11. package/src/app/[locale]/not-found.tsx +6 -3
  12. package/src/app/[locale]/page.tsx +14 -4
  13. package/src/app/api/attempts/[id]/workflow/route.ts +76 -0
  14. package/src/app/api/questions/answer/route.ts +58 -0
  15. package/src/app/api/questions/route.ts +68 -0
  16. package/src/app/api/tasks/[id]/compact/route.ts +62 -0
  17. package/src/components/access-anywhere/api-access-key-setup-modal.tsx +2 -2
  18. package/src/components/access-anywhere/tunnel-settings-dialog.tsx +6 -6
  19. package/src/components/access-anywhere/wizard-step-ctunnel.tsx +8 -8
  20. package/src/components/agent-factory/dependency-tree.tsx +5 -3
  21. package/src/components/agent-factory/discovery-dialog.tsx +26 -22
  22. package/src/components/agent-factory/plugin-detail-dialog.tsx +41 -38
  23. package/src/components/agent-factory/plugin-form-dialog.tsx +23 -20
  24. package/src/components/agent-factory/plugin-list.tsx +20 -17
  25. package/src/components/agent-factory/upload-dialog.tsx +17 -14
  26. package/src/components/auth/agent-provider-dialog.tsx +67 -65
  27. package/src/components/auth/api-key-dialog.tsx +14 -11
  28. package/src/components/auth/auth-error-message.tsx +6 -3
  29. package/src/components/editor/code-editor-with-inline-edit.tsx +4 -2
  30. package/src/components/editor/file-diff-resolver-modal.tsx +31 -26
  31. package/src/components/editor/inline-edit-dialog.tsx +9 -6
  32. package/src/components/editor/selection-mention-popup.tsx +3 -1
  33. package/src/components/header/project-selector.tsx +7 -4
  34. package/src/components/header.tsx +70 -4
  35. package/src/components/kanban/column.tsx +11 -0
  36. package/src/components/kanban/task-card.tsx +70 -4
  37. package/src/components/project-settings/component-selector.tsx +3 -1
  38. package/src/components/project-settings/plugin-upload-dialog.tsx +7 -5
  39. package/src/components/project-settings/project-settings-dialog.tsx +5 -3
  40. package/src/components/questions/questions-panel.tsx +136 -0
  41. package/src/components/settings/folder-browser-dialog.tsx +29 -25
  42. package/src/components/settings/settings-page.tsx +64 -18
  43. package/src/components/settings/setup-dialog.tsx +26 -23
  44. package/src/components/setup/unified-setup-wizard.tsx +12 -9
  45. package/src/components/sidebar/file-browser/file-create-buttons.tsx +7 -3
  46. package/src/components/sidebar/file-browser/file-tab-content.tsx +19 -15
  47. package/src/components/sidebar/file-browser/file-tabs-panel.tsx +7 -4
  48. package/src/components/sidebar/file-browser/file-tree.tsx +3 -1
  49. package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +6 -4
  50. package/src/components/sidebar/git-changes/commit-details-modal.tsx +5 -3
  51. package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +3 -1
  52. package/src/components/sidebar/git-changes/git-file-item.tsx +8 -6
  53. package/src/components/sidebar/git-changes/git-graph.tsx +8 -5
  54. package/src/components/sidebar/git-changes/git-panel.tsx +28 -27
  55. package/src/components/sidebar/git-changes/git-section.tsx +5 -3
  56. package/src/components/sidebar/shells/shell-panel.tsx +3 -1
  57. package/src/components/task/attachment-bar.tsx +4 -1
  58. package/src/components/task/attempt-item.tsx +7 -5
  59. package/src/components/task/conversation-view.tsx +21 -13
  60. package/src/components/task/floating-chat-window.tsx +14 -5
  61. package/src/components/task/interactive-command/checkpoint-list.tsx +5 -3
  62. package/src/components/task/interactive-command/confirm-dialog.tsx +9 -4
  63. package/src/components/task/interactive-command/interactive-command-overlay.tsx +23 -9
  64. package/src/components/task/interactive-command/question-prompt.tsx +12 -8
  65. package/src/components/task/pending-question-indicator.tsx +5 -3
  66. package/src/components/task/prompt-input.tsx +1 -1
  67. package/src/components/task/shell-log-view.tsx +3 -1
  68. package/src/components/task/status-line.tsx +84 -23
  69. package/src/components/task/task-detail-panel.tsx +27 -27
  70. package/src/components/task/task-shell-indicator.tsx +10 -6
  71. package/src/components/terminal/terminal-context-menu.tsx +6 -4
  72. package/src/components/terminal/terminal-instance.tsx +11 -3
  73. package/src/components/terminal/terminal-panel.tsx +6 -3
  74. package/src/components/terminal/terminal-shortcut-bar.tsx +3 -1
  75. package/src/components/terminal/terminal-tab-bar.tsx +5 -3
  76. package/src/components/workflow/workflow-panel.tsx +181 -0
  77. package/src/hooks/use-attempt-stream.ts +96 -3
  78. package/src/lib/agent-manager.ts +89 -3
  79. package/src/lib/db/index.ts +18 -0
  80. package/src/lib/db/schema.ts +29 -0
  81. package/src/lib/process-manager.ts +28 -7
  82. package/src/lib/session-manager.ts +60 -0
  83. package/src/lib/usage-tracker.ts +19 -19
  84. package/src/lib/workflow-tracker.ts +118 -20
  85. package/src/stores/questions-store.ts +76 -0
  86. package/src/stores/workflow-store.ts +71 -0
@@ -15,6 +15,7 @@ import { EventEmitter } from 'events';
15
15
  import { existsSync, readFileSync } from 'fs';
16
16
  import { join, resolve } from 'path';
17
17
  import { homedir } from 'os';
18
+ import { normalize } from 'path';
18
19
  import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
19
20
  import type { ClaudeOutput } from '../types';
20
21
  import { adaptSDKMessage, isValidSDKMessage, type BackgroundShellInfo, type SDKResultMessage } from './sdk-event-adapter';
@@ -224,8 +225,10 @@ interface AgentEvents {
224
225
  stderr: (data: { attemptId: string; content: string }) => void;
225
226
  exit: (data: { attemptId: string; code: number | null }) => void;
226
227
  question: (data: { attemptId: string; toolUseId: string; questions: unknown[] }) => void;
228
+ questionResolved: (data: { attemptId: string }) => void;
227
229
  backgroundShell: (data: { attemptId: string; shell: BackgroundShellInfo }) => void;
228
230
  trackedProcess: (data: { attemptId: string; pid: number; command: string; logFile?: string }) => void;
231
+ promptTooLong: (data: { attemptId: string }) => void;
229
232
  }
230
233
 
231
234
  export interface AgentStartOptions {
@@ -413,6 +416,30 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
413
416
  log.debug({ path: `${projectPath}/.mcp.json` }, 'No MCP config found');
414
417
  }
415
418
 
419
+ // Resolve claude executable path for SDK (Windows only)
420
+ // On Windows, the SDK defaults to running its bundled cli.js via `bun cli.js`,
421
+ // which causes EPERM on C:\Windows\System32\ due to a Bun PATH-reading bug.
422
+ // Fix: pass the real claude.exe path directly so the SDK spawns it as a native binary.
423
+ // On other platforms (Linux/macOS), leave undefined so SDK uses its default.
424
+ const resolvedClaudePath = (() => {
425
+ if (process.platform !== 'win32') return undefined;
426
+ const envPath = process.env.CLAUDE_PATH;
427
+ if (envPath && existsSync(normalize(envPath))) {
428
+ return normalize(envPath);
429
+ }
430
+ // Fallback: search common Windows locations
431
+ const home = process.env.USERPROFILE || process.env.HOME || '';
432
+ const candidates = [
433
+ join(home, '.local', 'bin', 'claude.exe'),
434
+ join(home, 'AppData', 'Local', 'Programs', 'claude', 'claude.exe'),
435
+ ];
436
+ for (const c of candidates) {
437
+ if (existsSync(c)) return c;
438
+ }
439
+ return undefined;
440
+
441
+ })();
442
+
416
443
  // Configure SDK query options
417
444
  // resumeSessionAt: resume conversation at specific message UUID (for rewind)
418
445
  // Model priority: provided model > DEFAULT_MODEL ('opus')
@@ -525,10 +552,14 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
525
552
  ? `You are powered by the model named ${modelDisplayName}. The exact model ID is ${effectiveModel}.`
526
553
  : `You are powered by the model ${effectiveModel}.`;
527
554
 
555
+ log.info({ resolvedClaudePath }, 'Using claude executable path');
528
556
  const response = query({
529
557
  prompt,
530
558
  options: {
531
559
  ...queryOptions,
560
+ // Pass the real claude.exe path so the SDK doesn't fall back to its bundled cli.js
561
+ // Running `bun <sdk_cli.js>` on Windows causes EPERM errors on C:\Windows\System32\
562
+ ...(resolvedClaudePath ? { pathToClaudeCodeExecutable: resolvedClaudePath } : {}),
532
563
  systemPrompt: { type: 'preset' as const, preset: 'claude_code' as const, append: modelIdentity },
533
564
  },
534
565
  });
@@ -573,18 +604,33 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
573
604
  // Track subagent workflow (from assistant messages with Task tool)
574
605
  // Also track Bash tool_uses to correlate with BGPID results
575
606
  if (message.type === 'assistant' && 'message' in message) {
576
- const assistantMsg = message as { message: { content: Array<{ type: string; id?: string; name?: string; input?: unknown }> }; parent_tool_use_id: string | null };
607
+ const assistantMsg = message as unknown as { message: { content: Array<{ type: string; id?: string; name?: string; input?: unknown }> }; parent_tool_use_id: string | null };
577
608
  for (const block of assistantMsg.message.content) {
578
609
  if (block.type === 'tool_use' && block.name === 'Task' && block.id) {
579
- const taskInput = (block as { input?: { subagent_type?: string } }).input;
610
+ const taskInput = (block as { input?: { subagent_type?: string; team_name?: string; name?: string } }).input;
580
611
  const subagentType = taskInput?.subagent_type || 'unknown';
581
612
  workflowTracker.trackSubagentStart(
582
613
  attemptId,
583
614
  block.id,
584
615
  subagentType,
585
- assistantMsg.parent_tool_use_id
616
+ assistantMsg.parent_tool_use_id,
617
+ { teamName: taskInput?.team_name, name: taskInput?.name }
586
618
  );
587
619
  }
620
+ // Track TeamCreate tool usage for workflow visualization
621
+ if (block.type === 'tool_use' && block.name === 'TeamCreate' && block.id) {
622
+ const teamInput = (block as { input?: { team_name?: string } }).input;
623
+ if (teamInput?.team_name) {
624
+ workflowTracker.trackTeamCreate(attemptId, teamInput.team_name);
625
+ }
626
+ }
627
+ // Track SendMessage tool usage for inter-agent message visualization
628
+ if (block.type === 'tool_use' && block.name === 'SendMessage' && block.id) {
629
+ const msgInput = (block as { input?: { type?: string; recipient?: string; content?: string; summary?: string } }).input;
630
+ if (msgInput) {
631
+ workflowTracker.trackMessage(attemptId, msgInput);
632
+ }
633
+ }
588
634
  // Track Bash tool_uses for BGPID correlation
589
635
  if (block.type === 'tool_use' && block.name === 'Bash' && block.id) {
590
636
  const bashInput = block.input as { command?: string } | undefined;
@@ -783,6 +829,13 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
783
829
 
784
830
  this.emit('stderr', { attemptId, content: `${errorName}: ${errorMessage}` });
785
831
 
832
+ // Detect "prompt too long" errors for auto-compact handling
833
+ const isPromptTooLong = errorMessage.toLowerCase().includes('prompt is too long') ||
834
+ errorMessage.toLowerCase().includes('request too large');
835
+ if (isPromptTooLong) {
836
+ this.emit('promptTooLong', { attemptId });
837
+ }
838
+
786
839
  // Determine exit code based on error type
787
840
  const code = wasAborted ? null : 1;
788
841
 
@@ -821,6 +874,7 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
821
874
  pending.resolve({ questions, answers });
822
875
  this.pendingQuestions.delete(attemptId);
823
876
  this.pendingQuestionData.delete(attemptId);
877
+ this.emit('questionResolved', { attemptId });
824
878
  return true;
825
879
  }
826
880
 
@@ -839,6 +893,7 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
839
893
  pending.resolve(null);
840
894
  this.pendingQuestions.delete(attemptId);
841
895
  this.pendingQuestionData.delete(attemptId);
896
+ this.emit('questionResolved', { attemptId });
842
897
  return true;
843
898
  }
844
899
 
@@ -856,6 +911,16 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
856
911
  return this.pendingQuestionData.get(attemptId) || null;
857
912
  }
858
913
 
914
+ /**
915
+ * Get all pending questions across all running attempts
916
+ */
917
+ getAllPendingQuestions(): Array<{ attemptId: string; toolUseId: string; questions: unknown[]; timestamp: number }> {
918
+ return Array.from(this.pendingQuestionData.entries()).map(([attemptId, data]) => ({
919
+ attemptId,
920
+ ...data,
921
+ }));
922
+ }
923
+
859
924
  /**
860
925
  * Send input to a running agent (legacy method)
861
926
  * @deprecated Use answerQuestion() for AskUserQuestion responses
@@ -872,6 +937,27 @@ Your task is INCOMPLETE until:\n1. File exists with valid content\n2. You have R
872
937
  return false;
873
938
  }
874
939
 
940
+ /**
941
+ * Compact a conversation by starting a fresh session with context summary
942
+ * Cannot resume the old session (it's at/near context limit), so we start
943
+ * fresh and carry forward key context via the prompt.
944
+ */
945
+ async compact(options: { attemptId: string; projectPath: string; conversationSummary?: string }): Promise<void> {
946
+ const { attemptId, projectPath, conversationSummary } = options;
947
+
948
+ const compactPrompt = conversationSummary
949
+ ? `You are continuing a previous conversation that reached the context limit. Here is a summary of the previous context:\n\n${conversationSummary}\n\nPlease acknowledge this context briefly and let the user know you're ready to continue.`
950
+ : 'A previous conversation reached the context limit. Please let the user know you are ready to continue with a fresh context.';
951
+
952
+ // Start a FRESH session — do NOT resume the old session since it's at/near the context limit
953
+ await this.start({
954
+ attemptId,
955
+ projectPath,
956
+ prompt: compactPrompt,
957
+ maxTurns: 1,
958
+ });
959
+ }
960
+
875
961
  /**
876
962
  * Cancel a running agent
877
963
  * Uses SDK Query.close() for graceful termination, falls back to AbortController
@@ -282,6 +282,24 @@ export function initDb() {
282
282
  value TEXT NOT NULL,
283
283
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
284
284
  );
285
+
286
+ CREATE TABLE IF NOT EXISTS subagents (
287
+ id TEXT PRIMARY KEY,
288
+ attempt_id TEXT NOT NULL,
289
+ type TEXT NOT NULL,
290
+ name TEXT,
291
+ parent_id TEXT,
292
+ team_name TEXT,
293
+ status TEXT NOT NULL CHECK(status IN ('in_progress', 'completed', 'failed', 'orphaned')),
294
+ error TEXT,
295
+ started_at INTEGER,
296
+ completed_at INTEGER,
297
+ duration_ms INTEGER,
298
+ depth INTEGER NOT NULL DEFAULT 0,
299
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
300
+ );
301
+
302
+ CREATE INDEX IF NOT EXISTS idx_subagents_attempt ON subagents(attempt_id);
285
303
  `);
286
304
  }
287
305
 
@@ -275,6 +275,33 @@ export const shells = sqliteTable(
275
275
  ]
276
276
  );
277
277
 
278
+ // Subagents table - workflow agent tracking per attempt
279
+ export const subagents = sqliteTable(
280
+ 'subagents',
281
+ {
282
+ id: text('id').primaryKey(), // tool_use_id
283
+ attemptId: text('attempt_id').notNull(),
284
+ type: text('type').notNull(), // subagent_type
285
+ name: text('name'), // agent name
286
+ parentId: text('parent_id'), // parent tool_use_id
287
+ teamName: text('team_name'),
288
+ status: text('status', {
289
+ enum: ['in_progress', 'completed', 'failed', 'orphaned'],
290
+ }).notNull(),
291
+ error: text('error'),
292
+ startedAt: integer('started_at'),
293
+ completedAt: integer('completed_at'),
294
+ durationMs: integer('duration_ms'),
295
+ depth: integer('depth').notNull().default(0),
296
+ createdAt: integer('created_at', { mode: 'number' })
297
+ .notNull()
298
+ .$defaultFn(() => Date.now()),
299
+ },
300
+ (table) => [
301
+ index('idx_subagents_attempt').on(table.attemptId),
302
+ ]
303
+ );
304
+
278
305
  // App Settings table - global application settings
279
306
  export const appSettings = sqliteTable('app_settings', {
280
307
  key: text('key').primaryKey(),
@@ -307,5 +334,7 @@ export type PluginDependencyCache = typeof pluginDependencyCache.$inferSelect;
307
334
  export type NewPluginDependencyCache = typeof pluginDependencyCache.$inferInsert;
308
335
  export type Shell = typeof shells.$inferSelect;
309
336
  export type NewShell = typeof shells.$inferInsert;
337
+ export type Subagent = typeof subagents.$inferSelect;
338
+ export type NewSubagent = typeof subagents.$inferInsert;
310
339
  export type AppSetting = typeof appSettings.$inferSelect;
311
340
  export type NewAppSetting = typeof appSettings.$inferInsert;
@@ -57,11 +57,18 @@ class ProcessManager extends EventEmitter {
57
57
  let claudePath = process.env.CLAUDE_PATH;
58
58
 
59
59
  if (!claudePath) {
60
- const commonPaths = [
61
- '/home/roxane/.local/bin/claude',
62
- '/usr/local/bin/claude',
63
- '/opt/homebrew/bin/claude',
64
- ];
60
+ const isWindows = process.platform === 'win32';
61
+ const home = process.env.USERPROFILE || process.env.HOME || '';
62
+ const commonPaths = isWindows
63
+ ? [
64
+ `${home}\.local\bin\claude.exe`,
65
+ `${home}\AppData\Roaming\npm\claude.cmd`,
66
+ ]
67
+ : [
68
+ `/home/${process.env.USER || 'user'}/.local/bin/claude`,
69
+ '/usr/local/bin/claude',
70
+ '/opt/homebrew/bin/claude',
71
+ ];
65
72
  claudePath = commonPaths.find(p => existsSync(p));
66
73
  }
67
74
 
@@ -106,15 +113,29 @@ class ProcessManager extends EventEmitter {
106
113
 
107
114
  log.info({ claudePath, argsCount: args.length }, 'Spawning process');
108
115
 
116
+ // Normalize path separators for the current OS (fixes mixed slash issues on Windows)
117
+ const normalizedProjectPath = process.platform === 'win32'
118
+ ? projectPath.replace(/\//g, '\\')
119
+ : projectPath;
120
+
109
121
  const child = spawn(claudePath, args, {
110
- cwd: projectPath,
122
+ cwd: normalizedProjectPath,
111
123
  stdio: ['ignore', 'pipe', 'pipe'],
112
124
  env: {
113
125
  ...process.env,
114
126
  FORCE_COLOR: '0',
115
127
  NO_COLOR: '1',
116
128
  TERM: 'dumb',
117
- PATH: `${process.env.PATH}:/opt/homebrew/bin:/usr/local/bin`,
129
+ // On Windows, Bun has a bug where it calls readFile() on each PATH entry
130
+ // causing EPERM on C:\Windows\System32\ (a protected directory).
131
+ // Fix: strip Windows system directories from PATH before passing to claude.exe.
132
+ PATH: process.platform === 'win32'
133
+ ? (process.env.PATH || '').split(';').filter(p => {
134
+ const lp = p.toLowerCase().trim().replace(/\//g, '\\');
135
+ return !lp.startsWith('c:\\windows') &&
136
+ !lp.startsWith('c:\\program files (x86)\\windows kits');
137
+ }).join(';')
138
+ : `${process.env.PATH}:/opt/homebrew/bin:/usr/local/bin`,
118
139
  },
119
140
  });
120
141
 
@@ -310,6 +310,66 @@ export class SessionManager {
310
310
 
311
311
  return options;
312
312
  }
313
+
314
+ /**
315
+ * Extract a conversation summary from a task's attempt logs
316
+ * Used by compact to carry context into a fresh session
317
+ * Returns the last assistant message text (most recent context)
318
+ */
319
+ async getConversationSummary(taskId: string): Promise<string> {
320
+ // Get the most recent completed/cancelled attempt
321
+ const lastAttempt = await db.query.attempts.findFirst({
322
+ where: and(
323
+ eq(schema.attempts.taskId, taskId),
324
+ inArray(schema.attempts.status, ['completed', 'cancelled'])
325
+ ),
326
+ orderBy: [desc(schema.attempts.createdAt)],
327
+ });
328
+
329
+ if (!lastAttempt) return '';
330
+
331
+ // Get the original prompt from the first attempt for this task
332
+ const firstAttempt = await db.query.attempts.findFirst({
333
+ where: eq(schema.attempts.taskId, taskId),
334
+ orderBy: [schema.attempts.createdAt],
335
+ });
336
+ const originalPrompt = firstAttempt?.displayPrompt || firstAttempt?.prompt || '';
337
+
338
+ // Get the last assistant message from the most recent attempt
339
+ const logs = await db.query.attemptLogs.findMany({
340
+ where: eq(schema.attemptLogs.attemptId, lastAttempt.id),
341
+ });
342
+
343
+ let lastAssistantText = '';
344
+ for (let i = logs.length - 1; i >= 0; i--) {
345
+ if (logs[i].type !== 'json') continue;
346
+ try {
347
+ const data = JSON.parse(logs[i].content);
348
+ if (data.type === 'assistant' && data.message?.content) {
349
+ const text = data.message.content
350
+ .filter((b: any) => b.type === 'text')
351
+ .map((b: any) => b.text)
352
+ .join(' ');
353
+ if (text.trim()) {
354
+ lastAssistantText = text.substring(0, 4000);
355
+ break;
356
+ }
357
+ }
358
+ } catch {
359
+ // Skip parse errors
360
+ }
361
+ }
362
+
363
+ let summary = '';
364
+ if (originalPrompt) {
365
+ summary += `Original task: ${originalPrompt.substring(0, 500)}\n\n`;
366
+ }
367
+ if (lastAssistantText) {
368
+ summary += `Most recent assistant response:\n${lastAssistantText}`;
369
+ }
370
+
371
+ return summary;
372
+ }
313
373
  }
314
374
 
315
375
  // Singleton instance
@@ -26,11 +26,11 @@ export interface UsageStats {
26
26
  durationMs: number;
27
27
  durationApiMs: number;
28
28
 
29
- // Context usage tracking (Active Context Only)
30
- contextUsed: number; // Active context tokens (excludes cache_read)
29
+ // Context usage tracking (full context window)
30
+ contextUsed: number; // Total context tokens (input + cache_read + cache_creation + output)
31
31
  contextLimit: number; // Max context window (200K for Opus/Sonnet)
32
32
  contextPercentage: number; // (contextUsed / contextLimit) * 100
33
- baselineContext: number; // Cached baseline tokens (for reference, not in active count)
33
+ baselineContext: number; // First-turn cache_read tokens (for reference)
34
34
 
35
35
  // Context health metrics (ClaudeKit formulas)
36
36
  contextHealth?: ContextHealth;
@@ -142,20 +142,19 @@ class UsageTracker extends EventEmitter {
142
142
  stats.totalCacheReadTokens += usage.cache_read_input_tokens || 0;
143
143
  stats.totalTokens = stats.totalInputTokens + stats.totalOutputTokens;
144
144
 
145
- // Calculate ACTIVE context usage (excludes cache_read)
145
+ // Calculate context usage (all tokens in the context window)
146
146
  //
147
- // According to Anthropic's Prompt Caching:
148
- // - cache_read_input_tokens: Tokens loaded FROM cache (NOT in active window)
149
- // - cache_creation_input_tokens: NEW tokens being cached (IN active window)
150
- // - input_tokens: New user input (IN active window)
151
- // - output_tokens: Model response (IN active window)
147
+ // Anthropic's Prompt Caching:
148
+ // - cache_read_input_tokens: Tokens loaded from cache (still occupy context window)
149
+ // - cache_creation_input_tokens: New tokens being cached (occupy context window)
150
+ // - input_tokens: New user input (occupy context window)
151
+ // - output_tokens: Model response (occupy context window)
152
152
  //
153
- // Active Context Window (200K limit) includes:
153
+ // Context Window (200K limit) includes ALL of these:
154
154
  // - input_tokens: New user message
155
+ // - cache_read_input_tokens: Cached content (served from cache but still in window)
155
156
  // - cache_creation_input_tokens: New cache entries
156
157
  // - output_tokens: Current response
157
- //
158
- // Cache read does NOT count toward 200K limit (stored separately)
159
158
  const inputTokens = usage.input_tokens || 0;
160
159
  const cacheRead = usage.cache_read_input_tokens || 0;
161
160
  const cacheCreation = usage.cache_creation_input_tokens || 0;
@@ -167,17 +166,17 @@ class UsageTracker extends EventEmitter {
167
166
  log.info(`First turn baseline (cached): ${cacheRead} tokens`);
168
167
  }
169
168
 
170
- // Active context = NEW tokens only (cache_read NOT included)
171
- const activeContext = inputTokens + cacheCreation + outputTokens;
169
+ // Context = all tokens in the window (cache_read included)
170
+ const activeContext = inputTokens + cacheRead + cacheCreation + outputTokens;
172
171
 
173
172
  // Update stats with active context (what's actually in 200K window)
174
173
  stats.contextUsed = activeContext;
175
174
  stats.contextPercentage = (stats.contextUsed / stats.contextLimit) * 100;
176
175
 
177
- // Calculate health metrics using active context
176
+ // Calculate health metrics using full context
178
177
  stats.contextHealth = calculateContextHealth(
179
- inputTokens + cacheCreation, // active input
180
- outputTokens, // active output
178
+ inputTokens + cacheRead + cacheCreation, // total input (including cached)
179
+ outputTokens, // output
181
180
  stats.contextLimit
182
181
  );
183
182
 
@@ -187,10 +186,11 @@ class UsageTracker extends EventEmitter {
187
186
  contextPercentage: stats.contextPercentage.toFixed(1),
188
187
  healthStatus: stats.contextHealth.status,
189
188
  inputTokens,
189
+ cacheRead,
190
190
  cacheCreation,
191
191
  outputTokens,
192
- cacheRead,
193
- }, 'Active context updated');
192
+ totalInput: inputTokens + cacheRead + cacheCreation,
193
+ }, 'Context updated (includes cache_read)');
194
194
  }
195
195
 
196
196
  // Aggregate cost (common field)
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Workflow Tracker - Track subagent execution workflow with 2-depth support
2
+ * Workflow Tracker - Track subagent execution workflow with full tree support
3
3
  *
4
4
  * Monitors Task tool usage to build real-time workflow visualization:
5
- * docs-manager tester code-reviewer project-manager (7 done)
5
+ * Full agent tree with unlimited depth, team tracking, and inter-agent messages.
6
6
  */
7
7
 
8
8
  import { EventEmitter } from 'events';
@@ -14,7 +14,7 @@ const log = createLogger('WorkflowTracker');
14
14
  /**
15
15
  * Subagent status
16
16
  */
17
- export type SubagentStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
17
+ export type SubagentStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'orphaned';
18
18
 
19
19
  /**
20
20
  * Subagent node in workflow tree
@@ -22,14 +22,28 @@ export type SubagentStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
22
22
  export interface SubagentNode {
23
23
  id: string; // tool_use_id from Task tool
24
24
  type: string; // subagent_type
25
+ name?: string; // agent name (from Task tool input)
25
26
  status: SubagentStatus;
26
27
  parentId: string | null; // null for top-level
27
- depth: number; // 0 for top-level, 1 for nested
28
+ depth: number; // 0 for top-level, increments with nesting
29
+ teamName?: string; // team name if part of a team
28
30
  startedAt?: number;
29
31
  completedAt?: number;
32
+ durationMs?: number;
30
33
  error?: string;
31
34
  }
32
35
 
36
+ /**
37
+ * Inter-agent message
38
+ */
39
+ export interface AgentMessage {
40
+ fromType: string; // sender agent type or name
41
+ toType: string; // recipient agent type or name
42
+ content: string;
43
+ summary: string;
44
+ timestamp: number;
45
+ }
46
+
33
47
  /**
34
48
  * Workflow state for an attempt
35
49
  */
@@ -40,6 +54,18 @@ export interface WorkflowState {
40
54
  activeNodes: string[]; // Currently running
41
55
  completedNodes: string[]; // Successfully completed
42
56
  failedNodes: string[]; // Failed agents
57
+ messages: AgentMessage[]; // Inter-agent messages
58
+ teams: string[]; // Team names created in this workflow
59
+ }
60
+
61
+ /**
62
+ * Workflow summary for status line and global events
63
+ */
64
+ export interface WorkflowSummary {
65
+ chain: string[];
66
+ completedCount: number;
67
+ activeCount: number;
68
+ totalCount: number;
43
69
  }
44
70
 
45
71
  interface WorkflowTrackerEvents {
@@ -70,6 +96,8 @@ class WorkflowTracker extends EventEmitter {
70
96
  activeNodes: [],
71
97
  completedNodes: [],
72
98
  failedNodes: [],
99
+ messages: [],
100
+ teams: [],
73
101
  };
74
102
  this.workflows.set(attemptId, workflow);
75
103
  }
@@ -83,29 +111,26 @@ class WorkflowTracker extends EventEmitter {
83
111
  attemptId: string,
84
112
  toolUseId: string,
85
113
  subagentType: string,
86
- parentToolUseId: string | null
114
+ parentToolUseId: string | null,
115
+ options?: { teamName?: string; name?: string }
87
116
  ): void {
88
117
  const workflow = this.initWorkflow(attemptId);
89
118
 
90
- // Determine depth (max 2 depth: 0 for top-level, 1 for nested)
119
+ // Determine depth - no cap, full tree support
91
120
  let depth = 0;
92
121
  if (parentToolUseId && workflow.nodes.has(parentToolUseId)) {
93
122
  const parent = workflow.nodes.get(parentToolUseId)!;
94
- depth = Math.min(parent.depth + 1, 1); // Cap at depth 1
95
- }
96
-
97
- // Only track if depth <= 1 (2 levels max)
98
- if (depth > 1) {
99
- log.info(`Skipping subagent at depth ${depth}: ${subagentType}`);
100
- return;
123
+ depth = parent.depth + 1;
101
124
  }
102
125
 
103
126
  const node: SubagentNode = {
104
127
  id: toolUseId,
105
128
  type: subagentType,
129
+ name: options?.name,
106
130
  status: 'in_progress',
107
131
  parentId: parentToolUseId,
108
132
  depth,
133
+ teamName: options?.teamName,
109
134
  startedAt: Date.now(),
110
135
  };
111
136
 
@@ -141,6 +166,7 @@ class WorkflowTracker extends EventEmitter {
141
166
  // Update node status
142
167
  node.status = success ? 'completed' : 'failed';
143
168
  node.completedAt = Date.now();
169
+ node.durationMs = node.startedAt ? Date.now() - node.startedAt : undefined;
144
170
  if (error) node.error = error;
145
171
 
146
172
  // Remove from active
@@ -157,6 +183,59 @@ class WorkflowTracker extends EventEmitter {
157
183
  this.emit('workflow-update', { attemptId, workflow });
158
184
  }
159
185
 
186
+ /**
187
+ * Track a TeamCreate tool use
188
+ */
189
+ trackTeamCreate(attemptId: string, teamName: string): void {
190
+ const workflow = this.initWorkflow(attemptId);
191
+ if (!workflow.teams.includes(teamName)) {
192
+ workflow.teams.push(teamName);
193
+ }
194
+ this.emit('workflow-update', { attemptId, workflow });
195
+ }
196
+
197
+ /**
198
+ * Track a SendMessage tool use
199
+ */
200
+ trackMessage(
201
+ attemptId: string,
202
+ input: { type?: string; recipient?: string; content?: string; summary?: string }
203
+ ): void {
204
+ const workflow = this.initWorkflow(attemptId);
205
+
206
+ const message: AgentMessage = {
207
+ fromType: 'agent', // We don't have sender info from the tool_use block
208
+ toType: input.recipient || input.type || 'unknown',
209
+ content: input.content || '',
210
+ summary: input.summary || '',
211
+ timestamp: Date.now(),
212
+ };
213
+
214
+ workflow.messages.push(message);
215
+ this.emit('workflow-update', { attemptId, workflow });
216
+ }
217
+
218
+ /**
219
+ * Mark all in-progress subagents as orphaned (for disconnect cleanup)
220
+ */
221
+ markOrphaned(attemptId: string): SubagentNode[] {
222
+ const workflow = this.workflows.get(attemptId);
223
+ if (!workflow) return [];
224
+
225
+ const orphaned: SubagentNode[] = [];
226
+ for (const nodeId of workflow.activeNodes) {
227
+ const node = workflow.nodes.get(nodeId);
228
+ if (node) {
229
+ node.status = 'orphaned';
230
+ node.completedAt = Date.now();
231
+ node.durationMs = node.startedAt ? Date.now() - node.startedAt : undefined;
232
+ orphaned.push(node);
233
+ }
234
+ }
235
+ workflow.activeNodes = [];
236
+ return orphaned;
237
+ }
238
+
160
239
  /**
161
240
  * Get workflow state for an attempt
162
241
  */
@@ -168,12 +247,7 @@ class WorkflowTracker extends EventEmitter {
168
247
  * Get workflow summary for status line display
169
248
  * Format: "docs-manager → tester → code-reviewer (3 done)"
170
249
  */
171
- getWorkflowSummary(attemptId: string): {
172
- chain: string[];
173
- completedCount: number;
174
- activeCount: number;
175
- totalCount: number;
176
- } | null {
250
+ getWorkflowSummary(attemptId: string): WorkflowSummary | null {
177
251
  const workflow = this.workflows.get(attemptId);
178
252
  if (!workflow) return null;
179
253
 
@@ -182,7 +256,7 @@ class WorkflowTracker extends EventEmitter {
182
256
  for (const rootId of workflow.rootNodes) {
183
257
  const node = workflow.nodes.get(rootId);
184
258
  if (node) {
185
- chain.push(node.type);
259
+ chain.push(node.name || node.type);
186
260
  }
187
261
  }
188
262
 
@@ -194,6 +268,30 @@ class WorkflowTracker extends EventEmitter {
194
268
  };
195
269
  }
196
270
 
271
+ /**
272
+ * Get full expanded workflow data for socket emission
273
+ */
274
+ getExpandedWorkflow(attemptId: string): {
275
+ nodes: SubagentNode[];
276
+ messages: AgentMessage[];
277
+ summary: WorkflowSummary;
278
+ } | null {
279
+ const workflow = this.workflows.get(attemptId);
280
+ if (!workflow) return null;
281
+
282
+ const summary = this.getWorkflowSummary(attemptId);
283
+ if (!summary) return null;
284
+
285
+ return {
286
+ nodes: Array.from(workflow.nodes.values()).sort((a, b) => {
287
+ if (a.depth !== b.depth) return a.depth - b.depth;
288
+ return (a.startedAt || 0) - (b.startedAt || 0);
289
+ }),
290
+ messages: workflow.messages,
291
+ summary,
292
+ };
293
+ }
294
+
197
295
  /**
198
296
  * Get detailed workflow tree (for debugging/advanced UI)
199
297
  */