@jini-ai/chat 0.6.6 → 0.6.10

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 (160) hide show
  1. package/CHANGELOG.md +129 -1
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +187 -98
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
@@ -1,4 +1,84 @@
1
1
  import { assistantContentFromEvents, isReplyBoundary, legacyAssistantContentFromEvents } from '../core/index.js';
2
+ /** Whether there is anything besides plain text to place: a tool row, an ext event, or (when asked
3
+ * for) non-blank thinking. With none of these the flat path is identical. */
4
+ function hasSomethingToInterleave({ events, rows }, { includeThinking }) {
5
+ if (rows.length > 0)
6
+ return true;
7
+ return events.some((ev) => ev.kind === 'ext' || (includeThinking && ev.kind === 'thinking' && ev.text.trim() !== ''));
8
+ }
9
+ /** Whether `content` is demonstrably built from the text events. */
10
+ function contentMatchesEvents({ events, content }) {
11
+ // Both producers' shapes count: today's (a paragraph break between steps, see
12
+ // `assistantContentFromEvents`) and the old plain concatenation older saved rows carry.
13
+ return content === assistantContentFromEvents({ events }) || content === legacyAssistantContentFromEvents({ events });
14
+ }
15
+ function flushText({ state }) {
16
+ if (state.buffer.length === 0)
17
+ return;
18
+ state.blocks.push({ kind: 'text', text: state.buffer, key: `text-${state.blocks.length}` });
19
+ state.buffer = '';
20
+ }
21
+ function placeThinking({ state, text }) {
22
+ flushText({ state });
23
+ const { blocks } = state;
24
+ const last = blocks[blocks.length - 1];
25
+ if (last && last.kind === 'thinking')
26
+ blocks[blocks.length - 1] = { kind: 'thinking', text: last.text + text, key: last.key };
27
+ else
28
+ blocks.push({ kind: 'thinking', text, key: `thinking-${blocks.length}` });
29
+ }
30
+ function placeReplyEnd({ state }) {
31
+ flushText({ state });
32
+ state.blocks.push({ kind: 'reply-end', index: state.replies, key: `reply-end-${state.replies}` });
33
+ state.replies += 1;
34
+ }
35
+ function placeExt({ state, ev }, { slotOf }) {
36
+ // One block per SLOT, at its first occurrence — later events in the same slot fold into it
37
+ // (`useExtEventGroups`'s job in `MessageRow.tsx`), not a second block. A slot is the `name`
38
+ // unless its renderer keys events into separate surfaces (see `ExtEventSlotKey`).
39
+ const slot = slotOf(ev);
40
+ if (state.emittedExtSlots.has(slot))
41
+ return;
42
+ state.emittedExtSlots.add(slot);
43
+ flushText({ state });
44
+ state.blocks.push({ kind: 'ext', name: ev.name, slot, key: `ext-${slot}` });
45
+ }
46
+ function placeToolUse({ state, id }) {
47
+ const row = state.rowById.get(id);
48
+ // A `tool_use` the timeline did not surface (deduped away as a repeat of one already seen).
49
+ // Skipping it keeps this function's output consistent with `rows`, which is the authority on
50
+ // what gets a card.
51
+ if (!row || state.emitted.has(id))
52
+ return;
53
+ state.emitted.add(id);
54
+ flushText({ state });
55
+ const { blocks } = state;
56
+ const last = blocks[blocks.length - 1];
57
+ if (last && last.kind === 'tools') {
58
+ blocks[blocks.length - 1] = { kind: 'tools', rows: [...last.rows, row], key: last.key };
59
+ }
60
+ else {
61
+ blocks.push({ kind: 'tools', rows: [row], key: `tools-${blocks.length}` });
62
+ }
63
+ }
64
+ function placeEvent({ state, ev }, { slotOf, includeThinking }) {
65
+ if (ev.kind === 'text') {
66
+ state.buffer += ev.text;
67
+ return;
68
+ }
69
+ if (ev.kind === 'thinking') {
70
+ if (includeThinking)
71
+ placeThinking({ state, text: ev.text });
72
+ return;
73
+ }
74
+ // Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
75
+ if (isReplyBoundary({ event: ev }))
76
+ placeReplyEnd({ state });
77
+ if (ev.kind === 'ext')
78
+ placeExt({ state, ev }, { slotOf });
79
+ else if (ev.kind === 'tool_use')
80
+ placeToolUse({ state, id: ev.id });
81
+ }
2
82
  /**
3
83
  * Rebuilds an assistant message as ordered blocks, or returns `null` if that cannot be done
4
84
  * losslessly.
@@ -17,85 +97,27 @@ import { assistantContentFromEvents, isReplyBoundary, legacyAssistantContentFrom
17
97
  export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (ev) => ev.name, includeThinking = false } = {}) {
18
98
  if (!events || events.length === 0)
19
99
  return null;
20
- const hasExtEvent = events.some((ev) => ev.kind === 'ext');
21
- const hasThinking = includeThinking && events.some((ev) => ev.kind === 'thinking' && ev.text.trim() !== '');
22
- if (rows.length === 0 && !hasExtEvent && !hasThinking)
100
+ if (!hasSomethingToInterleave({ events, rows }, { includeThinking }))
23
101
  return null;
24
102
  // The reconstruction is only trustworthy if the text events are demonstrably the source `content`
25
103
  // was built from. Anything else and the offsets below are fiction.
26
- // Both producers' shapes count: today's (a paragraph break between steps, see
27
- // `assistantContentFromEvents`) and the old plain concatenation older saved rows carry.
28
- if (content !== assistantContentFromEvents({ events }) && content !== legacyAssistantContentFromEvents({ events: events }))
104
+ if (!contentMatchesEvents({ events, content }))
29
105
  return null;
30
- const rowById = new Map(rows.map((row) => [row.id, row]));
31
- const blocks = [];
32
- const emitted = new Set();
33
- const emittedExtSlots = new Set();
34
- let buffer = '';
35
- let replies = 0;
36
- const flushText = () => {
37
- if (buffer.length === 0)
38
- return;
39
- blocks.push({ kind: 'text', text: buffer, key: `text-${blocks.length}` });
40
- buffer = '';
106
+ const state = {
107
+ rowById: new Map(rows.map((row) => [row.id, row])),
108
+ blocks: [],
109
+ emitted: new Set(),
110
+ emittedExtSlots: new Set(),
111
+ buffer: '',
112
+ replies: 0,
41
113
  };
42
- for (const ev of events) {
43
- if (ev.kind === 'text') {
44
- buffer += ev.text;
45
- continue;
46
- }
47
- if (ev.kind === 'thinking') {
48
- if (!includeThinking)
49
- continue;
50
- flushText();
51
- const last = blocks[blocks.length - 1];
52
- if (last && last.kind === 'thinking')
53
- blocks[blocks.length - 1] = { kind: 'thinking', text: last.text + ev.text, key: last.key };
54
- else
55
- blocks.push({ kind: 'thinking', text: ev.text, key: `thinking-${blocks.length}` });
56
- continue;
57
- }
58
- if (isReplyBoundary({ event: ev })) {
59
- // Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
60
- flushText();
61
- blocks.push({ kind: 'reply-end', index: replies, key: `reply-end-${replies}` });
62
- replies += 1;
63
- }
64
- if (ev.kind === 'ext') {
65
- // One block per SLOT, at its first occurrence — later events in the same slot fold into it
66
- // (`useExtEventGroups`'s job in `MessageRow.tsx`), not a second block. A slot is the `name`
67
- // unless its renderer keys events into separate surfaces (see `ExtEventSlotKey`).
68
- const slot = slotOf(ev);
69
- if (emittedExtSlots.has(slot))
70
- continue;
71
- emittedExtSlots.add(slot);
72
- flushText();
73
- blocks.push({ kind: 'ext', name: ev.name, slot, key: `ext-${slot}` });
74
- continue;
75
- }
76
- if (ev.kind !== 'tool_use')
77
- continue;
78
- const row = rowById.get(ev.id);
79
- // A `tool_use` the timeline did not surface (deduped away as a repeat of one already seen).
80
- // Skipping it keeps this function's output consistent with `rows`, which is the authority on
81
- // what gets a card.
82
- if (!row || emitted.has(ev.id))
83
- continue;
84
- emitted.add(ev.id);
85
- flushText();
86
- const last = blocks[blocks.length - 1];
87
- if (last && last.kind === 'tools') {
88
- blocks[blocks.length - 1] = { kind: 'tools', rows: [...last.rows, row], key: last.key };
89
- }
90
- else {
91
- blocks.push({ kind: 'tools', rows: [row], key: `tools-${blocks.length}` });
92
- }
93
- }
94
- flushText();
114
+ for (const ev of events)
115
+ placeEvent({ state, ev }, { slotOf, includeThinking });
116
+ flushText({ state });
95
117
  // Every row must have found its place. If one did not, the message would render with a tool card
96
118
  // missing entirely — strictly worse than the flat layout it would have had.
97
- if (emitted.size !== rows.length)
119
+ if (state.emitted.size !== rows.length)
98
120
  return null;
99
- return blocks;
121
+ return state.blocks;
100
122
  }
101
123
  //# sourceMappingURL=message-blocks.js.map
@@ -179,8 +179,8 @@ export interface ComposerSlots {
179
179
  * Host controls rendered at the START of the footer row, immediately after the attach/discovery
180
180
  * "+" trigger — for a control that belongs with the composer's other action-row buttons (e.g. a
181
181
  * push-to-talk mic button) rather than in {@link leadingAccessories}'s pinned-context zone above
182
- * the input, or in {@link footerAccessories}'s trailing slot (reserved for an agent/model picker
183
- * and pushed to the row's far end via `margin-left: auto`).
182
+ * the input, or in {@link footerAccessories}'s slot (reserved for an agent/model picker, which
183
+ * follows this one; only the send group is pushed to the row's far end).
184
184
  */
