@jini-ai/chat 0.6.12 → 0.6.15-rc.1

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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/contracts/chat-bubble-style.d.ts +3 -2
  3. package/dist/contracts/chat-bubble-style.js +3 -2
  4. package/dist/core/accepted-input.d.ts +20 -1
  5. package/dist/core/accepted-input.js +65 -17
  6. package/dist/core/ag-ui/entry.d.ts +2 -4
  7. package/dist/core/ag-ui/entry.js +11 -5
  8. package/dist/core/assistant-client/ag-ui-transport.js +6 -1
  9. package/dist/core/assistant-client/durable-run-subscription.d.ts +2 -1
  10. package/dist/core/assistant-client/durable-run-subscription.js +3 -1
  11. package/dist/core/assistant-client/transport.js +5 -1
  12. package/dist/core/assistant-content.d.ts +8 -1
  13. package/dist/core/assistant-content.js +17 -4
  14. package/dist/core/durable-runs/continuation.js +6 -2
  15. package/dist/core/events.d.ts +3 -0
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/core/question-answer.d.ts +8 -0
  19. package/dist/core/question-answer.js +16 -4
  20. package/dist/core/run-activity.d.ts +1 -1
  21. package/dist/core/run-activity.js +46 -4
  22. package/dist/core/run-events/entry.js +5 -2
  23. package/dist/embed-widget/vanilla/style.css +1 -1
  24. package/dist/react/components/CopyMessageButton.d.ts +18 -0
  25. package/dist/react/components/CopyMessageButton.js +22 -0
  26. package/dist/react/components/Markdown.js +24 -5
  27. package/dist/react/components/McpUiSurfaceCard.js +50 -17
  28. package/dist/react/components/MessageList.hooks.d.ts +2 -1
  29. package/dist/react/components/MessageList.hooks.js +14 -4
  30. package/dist/react/components/MessageList.js +5 -3
  31. package/dist/react/components/MessageRow.d.ts +3 -1
  32. package/dist/react/components/MessageRow.js +66 -38
  33. package/dist/react/components/MidRunUserMessage.d.ts +1 -1
  34. package/dist/react/components/MidRunUserMessage.js +11 -7
  35. package/dist/react/components/QuestionForm.js +22 -2
  36. package/dist/react/components/ToolCard.js +21 -8
  37. package/dist/react/components/mcp-ui-question-input.d.ts +20 -0
  38. package/dist/react/components/mcp-ui-question-input.js +72 -0
  39. package/dist/react/ext-event-renderer-registry.d.ts +2 -0
  40. package/dist/react/features/chat-pane/components/ChatPane.js +10 -3
  41. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +6 -0
  42. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +70 -15
  43. package/dist/react/hooks/useAnsweredCards.js +36 -20
  44. package/dist/react/hooks/useConversation.d.ts +6 -0
  45. package/dist/react/hooks/useConversation.js +10 -3
  46. package/dist/react/index.d.ts +7 -0
  47. package/dist/react/index.js +4 -0
  48. package/dist/react/message-blocks.d.ts +6 -0
  49. package/dist/react/message-blocks.js +27 -1
  50. package/dist/server/durable-runs/accept.d.ts +4 -0
  51. package/dist/server/durable-runs/accept.js +8 -1
  52. package/dist/server/durable-runs/recover.js +5 -4
  53. package/package.json +15 -15
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
3
3
  import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
4
+ import { useSurfaceAnswerContexts } from './mcp-ui-question-input.js';
4
5
  import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
5
6
  import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
