@sumicom/quicksave 0.8.9 → 0.8.11
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/dist/ai/cardBuilder.d.ts +67 -2
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +364 -26
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +14 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +83 -2
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeCodeProvider.d.ts.map +1 -1
- package/dist/ai/claudeCodeProvider.js +2 -0
- package/dist/ai/claudeCodeProvider.js.map +1 -1
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +13 -2
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/claudeTerminal/cardSynth.d.ts +63 -0
- package/dist/ai/claudeTerminal/cardSynth.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/cardSynth.js +92 -0
- package/dist/ai/claudeTerminal/cardSynth.js.map +1 -0
- package/dist/ai/claudeTerminal/hookBridge.d.ts +56 -0
- package/dist/ai/claudeTerminal/hookBridge.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/hookBridge.js +131 -0
- package/dist/ai/claudeTerminal/hookBridge.js.map +1 -0
- package/dist/ai/claudeTerminal/hookHandler.d.ts +18 -0
- package/dist/ai/claudeTerminal/hookHandler.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/hookHandler.js +124 -0
- package/dist/ai/claudeTerminal/hookHandler.js.map +1 -0
- package/dist/ai/claudeTerminal/index.d.ts +5 -0
- package/dist/ai/claudeTerminal/index.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/index.js +7 -0
- package/dist/ai/claudeTerminal/index.js.map +1 -0
- package/dist/ai/claudeTerminal/jsonlTail.d.ts +40 -0
- package/dist/ai/claudeTerminal/jsonlTail.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/jsonlTail.js +129 -0
- package/dist/ai/claudeTerminal/jsonlTail.js.map +1 -0
- package/dist/ai/claudeTerminal/provider.d.ts +106 -0
- package/dist/ai/claudeTerminal/provider.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/provider.js +579 -0
- package/dist/ai/claudeTerminal/provider.js.map +1 -0
- package/dist/ai/claudeTerminal/settingsBuilder.d.ts +44 -0
- package/dist/ai/claudeTerminal/settingsBuilder.d.ts.map +1 -0
- package/dist/ai/claudeTerminal/settingsBuilder.js +40 -0
- package/dist/ai/claudeTerminal/settingsBuilder.js.map +1 -0
- package/dist/ai/codexAppServer/cardAdapter.d.ts.map +1 -1
- package/dist/ai/codexAppServer/cardAdapter.js +13 -5
- package/dist/ai/codexAppServer/cardAdapter.js.map +1 -1
- package/dist/ai/codexAppServer/provider.d.ts +13 -2
- package/dist/ai/codexAppServer/provider.d.ts.map +1 -1
- package/dist/ai/codexAppServer/provider.js +161 -26
- package/dist/ai/codexAppServer/provider.js.map +1 -1
- package/dist/ai/profiles/project-sandbox.sb +26 -0
- package/dist/ai/provider.d.ts +29 -1
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +1 -0
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/queuedUserPrompts.d.ts +8 -0
- package/dist/ai/queuedUserPrompts.d.ts.map +1 -0
- package/dist/ai/queuedUserPrompts.js +19 -0
- package/dist/ai/queuedUserPrompts.js.map +1 -0
- package/dist/ai/realtimeTranscription.d.ts +39 -0
- package/dist/ai/realtimeTranscription.d.ts.map +1 -0
- package/dist/ai/realtimeTranscription.js +154 -0
- package/dist/ai/realtimeTranscription.js.map +1 -0
- package/dist/ai/sandboxMcp.d.ts +7 -0
- package/dist/ai/sandboxMcp.d.ts.map +1 -1
- package/dist/ai/sandboxMcp.js +4 -0
- package/dist/ai/sandboxMcp.js.map +1 -1
- package/dist/ai/sandboxMcpStdio.d.ts +1 -1
- package/dist/ai/sandboxMcpStdio.js +120 -47
- package/dist/ai/sandboxMcpStdio.js.map +1 -1
- package/dist/ai/sessionManager.d.ts +14 -0
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +147 -3
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts +6 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +13 -0
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/ai/sessionRegistryLocator.d.ts +14 -0
- package/dist/ai/sessionRegistryLocator.d.ts.map +1 -0
- package/dist/ai/sessionRegistryLocator.js +44 -0
- package/dist/ai/sessionRegistryLocator.js.map +1 -0
- package/dist/ai/systemPrompt.d.ts.map +1 -1
- package/dist/ai/systemPrompt.js +1 -0
- package/dist/ai/systemPrompt.js.map +1 -1
- package/dist/ai/voiceStream.d.ts +74 -0
- package/dist/ai/voiceStream.d.ts.map +1 -0
- package/dist/ai/voiceStream.js +179 -0
- package/dist/ai/voiceStream.js.map +1 -0
- package/dist/ai/voiceTranscription.d.ts +33 -0
- package/dist/ai/voiceTranscription.d.ts.map +1 -0
- package/dist/ai/voiceTranscription.js +122 -0
- package/dist/ai/voiceTranscription.js.map +1 -0
- package/dist/files/fileBrowser.d.ts.map +1 -1
- package/dist/files/fileBrowser.js +3 -2
- package/dist/files/fileBrowser.js.map +1 -1
- package/dist/handlers/legacyBusAdapter.d.ts +1 -1
- package/dist/handlers/legacyBusAdapter.d.ts.map +1 -1
- package/dist/handlers/legacyBusAdapter.js +6 -1
- package/dist/handlers/legacyBusAdapter.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +5 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +100 -5
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/ipcClient.js.map +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +74 -3
- package/dist/service/run.js.map +1 -1
- package/dist/service/types.js +1 -1
- package/package.json +6 -3
package/dist/ai/cardBuilder.js
CHANGED
|
@@ -46,6 +46,47 @@ const TOOL_RESULT_TRUNCATE_LENGTH = 500;
|
|
|
46
46
|
function cardHistoryPath(sessionId) {
|
|
47
47
|
return join(getCardHistoryDir(), `${sessionId}.json`);
|
|
48
48
|
}
|
|
49
|
+
function cleanPersistedCard(card) {
|
|
50
|
+
const { pendingInput, ...rest } = card;
|
|
51
|
+
void pendingInput;
|
|
52
|
+
if (rest.type === 'assistant_text') {
|
|
53
|
+
return { ...rest, streaming: false };
|
|
54
|
+
}
|
|
55
|
+
return rest;
|
|
56
|
+
}
|
|
57
|
+
function mergeCardsById(existing, incoming) {
|
|
58
|
+
const byId = new Map();
|
|
59
|
+
for (const card of existing)
|
|
60
|
+
byId.set(card.id, card);
|
|
61
|
+
for (const card of incoming)
|
|
62
|
+
byId.set(card.id, card);
|
|
63
|
+
return sortCardsChronologically(Array.from(byId.values()));
|
|
64
|
+
}
|
|
65
|
+
function sortCardsChronologically(cards) {
|
|
66
|
+
return cards
|
|
67
|
+
.map((card, index) => ({ card, index }))
|
|
68
|
+
.sort((a, b) => {
|
|
69
|
+
const at = Number.isFinite(a.card.timestamp) ? a.card.timestamp : 0;
|
|
70
|
+
const bt = Number.isFinite(b.card.timestamp) ? b.card.timestamp : 0;
|
|
71
|
+
return at - bt || a.index - b.index;
|
|
72
|
+
})
|
|
73
|
+
.map(({ card }) => card);
|
|
74
|
+
}
|
|
75
|
+
function maxCardSequenceForSession(cards, sessionId) {
|
|
76
|
+
const prefix = `${sessionId}:`;
|
|
77
|
+
let max = 0;
|
|
78
|
+
for (const card of cards) {
|
|
79
|
+
if (!card.id.startsWith(prefix))
|
|
80
|
+
continue;
|
|
81
|
+
const raw = card.id.slice(prefix.length);
|
|
82
|
+
if (!/^\d+$/.test(raw))
|
|
83
|
+
continue;
|
|
84
|
+
const seq = Number(raw);
|
|
85
|
+
if (Number.isSafeInteger(seq) && seq > max)
|
|
86
|
+
max = seq;
|
|
87
|
+
}
|
|
88
|
+
return max;
|
|
89
|
+
}
|
|
49
90
|
/**
|
|
50
91
|
* Load persisted card history for a memory-mode session.
|
|
51
92
|
* Returns cards in insertion order, or empty array if none exist.
|
|
@@ -57,20 +98,20 @@ export async function loadPersistedCards(sessionId) {
|
|
|
57
98
|
try {
|
|
58
99
|
const raw = await readFile(p, 'utf-8');
|
|
59
100
|
const data = JSON.parse(raw);
|
|
60
|
-
return Array.isArray(data) ? data : [];
|
|
101
|
+
return Array.isArray(data) ? sortCardsChronologically(data) : [];
|
|
61
102
|
}
|
|
62
103
|
catch {
|
|
63
104
|
return [];
|
|
64
105
|
}
|
|
65
106
|
}
|
|
66
107
|
// ── Direct JSONL file reading (replaces SDK getSessionMessages/listSubagents) ──
|
|
67
|
-
function encodeCwdPath(cwd) {
|
|
108
|
+
export function encodeCwdPath(cwd) {
|
|
68
109
|
return cwd.replace(/\//g, '-');
|
|
69
110
|
}
|
|
70
|
-
function claudeProjectDir(cwd) {
|
|
111
|
+
export function claudeProjectDir(cwd) {
|
|
71
112
|
return join(homedir(), '.claude', 'projects', encodeCwdPath(cwd));
|
|
72
113
|
}
|
|
73
|
-
function jsonlPath(sessionId, cwd) {
|
|
114
|
+
export function jsonlPath(sessionId, cwd) {
|
|
74
115
|
return join(claudeProjectDir(cwd), sessionId + '.jsonl');
|
|
75
116
|
}
|
|
76
117
|
function parseJSONLContent(content) {
|
|
@@ -220,6 +261,58 @@ function extractToolResultText(content) {
|
|
|
220
261
|
}
|
|
221
262
|
return JSON.stringify(content);
|
|
222
263
|
}
|
|
264
|
+
/** Compact "1m 25s" / "45s" duration for the `text` fallback. The PWA renders
|
|
265
|
+
* its own badge from `meta.durationMs`; this keeps debug logs and non-PWA
|
|
266
|
+
* consumers readable. */
|
|
267
|
+
export function formatDurationMs(ms) {
|
|
268
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
269
|
+
return '0s';
|
|
270
|
+
const totalSec = Math.round(ms / 1000);
|
|
271
|
+
const m = Math.floor(totalSec / 60);
|
|
272
|
+
const s = totalSec % 60;
|
|
273
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
274
|
+
}
|
|
275
|
+
/** Build a structured `turn_duration` system card from a Claude JSONL entry. */
|
|
276
|
+
export function turnDurationCard(msg, id) {
|
|
277
|
+
const durationMs = typeof msg.durationMs === 'number' ? msg.durationMs : 0;
|
|
278
|
+
const messageCount = typeof msg.messageCount === 'number' ? msg.messageCount : 0;
|
|
279
|
+
return {
|
|
280
|
+
type: 'system',
|
|
281
|
+
id,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
text: `Turn: ${formatDurationMs(durationMs)} · ${messageCount} messages`,
|
|
284
|
+
subtype: 'info',
|
|
285
|
+
meta: { kind: 'turn_duration', durationMs, messageCount },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** Build a structured `stop_hook_summary` system card from a Claude JSONL entry.
|
|
289
|
+
* Flags as `warning` when any hook errored or blocked continuation — that's the
|
|
290
|
+
* channel that surfaces hook misconfigs (e.g. a bad interpreter path). */
|
|
291
|
+
export function stopHookSummaryCard(msg, id) {
|
|
292
|
+
const hooks = Array.isArray(msg.hookInfos)
|
|
293
|
+
? msg.hookInfos.map((h) => ({
|
|
294
|
+
command: typeof h?.command === 'string' ? h.command : '',
|
|
295
|
+
durationMs: typeof h?.durationMs === 'number' ? h.durationMs : 0,
|
|
296
|
+
}))
|
|
297
|
+
: [];
|
|
298
|
+
const errors = Array.isArray(msg.hookErrors)
|
|
299
|
+
? msg.hookErrors.filter((e) => typeof e === 'string')
|
|
300
|
+
: [];
|
|
301
|
+
const preventedContinuation = msg.preventedContinuation === true;
|
|
302
|
+
const level = typeof msg.level === 'string' ? msg.level : undefined;
|
|
303
|
+
const stopReason = typeof msg.stopReason === 'string' && msg.stopReason ? msg.stopReason : undefined;
|
|
304
|
+
const text = errors.length > 0
|
|
305
|
+
? `Stop hook: ${hooks.length} ran, ${errors.length} error${errors.length > 1 ? 's' : ''}`
|
|
306
|
+
: `Stop hook: ${hooks.length} ran`;
|
|
307
|
+
return {
|
|
308
|
+
type: 'system',
|
|
309
|
+
id,
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
text,
|
|
312
|
+
subtype: errors.length > 0 || preventedContinuation ? 'warning' : 'info',
|
|
313
|
+
meta: { kind: 'stop_hook_summary', hooks, errors, level, preventedContinuation, stopReason },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
223
316
|
// ============================================================================
|
|
224
317
|
// StreamCardBuilder — streaming session, emits CardEvents
|
|
225
318
|
// ============================================================================
|
|
@@ -234,6 +327,14 @@ export class StreamCardBuilder {
|
|
|
234
327
|
agentIdToCardId = new Map();
|
|
235
328
|
/** Cards created for subagent permissions — removed after resolution. */
|
|
236
329
|
ephemeralCards = new Set();
|
|
330
|
+
/** Agent tool_use input stashed from assistant message, keyed by tool_use block.id.
|
|
331
|
+
* Consumed when task_started arrives with the matching tool_use_id. */
|
|
332
|
+
agentToolUseStash = new Map();
|
|
333
|
+
/** Card ID of the currently running subagent; tool calls are nested here instead of
|
|
334
|
+
* being emitted as top-level ToolCallCards. Cleared on task_notification. */
|
|
335
|
+
activeSubagentCardId = null;
|
|
336
|
+
/** Nested tool_use_id → subagentCardId — lets toolResult() route to the right card. */
|
|
337
|
+
nestedToolUseToSubagentCard = new Map();
|
|
237
338
|
/** Current streaming assistant_text card (for append_text events) */
|
|
238
339
|
currentTextCardId = null;
|
|
239
340
|
/** JSONL file byte offset at the start of the current turn — history reads stop here. */
|
|
@@ -248,6 +349,13 @@ export class StreamCardBuilder {
|
|
|
248
349
|
updateSessionId(sessionId) {
|
|
249
350
|
this.sessionId = sessionId;
|
|
250
351
|
}
|
|
352
|
+
/** Continue the local card id counter after persisted memory-mode history.
|
|
353
|
+
* Used on cold resume after daemon restart so new Codex/OpenCode cards
|
|
354
|
+
* append after existing cards instead of reusing `sessionId:1`, `:2`, ...
|
|
355
|
+
* and overwriting or reordering older history. */
|
|
356
|
+
seedSequenceFromCards(cards) {
|
|
357
|
+
this.seq = Math.max(this.seq, maxCardSequenceForSession(cards, this.sessionId));
|
|
358
|
+
}
|
|
251
359
|
/** Start a new turn: reset per-turn state, keep accumulated cards. */
|
|
252
360
|
startNewTurn() {
|
|
253
361
|
this.currentTextCardId = null;
|
|
@@ -258,6 +366,9 @@ export class StreamCardBuilder {
|
|
|
258
366
|
this.toolUseIdToCardId.clear();
|
|
259
367
|
this.agentIdToCardId.clear();
|
|
260
368
|
this.ephemeralCards.clear();
|
|
369
|
+
this.agentToolUseStash.clear();
|
|
370
|
+
this.activeSubagentCardId = null;
|
|
371
|
+
this.nestedToolUseToSubagentCard.clear();
|
|
261
372
|
this.currentTextCardId = null;
|
|
262
373
|
}
|
|
263
374
|
/**
|
|
@@ -269,17 +380,20 @@ export class StreamCardBuilder {
|
|
|
269
380
|
const cards = this.getCards();
|
|
270
381
|
if (cards.length === 0)
|
|
271
382
|
return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
383
|
+
await this.persistCardBatch(cards);
|
|
384
|
+
}
|
|
385
|
+
/** Persist a single card immediately. Used when a memory-mode provider
|
|
386
|
+
* injects a user prompt into an already-running turn; the card must survive
|
|
387
|
+
* refresh before the turn's normal end-of-turn persist runs. */
|
|
388
|
+
async persistCard(card) {
|
|
389
|
+
await this.persistCardBatch([card]);
|
|
390
|
+
}
|
|
391
|
+
async persistCardBatch(cards) {
|
|
392
|
+
if (cards.length === 0)
|
|
393
|
+
return;
|
|
394
|
+
const cleaned = cards.map(cleanPersistedCard);
|
|
280
395
|
const dir = getCardHistoryDir();
|
|
281
396
|
const p = cardHistoryPath(this.sessionId);
|
|
282
|
-
// Append to existing history
|
|
283
397
|
let existing = [];
|
|
284
398
|
try {
|
|
285
399
|
if (existsSync(p)) {
|
|
@@ -290,7 +404,7 @@ export class StreamCardBuilder {
|
|
|
290
404
|
}
|
|
291
405
|
}
|
|
292
406
|
catch { /* start fresh */ }
|
|
293
|
-
const merged =
|
|
407
|
+
const merged = mergeCardsById(existing, cleaned);
|
|
294
408
|
await mkdir(dir, { recursive: true });
|
|
295
409
|
await writeFile(p, JSON.stringify(merged) + '\n');
|
|
296
410
|
}
|
|
@@ -364,6 +478,9 @@ export class StreamCardBuilder {
|
|
|
364
478
|
this.toolUseIdToCardId.clear();
|
|
365
479
|
this.agentIdToCardId.clear();
|
|
366
480
|
this.ephemeralCards.clear();
|
|
481
|
+
this.agentToolUseStash.clear();
|
|
482
|
+
this.activeSubagentCardId = null;
|
|
483
|
+
this.nestedToolUseToSubagentCard.clear();
|
|
367
484
|
this.currentTextCardId = null;
|
|
368
485
|
}
|
|
369
486
|
/** Cancel any pending deferred clear. Called on hot resume so the queued clear
|
|
@@ -468,6 +585,15 @@ export class StreamCardBuilder {
|
|
|
468
585
|
*/
|
|
469
586
|
toolUse(toolName, toolInput, toolUseId) {
|
|
470
587
|
this.currentTextCardId = null;
|
|
588
|
+
// Route to active subagent: nest this tool call inside the SubagentCard
|
|
589
|
+
if (this.activeSubagentCardId) {
|
|
590
|
+
const subagentCard = this.cards.get(this.activeSubagentCardId);
|
|
591
|
+
if (subagentCard) {
|
|
592
|
+
const toolCalls = [...(subagentCard.toolCalls ?? []), { id: toolUseId, toolName, toolInput }];
|
|
593
|
+
this.nestedToolUseToSubagentCard.set(toolUseId, this.activeSubagentCardId);
|
|
594
|
+
return this.updateEvent(this.activeSubagentCardId, { toolCalls });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
471
597
|
// Check if card was pre-created from canUseTool
|
|
472
598
|
const existingCardId = this.toolUseIdToCardId.get(toolUseId);
|
|
473
599
|
if (existingCardId) {
|
|
@@ -551,6 +677,17 @@ export class StreamCardBuilder {
|
|
|
551
677
|
return Array.from(this.cards.values());
|
|
552
678
|
}
|
|
553
679
|
toolResult(toolUseId, content, isError) {
|
|
680
|
+
// Route to active subagent's nested tool call
|
|
681
|
+
const subagentCardId = this.nestedToolUseToSubagentCard.get(toolUseId);
|
|
682
|
+
if (subagentCardId) {
|
|
683
|
+
const subagentCard = this.cards.get(subagentCardId);
|
|
684
|
+
if (!subagentCard?.toolCalls)
|
|
685
|
+
return null;
|
|
686
|
+
const truncated = content.length > TOOL_RESULT_TRUNCATE_LENGTH;
|
|
687
|
+
const resultContent = truncated ? content.slice(0, TOOL_RESULT_TRUNCATE_LENGTH) + ' [truncated]' : content;
|
|
688
|
+
const toolCalls = subagentCard.toolCalls.map(tc => tc.id === toolUseId ? { ...tc, result: { content: resultContent, isError } } : tc);
|
|
689
|
+
return this.updateEvent(subagentCardId, { toolCalls });
|
|
690
|
+
}
|
|
554
691
|
const cardId = this.toolUseIdToCardId.get(toolUseId);
|
|
555
692
|
if (!cardId)
|
|
556
693
|
return null;
|
|
@@ -569,9 +706,19 @@ export class StreamCardBuilder {
|
|
|
569
706
|
return null;
|
|
570
707
|
return this.updateEvent(cardId, { answers });
|
|
571
708
|
}
|
|
572
|
-
|
|
709
|
+
/** Stash Agent tool_use input fields for later use when task_started arrives. */
|
|
710
|
+
stashAgentToolUseInput(toolUseId, input) {
|
|
711
|
+
this.agentToolUseStash.set(toolUseId, input);
|
|
712
|
+
}
|
|
713
|
+
subagentStart(description, agentId, toolUseId, extras) {
|
|
573
714
|
this.currentTextCardId = null;
|
|
574
715
|
const id = this.nextId();
|
|
716
|
+
const stashed = toolUseId ? this.agentToolUseStash.get(toolUseId) : undefined;
|
|
717
|
+
if (toolUseId)
|
|
718
|
+
this.agentToolUseStash.delete(toolUseId);
|
|
719
|
+
const subagentType = stashed?.subagentType ?? extras?.subagentType;
|
|
720
|
+
const requestedModel = stashed?.requestedModel ?? extras?.requestedModel;
|
|
721
|
+
const prompt = extras?.prompt ?? stashed?.prompt;
|
|
575
722
|
const card = {
|
|
576
723
|
type: 'subagent', id, timestamp: Date.now(),
|
|
577
724
|
description,
|
|
@@ -579,9 +726,12 @@ export class StreamCardBuilder {
|
|
|
579
726
|
agentId,
|
|
580
727
|
status: 'running',
|
|
581
728
|
toolUseCount: 0,
|
|
729
|
+
...(subagentType ? { subagentType } : {}),
|
|
730
|
+
...(requestedModel ? { requestedModel } : {}),
|
|
731
|
+
...(prompt ? { prompt } : {}),
|
|
582
732
|
};
|
|
583
733
|
this.agentIdToCardId.set(agentId, id);
|
|
584
|
-
|
|
734
|
+
this.activeSubagentCardId = id;
|
|
585
735
|
const afterCardId = toolUseId ? this.toolUseIdToCardId.get(toolUseId) : undefined;
|
|
586
736
|
return this.addEvent(card, afterCardId);
|
|
587
737
|
}
|
|
@@ -602,12 +752,34 @@ export class StreamCardBuilder {
|
|
|
602
752
|
?? (toolUseId ? this.agentIdToCardId.get(toolUseId) : undefined);
|
|
603
753
|
if (!cardId)
|
|
604
754
|
return null;
|
|
755
|
+
if (cardId === this.activeSubagentCardId)
|
|
756
|
+
this.activeSubagentCardId = null;
|
|
605
757
|
return this.updateEvent(cardId, { status, summary });
|
|
606
758
|
}
|
|
607
759
|
systemMessage(text, subtype) {
|
|
608
760
|
const card = { type: 'system', id: this.nextId(), timestamp: Date.now(), text, subtype };
|
|
609
761
|
return this.addEvent(card);
|
|
610
762
|
}
|
|
763
|
+
/** Live structured `turn_duration` card — mirrors the history builder so a
|
|
764
|
+
* reload renders identically. */
|
|
765
|
+
turnDuration(raw) {
|
|
766
|
+
return this.addEvent(turnDurationCard(raw, this.nextId()));
|
|
767
|
+
}
|
|
768
|
+
/** Live structured `stop_hook_summary` card — see history builder counterpart. */
|
|
769
|
+
stopHookSummary(raw) {
|
|
770
|
+
return this.addEvent(stopHookSummaryCard(raw, this.nextId()));
|
|
771
|
+
}
|
|
772
|
+
generatedImage(prompt, status, savedPath) {
|
|
773
|
+
const card = {
|
|
774
|
+
type: 'generated_image',
|
|
775
|
+
id: this.nextId(),
|
|
776
|
+
timestamp: Date.now(),
|
|
777
|
+
prompt,
|
|
778
|
+
status,
|
|
779
|
+
...(savedPath ? { savedPath } : {}),
|
|
780
|
+
};
|
|
781
|
+
return this.addEvent(card);
|
|
782
|
+
}
|
|
611
783
|
recoverySuggested(reason, action, label) {
|
|
612
784
|
const card = {
|
|
613
785
|
type: 'recovery_suggested',
|
|
@@ -623,6 +795,54 @@ export class StreamCardBuilder {
|
|
|
623
795
|
return this.systemMessage(`Error: ${text}`, 'error');
|
|
624
796
|
}
|
|
625
797
|
}
|
|
798
|
+
/** Read tool calls from a subagent's JSONL — used by buildCardsFromHistory to nest them. */
|
|
799
|
+
async function readSubagentToolCalls(jsonlPath) {
|
|
800
|
+
try {
|
|
801
|
+
const content = await readFile(jsonlPath, 'utf-8');
|
|
802
|
+
const messages = parseJSONLContent(content);
|
|
803
|
+
const resultMap = new Map();
|
|
804
|
+
const toolCalls = [];
|
|
805
|
+
for (const msg of messages) {
|
|
806
|
+
const rawMsg = msg.message;
|
|
807
|
+
if (!Array.isArray(rawMsg?.content))
|
|
808
|
+
continue;
|
|
809
|
+
if (msg.type === 'user') {
|
|
810
|
+
for (const block of rawMsg.content) {
|
|
811
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
812
|
+
const text = extractToolResultText(block.content);
|
|
813
|
+
resultMap.set(block.tool_use_id, { content: text, isError: !!block.is_error });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else if (msg.type === 'assistant') {
|
|
818
|
+
for (const block of rawMsg.content) {
|
|
819
|
+
if (block.type === 'tool_use' && block.id && block.name !== 'Agent') {
|
|
820
|
+
toolCalls.push({
|
|
821
|
+
id: block.id,
|
|
822
|
+
toolName: block.name,
|
|
823
|
+
toolInput: typeof block.input === 'object' && block.input ? block.input : {},
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Attach results and apply truncation
|
|
830
|
+
for (const tc of toolCalls) {
|
|
831
|
+
const r = resultMap.get(tc.id);
|
|
832
|
+
if (r) {
|
|
833
|
+
const truncated = r.content.length > TOOL_RESULT_TRUNCATE_LENGTH;
|
|
834
|
+
tc.result = {
|
|
835
|
+
content: truncated ? r.content.slice(0, TOOL_RESULT_TRUNCATE_LENGTH) + ' [truncated]' : r.content,
|
|
836
|
+
isError: r.isError,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return toolCalls;
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
}
|
|
626
846
|
// ============================================================================
|
|
627
847
|
// buildCardsFromHistory — convert JSONL history into Card[]
|
|
628
848
|
// ============================================================================
|
|
@@ -682,7 +902,52 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
682
902
|
// The Claude CLI stores this as `toolUseResult` at the message envelope
|
|
683
903
|
// level, separate from the human-readable tool_result.content string.
|
|
684
904
|
const askUserResults = new Map();
|
|
905
|
+
const taskStarts = new Map();
|
|
906
|
+
const taskProgress = new Map();
|
|
907
|
+
const taskEnds = new Map();
|
|
908
|
+
const taskKey = (agentId, toolUseId) => {
|
|
909
|
+
if (typeof toolUseId === 'string' && toolUseId.length > 0)
|
|
910
|
+
return toolUseId;
|
|
911
|
+
if (typeof agentId === 'string' && agentId.length > 0)
|
|
912
|
+
return agentId;
|
|
913
|
+
return null;
|
|
914
|
+
};
|
|
685
915
|
for (const msg of allMessages) {
|
|
916
|
+
if (msg.type === 'system') {
|
|
917
|
+
const subtype = msg.subtype;
|
|
918
|
+
const key = taskKey(msg.task_id, msg.tool_use_id);
|
|
919
|
+
if (key && subtype === 'task_started') {
|
|
920
|
+
taskStarts.set(key, {
|
|
921
|
+
description: typeof msg.description === 'string' ? msg.description : '',
|
|
922
|
+
agentId: typeof msg.task_id === 'string' ? msg.task_id : key,
|
|
923
|
+
toolUseId: typeof msg.tool_use_id === 'string' ? msg.tool_use_id : key,
|
|
924
|
+
prompt: typeof msg.prompt === 'string' ? msg.prompt : undefined,
|
|
925
|
+
subagentType: typeof msg.subagent_type === 'string'
|
|
926
|
+
? msg.subagent_type
|
|
927
|
+
: typeof msg.subagentType === 'string'
|
|
928
|
+
? msg.subagentType
|
|
929
|
+
: undefined,
|
|
930
|
+
requestedModel: typeof msg.model === 'string'
|
|
931
|
+
? msg.model
|
|
932
|
+
: typeof msg.requested_model === 'string'
|
|
933
|
+
? msg.requested_model
|
|
934
|
+
: undefined,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
else if (key && subtype === 'task_progress') {
|
|
938
|
+
taskProgress.set(key, {
|
|
939
|
+
toolUseCount: typeof msg.usage?.tool_uses === 'number' ? msg.usage.tool_uses : undefined,
|
|
940
|
+
lastToolName: typeof msg.last_tool_name === 'string' ? msg.last_tool_name : undefined,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
else if (key && subtype === 'task_notification') {
|
|
944
|
+
const status = msg.status;
|
|
945
|
+
taskEnds.set(key, {
|
|
946
|
+
status: status === 'failed' || status === 'stopped' ? status : 'completed',
|
|
947
|
+
summary: typeof msg.summary === 'string' ? msg.summary : undefined,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
686
951
|
const rawMsg = msg.message;
|
|
687
952
|
const toolUseResult = msg.toolUseResult;
|
|
688
953
|
if (!Array.isArray(rawMsg?.content))
|
|
@@ -707,24 +972,32 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
707
972
|
// System messages (task_started) are NOT reliably present in the JSONL.
|
|
708
973
|
// Instead, read subagent meta.json files and match by description to the
|
|
709
974
|
// Agent tool_use input.description.
|
|
710
|
-
// Build description → toolUseId map
|
|
975
|
+
// Build description → toolUseId map and capture Agent tool_use input data
|
|
711
976
|
const descToToolUseId = new Map();
|
|
977
|
+
const agentInputByToolUseId = new Map();
|
|
712
978
|
for (const msg of allMessages) {
|
|
713
979
|
const rawMsg = msg.message;
|
|
714
980
|
if (!Array.isArray(rawMsg?.content))
|
|
715
981
|
continue;
|
|
716
982
|
for (const block of rawMsg.content) {
|
|
717
983
|
if (block.type === 'tool_use' && block.name === 'Agent' && block.id) {
|
|
718
|
-
const
|
|
984
|
+
const input = block.input;
|
|
985
|
+
const desc = input?.description ?? '';
|
|
719
986
|
if (desc)
|
|
720
987
|
descToToolUseId.set(desc, block.id);
|
|
988
|
+
agentInputByToolUseId.set(block.id, {
|
|
989
|
+
prompt: typeof input?.prompt === 'string' ? input.prompt : undefined,
|
|
990
|
+
subagentType: typeof input?.subagent_type === 'string' ? input.subagent_type : undefined,
|
|
991
|
+
requestedModel: typeof input?.model === 'string' ? input.model : undefined,
|
|
992
|
+
});
|
|
721
993
|
}
|
|
722
994
|
}
|
|
723
995
|
}
|
|
996
|
+
const subagentsDir = join(claudeProjectDir(cwd), sessionId, 'subagents');
|
|
724
997
|
const toolUseIdToAgentId = new Map();
|
|
998
|
+
const subagentToolCallsByToolUseId = new Map();
|
|
725
999
|
try {
|
|
726
1000
|
const agentIds = await listSubagentIdsFromDisk(sessionId, cwd);
|
|
727
|
-
const subagentsDir = join(claudeProjectDir(cwd), sessionId, 'subagents');
|
|
728
1001
|
for (const agentId of agentIds) {
|
|
729
1002
|
try {
|
|
730
1003
|
const meta = JSON.parse(await readFile(join(subagentsDir, `${agentId}.meta.json`), 'utf-8'));
|
|
@@ -740,6 +1013,12 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
740
1013
|
toolUseIdToAgentId.set(toolUseId, toolUseId);
|
|
741
1014
|
}
|
|
742
1015
|
}
|
|
1016
|
+
// Read each subagent's JSONL to extract nested tool calls
|
|
1017
|
+
for (const [toolUseId, agentId] of toolUseIdToAgentId) {
|
|
1018
|
+
const toolCalls = await readSubagentToolCalls(join(subagentsDir, `${agentId}.jsonl`));
|
|
1019
|
+
if (toolCalls.length > 0)
|
|
1020
|
+
subagentToolCallsByToolUseId.set(toolUseId, toolCalls);
|
|
1021
|
+
}
|
|
743
1022
|
}
|
|
744
1023
|
catch {
|
|
745
1024
|
// No subagents or SDK error — fallback: agentId = toolUseId
|
|
@@ -765,12 +1044,54 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
765
1044
|
let seq = Math.max(0, total - offset - limit);
|
|
766
1045
|
const nextId = () => `${sessionId}:h:${++seq}`;
|
|
767
1046
|
const cards = [];
|
|
1047
|
+
const emittedSubagentKeys = new Set();
|
|
1048
|
+
const slicedAgentToolUseIds = new Set();
|
|
1049
|
+
for (const msg of sliced) {
|
|
1050
|
+
const rawMsg = msg.message;
|
|
1051
|
+
if (!Array.isArray(rawMsg?.content))
|
|
1052
|
+
continue;
|
|
1053
|
+
for (const block of rawMsg.content) {
|
|
1054
|
+
if (block.type === 'tool_use' && block.name === 'Agent' && block.id) {
|
|
1055
|
+
slicedAgentToolUseIds.add(block.id);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const subagentCardFromTask = (task) => {
|
|
1060
|
+
const key = task.toolUseId || task.agentId;
|
|
1061
|
+
const end = taskEnds.get(key);
|
|
1062
|
+
const progress = taskProgress.get(key);
|
|
1063
|
+
return {
|
|
1064
|
+
type: 'subagent',
|
|
1065
|
+
id: nextId(),
|
|
1066
|
+
timestamp: Date.now(),
|
|
1067
|
+
description: task.description,
|
|
1068
|
+
toolUseId: task.toolUseId,
|
|
1069
|
+
agentId: task.agentId,
|
|
1070
|
+
status: end?.status ?? 'running',
|
|
1071
|
+
...(end?.summary ? { summary: end.summary } : {}),
|
|
1072
|
+
toolUseCount: progress?.toolUseCount ?? 0,
|
|
1073
|
+
...(progress?.lastToolName ? { lastToolName: progress.lastToolName } : {}),
|
|
1074
|
+
...(task.prompt ? { prompt: task.prompt } : {}),
|
|
1075
|
+
...(task.subagentType ? { subagentType: task.subagentType } : {}),
|
|
1076
|
+
...(task.requestedModel ? { requestedModel: task.requestedModel } : {}),
|
|
1077
|
+
};
|
|
1078
|
+
};
|
|
768
1079
|
for (const msg of sliced) {
|
|
769
1080
|
// ── System messages ──
|
|
770
1081
|
if (msg.type === 'system') {
|
|
771
1082
|
const subtype = msg.subtype;
|
|
772
|
-
|
|
773
|
-
if (subtype === 'task_started'
|
|
1083
|
+
const key = taskKey(msg.task_id, msg.tool_use_id);
|
|
1084
|
+
if (subtype === 'task_started' && key) {
|
|
1085
|
+
const task = taskStarts.get(key);
|
|
1086
|
+
if (task && !slicedAgentToolUseIds.has(task.toolUseId) && !emittedSubagentKeys.has(key)) {
|
|
1087
|
+
cards.push(subagentCardFromTask(task));
|
|
1088
|
+
emittedSubagentKeys.add(key);
|
|
1089
|
+
}
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// Progress/end task events update the task_started card; they do not
|
|
1093
|
+
// render as standalone history cards.
|
|
1094
|
+
if (subtype === 'task_progress' || subtype === 'task_notification')
|
|
774
1095
|
continue;
|
|
775
1096
|
// Skip init and status messages (not useful in history)
|
|
776
1097
|
if (subtype === 'init' || subtype === 'status' || subtype === 'session_state_changed')
|
|
@@ -778,6 +1099,12 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
778
1099
|
if (subtype === 'compact_boundary') {
|
|
779
1100
|
cards.push({ type: 'system', id: nextId(), timestamp: Date.now(), text: 'Context compacted', subtype: 'compacted' });
|
|
780
1101
|
}
|
|
1102
|
+
else if (subtype === 'turn_duration') {
|
|
1103
|
+
cards.push(turnDurationCard(msg, nextId()));
|
|
1104
|
+
}
|
|
1105
|
+
else if (subtype === 'stop_hook_summary') {
|
|
1106
|
+
cards.push(stopHookSummaryCard(msg, nextId()));
|
|
1107
|
+
}
|
|
781
1108
|
else {
|
|
782
1109
|
// Unknown system subtype — show as info
|
|
783
1110
|
cards.push({ type: 'system', id: nextId(), timestamp: Date.now(), text: subtype ?? 'System event', subtype: 'info' });
|
|
@@ -901,12 +1228,17 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
901
1228
|
break;
|
|
902
1229
|
case 'tool_use': {
|
|
903
1230
|
if (agentToolUseIds.has(block.id)) {
|
|
904
|
-
// Agent tool_use →
|
|
1231
|
+
// Agent tool_use → SubagentCard
|
|
905
1232
|
const agentId = toolUseIdToAgentId.get(block.id) ?? block.id;
|
|
906
1233
|
const result = toolResults.get(block.id);
|
|
907
1234
|
const description = block.input?.description ?? '';
|
|
908
1235
|
const summary = result ? result.content.slice(0, 200) : undefined;
|
|
909
|
-
|
|
1236
|
+
const agentInput = agentInputByToolUseId.get(block.id);
|
|
1237
|
+
const taskStart = taskStarts.get(block.id);
|
|
1238
|
+
const progress = taskProgress.get(block.id);
|
|
1239
|
+
const end = taskEnds.get(block.id);
|
|
1240
|
+
const prompt = agentInput?.prompt ?? taskStart?.prompt;
|
|
1241
|
+
const nestedToolCalls = subagentToolCallsByToolUseId.get(block.id);
|
|
910
1242
|
const subagentCard = {
|
|
911
1243
|
type: 'subagent',
|
|
912
1244
|
id: nextId(),
|
|
@@ -914,11 +1246,17 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
|
|
|
914
1246
|
description,
|
|
915
1247
|
toolUseId: block.id,
|
|
916
1248
|
agentId,
|
|
917
|
-
status: result ? 'completed' : 'running',
|
|
918
|
-
summary,
|
|
919
|
-
toolUseCount: 0,
|
|
1249
|
+
status: end?.status ?? (result ? 'completed' : 'running'),
|
|
1250
|
+
summary: end?.summary ?? summary,
|
|
1251
|
+
toolUseCount: progress?.toolUseCount ?? nestedToolCalls?.length ?? 0,
|
|
1252
|
+
...(progress?.lastToolName ? { lastToolName: progress.lastToolName } : {}),
|
|
1253
|
+
...(agentInput?.subagentType ? { subagentType: agentInput.subagentType } : {}),
|
|
1254
|
+
...(agentInput?.requestedModel ? { requestedModel: agentInput.requestedModel } : {}),
|
|
1255
|
+
...(prompt ? { prompt } : {}),
|
|
1256
|
+
...(nestedToolCalls?.length ? { toolCalls: nestedToolCalls } : {}),
|
|
920
1257
|
};
|
|
921
1258
|
cards.push(subagentCard);
|
|
1259
|
+
emittedSubagentKeys.add(block.id);
|
|
922
1260
|
}
|
|
923
1261
|
else {
|
|
924
1262
|
// Normal tool call
|