@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
@@ -10,25 +10,49 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  * scroll-anchoring state lives in `useMessageListAutoScroll`
11
11
  * (`MessageList.hooks.ts`); see that file for why it is presentational.
12
12
  */
13
- import { memo } from 'react';
13
+ import { memo, useMemo } from 'react';
14
+ import { projectAcceptedInputMessages } from '../../core/accepted-input.js';
14
15
  import { isTerminalRunStatus } from '../../core/index.js';
15
16
  import { useT } from '../hooks/context.js';
16
17
  import { useMessageListAutoScroll, useConversationUsage } from './MessageList.hooks.js';
17
18
  import { MessageRow } from './MessageRow.js';
19
+ import { SentAttachmentButton } from './SentAttachment.js';
20
+ import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
21
+ import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
22
+ function renderMessageRow({ message, isLast, isStreaming, usageSummary }, { activeQuestionFormMessageId, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, ...rowProps } = {}) {
23
+ const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
24
+ return (_jsx(MessageRow, { message: message, ...rowProps, ...(usageSummary === undefined ? {} : { usageSummary }), runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}) }, message.id));
25
+ }
18
26
  /**
19
27
  * Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
20
28
  * (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
21
- * 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompt` and
29
+ * 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompts` and
22
30
  * `projectFileNames` for exactly this.
23
31
  */
24
- export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, pendingPrompt = null, }) {
25
- const t = useT();
32
+ export const MessageList = memo(function MessageList({ messages: sourceMessages, isStreaming = false, scrollIntent = false, onScrolled, pendingPrompt, pendingPrompts, ...rowProps }) {
33
+ const messages = useMemo(() => projectAcceptedInputMessages({ messages: sourceMessages }), [sourceMessages]);
26
34
  const usageByMessage = useConversationUsage({ messages });
27
- const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
28
- return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
29
- const isLast = message.id === messages[messages.length - 1]?.id;
30
- const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
31
- return (_jsx(MessageRow, { message: message, ...(usageByMessage.has(message.id) ? { usageSummary: usageByMessage.get(message.id) } : {}), showUsageTotal: showUsageTotal, showUsageLine: showUsageLine, toolCallDisplay: toolCallDisplay, showThinking: showThinking, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
32
- }), pendingPrompt ? (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { className: "jini-chat-pane__queued-text", children: pendingPrompt.text }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: pendingPrompt.onCancel, title: t('Cancel queued message'), children: t('Cancel') }) })] })) : null] }));
35
+ const { containerRef, onScroll, awayFromLatest, jumpToLatest } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
36
+ return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => renderMessageRow({
37
+ message,
38
+ isLast: message.id === messages[messages.length - 1]?.id,
39
+ isStreaming,
40
+ usageSummary: usageByMessage.get(message.id),
41
+ }, rowProps)), (pendingPrompts ?? (pendingPrompt ? [pendingPrompt] : [])).map((prompt, index) => (_jsx(QueuedMessage, { prompt: prompt, renderAttachment: rowProps.renderAttachment }, prompt.id ?? index))), awayFromLatest && _jsx(JumpToLatest, { onJump: jumpToLatest })] }));
33
42
  });
43
+ /**
44
+ * Sticky, zero-height last child of the scroll container, so the button floats centred just above
45
+ * the composer without a wrapper element changing the pane's flex layout.
46
+ */
47
+ function JumpToLatest({ onJump }) {
48
+ const t = useT();
49
+ return (_jsx("div", { className: "jini-message-list__jump", children: _jsxs("button", { type: "button", className: "jini-message-list__jump-btn", onClick: onJump, children: [_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "m19 12-7 7-7-7" })] }), t('Jump to latest')] }) }));
50
+ }
51
+ /** One cancelable queue receipt; attachment rendering reuses the sent-message preview owner. */
52
+ function QueuedMessage({ prompt, renderAttachment }) {
53
+ const t = useT();
54
+ const attachments = prompt.attachments ?? [];
55
+ const gallery = useAttachmentGallery({ attachments });
56
+ return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
57
+ }
34
58
  //# sourceMappingURL=MessageList.js.map