6
7
  /**
@@ -36,7 +37,7 @@ import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, use
36
37
  * see that module's header for the conditions.
37
38
  *
38
39
  * Both message roles also carry a per-message copy affordance
39
- * (`CopyMessageButton` below), rendered inside a `.jini-message-actions` row.
40
+ * (`CopyMessageButton`), rendered inside a `.jini-message-actions` row.
40
41
  * Assistant rows render that row as an extension seam: `branch`/thumbs-up/
41
42
  * thumbs-down are meant to join `copy` there later, as siblings in the same
42
43
  * row, once their backends (conversation branching, feedback storage) exist
@@ -57,16 +58,15 @@ import React, { memo, useMemo } from 'react';
57
58
  import { assistantContentFromEvents, isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
58
59
  import { useToolTimeline } from '../hooks/useToolTimeline.js';
59
60
  import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
60
- import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
61
61
  import { useRunActivity } from '../hooks/useRunActivity.js';
62
62
  import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
63
- import { interleaveMessageBlocks } from '../message-blocks.js';
63
+ import { interleaveMessageBlocks, splitMidRunBlocks } from '../message-blocks.js';
64
64
  import { useT, useExtEventRendererRegistry } from '../hooks/context.js';
65
65
  import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
66
66
  import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
67
67
  import { SentAttachmentButton } from './SentAttachment.js';
68
68
  import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
69
- import { Icon } from './Icon.js';
69
+ import { CopyMessageButton } from './CopyMessageButton.js';
70
70
  import { Markdown } from './Markdown.js';
71
71
  import { ToolCard } from './ToolCard.js';
72
72
  import { QuestionForm } from './QuestionForm.js';
@@ -149,25 +149,9 @@ function ThinkingBlock({ text }) {
149
149
  function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
150
150
  return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
151
151
  }
152
- function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
152
+ function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard }, _optional = {}) {
153
153
  const t = useT();
154
- return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : 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] });
155
- }
156
- /**
157
- * Quiet, icon-only copy affordance shared by the user- and assistant-message
158
- * action rows below. Delegates the actual clipboard write (and its
159
- * secure-context fallback) to `useCopyToClipboard`; this component only owns
160
- * the icon swap and the accessible announcement of the transient state.
161
- * @param props.text - Exact string to copy, verbatim.
162
- * @param props.label - Idle accessible name (already translated by the caller).
163
- * @complexity O(1) — one child hook call, no branching beyond the copied-state read.
164
- */
165
- function CopyMessageButton({ text, label }) {
166
- const t = useT();
167
- const { copied, copy } = useCopyToClipboard();
168
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "jini-message-action-btn", "aria-label": copied ? t('Copied') : label, onClick: () => {
169
- void copy(text);
170
- }, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
154
+ return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : 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, sending: runSending }) : null] });
171
155
  }
172
156
  /** Stable empty list for a message with no attachments. */
173
157
  const NO_ATTACHMENTS = [];
@@ -176,13 +160,14 @@ const NO_ATTACHMENTS = [];
176
160
  * re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
177
161
  * are replaced, never mutated, so identity is a sound change signal.
178
162
  */
179
- export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
163
+ export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSending = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
164
+ const answerContexts = useSurfaceAnswerContexts({ events: props.message.events });
180
165
  const message = useMemo(() => props.message.events === undefined ? props.message : {
181
- ...props.message, events: projectAcceptedInputEvents({ events: props.message.events }),
182
- }, [props.message]);
166
+ ...props.message, events: projectAcceptedInputEvents({ events: props.message.events }, answerContexts),
167
+ }, [props.message, answerContexts]);
183
168
  if (message.role === 'user')
184
169
  return _jsx(UserMessageRow, { ...props, message: message });
185
- return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive });
170
+ return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive });
186
171
  });
187
172
  function UserMessageRow({ message, renderAttachment }) {
188
173
  const t = useT();
@@ -193,7 +178,7 @@ function UserMessageRow({ message, renderAttachment }) {
193
178
  const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
194
179
  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] }));
195
180
  }
196
- function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
181
+ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
197
182
  const registry = useExtEventRendererRegistry();
198
183
  const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
199
184
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
@@ -219,17 +204,20 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
219
204
  ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
220
205
  : _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
221
206
  const flatTools = flatToolGroups({ message, rows: timeline.rows, registry, displayBlocks }, { toolCallDisplay });
207
+ const flatExtGroups = extGroups.filter(group => group.name !== MID_RUN_USER_MESSAGE_EXT_EVENT_NAME);
222
208
  const renderExtGroup = (group) => {
209
+ const transcriptRow = group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME;
223
210
  const renderer = registry.get({ name: group.name });
224
211
  if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
225
- return _jsx(MidRunUserMessage, { events: group.events, runStreaming: answeredCards.runStreaming }, group.slot);
212
+ return _jsx(MidRunUserMessage, { events: group.events, runStreaming: runInProgress, transcriptRow: true }, group.slot);
226
213
  if (!renderer)
227
214
  return null;
228
215
  const answerText = answeredCards.answerText(group.slot);
229
216
  const node = renderer({
230
217
  name: group.name,
231
218
  events: group.events,
232
- runStreaming: answeredCards.runStreaming,
219
+ runStreaming: group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME ? runInProgress : answeredCards.runStreaming,
220
+ ...(group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME ? { transcriptRow: true } : {}),
233
221
  runSucceeded,
234
222
  awaitingAnswer,
235
223
  runId: message.runId,
@@ -237,8 +225,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
237
225
  answered: answeredCards.isAnswered(group.slot),
238
226
  onAnswered: () => answeredCards.markAnswered(group.slot),
239
227
  onAnswerText: (text) => answeredCards.markAnswered(group.slot, text),
240
- ...(answeredCards.isRecorded(group.slot) ? { answerRecorded: true } : {}),
241
- ...(answerText === undefined ? {} : { answerText }),
228
+ ...answerRenderProps({ answeredCards, slot: group.slot, answerText }),
242
229
  });
243
230
  if (!node)
244
231
  return null;
@@ -246,7 +233,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
246
233
  // `key` includes the event count so a group that failed on an earlier, shorter event list
247
234
  // gets a fresh boundary instance (not the still-tripped one) once a new event actually
248
235
  // arrives for it, instead of staying tombstoned for the message's whole lifetime.
249
- _jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
236
+ _jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
250
237
  };
