@jini-ai/chat 0.5.6 → 0.5.8

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 (127) hide show
  1. package/dist/core/ag-ui/entry.js +2 -0
  2. package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
  3. package/dist/core/assistant-client/durable-run-subscription.js +13 -4
  4. package/dist/core/durable-projection.d.ts +3 -1
  5. package/dist/core/durable-projection.js +40 -7
  6. package/dist/core/events.d.ts +3 -0
  7. package/dist/core/history-tool-metadata.d.ts +23 -0
  8. package/dist/core/history-tool-metadata.js +13 -0
  9. package/dist/core/index.d.ts +4 -0
  10. package/dist/core/index.js +4 -0
  11. package/dist/core/persistence/title.d.ts +24 -0
  12. package/dist/core/persistence/title.js +60 -9
  13. package/dist/core/run-activity.d.ts +15 -2
  14. package/dist/core/run-activity.js +6 -4
  15. package/dist/core/run-events/entry.d.ts +12 -1
  16. package/dist/core/run-events/entry.js +19 -6
  17. package/dist/core/tool-call-detail.d.ts +51 -0
  18. package/dist/core/tool-call-detail.js +67 -0
  19. package/dist/core/turn-usage.d.ts +50 -0
  20. package/dist/core/turn-usage.js +114 -0
  21. package/dist/core/usage-format.d.ts +10 -0
  22. package/dist/core/usage-format.js +15 -0
  23. package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
  24. package/dist/embed-widget/browser/lazy-loader.js +116 -0
  25. package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
  26. package/dist/embed-widget/react/dialog.hooks.js +54 -0
  27. package/dist/embed-widget/react/embed.d.ts +1 -0
  28. package/dist/embed-widget/react/embed.js +5 -3
  29. package/dist/embed-widget/react/header.d.ts +3 -1
  30. package/dist/embed-widget/react/header.hooks.d.ts +13 -0
  31. package/dist/embed-widget/react/header.hooks.js +32 -0
  32. package/dist/embed-widget/react/header.js +4 -14
  33. package/dist/embed-widget/vanilla/index.d.ts +63 -0
  34. package/dist/embed-widget/vanilla/index.js +319 -0
  35. package/dist/embed-widget/vanilla/state.d.ts +48 -0
  36. package/dist/embed-widget/vanilla/state.js +120 -0
  37. package/dist/embed-widget/vanilla/style.css +29 -0
  38. package/dist/react/chat-pane.d.ts +2 -2
  39. package/dist/react/chat-pane.js +1 -1
  40. package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
  41. package/dist/react/components/A2uiSurfaceCard.js +9 -9
  42. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
  43. package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
  44. package/dist/react/components/ChatFab.d.ts +4 -1
  45. package/dist/react/components/ChatFab.js +2 -2
  46. package/dist/react/components/Composer.d.ts +2 -0
  47. package/dist/react/components/Composer.js +5 -2
  48. package/dist/react/components/ConversationList.d.ts +8 -1
  49. package/dist/react/components/ConversationList.hooks.d.ts +18 -0
  50. package/dist/react/components/ConversationList.hooks.js +25 -0
  51. package/dist/react/components/ConversationList.js +12 -5
  52. package/dist/react/components/ConversationList.rules.d.ts +25 -0
  53. package/dist/react/components/ConversationList.rules.js +19 -0
  54. package/dist/react/components/Markdown.js +6 -6
  55. package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
  56. package/dist/react/components/McpUiSurfaceCard.js +8 -4
  57. package/dist/react/components/MessageList.d.ts +8 -13
  58. package/dist/react/components/MessageList.hooks.d.ts +4 -0
  59. package/dist/react/components/MessageList.hooks.js +6 -1
  60. package/dist/react/components/MessageList.js +23 -4
  61. package/dist/react/components/MessageRow.d.ts +23 -1
  62. package/dist/react/components/MessageRow.js +61 -36
  63. package/dist/react/components/MessageRow.usage.d.ts +48 -0
  64. package/dist/react/components/MessageRow.usage.js +47 -0
  65. package/dist/react/components/MidRunUserMessage.d.ts +17 -0
  66. package/dist/react/components/MidRunUserMessage.js +41 -0
  67. package/dist/react/components/SentAttachment.d.ts +19 -0
  68. package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
  69. package/dist/react/components/SentAttachment.hooks.js +56 -0
  70. package/dist/react/components/SentAttachment.js +14 -0
  71. package/dist/react/components/ToolCallGroup.d.ts +12 -0
  72. package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
  73. package/dist/react/components/ToolCallGroup.hooks.js +23 -0
  74. package/dist/react/components/ToolCallGroup.js +13 -0
  75. package/dist/react/components/ToolCard.js +41 -22
  76. package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
  77. package/dist/react/components/mcp-ui-surface-state.js +26 -0
  78. package/dist/react/ext-event-renderer-registry.d.ts +7 -0
  79. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  80. package/dist/react/features/chat-pane/components/ChatPane.js +20 -7
  81. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
  82. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
  83. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
  84. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +65 -10
  85. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
  86. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
  87. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
  88. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
  89. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
  90. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
  91. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
  92. package/dist/react/features/chat-pane/index.d.ts +2 -1
  93. package/dist/react/features/chat-pane/index.js +1 -0
  94. package/dist/react/features/chat-pane/rules.d.ts +27 -0
  95. package/dist/react/features/chat-pane/rules.js +32 -0
  96. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  97. package/dist/react/features/chat-pane/styles.js +92 -1
  98. package/dist/react/features/chat-pane/types.d.ts +39 -1
  99. package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
  100. package/dist/react/hooks/composer-draft-cache.js +20 -0
  101. package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
  102. package/dist/react/hooks/useAnsweredCards.js +23 -0
  103. package/dist/react/hooks/useComposer.d.ts +14 -0
  104. package/dist/react/hooks/useComposer.js +78 -6
  105. package/dist/react/hooks/useConversation.d.ts +8 -0
  106. package/dist/react/hooks/useConversation.js +34 -0
  107. package/dist/react/hooks/useRunActivity.d.ts +5 -3
  108. package/dist/react/hooks/useRunActivity.js +4 -3
  109. package/dist/react/index.d.ts +8 -2
  110. package/dist/react/index.js +6 -1
  111. package/dist/react/message-blocks.d.ts +22 -3
  112. package/dist/react/message-blocks.js +25 -3
  113. package/dist/react/tool-call-display.d.ts +90 -0
  114. package/dist/react/tool-call-display.js +97 -0
  115. package/dist/server/durable-runs/recover.js +19 -11
  116. package/dist/server/run-finalizer/entry.js +2 -1
  117. package/dist/store/ports.d.ts +25 -2
  118. package/dist/store/runtime/durable-run-store.js +5 -2
  119. package/dist/store/runtime/entry.d.ts +1 -0
  120. package/dist/store/runtime/entry.js +1 -0
  121. package/dist/store/runtime/history-tools.d.ts +27 -0
  122. package/dist/store/runtime/history-tools.js +169 -0
  123. package/dist/store/runtime/run-ledger.js +5 -2
  124. package/dist/store/sql/store.js +52 -0
  125. package/dist/testing/index.d.ts +7 -0
  126. package/dist/testing/index.js +22 -0
  127. package/package.json +48 -18
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
3
+ import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
3
4
  /**
4
5
  * @module MessageRow
5
6
  *
@@ -50,27 +51,25 @@ import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-p
50
51
  * not appear until the whole message is done, not merely until its current
51
52
  * chunk stopped arriving.
52
53
  */
