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.
- package/locales/de.json +374 -12
- package/locales/en.json +374 -12
- package/locales/es.json +398 -11
- package/locales/fr.json +398 -11
- package/locales/ja.json +398 -11
- package/locales/ko.json +398 -11
- package/locales/vi.json +374 -12
- package/locales/zh.json +398 -11
- package/package.json +1 -1
- package/server.ts +283 -6
- package/src/app/[locale]/not-found.tsx +6 -3
- package/src/app/[locale]/page.tsx +14 -4
- package/src/app/api/attempts/[id]/workflow/route.ts +76 -0
- package/src/app/api/questions/answer/route.ts +58 -0
- package/src/app/api/questions/route.ts +68 -0
- package/src/app/api/tasks/[id]/compact/route.ts +62 -0
- package/src/components/access-anywhere/api-access-key-setup-modal.tsx +2 -2
- package/src/components/access-anywhere/tunnel-settings-dialog.tsx +6 -6
- package/src/components/access-anywhere/wizard-step-ctunnel.tsx +8 -8
- package/src/components/agent-factory/dependency-tree.tsx +5 -3
- package/src/components/agent-factory/discovery-dialog.tsx +26 -22
- package/src/components/agent-factory/plugin-detail-dialog.tsx +41 -38
- package/src/components/agent-factory/plugin-form-dialog.tsx +23 -20
- package/src/components/agent-factory/plugin-list.tsx +20 -17
- package/src/components/agent-factory/upload-dialog.tsx +17 -14
- package/src/components/auth/agent-provider-dialog.tsx +67 -65
- package/src/components/auth/api-key-dialog.tsx +14 -11
- package/src/components/auth/auth-error-message.tsx +6 -3
- package/src/components/editor/code-editor-with-inline-edit.tsx +4 -2
- package/src/components/editor/file-diff-resolver-modal.tsx +31 -26
- package/src/components/editor/inline-edit-dialog.tsx +9 -6
- package/src/components/editor/selection-mention-popup.tsx +3 -1
- package/src/components/header/project-selector.tsx +7 -4
- package/src/components/header.tsx +70 -4
- package/src/components/kanban/column.tsx +11 -0
- package/src/components/kanban/task-card.tsx +70 -4
- package/src/components/project-settings/component-selector.tsx +3 -1
- package/src/components/project-settings/plugin-upload-dialog.tsx +7 -5
- package/src/components/project-settings/project-settings-dialog.tsx +5 -3
- package/src/components/questions/questions-panel.tsx +136 -0
- package/src/components/settings/folder-browser-dialog.tsx +29 -25
- package/src/components/settings/settings-page.tsx +64 -18
- package/src/components/settings/setup-dialog.tsx +26 -23
- package/src/components/setup/unified-setup-wizard.tsx +12 -9
- package/src/components/sidebar/file-browser/file-create-buttons.tsx +7 -3
- package/src/components/sidebar/file-browser/file-tab-content.tsx +19 -15
- package/src/components/sidebar/file-browser/file-tabs-panel.tsx +7 -4
- package/src/components/sidebar/file-browser/file-tree.tsx +3 -1
- package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +6 -4
- package/src/components/sidebar/git-changes/commit-details-modal.tsx +5 -3
- package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +3 -1
- package/src/components/sidebar/git-changes/git-file-item.tsx +8 -6
- package/src/components/sidebar/git-changes/git-graph.tsx +8 -5
- package/src/components/sidebar/git-changes/git-panel.tsx +28 -27
- package/src/components/sidebar/git-changes/git-section.tsx +5 -3
- package/src/components/sidebar/shells/shell-panel.tsx +3 -1
- package/src/components/task/attachment-bar.tsx +4 -1
- package/src/components/task/attempt-item.tsx +7 -5
- package/src/components/task/conversation-view.tsx +21 -13
- package/src/components/task/floating-chat-window.tsx +14 -5
- package/src/components/task/interactive-command/checkpoint-list.tsx +5 -3
- package/src/components/task/interactive-command/confirm-dialog.tsx +9 -4
- package/src/components/task/interactive-command/interactive-command-overlay.tsx +23 -9
- package/src/components/task/interactive-command/question-prompt.tsx +12 -8
- package/src/components/task/pending-question-indicator.tsx +5 -3
- package/src/components/task/prompt-input.tsx +1 -1
- package/src/components/task/shell-log-view.tsx +3 -1
- package/src/components/task/status-line.tsx +84 -23
- package/src/components/task/task-detail-panel.tsx +27 -27
- package/src/components/task/task-shell-indicator.tsx +10 -6
- package/src/components/terminal/terminal-context-menu.tsx +6 -4
- package/src/components/terminal/terminal-instance.tsx +11 -3
- package/src/components/terminal/terminal-panel.tsx +6 -3
- package/src/components/terminal/terminal-shortcut-bar.tsx +3 -1
- package/src/components/terminal/terminal-tab-bar.tsx +5 -3
- package/src/components/workflow/workflow-panel.tsx +181 -0
- package/src/hooks/use-attempt-stream.ts +96 -3
- package/src/lib/agent-manager.ts +89 -3
- package/src/lib/db/index.ts +18 -0
- package/src/lib/db/schema.ts +29 -0
- package/src/lib/process-manager.ts +28 -7
- package/src/lib/session-manager.ts +60 -0
- package/src/lib/usage-tracker.ts +19 -19
- package/src/lib/workflow-tracker.ts +118 -20
- package/src/stores/questions-store.ts +76 -0
- package/src/stores/workflow-store.ts +71 -0
package/src/lib/agent-manager.ts
CHANGED
|
@@ -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
|
package/src/lib/db/index.ts
CHANGED
|
@@ -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
|
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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
|
|
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
|
package/src/lib/usage-tracker.ts
CHANGED
|
@@ -26,11 +26,11 @@ export interface UsageStats {
|
|
|
26
26
|
durationMs: number;
|
|
27
27
|
durationApiMs: number;
|
|
28
28
|
|
|
29
|
-
// Context usage tracking (
|
|
30
|
-
contextUsed: number; //
|
|
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; //
|
|
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
|
|
145
|
+
// Calculate context usage (all tokens in the context window)
|
|
146
146
|
//
|
|
147
|
-
//
|
|
148
|
-
// - cache_read_input_tokens: Tokens loaded
|
|
149
|
-
// - cache_creation_input_tokens:
|
|
150
|
-
// - input_tokens: New user input (
|
|
151
|
-
// - output_tokens: Model response (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
176
|
+
// Calculate health metrics using full context
|
|
178
177
|
stats.contextHealth = calculateContextHealth(
|
|
179
|
-
inputTokens + cacheCreation, //
|
|
180
|
-
outputTokens,
|
|
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
|
-
}, '
|
|
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
|
+
* 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
|
-
*
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
*/
|