@@ -63,7 +63,7 @@ export interface MessageRowProps {
63
63
  * included (a host's "Done line" display choice, 2026-10-11). */
64
64
  showUsageLine?: boolean;
65
65
  /** How tool calls show (`../tool-call-display.ts`): every card (`'show'`, the default), one
66
- * fold-up line per turn (`'collapsed'`), or only failed ones (`'hidden'`). */
66
+ * fold-up line per turn (`'collapsed'`), or compact unrecovered failures (`'hidden'`). */
67
67
  toolCallDisplay?: ToolCallDisplay;
68
68
  /** Show the model's reasoning (`thinking` events) as a fold-up block where it happened. Off by
69
69
  * default — the transcript never showed it before this option. Needs the interleaved layout; a
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
3
+ import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
2
4
  import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
3
5
  import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
4
6
  /**
@@ -51,8 +53,8 @@ import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary }
51
53
  * not appear until the whole message is done, not merely until its current
52
54
  * chunk stopped arriving.
53
55
  */
54
- import React, { memo } from 'react';
55
- import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
56
+ import React, { memo, useMemo } from 'react';
57
+ import { assistantContentFromEvents, isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
56
58
  import { useToolTimeline } from '../hooks/useToolTimeline.js';
57
59
  import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
58
60
  import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
@@ -116,16 +118,25 @@ function UsageSummary({ usage, showTotal }) {
116
118
  return null;
117
119
  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] }));
118
120
  }
119
- function messageBlocks({ visibleContent, message, rows, registry }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
120
- // Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
121
- // whole string and trims, so the surviving text no longer lines up with the per-run offsets the
122
- // event walk produces, and an artifact block could legitimately span a tool call. Rather than
123
- // reason about partial overlaps, an artifact-bearing message keeps the flat layout — those
124
- // messages are dominated by the artifact panel anyway, so the ordering matters least there.
125
- if (visibleContent !== message.content)
126
- return null;
127
- const blocks = interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => registry.slot({ name: ev.name, data: ev.data }), includeThinking });
128
- return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }) : null;
121
+ function messageBlocks({ message, rows, registry, content = message.content }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
122
+ const blocks = interleaveMessageBlocks({ events: message.events, content, rows }, { slotOf: (ev) => registry.slot({ name: ev.name, data: ev.data }), includeThinking });
123
+ return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }, { runFailed: message.runStatus === 'failed' }) : null;
124
+ }
125
+ /**
126
+ * The flat layout's tool groups. Even when host text prevents interleaving, event ordering can still
127
+ * prove tool recovery and reply boundaries, so hidden mode reconstructs the blocks from the events —
128
+ * only for this display decision; the host's text is never replaced. Otherwise one flat list.
129
+ * @complexity O(n) in events + rows.
130
+ */
131
+ function flatToolGroups({ message, rows, registry, displayBlocks }, { toolCallDisplay }) {
132
+ if (toolCallDisplay === 'hidden') {
133
+ const recovery = displayBlocks
134
+ ?? messageBlocks({ message, rows, registry, content: assistantContentFromEvents({ events: message.events ?? [] }) }, { toolCallDisplay });
135
+ if (recovery)
136
+ return recovery.flatMap((block) => (block.kind === 'tools' ? [{ rows: block.rows, key: block.key }] : []));
137
+ }
138
+ const flat = toolRowsForDisplay({ rows, mode: toolCallDisplay }, { runFailed: message.runStatus === 'failed' });
139
+ return flat.length > 0 ? [{ rows: flat, key: 'tools' }] : [];
129
140
  }
130
141
  /** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
131
142
  function ThinkingBlock({ text }) {
@@ -164,50 +175,66 @@ const NO_ATTACHMENTS = [];
164
175
  * re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
165
176
  * are replaced, never mutated, so identity is a sound change signal.
166
177
  */
