glad-web 1.0.29 → 1.0.31
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/lib/claude/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/codex/structured-session.js +64 -7
- package/lib/commands/web.js +59 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +117 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +519 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +46 -3672
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +388 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function stripTerminalFormatting(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
4
|
+
.replace(/\r/g, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseTokenCount(value) {
|
|
8
|
+
const text = String(value || '').trim().replace(/,/g, '');
|
|
9
|
+
const match = text.match(/^([\d.]+)\s*([kmb])?$/i);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
const amount = Number(match[1]);
|
|
12
|
+
if (!Number.isFinite(amount)) return null;
|
|
13
|
+
const multiplier = { k: 1_000, m: 1_000_000, b: 1_000_000_000 }[String(match[2] || '').toLowerCase()] || 1;
|
|
14
|
+
return Math.round(amount * multiplier);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseClaudeUsageOutput(output) {
|
|
18
|
+
const text = stripTerminalFormatting(output);
|
|
19
|
+
const cost = text.match(/Total cost:\s*\$([\d,.]+)/i);
|
|
20
|
+
const apiDuration = text.match(/Total duration \(API\):\s*([^\n]+)/i);
|
|
21
|
+
const wallDuration = text.match(/Total duration \(wall\):\s*([^\n]+)/i);
|
|
22
|
+
const codeChanges = text.match(/Total code changes:\s*([\d,]+) lines added,\s*([\d,]+) lines removed/i);
|
|
23
|
+
const tokens = text.match(/Usage:\s*([\d,.kmb]+) input,\s*([\d,.kmb]+) output,\s*([\d,.kmb]+) cache read,\s*([\d,.kmb]+) cache write/i);
|
|
24
|
+
const models = [];
|
|
25
|
+
const modelPattern = /^\s*(.+?):\s*([\d,.kmb]+) input,\s*([\d,.kmb]+) output,\s*([\d,.kmb]+) cache read,\s*([\d,.kmb]+) cache write(?:\s*\(\$([\d,.]+)\))?\s*$/gim;
|
|
26
|
+
for (const match of text.matchAll(modelPattern)) {
|
|
27
|
+
if (/^Usage$/i.test(match[1].trim())) continue;
|
|
28
|
+
models.push({
|
|
29
|
+
model: match[1].trim(),
|
|
30
|
+
inputTokens: parseTokenCount(match[2]),
|
|
31
|
+
outputTokens: parseTokenCount(match[3]),
|
|
32
|
+
cacheReadTokens: parseTokenCount(match[4]),
|
|
33
|
+
cacheWriteTokens: parseTokenCount(match[5]),
|
|
34
|
+
costUsd: match[6] ? Number(match[6].replace(/,/g, '')) : null
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!cost && !apiDuration && !wallDuration && !codeChanges && !tokens && models.length === 0) {
|
|
39
|
+
throw new Error('Claude CLI returned an unrecognized /usage response');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const totalModelValue = key => models.length > 0
|
|
43
|
+
? models.reduce((sum, model) => sum + Number(model[key] || 0), 0)
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
totalCostUsd: cost ? Number(cost[1].replace(/,/g, '')) : null,
|
|
48
|
+
apiDuration: apiDuration ? apiDuration[1].trim() : null,
|
|
49
|
+
wallDuration: wallDuration ? wallDuration[1].trim() : null,
|
|
50
|
+
linesAdded: codeChanges ? Number(codeChanges[1].replace(/,/g, '')) : null,
|
|
51
|
+
linesRemoved: codeChanges ? Number(codeChanges[2].replace(/,/g, '')) : null,
|
|
52
|
+
inputTokens: tokens ? parseTokenCount(tokens[1]) : totalModelValue('inputTokens'),
|
|
53
|
+
outputTokens: tokens ? parseTokenCount(tokens[2]) : totalModelValue('outputTokens'),
|
|
54
|
+
cacheReadTokens: tokens ? parseTokenCount(tokens[3]) : totalModelValue('cacheReadTokens'),
|
|
55
|
+
cacheWriteTokens: tokens ? parseTokenCount(tokens[4]) : totalModelValue('cacheWriteTokens'),
|
|
56
|
+
models
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseClaudeContextOutput(output) {
|
|
61
|
+
const text = stripTerminalFormatting(output);
|
|
62
|
+
const model = text.match(/^\*\*Model:\*\*\s*(.+?)\s*$/mi);
|
|
63
|
+
const tokens = text.match(/^\*\*Tokens:\*\*\s*([\d,.]+\s*[kmb]?)\s*\/\s*([\d,.]+\s*[kmb]?)\s*\(([\d.]+)%\)/mi);
|
|
64
|
+
if (!tokens) throw new Error('Claude CLI returned an unrecognized /context response');
|
|
65
|
+
|
|
66
|
+
const usedTokens = parseTokenCount(tokens[1]);
|
|
67
|
+
const maxTokens = parseTokenCount(tokens[2]);
|
|
68
|
+
const categories = [];
|
|
69
|
+
for (const line of text.split('\n')) {
|
|
70
|
+
const columns = line.split('|').slice(1, -1).map(value => value.trim());
|
|
71
|
+
if (columns.length < 3 || !columns[0] || /^category$/i.test(columns[0]) || /^-+$/.test(columns[0])) continue;
|
|
72
|
+
const categoryTokens = parseTokenCount(columns[1]);
|
|
73
|
+
if (categoryTokens == null || !/^<?[\d.]+%$/.test(columns[2])) continue;
|
|
74
|
+
categories.push({
|
|
75
|
+
label: columns[0].replace(/\*\*/g, ''),
|
|
76
|
+
tokens: categoryTokens,
|
|
77
|
+
percent: columns[2]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
model: model ? model[1].trim() : null,
|
|
83
|
+
usedTokens,
|
|
84
|
+
maxTokens,
|
|
85
|
+
usedPercent: Number(tokens[3]),
|
|
86
|
+
remainingTokens: usedTokens == null || maxTokens == null ? null : Math.max(0, maxTokens - usedTokens),
|
|
87
|
+
categories
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
parseClaudeContextOutput,
|
|
93
|
+
parseClaudeUsageOutput,
|
|
94
|
+
parseTokenCount
|
|
95
|
+
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
|
|
4
|
+
const { parseClaudeContextOutput, parseClaudeUsageOutput } = require('./cli-usage');
|
|
4
5
|
|
|
5
6
|
const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
|
|
6
7
|
const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
7
8
|
const EXIT_PLAN_TOOLS = new Set(['exit_plan_mode', 'ExitPlanMode']);
|
|
8
9
|
const DENY_PERMISSION_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
10
|
+
const LOCAL_COMMAND_TIMEOUT_MS = 30_000;
|
|
9
11
|
|
|
10
12
|
function normalizePermissionMode(value) {
|
|
11
13
|
const mode = String(value || 'default');
|
|
@@ -65,7 +67,15 @@ class AsyncMessageQueue {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
class ClaudeStructuredSession extends EventEmitter {
|
|
68
|
-
constructor({
|
|
70
|
+
constructor({
|
|
71
|
+
id,
|
|
72
|
+
tool,
|
|
73
|
+
workingDir,
|
|
74
|
+
name,
|
|
75
|
+
logger,
|
|
76
|
+
options = {},
|
|
77
|
+
localCommandRunner = null
|
|
78
|
+
}) {
|
|
69
79
|
super();
|
|
70
80
|
this.id = id;
|
|
71
81
|
this.tool = tool;
|
|
@@ -87,7 +97,12 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
87
97
|
this.inputQueue = null;
|
|
88
98
|
this.query = null;
|
|
89
99
|
this.runnerStarted = false;
|
|
100
|
+
this.runnerReadyPromise = null;
|
|
101
|
+
this.runnerInitializationAnnounced = false;
|
|
90
102
|
this.abortRequested = false;
|
|
103
|
+
this.localCommandRunner = localCommandRunner;
|
|
104
|
+
this.localCommandChain = Promise.resolve();
|
|
105
|
+
this.pendingLocalCommand = null;
|
|
91
106
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
92
107
|
this.model = normalizeModel(options.model);
|
|
93
108
|
this.effort = normalizeEffort(options.effort);
|
|
@@ -97,8 +112,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
97
112
|
this.resumeSessionId = options.resume || null;
|
|
98
113
|
this.claudeSessionId = options.resume || null;
|
|
99
114
|
this.activeOptionSignature = null;
|
|
100
|
-
this.
|
|
101
|
-
this.latestUsage = null;
|
|
115
|
+
this.turnQueue = [];
|
|
102
116
|
|
|
103
117
|
// Compatibility with existing session-scoped Git/file APIs.
|
|
104
118
|
this.ptyManager = {
|
|
@@ -146,8 +160,6 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
146
160
|
status: this.status,
|
|
147
161
|
claudeSessionId: this.claudeSessionId || null,
|
|
148
162
|
resumeSessionId: this.resumeSessionId || null,
|
|
149
|
-
contextRemaining: this.contextRemaining,
|
|
150
|
-
latestUsage: this.latestUsage,
|
|
151
163
|
canAbort: this.status === 'thinking',
|
|
152
164
|
pendingPermissionCount: this.pendingPermissions.size
|
|
153
165
|
};
|
|
@@ -199,19 +211,29 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
199
211
|
return true;
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
sendUserMessage(text) {
|
|
214
|
+
sendUserMessage(text, attachments = []) {
|
|
203
215
|
if (!this.running) return false;
|
|
204
216
|
const prompt = String(text || '').trim();
|
|
205
|
-
|
|
217
|
+
const images = Array.isArray(attachments) ? attachments.filter(item => item?.data && item?.mediaType) : [];
|
|
218
|
+
if (!prompt && images.length === 0) return false;
|
|
206
219
|
this.hasUnreadCompletion = false;
|
|
207
|
-
this.
|
|
220
|
+
const turn = this.beginTurn(prompt, images.map(item => ({ name: item.name, size: item.size })));
|
|
221
|
+
this.setStatus('thinking');
|
|
222
|
+
|
|
223
|
+
const content = images.length > 0 ? [
|
|
224
|
+
...(prompt ? [{ type: 'text', text: prompt }] : []),
|
|
225
|
+
...images.map(item => ({
|
|
226
|
+
type: 'image',
|
|
227
|
+
source: { type: 'base64', media_type: item.mediaType, data: item.data }
|
|
228
|
+
}))
|
|
229
|
+
] : prompt;
|
|
208
230
|
|
|
209
231
|
const sdkMessage = {
|
|
210
232
|
type: 'user',
|
|
211
233
|
parent_tool_use_id: null,
|
|
212
234
|
message: {
|
|
213
235
|
role: 'user',
|
|
214
|
-
content
|
|
236
|
+
content
|
|
215
237
|
}
|
|
216
238
|
};
|
|
217
239
|
|
|
@@ -223,6 +245,37 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
223
245
|
return true;
|
|
224
246
|
}
|
|
225
247
|
|
|
248
|
+
beginTurn(prompt, attachments = []) {
|
|
249
|
+
const turn = { id: crypto.randomUUID(), startedAt: Date.now() };
|
|
250
|
+
this.turnQueue.push(turn);
|
|
251
|
+
this.appendMessage({ kind: 'turn-start', turnId: turn.id, createdAt: turn.startedAt });
|
|
252
|
+
this.appendMessage({ kind: 'user', text: prompt, attachments, turnId: turn.id, createdAt: turn.startedAt });
|
|
253
|
+
return turn;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
currentTurn() {
|
|
257
|
+
return this.turnQueue[0] || null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
completeCurrentTurn(status = 'completed', reportedDurationMs = null) {
|
|
261
|
+
const turn = this.turnQueue.shift();
|
|
262
|
+
if (!turn) return null;
|
|
263
|
+
const completedAt = Date.now();
|
|
264
|
+
return this.appendMessage({
|
|
265
|
+
kind: 'turn-end',
|
|
266
|
+
turnId: turn.id,
|
|
267
|
+
turnStatus: status,
|
|
268
|
+
durationMs: Number.isFinite(Number(reportedDurationMs))
|
|
269
|
+
? Math.max(0, Number(reportedDurationMs))
|
|
270
|
+
: Math.max(0, completedAt - turn.startedAt),
|
|
271
|
+
createdAt: completedAt
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
sealTurns(status = 'cancelled') {
|
|
276
|
+
while (this.turnQueue.length > 0) this.completeCurrentTurn(status);
|
|
277
|
+
}
|
|
278
|
+
|
|
226
279
|
createMessageItem(message) {
|
|
227
280
|
return {
|
|
228
281
|
id: crypto.randomUUID(),
|
|
@@ -287,10 +340,14 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
287
340
|
this.inputQueue?.close();
|
|
288
341
|
this.query?.close?.();
|
|
289
342
|
this.abortController?.abort();
|
|
343
|
+
this.rejectPendingLocalCommand(new Error(reason));
|
|
290
344
|
this.runnerStarted = false;
|
|
345
|
+
this.runnerReadyPromise = null;
|
|
346
|
+
this.runnerInitializationAnnounced = false;
|
|
291
347
|
this.inputQueue = null;
|
|
292
348
|
this.query = null;
|
|
293
349
|
this.abortController = null;
|
|
350
|
+
this.sealTurns('cancelled');
|
|
294
351
|
this.setStatus('idle');
|
|
295
352
|
this.appendMessage({ kind: 'event', level: 'info', text: reason });
|
|
296
353
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
@@ -300,14 +357,21 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
300
357
|
resetRunnerForNextTurn() {
|
|
301
358
|
this.inputQueue?.close();
|
|
302
359
|
this.query?.close?.();
|
|
360
|
+
this.rejectPendingLocalCommand(new Error('Claude CLI session was reset'));
|
|
303
361
|
this.runnerStarted = false;
|
|
362
|
+
this.runnerReadyPromise = null;
|
|
363
|
+
this.runnerInitializationAnnounced = false;
|
|
304
364
|
this.inputQueue = null;
|
|
305
365
|
this.query = null;
|
|
306
366
|
this.abortController = null;
|
|
367
|
+
this.turnQueue = [];
|
|
307
368
|
}
|
|
308
369
|
|
|
309
370
|
finishRunner() {
|
|
371
|
+
this.rejectPendingLocalCommand(new Error('Claude CLI session ended before returning the command output'));
|
|
310
372
|
this.runnerStarted = false;
|
|
373
|
+
this.runnerReadyPromise = null;
|
|
374
|
+
this.runnerInitializationAnnounced = false;
|
|
311
375
|
this.inputQueue?.close();
|
|
312
376
|
this.inputQueue = null;
|
|
313
377
|
this.query = null;
|
|
@@ -466,15 +530,27 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
466
530
|
this.completionReadInputSeq = this.inputSeq || 0;
|
|
467
531
|
}
|
|
468
532
|
|
|
469
|
-
|
|
533
|
+
startRunner(initialMessage = null) {
|
|
534
|
+
if (this.runnerStarted) {
|
|
535
|
+
if (initialMessage && this.inputQueue) this.inputQueue.push(initialMessage);
|
|
536
|
+
return this.runnerReadyPromise || Promise.resolve(this.query);
|
|
537
|
+
}
|
|
470
538
|
this.runnerStarted = true;
|
|
539
|
+
this.runnerInitializationAnnounced = false;
|
|
471
540
|
this.inputQueue = new AsyncMessageQueue();
|
|
472
|
-
this.inputQueue.push(initialMessage);
|
|
541
|
+
if (initialMessage) this.inputQueue.push(initialMessage);
|
|
473
542
|
this.abortController = new AbortController();
|
|
474
543
|
this.abortRequested = false;
|
|
475
544
|
this.activeOptionSignature = this.getOptionSignature();
|
|
476
|
-
this.setStatus('thinking');
|
|
545
|
+
if (initialMessage) this.setStatus('thinking');
|
|
546
|
+
|
|
547
|
+
let resolveReady;
|
|
548
|
+
this.runnerReadyPromise = new Promise(resolve => { resolveReady = resolve; });
|
|
549
|
+
void this.runRunner(resolveReady);
|
|
550
|
+
return this.runnerReadyPromise;
|
|
551
|
+
}
|
|
477
552
|
|
|
553
|
+
async runRunner(resolveReady) {
|
|
478
554
|
try {
|
|
479
555
|
const sdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
480
556
|
const resolvedModel = resolveClaudeModel(this.model, process.env);
|
|
@@ -497,16 +573,25 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
497
573
|
prompt: this.inputQueue,
|
|
498
574
|
options
|
|
499
575
|
});
|
|
576
|
+
if (typeof this.query.initializationResult === 'function') {
|
|
577
|
+
await this.query.initializationResult();
|
|
578
|
+
}
|
|
579
|
+
resolveReady(this.query);
|
|
500
580
|
|
|
501
581
|
for await (const message of this.query) {
|
|
502
582
|
this.handleSdkMessage(message);
|
|
503
583
|
}
|
|
504
584
|
this.finishRunner();
|
|
585
|
+
this.sealTurns('completed');
|
|
505
586
|
this.setStatus('idle');
|
|
506
587
|
} catch (error) {
|
|
588
|
+
resolveReady(null);
|
|
589
|
+
const wasAborted = this.abortRequested;
|
|
590
|
+
this.inputQueue?.close();
|
|
591
|
+
this.query?.close?.();
|
|
592
|
+
this.finishRunner();
|
|
507
593
|
if (!this.running) return;
|
|
508
|
-
if (
|
|
509
|
-
this.finishRunner();
|
|
594
|
+
if (wasAborted) {
|
|
510
595
|
this.setStatus('idle');
|
|
511
596
|
return;
|
|
512
597
|
}
|
|
@@ -515,10 +600,87 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
515
600
|
level: 'error',
|
|
516
601
|
text: `Claude session error: ${error && error.message ? error.message : String(error)}`
|
|
517
602
|
});
|
|
603
|
+
this.sealTurns('failed');
|
|
518
604
|
this.setStatus('error');
|
|
519
605
|
}
|
|
520
606
|
}
|
|
521
607
|
|
|
608
|
+
async showUsage() {
|
|
609
|
+
try {
|
|
610
|
+
const output = await this.runLocalCommand('/usage');
|
|
611
|
+
const usage = { source: 'claude-cli-command', session: parseClaudeUsageOutput(output), fetchedAt: Date.now() };
|
|
612
|
+
this.appendMessage({ kind: 'usage', title: 'Claude usage', usage });
|
|
613
|
+
return true;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.appendMessage({
|
|
616
|
+
kind: 'usage',
|
|
617
|
+
title: 'Claude usage',
|
|
618
|
+
error: error && error.message ? error.message : String(error)
|
|
619
|
+
});
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async showContext() {
|
|
625
|
+
try {
|
|
626
|
+
const output = await this.runLocalCommand('/context');
|
|
627
|
+
this.appendMessage({ kind: 'context', title: 'Claude context', context: parseClaudeContextOutput(output) });
|
|
628
|
+
return true;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
this.appendMessage({
|
|
631
|
+
kind: 'context',
|
|
632
|
+
title: 'Claude context',
|
|
633
|
+
error: error && error.message ? error.message : String(error)
|
|
634
|
+
});
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
runLocalCommand(command) {
|
|
640
|
+
const task = this.localCommandChain.then(() => this.executeLocalCommand(command));
|
|
641
|
+
this.localCommandChain = task.catch(() => {});
|
|
642
|
+
return task;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async executeLocalCommand(command) {
|
|
646
|
+
if (this.status === 'thinking') throw new Error('Wait for Claude to finish before running a local command');
|
|
647
|
+
if (this.localCommandRunner) return this.localCommandRunner(command);
|
|
648
|
+
|
|
649
|
+
const query = await this.startRunner();
|
|
650
|
+
if (!query || !this.inputQueue) throw new Error('Claude CLI session is not available');
|
|
651
|
+
if (this.pendingLocalCommand) throw new Error('Another Claude local command is already running');
|
|
652
|
+
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const timeout = setTimeout(() => {
|
|
655
|
+
if (this.pendingLocalCommand?.command !== command) return;
|
|
656
|
+
this.pendingLocalCommand = null;
|
|
657
|
+
reject(new Error(`Claude ${command} command timed out`));
|
|
658
|
+
}, LOCAL_COMMAND_TIMEOUT_MS);
|
|
659
|
+
this.pendingLocalCommand = {
|
|
660
|
+
command,
|
|
661
|
+
resolve: output => {
|
|
662
|
+
clearTimeout(timeout);
|
|
663
|
+
this.pendingLocalCommand = null;
|
|
664
|
+
resolve(output);
|
|
665
|
+
},
|
|
666
|
+
reject: error => {
|
|
667
|
+
clearTimeout(timeout);
|
|
668
|
+
this.pendingLocalCommand = null;
|
|
669
|
+
reject(error);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
this.inputQueue.push({
|
|
673
|
+
type: 'user',
|
|
674
|
+
parent_tool_use_id: null,
|
|
675
|
+
message: { role: 'user', content: command }
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
rejectPendingLocalCommand(error) {
|
|
681
|
+
this.pendingLocalCommand?.reject(error);
|
|
682
|
+
}
|
|
683
|
+
|
|
522
684
|
getOptionSignature() {
|
|
523
685
|
return JSON.stringify({
|
|
524
686
|
permissionMode: this.permissionMode,
|
|
@@ -550,6 +712,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
550
712
|
canAllowEdits: EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName),
|
|
551
713
|
canBypass: EXIT_PLAN_TOOLS.has(toolName),
|
|
552
714
|
input,
|
|
715
|
+
toolUseId: options.toolUseID || null,
|
|
553
716
|
createdAt: Date.now(),
|
|
554
717
|
status: 'pending'
|
|
555
718
|
};
|
|
@@ -569,28 +732,56 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
569
732
|
if (!message || typeof message !== 'object') return;
|
|
570
733
|
|
|
571
734
|
if (message.type === 'system' && message.subtype === 'init') {
|
|
735
|
+
const signatureBeforeInit = this.getOptionSignature();
|
|
572
736
|
this.claudeSessionId = message.session_id || this.claudeSessionId;
|
|
573
737
|
this.resumeSessionId = this.claudeSessionId || this.resumeSessionId;
|
|
574
738
|
if (message.model) this.model = String(message.model);
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
739
|
+
// Resolving runtime session/model values is not a user settings change
|
|
740
|
+
// and must not recycle the live runner after the turn.
|
|
741
|
+
if (this.activeOptionSignature === signatureBeforeInit) {
|
|
742
|
+
this.activeOptionSignature = this.getOptionSignature();
|
|
743
|
+
}
|
|
744
|
+
if (!this.runnerInitializationAnnounced) {
|
|
745
|
+
this.runnerInitializationAnnounced = true;
|
|
746
|
+
this.appendMessage({
|
|
747
|
+
kind: 'event',
|
|
748
|
+
level: 'info',
|
|
749
|
+
text: `Claude ready${message.model ? ` (${message.model})` : ''}`
|
|
750
|
+
});
|
|
751
|
+
}
|
|
580
752
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
581
753
|
return;
|
|
582
754
|
}
|
|
583
755
|
|
|
756
|
+
if (message.type === 'system' && message.subtype === 'local_command_output') {
|
|
757
|
+
if (this.pendingLocalCommand) {
|
|
758
|
+
this.pendingLocalCommand.resolve(String(message.content || ''));
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (this.pendingLocalCommand && message.type === 'assistant') return;
|
|
764
|
+
|
|
765
|
+
if (this.pendingLocalCommand && message.type === 'result') {
|
|
766
|
+
const output = typeof message.result === 'string' ? message.result : '';
|
|
767
|
+
if (message.is_error || !output) {
|
|
768
|
+
this.pendingLocalCommand.reject(new Error(output || `Claude ${this.pendingLocalCommand.command} command failed`));
|
|
769
|
+
} else {
|
|
770
|
+
this.pendingLocalCommand.resolve(output);
|
|
771
|
+
}
|
|
772
|
+
this.setStatus('idle');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
584
776
|
if (message.type === 'assistant') {
|
|
585
|
-
const
|
|
586
|
-
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.message.model);
|
|
777
|
+
const turn = this.currentTurn();
|
|
587
778
|
const content = message.message && message.message.content;
|
|
588
779
|
const text = textFromContent(content).trim();
|
|
589
780
|
const toolBlocks = Array.isArray(content)
|
|
590
781
|
? content.filter(item => item && item.type === 'tool_use')
|
|
591
782
|
: [];
|
|
592
783
|
if (text) {
|
|
593
|
-
this.appendMessage({ kind: 'assistant', text, raw: message });
|
|
784
|
+
this.appendMessage({ kind: 'assistant', text, raw: message, turnId: turn?.id || null });
|
|
594
785
|
}
|
|
595
786
|
for (const block of toolBlocks) {
|
|
596
787
|
this.appendMessage({
|
|
@@ -598,7 +789,9 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
598
789
|
name: block.name || 'tool',
|
|
599
790
|
summary: this.summarizeToolInput(block.input),
|
|
600
791
|
input: block.input,
|
|
601
|
-
toolUseId: block.id
|
|
792
|
+
toolUseId: block.id,
|
|
793
|
+
turnId: turn?.id || null,
|
|
794
|
+
startedAtMs: Date.now()
|
|
602
795
|
});
|
|
603
796
|
}
|
|
604
797
|
this.setStatus('thinking');
|
|
@@ -606,6 +799,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
606
799
|
}
|
|
607
800
|
|
|
608
801
|
if (message.type === 'user') {
|
|
802
|
+
const turn = this.currentTurn();
|
|
609
803
|
const content = message.message && message.message.content;
|
|
610
804
|
if (Array.isArray(content)) {
|
|
611
805
|
for (const item of content) {
|
|
@@ -614,7 +808,9 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
614
808
|
kind: 'tool-result',
|
|
615
809
|
toolUseId: item.tool_use_id,
|
|
616
810
|
text: textFromContent([item]).trim(),
|
|
617
|
-
isError: Boolean(item.is_error)
|
|
811
|
+
isError: Boolean(item.is_error),
|
|
812
|
+
turnId: turn?.id || null,
|
|
813
|
+
completedAtMs: Date.now()
|
|
618
814
|
});
|
|
619
815
|
}
|
|
620
816
|
}
|
|
@@ -623,8 +819,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
623
819
|
}
|
|
624
820
|
|
|
625
821
|
if (message.type === 'result') {
|
|
626
|
-
this.
|
|
627
|
-
this.setStatus('idle');
|
|
822
|
+
this.completeCurrentTurn(message.is_error || message.subtype !== 'success' ? 'failed' : 'completed', message.duration_ms);
|
|
823
|
+
this.setStatus(this.turnQueue.length > 0 ? 'thinking' : 'idle');
|
|
628
824
|
if (this.inputSeq > this.completionReadInputSeq) {
|
|
629
825
|
this.hasUnreadCompletion = true;
|
|
630
826
|
}
|
|
@@ -635,31 +831,6 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
635
831
|
}
|
|
636
832
|
}
|
|
637
833
|
|
|
638
|
-
updateUsageFromResult(message) {
|
|
639
|
-
const usage = message && (message.usage || message.total_usage || message.result && message.result.usage);
|
|
640
|
-
const context = usage && (usage.context_remaining || usage.contextRemaining || usage.remaining_context || usage.remainingContext);
|
|
641
|
-
this.contextRemaining = typeof context === 'number' ? context : null;
|
|
642
|
-
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.model || message.result && message.result.model);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
updateLatestUsageFromClaudeUsage(usage, model = null) {
|
|
646
|
-
const inputTokens = Number(usage.input_tokens || usage.inputTokens || 0);
|
|
647
|
-
const outputTokens = Number(usage.output_tokens || usage.outputTokens || 0);
|
|
648
|
-
const cacheCreation = Number(usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0);
|
|
649
|
-
const cacheRead = Number(usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
|
|
650
|
-
const contextSize = inputTokens + cacheCreation + cacheRead;
|
|
651
|
-
this.latestUsage = {
|
|
652
|
-
inputTokens,
|
|
653
|
-
outputTokens,
|
|
654
|
-
cacheCreation,
|
|
655
|
-
cacheRead,
|
|
656
|
-
totalTokens: inputTokens + outputTokens + cacheCreation + cacheRead,
|
|
657
|
-
contextSize,
|
|
658
|
-
model: model || null,
|
|
659
|
-
updatedAt: Date.now()
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
834
|
summarizeToolInput(input) {
|
|
664
835
|
if (!input || typeof input !== 'object') return '';
|
|
665
836
|
if (typeof input.command === 'string') return input.command;
|
|
@@ -673,6 +844,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
673
844
|
const item = this.createMessageItem(message);
|
|
674
845
|
this.messages.push(item);
|
|
675
846
|
this.emitEvent({ type: 'message', message: item });
|
|
847
|
+
return item;
|
|
676
848
|
}
|
|
677
849
|
|
|
678
850
|
setStatus(status) {
|