53
- import React, { useState } from 'react';
54
+ import React, { memo, useState } from 'react';
54
55
  import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
55
56
  import { useToolTimeline } from '../hooks/useToolTimeline.js';
56
57
  import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
57
58
  import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
58
59
  import { useRunActivity } from '../hooks/useRunActivity.js';
60
+ import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
59
61
  import { interleaveMessageBlocks } from '../message-blocks.js';
60
62
  import { useT } from '../hooks/context.js';
61
63
  import { extEventSlot, getExtEventRenderer } from '../ext-event-renderer-registry.js';
62
64
  import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
63
65
  import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
66
+ import { SentAttachmentButton } from './SentAttachment.js';
64
67
  import { Icon } from './Icon.js';
65
68
  import { Markdown } from './Markdown.js';
66
69
  import { ToolCard } from './ToolCard.js';
67
70
  import { QuestionForm } from './QuestionForm.js';
68
- function formatDuration(ms) {
69
- const totalSeconds = Math.round(ms / 1000);
70
- const minutes = Math.floor(totalSeconds / 60);
71
- const seconds = totalSeconds % 60;
72
- return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
73
- }
71
+ import { ToolCallGroup } from './ToolCallGroup.js';
72
+ import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
74
73
  /**
75
74
  * True while an assistant message is still fully pending — no visible text
76
75
  * and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
@@ -107,21 +106,17 @@ function isPendingWithNoContent(message, visibleContent, toolRowCount) {
107
106
  function isRunInProgress(runStatus) {
108
107
  return runStatus !== undefined && !isTerminalRunStatus({ status: runStatus });
109
108
  }
110
- /** One "Done · 6m 29s · 2612 out · $0.4028" summary line, from the run's own `kind:'usage'` event — never estimated client-side. Renders only the fields the event actually carries, so a transport that supplies partial usage data degrades gracefully instead of showing fabricated zeros. */
111
- function UsageSummary({ usage }) {
109
+ /** One "Done · 6m 29s · $0.41 · total $2.50" line from reported usage, never estimated (the total is
110
+ * left off when `showTotal` is false). Partial usage stays partial; output tokens are available to
111
+ * assistive technology and in the tooltip. */
112
+ function UsageSummary({ usage, showTotal }) {
112
113
  const t = useT();
113
- const parts = [];
114
- if (usage.durationMs !== undefined)
115
- parts.push(formatDuration(usage.durationMs));
116
- if (usage.outputTokens !== undefined)
117
- parts.push(t('{n} out', { n: usage.outputTokens }));
118
- if (usage.costUsd !== undefined)
119
- parts.push(`$${usage.costUsd.toFixed(4)}`);
120
- if (parts.length === 0)
114
+ const { text, title, ariaLabel } = useUsageSummary({ usage }, { showTotal });
115
+ if (!text)
121
116
  return null;
122
- return (_jsxs("div", { className: "jini-message-usage", children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ", parts.join(' · ')] }));
117
+ return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ", text] }));
123
118
  }
124
- function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
119
+ function messageBlocks({ visibleContent, message, rows }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
125
120
  // Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
126
121
  // whole string and trims, so the surviving text no longer lines up with the per-run offsets the
127
122
  // event walk produces, and an artifact block could legitimately span a tool call. Rather than
@@ -129,14 +124,22 @@ function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
129
124
  // messages are dominated by the artifact panel anyway, so the ordering matters least there.
130
125
  if (visibleContent !== message.content)
131
126
  return null;
132
- return interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }) });
127
+ const blocks = interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }), includeThinking });
128
+ return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }) : null;
129
+ }
130
+ /** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
131
+ function ThinkingBlock({ text }) {
132
+ const t = useT();
133
+ if (!text.trim())
134
+ return null;
135
+ return (_jsxs("details", { className: "jini-message-thinking", children: [_jsx("summary", { children: t('Thinking') }), _jsx("div", { className: "jini-message-thinking__text", children: text })] }));
133
136
  }
134
- function messageAwaitingAnswer({ runStreaming, runInProgress, events }, _optional = {}) {
135
- return runStreaming === true && runInProgress && isAwaitingAnswer({ events });
137
+ function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
138
+ return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
136
139
  }
137
- function AssistantMessageFooter({ message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent }, _optional = {}) {
140
+ function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
138
141
  const t = useT();
139
- return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress) }) : null] });
142
+ return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
140
143
  }
141
144
  /**
142
145
  * Quiet, icon-only copy affordance shared by the user- and assistant-message
@@ -154,26 +157,39 @@ function CopyMessageButton({ text, label }) {
154
157
  void copy(text);
155
158
  }, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
156
159
  }
157
- export function MessageRow({ message, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
160
+ /**
161
+ * Memoized so a transcript re-render (a streaming update to the newest message, or a parent that
162
+ * re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
163
+ * are replaced, never mutated, so identity is a sound change signal.
164
+ */
165
+ export const MessageRow = memo(function MessageRow({ message, usageSummary, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
158
166
  const t = useT();
167
+ const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
159
168
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
160
169
  const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => extEventSlot({ name, data }) });
170
+ const answeredCards = useAnsweredCards({ slotOf: extEventSlot });
161
171
  // Which attachment chip (if any) this row's preview modal is open for. Row-local, not
162
172
  // module-level: two attachments in the same message can never be "open" at once, but two
163
173
  // different messages opening their own attachments independently is fine and expected.
164
174
  const [openAttachment, setOpenAttachment] = useState(null);
165
175
  if (message.role === 'user') {
166
- return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx("button", { type: "button", className: "jini-message-attachment-chip", onClick: () => setOpenAttachment(a), title: t('Open {name}', { name: a.name }), "aria-label": t('Open {name}', { name: a.name }), children: renderAttachment ? renderAttachment(a) : a.name }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), openAttachment ? (_jsx(AttachmentPreviewModal, { attachment: openAttachment, onClose: () => setOpenAttachment(null) })) : null] }));
176
+ return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: setOpenAttachment, renderAttachment: renderAttachment }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), openAttachment ? (_jsx(AttachmentPreviewModal, { attachment: openAttachment, onClose: () => setOpenAttachment(null) })) : null] }));
167
177
  }