185
185
  footerLeadingAccessory?: ReactNode;
186
186
  onAttach?: (a: ChatAttachment) => void;
@@ -6,8 +6,8 @@
6
6
  * - `'show'` — every tool card, where it happened. The layout every host had before this.
7
7
  * - `'collapsed'` — each turn's tool cards fold into ONE line ("Used 3 tools") that expands on
8
8
  * click; while a tool runs the line names it instead ("Running Bash…").
9
- * - `'hidden'` — no tool cards at all, EXCEPT a failed one: an error must never vanish from the
10
- * transcript just because the reader asked for a quieter view.
9
+ * - `'hidden'` — recovered failures disappear with successful calls. Unrecovered failures stay
10
+ * behind one compact disclosure per reply: work that stopped must never vanish silently.
11
11
  *
12
12
  * Pure rules over `interleaveMessageBlocks`' output (or the flat row list when that refused), kept
13
13
  * out of `MessageRow.tsx` so every mode is testable without rendering.
@@ -25,7 +25,7 @@ export interface ToolDisplayRow {
25
25
  };
26
26
  }
27
27
  /**
28
- * Whether a row is a failure the reader must always see: the tool returned an error, or the run
28
+ * Whether a row failed: the tool returned an error, or the run
29
29
  * ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
30
30
  * @complexity O(1).
31
31
  */