251
238
  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}`)));
252
239
  const renderBlock = (block) => {
@@ -262,25 +249,66 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
262
249
  return renderTools(block.rows, block.key);
263
250
  if (block.kind === 'thinking')
264
251
  return _jsx(ThinkingBlock, { text: block.text }, block.key);
252
+ return renderExtBlock(block);
253
+ };
254
+ // A mid-run bubble is its own transcript row, so it skips the ext-events wrapper and its spacing.
255
+ const renderExtBlock = (block) => {
265
256
  const group = extGroups.find((g) => g.slot === block.slot);
266
257
  if (!group)
267
258
  return null;
259
+ if (group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
260
+ return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
268
261
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
269
262
  };
270
- return (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
263
+ const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
271
264
  ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
272
- : '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
273
- ? blocks.map(renderBlock)
274
- : (_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 })] }));
265
+ : '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, section
266
+ ? section.map(renderBlock)
267
+ : (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), flatExtGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: flatExtGroups.map(renderExtGroup) }) : null] })), footer && _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, key));
268
+ // The lossless fallback keeps the host's text intact; user receipts still own separate rows,
269
+ // in their original receipt order, outside the assistant's actions and usage footer.
270
+ if (blocks === null)
271
+ return _jsxs(_Fragment, { children: [renderAssistantSection({ section: null, key: message.id, footer: true }), extGroups.filter(group => group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME).map(renderExtGroup)] });
272
+ // A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
273
+ const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
274
+ const { sections, footerIndex } = transcriptSections({ blocks: visibleBlocks, runInProgress });
275
+ return _jsx(_Fragment, { children: sections.map((section, index) => isUserSection({ section })
276
+ ? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
277
+ : renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex })) });
278
+ }
279
+ /** Preserve optional render-prop presence for custom extension renderers. */
280
+ function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
281
+ return { ...(answeredCards.isRecorded(slot) ? { answerRecorded: true } : {}),
282
+ ...(answerText === undefined ? {} : { answerText }) };
283
+ }
284
+ /** Place the one footer on the last assistant section, never below a completed trailing input. */
285
+ function transcriptSections({ blocks, runInProgress }, _options = {}) {
286
+ const sections = splitMidRunBlocks({ blocks });
287
+ if (sections.length === 0)
288
+ sections.push([]);
289
+ // A live trailing input starts the next reply. A completed run needs no empty assistant row
290
+ // (its actions/usage used to leave a large gap below the final user bubble).
291
+ if (runInProgress && isUserSection({ section: sections.at(-1) }))
292
+ sections.push([]);
293
+ const footerIndex = sections.findLastIndex(section => !isUserSection({ section }));
294
+ return { sections, footerIndex };
295
+ }
296
+ /** A user receipt occupies its own section; every other block belongs to an assistant row. */
297
+ function isUserSection({ section }, _options = {}) {
298
+ const block = section?.[0];
299
+ return block?.kind === 'ext' && block.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME;
275
300
  }
276
301
  /**
277
302
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
278
303
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
279
304
  * Kept a separate component so its one-second tick re-renders only this line, not the whole row.
280
305
  */
281
- function RunActivityLine({ events, active, isAnsweredCard }) {
306
+ function RunActivityLine({ events, active, isAnsweredCard, sending }) {
307
+ const t = useT();
282
308
  const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
283
- const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
309
+ // Until the server accepts the start the turn is still on its way: "Thinking…" there read as the
310
+ // model working while the text had not even arrived (owner, 2026-10-27). No timeout.
311
+ const label = active && sending ? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
284
312
  if (label === null)
285
313
  return null;
286
314
  return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
@@ -7,7 +7,7 @@ export declare function midRunUserMessageSlotKey({ data }: {
7
7
  data: unknown;
8
8
  }): string | undefined;
9
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, runStreaming }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming'>>): import("react").JSX.Element | null;
10
+ export declare function MidRunUserMessage({ events, runStreaming, transcriptRow }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming' | 'transcriptRow'>>): import("react").JSX.Element | null;
11
11
  /**
12
12
  * Registers {@link MidRunUserMessage} against the ext-event registry.
13
13
  *
@@ -3,15 +3,16 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  * @module MidRunUserMessage
4
4
  *
5
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.
6
+ * separate user row between assistant sections, at the point in the stream where the live agent received it.
7
7
  * The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
8
8
  * the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
9
9
  *
10
- * It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
10
+ * It is stored as 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
13
  import { acceptedInputEvent } from '../../core/accepted-input.js';
14
14
  import { SentAttachmentButton } from './SentAttachment.js';
15
+ import { CopyMessageButton } from './CopyMessageButton.js';
15
16
  import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
16
17
  import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
17
18
  import { useT } from '../hooks/context.js';
@@ -29,7 +30,7 @@ export function midRunUserMessageSlotKey({ data }) {
29
30
  return field(data, 'id');
30
31
  }
31
32
  /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
32
- export function MidRunUserMessage({ events, runStreaming = false }) {
33
+ export function MidRunUserMessage({ events, runStreaming = false, transcriptRow = false }) {
33
34
  const data = events.at(-1);
34
35
  const text = field(data, 'text');
35
36
  if (text === undefined)
@@ -38,13 +39,16 @@ export function MidRunUserMessage({ events, runStreaming = false }) {
38
39
  // MOVE: A single live delivery may contain several queued inputs; display their original receipts.
39
40
  const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
40
41
  // A run that ended without reading the message hands it to the next turn; it is no longer queued here.
41
- return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
42
+ return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, transcriptRow: transcriptRow, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
42
43
  }
43
- function MidRunInput({ input, queued }) {
44
+ /** One receipt's text, files and shared copy action; queued state belongs to this input alone.
45
+ * @complexity O(a) time/space for a attachments; clipboard and gallery effects stay with their owners.
46
+ */
47
+ function MidRunInput({ input, queued, transcriptRow }) {
44
48
  const t = useT();
45
49
  const attachments = input.attachments ?? [];
46
50
  const gallery = useAttachmentGallery({ attachments });
47
- 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: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _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 })] }));
51
+ return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _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)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
48
52
  }
49
53
  /**
50
54
  * Registers {@link MidRunUserMessage} against the ext-event registry.
@@ -52,6 +56,6 @@ function MidRunInput({ input, queued }) {
52
56
  * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
53
57
  */
54
58
  export function registerMidRunUserMessageRenderer() {
55
- return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming }) }, { slotKey: midRunUserMessageSlotKey });
59
+ return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming, transcriptRow: props.transcriptRow === true }) }, { slotKey: midRunUserMessageSlotKey });
56
60
  }
57
61
  //# sourceMappingURL=MidRunUserMessage.js.map
@@ -17,6 +17,7 @@ import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
17
17
  import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
18
18
  import { formatFormAnswers, formOptionValueForLabel, redactQuestionAnswers, isSecretQuestion, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from '../../core/index.js';
19
19
  import { useT } from '../hooks/context.js';
20
+ import { SecretInput, SecretTextarea } from '@jini-ai/ui';
20
21
  export const QuestionForm = forwardRef(function QuestionForm({ form: incomingForm, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit, redactUserText, onSecretRedacted }, ref) {
21
22
  const t = useT();
22
23
  const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
@@ -172,6 +173,25 @@ function DirectionCardsControl(ctx) {
172
173
  return null;
173
174
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: formId, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) }), singleCustomChoice(ctx)] }));
174
175
  }
176
+ /** A credential-shaped text question gets the shared eye toggle (hidden by default). */
177
+ function TextControl({ q, value, locked, update, t }) {
178
+ const shared = { className: 'qf-input', value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) };
179
+ const props = questionTextInputProps({ question: q });
180
+ if (props.type === 'password') {
181
+ const { type: _type, ...secretProps } = props;
182
+ return _jsx(SecretInput, { ...secretProps, ...shared, "aria-label": q.label, label: q.label, toggleLabels: { show: `${t('Show')} ${q.label}`, hide: `${t('Hide')} ${q.label}` } });
183
+ }
184
+ return _jsx("input", { ...props, ...shared });
185
+ }
186
+ /** A credential-shaped textarea gets the same eye toggle; `SecretTextarea` owns the masking style. */
187
+ function TextareaControl({ q, value, locked, update, t }) {
188
+ const shared = { className: 'qf-textarea', value: stringValue(value), placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) };
189
+ const props = questionTextareaProps({ question: q });
190
+ if (isSecretQuestion({ question: q })) {
191
+ return _jsx(SecretTextarea, { ...props, ...shared, "aria-label": q.label, label: q.label, toggleLabels: { show: `${t('Show')} ${q.label}`, hide: `${t('Hide')} ${q.label}` } });
192
+ }
193
+ return _jsx("textarea", { ...props, ...shared });
194
+ }
175
195
  /** date/time/datetime-local and url/email/tel share one plain input whose `type` is the question type. */
176
196
  function TypedInputControl({ q, value, locked, update }) {
177
197
  return _jsx("input", { type: q.type, className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) });
@@ -181,7 +201,7 @@ const QUESTION_CONTROLS = {
181
201
  checkbox: CheckboxControl,
182
202
  select: SelectControl,
183
203
  'direction-cards': DirectionCardsControl,
184
- text: ({ q, value, locked, update }) => _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
204
+ text: TextControl,
185
205
  number: ({ q, value, locked, update }) => _jsx("input", { type: "number", className: "qf-input", value: stringValue(value), placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
186
206
  range: ({ q, value, locked, update }) => (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: rangeValue(q, value), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: rangeValue(q, value) })] })),
187
207
  date: TypedInputControl,
@@ -193,7 +213,7 @@ const QUESTION_CONTROLS = {
193
213
  color: ({ q, value, locked, update }) => _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
194
214
  file: ({ q, value, locked, selectFiles }) => (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => selectFiles(q, e.target.files) }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })),
195
215
  switch: ({ q, value, locked, update }) => (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })),
196
- textarea: ({ q, value, locked, update }) => _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: stringValue(value), placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }),
216
+ textarea: TextareaControl,
197
217
  };
198
218
  function OptionCopy({ option }) {
199
219
  return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));
@@ -21,7 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
21
21
  * generic rendering for it.
22
22
  */
23
23
  import { useId, useState } from 'react';
24
- import { describeCollapsedToolCall, describeToolCallDetail, formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
24
+ import { COLLAPSED_DETAIL_MAX_CHARS, describeCollapsedToolCall, describeToolCallDetail, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, redactUserText, toRenderProps } from '../../core/index.js';
25
25
  import { useT, useToolRendererRegistry } from '../hooks/context.js';
26
26
  import { Icon } from './Icon.js';
27
27
  import { TodoCard } from './TodoCard.js';
@@ -146,7 +146,7 @@ function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded })
146
146
  const args = isDelegatedWrapperToolName({ name: name }) ? wrapper.input : input;
147
147
  const target = primaryTarget(args);
148
148
  const isRunning = runStreaming && !result;
149
- return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: target }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
149
+ return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: headSummary({ text: target }) }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
150
150
  }
151
151
  function SearchToolsCard({ input, result, runStreaming, runSucceeded }) {
152
152
  const t = useT();
@@ -211,7 +211,7 @@ function BashCard({ input, result, runStreaming, runSucceeded }) {
211
211
  const command = obj.command ?? '';
212
212
  const desc = obj.description;
213
213
  const isRunning = runStreaming && !result;
214
- return (_jsx(ExpandableCard, { className: "op-bash", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children: _jsxs("div", { className: "op-card-detail", children: [_jsx("pre", { className: "op-command", children: truncate(command, 400) }), result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 4000) }) : null] }) }));
214
+ return (_jsx(ExpandableCard, { className: "op-bash", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children: _jsx(ToolCallDetail, { input: command, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
215
215
  }
216
216
  function GlobCard({ input, result, runStreaming, runSucceeded }) {
217
217
  const t = useT();
@@ -236,7 +236,7 @@ function WebSearchCard({ input, result, runStreaming, runSucceeded }) {
236
236
  /** Any tool this package has no family card for — an MCP tool, or a card tool such as `assistant_ask_choice` whose form renders elsewhere in the turn. The row still opens to the full call. */
237
237
  function GenericCard({ name, input, result, runStreaming, runSucceeded }) {
238
238
  const summary = describeInput(input);
239
- return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: truncate(summary, 200) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
239
+ return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: headSummary({ text: summary }) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
240
240
  }
241
241
  /**
242
242
  * The one row shell every card shares: a real `<button>` head (Enter/Space toggle it) that reports
@@ -260,7 +260,9 @@ function CollapsedReaderText({ call }) {
260
260
  /** The call's input, its result (or why there is none), and any extra `children` — see `describeToolCallDetail` for the redaction and formatting rules. */
261
261
  function ToolCallDetail({ input, result, runStreaming, runSucceeded, children }) {
262
262
  const t = useT();
263
- const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded });
263
+ // Only mounted on expansion: keep the compact preview cheap without losing any of the stored
264
+ // call when the person opens it. The core formatter still owns formatting and secret redaction.
265
+ const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded, maxChars: Infinity });
264
266
  return (_jsxs("div", { className: "op-card-detail", children: [detail.inputText !== null ? _jsx("pre", { className: "op-command op-input", children: detail.inputText }) : null, detail.outputText !== null ? _jsx("pre", { className: "op-output", children: detail.outputText }) : null, detail.noteKey !== null ? _jsx("p", { className: "op-detail-note", children: t(detail.noteKey) }) : null, children] }));
265
267
  }
266
268
  function ResultBadge({ result, runStreaming, runSucceeded }) {
@@ -270,7 +272,7 @@ function ResultBadge({ result, runStreaming, runSucceeded }) {
270
272
  if (!result && !runSucceeded)
271
273
  return (_jsx("span", { className: "op-status op-status-error", title: t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
272
274
  if (result?.isError)
273
- return (_jsx("span", { className: "op-status op-status-error", title: result.content || t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
275
+ return (_jsx("span", { className: "op-status op-status-error", title: describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS }).outputText ?? t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
274
276
  return (_jsx("span", { className: "op-status op-status-ok", title: t('Done'), children: _jsx(Icon, { name: "check", size: 14 }) }));
275
277
  }
276
278
  /**
@@ -295,9 +297,12 @@ function ToolResultMedia({ result }) {
295
297
  return (_jsx("div", { className: "op-media", children: images.map((block, index) => (_jsx("img", { className: "op-media-image", src: `data:${block.mimeType};base64,${block.data}`, alt: "", style: { maxWidth: '100%', height: 'auto', display: 'block' } }, index))) }));
296
298
  }
297
299
  function FileErrorDetail({ result }) {
298
- if (!result?.isError || !result.content.trim())
300
+ if (!result?.isError)
299
301
  return null;
300
- return _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 1200) });
302
+ // This block and the badge tooltip stay mounted even when collapsed. Share the reader's cap
303
+ // and redactor so large failures cannot retain their full output in those alternate DOM sinks.
304
+ const detail = describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS });
305
+ return detail.outputText === null ? null : _jsx("pre", { className: "op-output", children: detail.outputText });
301
306
  }
302
307
  function describeInput(input) {
303
308
  if (input == null)
@@ -319,6 +324,14 @@ function describeInput(input) {
319
324
  return '';
320
325
  }
321
326
  }
327
+ /**
328
+ * A head's one-line summary of an arbitrary argument. Heads stay mounted for every collapsed row,
329
+ * and the fallback argument can be a 20k-char payload or a key, so it gets the reader text's
330
+ * order: redact first (a cut could split a key past its pattern), then cap.
331
+ */
332
+ function headSummary({ text }) {
333
+ return truncate(redactUserText({ text: text }).text, 200);
334
+ }
322
335
  function truncate(s, n) {
323
336
  if (s.length <= n)
324
337
  return s;
@@ -0,0 +1,20 @@
1
+ /** Read display context from a standalone form; its host may not have emitted a tool call. */
2
+ import { type UIResource } from '@jini-ai/ui/mcp-ui';
3
+ import type { AgentEvent } from '../../core/events.js';
4
+ import type { AcceptedInputProjectionOptions } from '../../core/accepted-input.js';
5
+ /** Feed recovered form context into the existing receipt projection, including after history reload.
6
+ * Grouping owns tool/surface association; this hook only supplies missing presentation context.
7
+ * @complexity O(e + h + c) for events, parsed HTML and controls; no mutation or I/O.
8
+ */
9
+ export declare function useSurfaceAnswerContexts({ events }: {
10
+ events: AgentEvent[] | undefined;
11
+ }, _options?: {}): AcceptedInputProjectionOptions;
12
+ /** Parse inert markup only: never mount the document, execute scripts or read callback secrets.
13
+ * @returns Title and offered labels in the existing question-answer input shape.
14
+ * @complexity O(h + c) time/space for HTML length h and choice controls c; no I/O.
15
+ */
16
+ export declare function surfaceQuestionInput({ resource, input }: {
17
+ resource: UIResource | undefined;
18
+ input: unknown;
19
+ }, _options?: {}): unknown;
20
+ //# sourceMappingURL=mcp-ui-question-input.d.ts.map
@@ -0,0 +1,72 @@
1
+ /** Read display context from a standalone form; its host may not have emitted a tool call. */
2
+ import { parseUIResource, readActionPlan } from '@jini-ai/ui/mcp-ui';
3
+ import { useMemo, useRef } from 'react';
4
+ import { SURFACE_QUESTION_META_KEY } from '../../core/question-answer.js';
5
+ import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
6
+ import { useExtEventRendererRegistry } from '../hooks/context.js';
7
+ /** Feed recovered form context into the existing receipt projection, including after history reload.
8
+ * Grouping owns tool/surface association; this hook only supplies missing presentation context.
9
+ * @complexity O(e + h + c) for events, parsed HTML and controls; no mutation or I/O.
10
+ */
11
+ export function useSurfaceAnswerContexts({ events }, _options = {}) {
12
+ const registry = useExtEventRendererRegistry();
13
+ const groups = useExtEventGroups({ events }, { slotOf: (name, data) => registry.slot({ name, data }) });
14
+ const cache = useRef(new Map());
15
+ return useMemo(() => ({ answerContexts: new Map(groups.flatMap(group => {
16
+ if (group.name !== 'mcp-ui' || group.call?.id === undefined)
17
+ return [];
18
+ // Retain the question that offered the values, even when a later document shows its outcome.
19
+ const data = group.events[0];
20
+ let cached = cache.current.get(data);
21
+ if (cached === undefined || cached.sourceInput !== group.call.input) {
22
+ const input = surfaceQuestionInput({ resource: parseUIResource(data), input: group.call.input });
23
+ cached = { sourceInput: group.call.input, context: { input, includeTitle: input !== group.call.input } };
24
+ cache.current.set(data, cached);
25
+ }
26
+ return [[group.call.id, cached.context]];
27
+ })) }), [groups]);
28
+ }
29
+ /** Parse inert markup only: never mount the document, execute scripts or read callback secrets.
30
+ * @returns Title and offered labels in the existing question-answer input shape.
31
+ * @complexity O(h + c) time/space for HTML length h and choice controls c; no I/O.
32
+ */
33
+ export function surfaceQuestionInput({ resource, input }, _options = {}) {
34
+ const context = surfaceQuestionContext({ resource });
35
+ if (hasQuestionOptions({ input: context }))
36
+ return context;
37
+ if (hasQuestionOptions({ input }))
38
+ return input;
39
+ if (resource === undefined || typeof DOMParser === 'undefined')
40
+ return input;
41
+ const { title, options } = markupQuestion({ resource });
42
+ if (!title && options.length === 0)
43
+ return input;
44
+ // The call's explicit question remains authoritative. Markup fills the standalone form's gap.
45
+ return { title, options, ...(typeof input === 'object' && input !== null ? input : {}) };
46
+ }
47
+ /** Read producer-supplied display context without parsing or executing the surface. O(1). */
48
+ function surfaceQuestionContext({ resource }, _options = {}) {
49
+ return resource?.resource._meta?.[SURFACE_QUESTION_META_KEY];
50
+ }
51
+ /** The title and offered choices an inert parse of the surface document shows. */
52
+ function markupQuestion({ resource }, _options = {}) {
53
+ const document = new DOMParser().parseFromString(resource.resource.text, 'text/html');
54
+ const title = readActionPlan(resource)?.title ?? document.querySelector('h1')?.textContent?.trim();
55
+ const labels = new Map([...document.querySelectorAll('label')].map(label => [label.htmlFor, label.textContent?.trim() ?? '']));
56
+ const options = [...document.querySelectorAll('input[type="radio"], input[type="checkbox"], option')].map(control => ({ value: control.value, label: optionText({ labels, control }) }));
57
+ return { title, options };
58
+ }
59
+ /** Explicit question options already belong to the shared answer mapper; no markup fallback needed. */
60
+ function hasQuestionOptions({ input }, _options = {}) {
61
+ if (typeof input !== 'object' || input === null)
62
+ return false;
63
+ const question = input;
64
+ return [question, question.singleSelect, question.multiSelect].some(group => typeof group === 'object' && group !== null && Array.isArray(group.options));
65
+ }
66
+ /** Resolve native label associations without interpolating caller-controlled ids into selectors. */
67
+ function optionText({ labels, control }, _options = {}) {
68
+ if (control.tagName === 'OPTION')
69
+ return control.textContent?.trim() ?? '';
70
+ return labels.get(control.id) ?? control.closest('label')?.textContent?.trim() ?? '';
71
+ }
72
+ //# sourceMappingURL=mcp-ui-question-input.js.map
@@ -41,6 +41,8 @@ export interface ExtEventRenderProps {
41
41
  events: readonly unknown[];
42
42
  /** Whether the owning run is still streaming — more events with this `name` may still arrive. */
43
43
  runStreaming: boolean;
44
+ /** The event is a user row alongside assistant sections, with ordinary transcript spacing. */
45
+ transcriptRow?: boolean;
44
46
  runSucceeded: boolean;
45
47
  /**
46
48
  * Whether the owning run is waiting on the person — a card (form, confirm, sign-in) sits on a
@@ -119,11 +119,18 @@ function ChatPaneTypedAnswerNotice({ typedAnswerNotice, t, }) {
119
119
  return null;
120
120
  return (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t(describeTypedAnswerNotice({ notice: typedAnswerNotice })) }));
121
121
  }
122
+ /** A send the server never accepted. Its text went back into the composer if that was still empty;
123
+ * Retry sends it again either way (see `useChatPane`'s `retrySend`). */
124
+ function ChatPaneSendError({ sendError, onRetry, t }) {
125
+ if (!sendError)
126
+ return null;
127
+ return (_jsxs("div", { className: "jini-chat-pane__error", role: "alert", children: [t('Not sent:'), " ", sendError.message, ' ', _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: onRetry, children: t('Retry') })] }));
128
+ }
122
129
  /** The pane's stack of mutually-independent error/status banners — each condition is its own
123
130
  * source of truth, so they are rendered as siblings rather than folded into one derived state. */
124
- function ChatPaneStatusMessages({ attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, ...availability }) {
131
+ function ChatPaneStatusMessages({ sendError, onRetrySend, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, ...availability }) {
125
132
  const { t } = availability;
126
- return (_jsxs(_Fragment, { children: [_jsx(ChatPaneAvailabilityStatus, { ...availability }), attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, _jsx(ChatPaneWorkingDirectoryStatus, { workingDirectoryPending: workingDirectoryPending, workingDirectoryInvalid: workingDirectoryInvalid, t: t }), runtimeInventoryError && !availability.connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, _jsx(ChatPaneTypedAnswerNotice, { typedAnswerNotice: typedAnswerNotice, t: t })] }));
133
+ return (_jsxs(_Fragment, { children: [_jsx(ChatPaneAvailabilityStatus, { ...availability }), _jsx(ChatPaneSendError, { sendError: sendError, onRetry: onRetrySend, t: t }), attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, _jsx(ChatPaneWorkingDirectoryStatus, { workingDirectoryPending: workingDirectoryPending, workingDirectoryInvalid: workingDirectoryInvalid, t: t }), runtimeInventoryError && !availability.connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, _jsx(ChatPaneTypedAnswerNotice, { typedAnswerNotice: typedAnswerNotice, t: t })] }));
127
134
  }
128
135
  function ChatPaneWorkingDirectoryBlock({ workingDirectoryAccess, workingDirectoryControlPlacement, pane, t, }) {
129
136
  if (workingDirectoryAccess && workingDirectoryControlPlacement === 'below') {
@@ -400,6 +407,6 @@ export function ChatPane(props) {
400
407
  const selectSuggestion = useChatPaneSuggestion({ rootRef, setDraft: pane.composer.setDraft });
401
408
  const openLinkInNewTab = useChatPaneNewTabLinks();
402
409
  const { pendingPrompts, projectFileNameSet } = useChatPaneTranscriptProps({ pane }, definedProps({ source: { projectFileNames: p.projectFileNames } }));
403
- return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(p.header, p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
410
+ return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(p.header, p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, sendError: pane.conversation.sendError, onRetrySend: () => void pane.retrySend(), attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
404
411
  }
405
412
  //# sourceMappingURL=ChatPane.js.map
@@ -82,6 +82,12 @@ export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
82
82
  * silent no-op.
83
83
  */
84
84
  sendPrompt: (prompt: string) => Promise<void>;
85
+ /**
86
+ * Sends the newest refused composer send again (see `conversation.sendError`). When that text is
87
+ * back in the composer it goes out from there like {@link send}; when something new was typed
88
+ * since, the refused text goes out on its own and the composer keeps the new typing.
89
+ */
90
+ retrySend: () => Promise<void>;
85
91
  /**
86
92
  * The prompt waiting for the in-flight run to finish, or `null` when nothing is queued. Exposed
87
93
  * so the pane can SHOW it — a queued turn that is invisible is indistinguishable from one that