168
178
  const terminalNotice = terminalMessageNotice({ message }, {});
169
179
  const visibleContent = stripArtifact({ content: message.content });
170
180
  const segments = splitOnQuestionForms({ input: visibleContent });
171
- const usageEvent = message.events?.filter((ev) => ev.kind === 'usage').pop();
172
- const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
181
+ const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, { toolCallDisplay, includeThinking: showThinking });
173
182
  const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
174
183
  const runInProgress = isRunInProgress(message.runStatus);
175
- const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, {});
184
+ const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
185
+ const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, { runStreaming });
186
+ const footerUsage = showUsageLine ? usage.footer : undefined;
176
187
  const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
188
+ // One turn's cards: a fold-up line when collapsed, the plain card list otherwise.
189
+ const renderTools = (rows, key) => (toolCallDisplay === 'collapsed'
190
+ ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard }, key)
191
+ : _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
192
+ const flatToolRows = toolRowsForDisplay({ rows: timeline.rows, mode: toolCallDisplay });
177
193
  const renderExtGroup = (group) => {
178
194
  const renderer = getExtEventRenderer({ name: group.name });
179
195
  if (!renderer)
@@ -186,6 +202,8 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
186
202
  awaitingAnswer,
187
203
  runId: message.runId,
188
204
  ...(group.call === undefined ? {} : { call: group.call }),
205
+ answered: answeredCards.isAnswered(group.slot),
206
+ onAnswered: () => answeredCards.markAnswered(group.slot),
189
207
  });