167
- 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, }) {
178
+ export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
179
+ const message = useMemo(() => props.message.events === undefined ? props.message : {
180
+ ...props.message, events: projectAcceptedInputEvents({ events: props.message.events }),
181
+ }, [props.message]);
182
+ if (message.role === 'user')
183
+ return _jsx(UserMessageRow, { ...props, message: message });
184
+ return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive });
185
+ });
186
+ function UserMessageRow({ message, renderAttachment }) {
168
187
  const t = useT();
169
- const registry = useExtEventRendererRegistry();
170
- const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
171
- const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
172
- const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
173
- const answeredCards = useAnsweredCards({ slotOf: registry.slot });
174
188
  // Which attachment chip (if any) this row's preview modal is open for. Row-local, not
175
189
  // module-level: two attachments in the same message can never be "open" at once, but two
176
190
  // different messages opening their own attachments independently is fine and expected.
177
191
  // Previous/next steps through this message's images only.
178
192
  const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
179
- if (message.role === 'user') {
180
- 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: gallery.open, 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') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
181
- }
193
+ 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: gallery.open, 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') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
194
+ }
195
+ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
196
+ const registry = useExtEventRendererRegistry();
197
+ const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
198
+ const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
199
+ const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
200
+ const answeredCards = useAnsweredCards({ slotOf: registry.slot, events: message.events ?? [], groups: extGroups, runStreaming, runStatus: message.runStatus });
182
201
  const terminalNotice = terminalMessageNotice({ message }, {});
183
202
  const visibleContent = stripArtifact({ content: message.content });
184
- const segments = splitOnQuestionForms({ input: visibleContent });
185
- const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
203
+ const displayBlocks = messageBlocks({ message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
204
+ // Artifact stripping trims/replaces text, invalidating the event offsets for rendering. Keep the
205
+ // flat layout there; displayBlocks still supplies the recovery decision from the original events.
206
+ const blocks = visibleContent === message.content ? displayBlocks : null;
186
207
  const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
187
208
  const runInProgress = isRunInProgress(message.runStatus);
188
209
  const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
189
210
  const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, { runStreaming });
190
211
  const footerUsage = showUsageLine ? usage.footer : undefined;
191
212
  const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
192
- // One turn's cards: a fold-up line when collapsed, the plain card list otherwise.
193
- const renderTools = (rows, key) => (toolCallDisplay === 'collapsed'
194
- ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard }, key)
213
+ // Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
214
+ const renderTools = (rows, key) => (toolCallDisplay !== 'show'
215
+ ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
195
216
  : _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
196
- const flatToolRows = toolRowsForDisplay({ rows: timeline.rows, mode: toolCallDisplay });
217
+ const flatTools = flatToolGroups({ message, rows: timeline.rows, registry, displayBlocks }, { toolCallDisplay });
197
218
  const renderExtGroup = (group) => {
198
219
  const renderer = registry.get({ name: group.name });
220
+ if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
221
+ return _jsx(MidRunUserMessage, { events: group.events });
199
222
  if (!renderer)
200
223
  return null;
224
+ const answerText = answeredCards.answerText(group.slot);
201
225
  const node = renderer({
202
226
  name: group.name,
203
227
  events: group.events,
204
- runStreaming,
228
+ runStreaming: answeredCards.runStreaming,
205
229
  runSucceeded,
206
230
  awaitingAnswer,
207
231
  runId: message.runId,
208
232
  ...(group.call === undefined ? {} : { call: group.call }),
209
233
  answered: answeredCards.isAnswered(group.slot),
210
234
  onAnswered: () => answeredCards.markAnswered(group.slot),
235
+ onAnswerText: (text) => answeredCards.markAnswered(group.slot, text),
236
+ ...(answeredCards.isRecorded(group.slot) ? { answerRecorded: true } : {}),
237
+ ...(answerText === undefined ? {} : { answerText }),
211
238
  });
212
239
  if (!node)
213
240
  return null;
@@ -218,28 +245,29 @@ export const MessageRow = memo(function MessageRow({ message, usageSummary, show
218
245
  _jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
219
246
  };
220
247
  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}`)));
248
+ const renderBlock = (block) => {
249
+ if (block.kind === 'text') {
250
+ return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
251
+ }
252
+ if (block.kind === 'reply-end') {
253
+ const reply = usage.replies[block.index];
254
+ return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
255
+ }
256
+ if (block.kind === 'tools')
257
+ return renderTools(block.rows, block.key);
258
+ if (block.kind === 'thinking')
259
+ return _jsx(ThinkingBlock, { text: block.text }, block.key);
260
+ const group = extGroups.find((g) => g.slot === block.slot);
261
+ if (!group)
262
+ return null;
263
+ return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
264
+ };
221
265
  return (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
222
266
  ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
223
267
  : '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
224
- ? blocks.map((block) => {
225
- if (block.kind === 'text') {
226
- return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
227
- }
228
- if (block.kind === 'reply-end') {
229
- const reply = usage.replies[block.index];
230
- return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
231
- }
232
- if (block.kind === 'tools')
233
- return renderTools(block.rows, block.key);
234
- if (block.kind === 'thinking')
235
- return _jsx(ThinkingBlock, { text: block.text }, block.key);
236
- const group = extGroups.find((g) => g.slot === block.slot);
237
- if (!group)
238
- return null;
239
- return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
240
- })
241
- : (_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 })] }));
242
- });
268
+ ? blocks.map(renderBlock)
269
+ : (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), 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 })] }));
270
+ }
243
271
  /**
244
272
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
245
273
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
@@ -33,19 +33,24 @@ export function usageLineMeetsActions({ footer, terminalNotice, pending, runInPr
33
33
  * the current turn's cost is unknown. `showTotal: false` turns the total off. */
34
34
  export function useUsageSummary({ usage }, { showTotal = true } = {}) {
35
35
  const t = useT();
36
- const parts = [];
37
- if (usage.durationMs !== undefined) {
38
- const seconds = Math.round(usage.durationMs / 1000);
39
- parts.push(seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`);
40
- }
41
- if (usage.costUsd !== undefined) {
42
- parts.push(formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) }));
43
- }
44
- else if (showTotal && usage.totalCostUsd !== undefined) {
45
- parts.push(`total ${formatUsageCost({ costUsd: usage.totalCostUsd })}`);
46
- }
36
+ const parts = [usageDuration({ durationMs: usage.durationMs }), usageCost({ usage }, { showTotal })]
37
+ .filter((part) => part !== undefined);
47
38
  const text = parts.join(' · ');