@@ -41,7 +41,8 @@ export declare function isFailedToolRow({ row }: {
41
41
  * one line per reply. Keeping the first block's key keeps the line's open/closed state across
42
42
  * streaming updates, since later blocks append after it.
43
43
  *
44
- * `'hidden'` keeps only failed rows and drops `tools` blocks left empty.
44
+ * `'hidden'` keeps unrecovered failures in one block per reply. Later assistant text or a successful
45
+ * tool proves recovery; running tools and reasoning do not. An errored final reply keeps its failures.
45
46
  *
46
47
  * @param input.blocks - `interleaveMessageBlocks` output.
47
48
  * @param input.mode - The display mode.
@@ -51,15 +52,20 @@ export declare function isFailedToolRow({ row }: {
51
52
  export declare function applyToolCallDisplay<Row extends ToolDisplayRow>({ blocks, mode }: {
52
53
  blocks: readonly MessageBlock<Row>[];
53
54
  mode: ToolCallDisplay;
55
+ }, { runFailed }?: {
56
+ runFailed?: boolean;
54
57
  }): readonly MessageBlock<Row>[];
55
58
  /**
56
- * The flat layout's counterpart (used when `interleaveMessageBlocks` refused): `'hidden'` keeps only
57
- * failed rows; the other modes keep every row (`'collapsed'` folds them into one line at render).
59
+ * Row-level recovery scan, also used by the flat fallback. Without trustworthy text ordering,
60
+ * only later successful rows prove recovery. Other modes keep every row.
58
61
  * @complexity O(n) in rows.
59
62
  */
60
63
  export declare function toolRowsForDisplay<Row extends ToolDisplayRow>({ rows, mode }: {
61
64
  rows: readonly Row[];
62
65
  mode: ToolCallDisplay;
66
+ }, { recovered, runFailed }?: {
67
+ recovered?: boolean;
68
+ runFailed?: boolean;
63
69
  }): readonly Row[];
64
70
  /** What a collapsed tool line says: the tool running now, or how many ran (and how many failed). */
65
71
  export type ToolGroupSummary = {
@@ -1,6 +1,6 @@
1
1
  import { humanizeToolName } from '../core/index.js';
2
2
  /**
3
- * Whether a row is a failure the reader must always see: the tool returned an error, or the run
3
+ * Whether a row failed: the tool returned an error, or the run
4
4
  * ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
5
5
  * @complexity O(1).
6
6
  */
@@ -20,55 +20,84 @@ function isRunningToolRow(row) {
20
20
  * one line per reply. Keeping the first block's key keeps the line's open/closed state across
21
21
  * streaming updates, since later blocks append after it.
22
22
  *
23
- * `'hidden'` keeps only failed rows and drops `tools` blocks left empty.
23
+ * `'hidden'` keeps unrecovered failures in one block per reply. Later assistant text or a successful
24
+ * tool proves recovery; running tools and reasoning do not. An errored final reply keeps its failures.
24
25
  *
25
26
  * @param input.blocks - `interleaveMessageBlocks` output.
26
27
  * @param input.mode - The display mode.
27
28
  * @returns New blocks; the input is never mutated. `'show'` returns `blocks` itself.
28
29
  * @complexity O(n) in blocks + rows.
29
30
  */
30
- export function applyToolCallDisplay({ blocks, mode }) {
31
+ export function applyToolCallDisplay({ blocks, mode }, { runFailed = false } = {}) {
31
32
  if (mode === 'show')
32
33
  return blocks;
33
- if (mode === 'hidden') {
34
- const out = [];
35
- for (const block of blocks) {
36
- if (block.kind !== 'tools') {
37
- out.push(block);
38
- continue;
39
- }
40
- const failed = block.rows.filter((row) => isFailedToolRow({ row }));
41
- if (failed.length > 0)
42
- out.push({ kind: 'tools', rows: failed, key: block.key });
34
+ return mode === 'hidden' ? hideToolCalls({ blocks, runFailed }) : collapseToolCalls({ blocks });
35
+ }
36
+ /**
37
+ * Scan backwards so only later content in the SAME reply can prove recovery. Failures stay visible
38
+ * while streaming until that evidence arrives. This is a display filter, never a deletion from the
39
+ * durable events: see assistant-client/transport.ts's forensics rationale.
40
+ * @complexity O(n) in blocks + rows, excluding the shared grouping pass.
41
+ */
42
+ function hideToolCalls({ blocks, runFailed }) {
43
+ const out = [];
44
+ let recovered = false;
45
+ let failedReply = runFailed;
46
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
47
+ const block = blocks[i];
48
+ if (block.kind === 'reply-end') {
49
+ recovered = false;
50
+ failedReply = false;
51
+ }
52
+ if (block.kind === 'text' && block.text.trim())
53
+ recovered = true;
54
+ if (block.kind !== 'tools') {
55
+ out.push(block);
56
+ continue;
43
57
  }
44
- return out;
58
+ const rows = toolRowsForDisplay({ rows: block.rows, mode: 'hidden' }, { recovered, runFailed: failedReply });
59
+ if (rows.length > 0)
60
+ out.push({ ...block, rows });
61
+ if (block.rows.some((row) => row.status === 'complete'))
62
+ recovered = true;
45
63
  }
64
+ return collapseToolCalls({ blocks: out.reverse() });
65
+ }
66
+ /** `'collapsed'`: merges every `tools` block of a turn into the turn's first one (see above). */
67
+ function collapseToolCalls({ blocks }) {
46
68
  const out = [];
47
69
  let turnToolsIndex = -1;
48
70
  for (const block of blocks) {
49
71
  if (block.kind === 'reply-end')
50
72
  turnToolsIndex = -1;
51
- if (block.kind !== 'tools') {
52
- out.push(block);
53
- continue;
54
- }
55
73
  const first = turnToolsIndex === -1 ? undefined : out[turnToolsIndex];
56
- if (first && first.kind === 'tools') {
74
+ if (block.kind === 'tools' && first?.kind === 'tools') {
57
75
  out[turnToolsIndex] = { kind: 'tools', rows: [...first.rows, ...block.rows], key: first.key };
58
76
  continue;
59
77
  }
60
- turnToolsIndex = out.length;
78
+ if (block.kind === 'tools')
79
+ turnToolsIndex = out.length;
61
80
  out.push(block);
62
81
  }
63
82
  return out;
64
83
  }
65
84
  /**
66
- * The flat layout's counterpart (used when `interleaveMessageBlocks` refused): `'hidden'` keeps only
67
- * failed rows; the other modes keep every row (`'collapsed'` folds them into one line at render).
85
+ * Row-level recovery scan, also used by the flat fallback. Without trustworthy text ordering,
86
+ * only later successful rows prove recovery. Other modes keep every row.
68
87
  * @complexity O(n) in rows.
69
88
  */
70
- export function toolRowsForDisplay({ rows, mode }) {
71
- return mode === 'hidden' ? rows.filter((row) => isFailedToolRow({ row })) : rows;
89
+ export function toolRowsForDisplay({ rows, mode }, { recovered = false, runFailed = false } = {}) {
90
+ if (mode !== 'hidden')
91
+ return rows;
92
+ const failed = [];
93
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
94
+ const row = rows[i];
95
+ if (isFailedToolRow({ row }) && (runFailed || !recovered))
96
+ failed.push(row);
97
+ if (row.status === 'complete')
98
+ recovered = true;
99
+ }
100
+ return failed.reverse();
72
101
  }
73
102
  /**
74
103
  * Summarises one turn's tool rows. The LAST running row wins, so a line over several in-flight calls