190
208
  if (!node)
191
209
  return null;
@@ -196,28 +214,35 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
196
214
  _jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
197
215
  };
198
216
  const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`)));
199
- return (_jsxs("div", { className: "jini-message jini-message-assistant", "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null, blocks
217
+ return (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress })
218
+ ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
219
+ : 'jini-message jini-message-assistant', "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null, blocks
200
220
  ? blocks.map((block) => {
201
221
  if (block.kind === 'text') {
202
222
  return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
203
223
  }
204
- if (block.kind === 'tools') {
205
- return (_jsx("div", { className: "jini-message-tools", children: block.rows.map(renderToolCard) }, block.key));
224
+ if (block.kind === 'reply-end') {
225
+ const reply = usage.replies[block.index];
226
+ return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
206
227
  }
228
+ if (block.kind === 'tools')
229
+ return renderTools(block.rows, block.key);
230
+ if (block.kind === 'thinking')
231
+ return _jsx(ThinkingBlock, { text: block.text }, block.key);
207
232
  const group = extGroups.find((g) => g.slot === block.slot);
208
233
  if (!group)
209
234
  return null;
210
235
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
211
236
  })
212
- : (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent })] }));
213
- }
237
+ : (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), flatToolRows.length > 0 ? renderTools(flatToolRows) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }));
238
+ });
214
239
  /**
215
240
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
216
241
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
217
242
  * Kept a separate component so its one-second tick re-renders only this line, not the whole row.
218
243
  */
219
- function RunActivityLine({ events, active }) {
220
- const activityLabel = useRunActivity({ events: events, active: active });
244
+ function RunActivityLine({ events, active, isAnsweredCard }) {
245
+ const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
221
246
  const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
222
247
  if (label === null)
223
248
  return null;
@@ -0,0 +1,48 @@
1
+ import type { ChatMessage } from '../../core/messages.js';
2
+ import { type ChatUsageSummary } from '../../core/turn-usage.js';
3
+ /** A standalone row has one turn; MessageList supplies the full conversation projection. */
4
+ export declare function useMessageUsage({ message, summary }: {
5
+ message: ChatMessage;
6
+ summary?: ChatUsageSummary;
7
+ }, _optional?: {}): ChatUsageSummary | undefined;
8
+ /** Which "Done" lines a message shows. Each finished reply gets its own line where it ends; the reply
9
+ * still running gets none, so a stale line never sits above live work (owner, 2026-10-10). Per-reply
10
+ * lines need the ordered block layout to know where a reply ends; the flat fallback keeps one
11
+ * whole-message line under the message once the run is over. A row still streaming counts as running
12
+ * even when its `runStatus` was never set, so the previous reply's line cannot slide down under the
13
+ * live tool rows above "Running …" (owner bug, 2026-10-10). O(r) for r replies.
14
+ * @example usageLayout({ usage, interleaved: true, runInProgress: true }).footer // undefined */
15
+ export declare function usageLayout({ usage, interleaved, runInProgress }: {
16
+ usage: ChatUsageSummary | undefined;
17
+ interleaved: boolean;
18
+ runInProgress: boolean;
19
+ }, { runStreaming }?: {
20
+ runStreaming?: boolean;
21
+ }): {
22
+ footer: ChatUsageSummary | undefined;
23
+ replies: readonly (ChatUsageSummary | undefined)[];
24
+ };
25
+ /** Whether the footer's "Done" line renders directly above the copy row: a finished turn with a
26
+ * non-empty usage line and no notice between them. Rows get a modifier class from this so a host can
27
+ * lay the two out on one line with a plain class selector — a `:has(> usage + actions)` selector made
28
+ * every keystroke restyle the whole page in long chats (2026-10-11). O(1).
29
+ * @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
30
+ export declare function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }: {
31
+ footer: ChatUsageSummary | undefined;
32
+ terminalNotice: string | null;
33
+ pending: boolean;
34
+ runInProgress: boolean;
35
+ }, _optional?: {}): boolean;
36
+ /** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
37
+ * Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
38
+ * 2026-10-10: keep the total, always shown, even on the first turn). `showTotal: false` turns it off. */
39
+ export declare function useUsageSummary({ usage }: {
40
+ usage: ChatUsageSummary;
41
+ }, { showTotal }?: {
42
+ showTotal?: boolean;
43
+ }): {
44
+ text: string;
45
+ title: string | undefined;
46
+ ariaLabel: string | undefined;
47
+ };
48
+ //# sourceMappingURL=MessageRow.usage.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { useMemo } from 'react';
2
+ import { conversationUsage } from '../../core/turn-usage.js';
3
+ import { formatUsageCost } from '../../core/usage-format.js';
4
+ import { useT } from '../hooks/context.js';
5
+ /** A standalone row has one turn; MessageList supplies the full conversation projection. */
6
+ export function useMessageUsage({ message, summary }, _optional = {}) {
7
+ return useMemo(() => summary ?? conversationUsage({ messages: [message] }).get(message.id), [message, summary]);
8
+ }
9
+ /** Which "Done" lines a message shows. Each finished reply gets its own line where it ends; the reply
10
+ * still running gets none, so a stale line never sits above live work (owner, 2026-10-10). Per-reply
11
+ * lines need the ordered block layout to know where a reply ends; the flat fallback keeps one
12
+ * whole-message line under the message once the run is over. A row still streaming counts as running
13
+ * even when its `runStatus` was never set, so the previous reply's line cannot slide down under the
14
+ * live tool rows above "Running …" (owner bug, 2026-10-10). O(r) for r replies.
15
+ * @example usageLayout({ usage, interleaved: true, runInProgress: true }).footer // undefined */
16
+ export function usageLayout({ usage, interleaved, runInProgress }, { runStreaming = false } = {}) {
17
+ const segments = interleaved ? usage?.segments : undefined;
18
+ const last = segments ? segments.at(-1) : usage;
19
+ return { footer: runInProgress || runStreaming ? undefined : last, replies: segments ? segments.slice(0, -1) : [] };
20
+ }
21
+ /** Whether the footer's "Done" line renders directly above the copy row: a finished turn with a
22
+ * non-empty usage line and no notice between them. Rows get a modifier class from this so a host can
23
+ * lay the two out on one line with a plain class selector — a `:has(> usage + actions)` selector made
24
+ * every keystroke restyle the whole page in long chats (2026-10-11). O(1).
25
+ * @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
26
+ export function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }, _optional = {}) {
27
+ const hasText = footer !== undefined && (footer.durationMs !== undefined || footer.costUsd !== undefined);
28
+ return hasText && !terminalNotice && !pending && !runInProgress;
29
+ }
30
+ /** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
31
+ * Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
32
+ * 2026-10-10: keep the total, always shown, even on the first turn). `showTotal: false` turns it off. */
33
+ export function useUsageSummary({ usage }, { showTotal = true } = {}) {
34
+ const t = useT();
35
+ const parts = [];
36
+ if (usage.durationMs !== undefined) {
37
+ const seconds = Math.round(usage.durationMs / 1000);
38
+ parts.push(seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`);
39
+ }
40
+ if (usage.costUsd !== undefined) {
41
+ parts.push(formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) }));
42
+ }
43
+ const text = parts.join(' · ');
44
+ const title = usage.outputTokens === undefined ? undefined : t('{n} out', { n: usage.outputTokens });
45
+ return { text, title, ariaLabel: title === undefined ? undefined : `${t('Done')} · ${text} · ${title}` };
46
+ }
47
+ //# sourceMappingURL=MessageRow.usage.js.map
@@ -0,0 +1,17 @@
1
+ import { type ExtEventRenderProps } from '../ext-event-renderer-registry.js';
2
+ /** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
3
+ * the core reply split (`replySegments`), which ends a reply's usage at this same event. */
4
+ export declare const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = "user_message";
5
+ /** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
6
+ export declare function midRunUserMessageSlotKey({ data }: {
7
+ data: unknown;
8
+ }): string | undefined;
9
+ /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
10
+ export declare function MidRunUserMessage({ events }: Pick<ExtEventRenderProps, 'events'>): import("react").JSX.Element | null;
11
+ /**
12
+ * Registers {@link MidRunUserMessage} against the ext-event registry.
13
+ *
14
+ * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
15
+ */
16
+ export declare function registerMidRunUserMessageRenderer(): () => void;
17
+ //# sourceMappingURL=MidRunUserMessage.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @module MidRunUserMessage
4
+ *
5
+ * Renders a message the human sent while a run was going (`ChatPane`'s `deliverMidRunMessage`) as a
6
+ * user bubble inside the running turn, at the point in the stream where the live agent received it.
7
+ * The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
8
+ * the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
9
+ *
10
+ * It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
11
+ * into the turn it was already running — there is no separate reply to hang off it.
12
+ */
13
+ import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
14
+ import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
15
+ /** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
16
+ * the core reply split (`replySegments`), which ends a reply's usage at this same event. */
17
+ export const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = MID_RUN_USER_MESSAGE_EVENT_NAME;
18
+ function field(data, key) {
19
+ const value = typeof data === 'object' && data !== null ? data[key] : undefined;
20
+ return typeof value === 'string' && value !== '' ? value : undefined;
21
+ }
22
+ /** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
23
+ export function midRunUserMessageSlotKey({ data }) {
24
+ return field(data, 'id');
25
+ }
26
+ /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
27
+ export function MidRunUserMessage({ events }) {
28
+ const text = field(events.at(-1), 'text');
29
+ if (text === undefined)
30
+ return null;
31
+ return (_jsx("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: _jsx("div", { className: "jini-message-content", children: text }) }));
32
+ }
33
+ /**
34
+ * Registers {@link MidRunUserMessage} against the ext-event registry.
35
+ *
36
+ * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
37
+ */
38
+ export function registerMidRunUserMessageRenderer() {
39
+ return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events }) }, { slotKey: midRunUserMessageSlotKey });
40
+ }
41
+ //# sourceMappingURL=MidRunUserMessage.js.map
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ChatAttachment } from '../../core/index.js';
3
+ import { type ObjectUrlPort } from './SentAttachment.hooks.js';
4
+ export interface SentAttachmentButtonProps {
5
+ attachment: ChatAttachment;
6
+ /** Opens the row's preview modal for this attachment. */
7
+ onOpen: (attachment: ChatAttachment) => void;
8
+ /** Host-supplied chip content; when given it wins over the built-in thumbnail/filename. */
9
+ renderAttachment?: ((attachment: ChatAttachment) => ReactNode) | undefined;
10
+ /** Blob-URL port for the thumbnail; defaults to the platform's (see `SentAttachment.hooks.ts`). */
11
+ objectUrls?: ObjectUrlPort | undefined;
12
+ }
13
+ /**
14
+ * One attachment on a sent user message: a small image thumbnail when this browser still holds the
15
+ * image's bytes, otherwise the filename chip. Either way a click opens the preview modal, and the
16
+ * accessible name stays "Open {name}".
17
+ */
18
+ export declare function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }: SentAttachmentButtonProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=SentAttachment.d.ts.map
@@ -0,0 +1,44 @@
1
+ import type { ChatAttachment } from '../../core/index.js';
2
+ /** Blob-URL port, so a test (jsdom has no `URL.createObjectURL`) can supply a fake. */
3
+ export interface ObjectUrlPort {
4
+ create: (file: File) => string;
5
+ revoke: (url: string) => void;
6
+ }
7
+ /** The platform's Blob URLs, or `undefined` where they do not exist (jsdom, non-browser hosts). */
8
+ export declare function platformObjectUrls(): ObjectUrlPort | undefined;
9
+ /**
10
+ * The cached file to draw as a thumbnail, or `undefined` when the attachment should stay a chip:
11
+ * not an image (same `looksLikeImageAttachment` rule the preview modal uses), or no bytes cached.
12
+ * @complexity O(1).
13
+ */
14
+ export declare function sentAttachmentThumbnailSource({ attachment }: {
15
+ attachment: Pick<ChatAttachment, 'kind' | 'name' | 'path'>;
16
+ }, { getSource }?: {
17
+ getSource?: (required: {
18
+ path: string;
19
+ }) => File | undefined;
20
+ }): File | undefined;
21
+ export interface SentAttachmentThumbnail {
22
+ /** Object URL to put in the `<img>`, or `undefined` to render the filename chip. */
23
+ thumbnailUrl: string | undefined;
24
+ /** The `<img>`'s `onError`: an undecodable file falls back to the chip instead of a broken image. */
25
+ onThumbnailError: () => void;
26
+ }
27
+ /**
28
+ * Creates the thumbnail's object URL on mount and revokes it on unmount (an effect, not `useMemo`,
29
+ * so a Strict Mode double render cannot leak a URL - same reasoning as the preview modal's hook).
30
+ * @param input.hostRendered - True when a host `renderAttachment` draws this chip.
31
+ * @param options.objectUrls - Blob-URL port; defaults to the platform's, `undefined` where absent.
32
+ * @param options.getSource - Preview-cache lookup; defaults to the shared cache.
33
+ * @complexity O(1) per attachment.
34
+ */
35
+ export declare function useSentAttachmentThumbnail({ attachment, hostRendered }: {
36
+ attachment: ChatAttachment;
37
+ hostRendered?: boolean;
38
+ }, { objectUrls, getSource }?: {
39
+ objectUrls?: ObjectUrlPort | undefined;
40
+ getSource?: (required: {
41
+ path: string;
42
+ }) => File | undefined;
43
+ }): SentAttachmentThumbnail;
44
+ //# sourceMappingURL=SentAttachment.hooks.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @file Thumbnail state for one attachment on a SENT user message (`SentAttachment.tsx`).
3
+ *
4
+ * A sent image shows as a small thumbnail instead of a filename chip, so the operator can see at a
5
+ * glance which screenshots went with the turn. The bytes come from the same
6
+ * `attachment-preview-cache.ts` the preview modal reads: only files this browser staged for upload in
7
+ * this page session are there (the server cannot hand them back - see that module's doc). Anything
8
+ * else - a non-image, a reload, the cache's eviction cap, a browser that cannot decode the format -
9
+ * stays the filename chip, which still opens the preview modal's honest fallback.
10
+ */
11
+ import { useEffect, useState } from 'react';
12
+ import { getAttachmentPreviewSource } from '../hooks/attachment-preview-cache.js';
13
+ import { canCreateObjectUrl, looksLikeImageAttachment } from './AttachmentPreviewModal.hooks.js';
14
+ /** The platform's Blob URLs, or `undefined` where they do not exist (jsdom, non-browser hosts). */
15
+ export function platformObjectUrls() {
16
+ if (!canCreateObjectUrl())
17
+ return undefined;
18
+ return { create: (file) => URL.createObjectURL(file), revoke: (url) => URL.revokeObjectURL(url) };
19
+ }
20
+ /**
21
+ * The cached file to draw as a thumbnail, or `undefined` when the attachment should stay a chip:
22
+ * not an image (same `looksLikeImageAttachment` rule the preview modal uses), or no bytes cached.
23
+ * @complexity O(1).
24
+ */
25
+ export function sentAttachmentThumbnailSource({ attachment }, { getSource = getAttachmentPreviewSource } = {}) {
26
+ if (!looksLikeImageAttachment({ attachment }))
27
+ return undefined;
28
+ return getSource({ path: attachment.path });
29
+ }
30
+ /**
31
+ * Creates the thumbnail's object URL on mount and revokes it on unmount (an effect, not `useMemo`,
32
+ * so a Strict Mode double render cannot leak a URL - same reasoning as the preview modal's hook).
33
+ * @param input.hostRendered - True when a host `renderAttachment` draws this chip.
34
+ * @param options.objectUrls - Blob-URL port; defaults to the platform's, `undefined` where absent.
35
+ * @param options.getSource - Preview-cache lookup; defaults to the shared cache.
36
+ * @complexity O(1) per attachment.
37
+ */
38
+ export function useSentAttachmentThumbnail({ attachment, hostRendered = false }, { objectUrls = platformObjectUrls(), getSource = getAttachmentPreviewSource } = {}) {
39
+ const [failed, setFailed] = useState(false);
40
+ const [thumbnailUrl, setThumbnailUrl] = useState(undefined);
41
+ // A host `renderAttachment` owns the chip's content, so no thumbnail URL is made for it.
42
+ const source = failed || hostRendered || !objectUrls ? undefined : sentAttachmentThumbnailSource({ attachment }, { getSource });
43
+ useEffect(() => {
44
+ if (!source || !objectUrls) {
45
+ setThumbnailUrl(undefined);
46
+ return undefined;
47
+ }
48
+ const url = objectUrls.create(source);
49
+ setThumbnailUrl(url);
50
+ return () => objectUrls.revoke(url);
51
+ // `objectUrls` is a fresh object per render by default; the file is what the URL is made from.
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [source]);
54
+ return { thumbnailUrl, onThumbnailError: () => setFailed(true) };
55
+ }
56
+ //# sourceMappingURL=SentAttachment.hooks.js.map
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useT } from '../hooks/context.js';
3
+ import { useSentAttachmentThumbnail } from './SentAttachment.hooks.js';
4
+ /**
5
+ * One attachment on a sent user message: a small image thumbnail when this browser still holds the
6
+ * image's bytes, otherwise the filename chip. Either way a click opens the preview modal, and the
7
+ * accessible name stays "Open {name}".
8
+ */
9
+ export function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }) {
10
+ const t = useT();
11
+ const { thumbnailUrl, onThumbnailError } = useSentAttachmentThumbnail({ attachment, hostRendered: renderAttachment !== undefined }, objectUrls === undefined ? {} : { objectUrls });
12
+ return (_jsx("button", { type: "button", className: `jini-message-attachment-chip${thumbnailUrl ? ' is-thumbnail' : ''}`, onClick: () => onOpen(attachment), title: t('Open {name}', { name: attachment.name }), "aria-label": t('Open {name}', { name: attachment.name }), children: renderAttachment ? (renderAttachment(attachment)) : thumbnailUrl ? (_jsx("img", { className: "jini-message-attachment-thumb", src: thumbnailUrl, alt: "", onError: onThumbnailError })) : (attachment.name) }));
13
+ }
14
+ //# sourceMappingURL=SentAttachment.js.map
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ToolDisplayRow } from '../tool-call-display.js';
3
+ /**
4
+ * One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
5
+ * `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
6
+ * `useToolCallGroup`.
7
+ */
8
+ export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow }: {
9
+ rows: readonly Row[];
10
+ renderRow: (row: Row) => ReactNode;
11
+ }): import("react").JSX.Element;
12
+ //# sourceMappingURL=ToolCallGroup.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { type ToolDisplayRow } from '../tool-call-display.js';
2
+ export interface ToolCallGroupState {
3
+ /** Whether the turn's tool cards are showing under the line. Starts closed. */
4
+ open: boolean;
5
+ toggle: () => void;
6
+ /** "Used 3 tools", "Used 3 tools · 1 failed", or "Running Bash…" while a tool runs. */
7
+ label: string;
8
+ running: boolean;
9
+ failed: boolean;
10
+ }
11
+ /**
12
+ * State and copy for one collapsed tool line (`toolCallDisplay: 'collapsed'`). The open flag lives
13
+ * in the line's own component, so it lasts as long as the message stays in the transcript — the
14
+ * whole time its conversation is open — and resets when the pane remounts.
15
+ * @complexity O(n) in rows, for the summary.
16
+ */
17
+ export declare function useToolCallGroup({ rows }: {
18
+ rows: readonly ToolDisplayRow[];
19
+ }, _optional?: {}): ToolCallGroupState;
20
+ //# sourceMappingURL=ToolCallGroup.hooks.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useT } from '../hooks/context.js';
3
+ import { summarizeToolGroup, toolGroupLabel } from '../tool-call-display.js';
4
+ /**
5
+ * State and copy for one collapsed tool line (`toolCallDisplay: 'collapsed'`). The open flag lives
6
+ * in the line's own component, so it lasts as long as the message stays in the transcript — the
7
+ * whole time its conversation is open — and resets when the pane remounts.
8
+ * @complexity O(n) in rows, for the summary.
9
+ */
10
+ export function useToolCallGroup({ rows }, _optional = {}) {
11
+ const t = useT();
12
+ const [open, setOpen] = useState(false);
13
+ const toggle = useCallback(() => setOpen((value) => !value), []);
14
+ const summary = summarizeToolGroup({ rows });
15
+ return {
16
+ open,
17
+ toggle,
18
+ label: toolGroupLabel({ summary, t }),
19
+ running: summary.kind === 'running',
20
+ failed: summary.kind === 'done' && summary.failed > 0,
21
+ };
22
+ }
23
+ //# sourceMappingURL=ToolCallGroup.hooks.js.map
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useToolCallGroup } from './ToolCallGroup.hooks.js';
3
+ import { Icon } from './Icon.js';
4
+ /**
5
+ * One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
6
+ * `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
7
+ * `useToolCallGroup`.
8
+ */
9
+ export function ToolCallGroup({ rows, renderRow }) {
10
+ const group = useToolCallGroup({ rows });
11
+ return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [_jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: group.label }), _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
12
+ }
13
+ //# sourceMappingURL=ToolCallGroup.js.map