@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.
Files changed (112) hide show
  1. package/dist/ai/cardBuilder.d.ts +67 -2
  2. package/dist/ai/cardBuilder.d.ts.map +1 -1
  3. package/dist/ai/cardBuilder.js +364 -26
  4. package/dist/ai/cardBuilder.js.map +1 -1
  5. package/dist/ai/claudeCliProvider.d.ts +14 -0
  6. package/dist/ai/claudeCliProvider.d.ts.map +1 -1
  7. package/dist/ai/claudeCliProvider.js +83 -2
  8. package/dist/ai/claudeCliProvider.js.map +1 -1
  9. package/dist/ai/claudeCodeProvider.d.ts.map +1 -1
  10. package/dist/ai/claudeCodeProvider.js +2 -0
  11. package/dist/ai/claudeCodeProvider.js.map +1 -1
  12. package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
  13. package/dist/ai/claudeSdkProvider.js +13 -2
  14. package/dist/ai/claudeSdkProvider.js.map +1 -1
  15. package/dist/ai/claudeTerminal/cardSynth.d.ts +63 -0
  16. package/dist/ai/claudeTerminal/cardSynth.d.ts.map +1 -0
  17. package/dist/ai/claudeTerminal/cardSynth.js +92 -0
  18. package/dist/ai/claudeTerminal/cardSynth.js.map +1 -0
  19. package/dist/ai/claudeTerminal/hookBridge.d.ts +56 -0
  20. package/dist/ai/claudeTerminal/hookBridge.d.ts.map +1 -0
  21. package/dist/ai/claudeTerminal/hookBridge.js +131 -0
  22. package/dist/ai/claudeTerminal/hookBridge.js.map +1 -0
  23. package/dist/ai/claudeTerminal/hookHandler.d.ts +18 -0
  24. package/dist/ai/claudeTerminal/hookHandler.d.ts.map +1 -0
  25. package/dist/ai/claudeTerminal/hookHandler.js +124 -0
  26. package/dist/ai/claudeTerminal/hookHandler.js.map +1 -0
  27. package/dist/ai/claudeTerminal/index.d.ts +5 -0
  28. package/dist/ai/claudeTerminal/index.d.ts.map +1 -0
  29. package/dist/ai/claudeTerminal/index.js +7 -0
  30. package/dist/ai/claudeTerminal/index.js.map +1 -0
  31. package/dist/ai/claudeTerminal/jsonlTail.d.ts +40 -0
  32. package/dist/ai/claudeTerminal/jsonlTail.d.ts.map +1 -0
  33. package/dist/ai/claudeTerminal/jsonlTail.js +129 -0
  34. package/dist/ai/claudeTerminal/jsonlTail.js.map +1 -0
  35. package/dist/ai/claudeTerminal/provider.d.ts +106 -0
  36. package/dist/ai/claudeTerminal/provider.d.ts.map +1 -0
  37. package/dist/ai/claudeTerminal/provider.js +579 -0
  38. package/dist/ai/claudeTerminal/provider.js.map +1 -0
  39. package/dist/ai/claudeTerminal/settingsBuilder.d.ts +44 -0
  40. package/dist/ai/claudeTerminal/settingsBuilder.d.ts.map +1 -0
  41. package/dist/ai/claudeTerminal/settingsBuilder.js +40 -0
  42. package/dist/ai/claudeTerminal/settingsBuilder.js.map +1 -0
  43. package/dist/ai/codexAppServer/cardAdapter.d.ts.map +1 -1
  44. package/dist/ai/codexAppServer/cardAdapter.js +13 -5
  45. package/dist/ai/codexAppServer/cardAdapter.js.map +1 -1
  46. package/dist/ai/codexAppServer/provider.d.ts +13 -2
  47. package/dist/ai/codexAppServer/provider.d.ts.map +1 -1
  48. package/dist/ai/codexAppServer/provider.js +161 -26
  49. package/dist/ai/codexAppServer/provider.js.map +1 -1
  50. package/dist/ai/profiles/project-sandbox.sb +26 -0
  51. package/dist/ai/provider.d.ts +29 -1
  52. package/dist/ai/provider.d.ts.map +1 -1
  53. package/dist/ai/provider.js +1 -0
  54. package/dist/ai/provider.js.map +1 -1
  55. package/dist/ai/queuedUserPrompts.d.ts +8 -0
  56. package/dist/ai/queuedUserPrompts.d.ts.map +1 -0
  57. package/dist/ai/queuedUserPrompts.js +19 -0
  58. package/dist/ai/queuedUserPrompts.js.map +1 -0
  59. package/dist/ai/realtimeTranscription.d.ts +39 -0
  60. package/dist/ai/realtimeTranscription.d.ts.map +1 -0
  61. package/dist/ai/realtimeTranscription.js +154 -0
  62. package/dist/ai/realtimeTranscription.js.map +1 -0
  63. package/dist/ai/sandboxMcp.d.ts +7 -0
  64. package/dist/ai/sandboxMcp.d.ts.map +1 -1
  65. package/dist/ai/sandboxMcp.js +4 -0
  66. package/dist/ai/sandboxMcp.js.map +1 -1
  67. package/dist/ai/sandboxMcpStdio.d.ts +1 -1
  68. package/dist/ai/sandboxMcpStdio.js +120 -47
  69. package/dist/ai/sandboxMcpStdio.js.map +1 -1
  70. package/dist/ai/sessionManager.d.ts +14 -0
  71. package/dist/ai/sessionManager.d.ts.map +1 -1
  72. package/dist/ai/sessionManager.js +147 -3
  73. package/dist/ai/sessionManager.js.map +1 -1
  74. package/dist/ai/sessionRegistry.d.ts +6 -0
  75. package/dist/ai/sessionRegistry.d.ts.map +1 -1
  76. package/dist/ai/sessionRegistry.js +13 -0
  77. package/dist/ai/sessionRegistry.js.map +1 -1
  78. package/dist/ai/sessionRegistryLocator.d.ts +14 -0
  79. package/dist/ai/sessionRegistryLocator.d.ts.map +1 -0
  80. package/dist/ai/sessionRegistryLocator.js +44 -0
  81. package/dist/ai/sessionRegistryLocator.js.map +1 -0
  82. package/dist/ai/systemPrompt.d.ts.map +1 -1
  83. package/dist/ai/systemPrompt.js +1 -0
  84. package/dist/ai/systemPrompt.js.map +1 -1
  85. package/dist/ai/voiceStream.d.ts +74 -0
  86. package/dist/ai/voiceStream.d.ts.map +1 -0
  87. package/dist/ai/voiceStream.js +179 -0
  88. package/dist/ai/voiceStream.js.map +1 -0
  89. package/dist/ai/voiceTranscription.d.ts +33 -0
  90. package/dist/ai/voiceTranscription.d.ts.map +1 -0
  91. package/dist/ai/voiceTranscription.js +122 -0
  92. package/dist/ai/voiceTranscription.js.map +1 -0
  93. package/dist/files/fileBrowser.d.ts.map +1 -1
  94. package/dist/files/fileBrowser.js +3 -2
  95. package/dist/files/fileBrowser.js.map +1 -1
  96. package/dist/handlers/legacyBusAdapter.d.ts +1 -1
  97. package/dist/handlers/legacyBusAdapter.d.ts.map +1 -1
  98. package/dist/handlers/legacyBusAdapter.js +6 -1
  99. package/dist/handlers/legacyBusAdapter.js.map +1 -1
  100. package/dist/handlers/messageHandler.d.ts +5 -0
  101. package/dist/handlers/messageHandler.d.ts.map +1 -1
  102. package/dist/handlers/messageHandler.js +100 -5
  103. package/dist/handlers/messageHandler.js.map +1 -1
  104. package/dist/index.js +2 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/service/ipcClient.js +1 -1
  107. package/dist/service/ipcClient.js.map +1 -1
  108. package/dist/service/run.d.ts.map +1 -1
  109. package/dist/service/run.js +74 -3
  110. package/dist/service/run.js.map +1 -1
  111. package/dist/service/types.js +1 -1
  112. package/package.json +6 -3
@@ -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
- // Strip transient fields before persisting
273
- const cleaned = cards.map(c => {
274
- const { pendingInput, ...rest } = c;
275
- if (rest.type === 'assistant_text') {
276
- return { ...rest, streaming: false };
277
- }
278
- return rest;
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 = [...existing, ...cleaned];
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
- subagentStart(description, agentId, toolUseId) {
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
- // Position after the parent ToolCallCard if we have the toolUseId
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 from Agent tool_use blocks
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 desc = block.input?.description ?? '';
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
- // Skip subagent lifecycle events (handled via subagentCards below)
773
- if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification')
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 → create a ToolCallCard as anchor, then SubagentCard
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
- // Insert SubagentCard
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