48
39
  const title = usage.outputTokens === undefined ? undefined : t('{n} out', { n: usage.outputTokens });
49
40
  return { text, title, ariaLabel: title === undefined ? undefined : `${t('Done')} · ${text} · ${title}` };
50
41
  }
42
+ function usageDuration({ durationMs }, _options = {}) {
43
+ if (durationMs === undefined)
44
+ return undefined;
45
+ const seconds = Math.round(durationMs / 1000);
46
+ return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
47
+ }
48
+ function usageCost({ usage }, { showTotal = true } = {}) {
49
+ if (usage.costUsd !== undefined) {
50
+ return formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) });
51
+ }
52
+ if (showTotal && usage.totalCostUsd !== undefined)
53
+ return `total ${formatUsageCost({ costUsd: usage.totalCostUsd })}`;
54
+ return undefined;
55
+ }
51
56
  //# sourceMappingURL=MessageRow.usage.js.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * @module MidRunUserMessage
4
4
  *
@@ -10,6 +10,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
10
10
  * It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
11
11
  * into the turn it was already running — there is no separate reply to hang off it.
12
12
  */
13
+ import { acceptedInputEvent } from '../../core/accepted-input.js';
14
+ import { SentAttachmentButton } from './SentAttachment.js';
15
+ import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
16
+ import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
13
17
  import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
14
18
  import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
15
19
  /** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
@@ -25,10 +29,19 @@ export function midRunUserMessageSlotKey({ data }) {
25
29
  }
26
30
  /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
27
31
  export function MidRunUserMessage({ events }) {
28
- const text = field(events.at(-1), 'text');
32
+ const data = events.at(-1);
33
+ const text = field(data, 'text');
29
34
  if (text === undefined)
30
35
  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 }) }));
36
+ const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
37
+ // MOVE: A single live delivery may contain several queued inputs; display their original receipts.
38
+ const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
39
+ return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input }, input.id)) });
40
+ }
41
+ function MidRunInput({ input }) {
42
+ const attachments = input.attachments ?? [];
43
+ const gallery = useAttachmentGallery({ attachments });
44
+ return (_jsxs("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: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
32
45
  }
33
46
  /**
34
47
  * Registers {@link MidRunUserMessage} against the ext-event registry.