@jini-ai/chat 0.6.15-rc.2 → 0.6.16

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 (126) hide show
  1. package/README.md +6 -1
  2. package/dist/core/accepted-input.js +20 -11
  3. package/dist/core/ag-ui/entry.js +3 -1
  4. package/dist/core/agent-plugin-chat-state.js +3 -1
  5. package/dist/core/antigravity/tool-presentation.d.ts +13 -0
  6. package/dist/core/antigravity/tool-presentation.js +20 -0
  7. package/dist/core/assistant-client/ag-ui-transport.js +14 -2
  8. package/dist/core/assistant-client/attachment-liveness.js +1 -1
  9. package/dist/core/assistant-client/durable-run-subscription.js +2 -1
  10. package/dist/core/assistant-client/transport.js +13 -7
  11. package/dist/core/assistant-content.js +2 -1
  12. package/dist/core/composer-history.js +2 -1
  13. package/dist/core/durable-projection.js +2 -1
  14. package/dist/core/durable-runs/continuation.js +2 -1
  15. package/dist/core/events.d.ts +5 -1
  16. package/dist/core/messages.d.ts +5 -1
  17. package/dist/core/partial-json.js +18 -9
  18. package/dist/core/persistence/title.d.ts +1 -1
  19. package/dist/core/persistence/title.js +25 -7
  20. package/dist/core/question-form/normalize.js +1 -1
  21. package/dist/core/question-form/scan.js +25 -15
  22. package/dist/core/question-form/secret-answers.js +2 -1
  23. package/dist/core/run-activity.js +8 -4
  24. package/dist/core/run-events/entry.js +6 -2
  25. package/dist/core/tool-events.d.ts +18 -8
  26. package/dist/core/tool-events.js +72 -16
  27. package/dist/core/tool-presentation.d.ts +14 -0
  28. package/dist/core/tool-presentation.js +8 -0
  29. package/dist/core/transcript.js +13 -6
  30. package/dist/core/turn-usage.js +7 -2
  31. package/dist/core/user-text-redaction.d.ts +4 -6
  32. package/dist/core/user-text-redaction.js +13 -6
  33. package/dist/core/util/manifest.js +13 -11
  34. package/dist/core/util/markdown-context.d.ts +14 -0
  35. package/dist/core/util/markdown-context.js +42 -4
  36. package/dist/core/util/markup-attributes.js +1 -1
  37. package/dist/core/util/parser.js +7 -7
  38. package/dist/core/util/pointer.js +22 -6
  39. package/dist/core/util/recover.js +5 -8
  40. package/dist/core/util/strip.js +12 -15
  41. package/dist/core/util/validate.js +30 -18
  42. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
  43. package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
  44. package/dist/embed-widget/browser/lazy-loader.js +16 -12
  45. package/dist/embed-widget/browser/session-store.d.ts +3 -9
  46. package/dist/embed-widget/browser/session-store.js +6 -38
  47. package/dist/embed-widget/react/dialog.hooks.js +2 -2
  48. package/dist/embed-widget/react/embed.d.ts +3 -10
  49. package/dist/embed-widget/react/embed.js +8 -52
  50. package/dist/embed-widget/react/header.hooks.js +1 -1
  51. package/dist/embed-widget/transports/fetch-sse.js +2 -1
  52. package/dist/embed-widget/vanilla/index.d.ts +0 -5
  53. package/dist/embed-widget/vanilla/index.js +23 -26
  54. package/dist/embed-widget/vanilla/state.d.ts +3 -11
  55. package/dist/embed-widget/vanilla/state.js +10 -39
  56. package/dist/embed-widget/vanilla/style.css +1 -1
  57. package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
  58. package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
  59. package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
  60. package/dist/react/components/ChatSettingsDialog.js +18 -9
  61. package/dist/react/components/Composer.js +5 -4
  62. package/dist/react/components/ConversationList.js +5 -4
  63. package/dist/react/components/Icon.d.ts +1 -1
  64. package/dist/react/components/Icon.js +1 -0
  65. package/dist/react/components/Markdown.d.ts +1 -0
  66. package/dist/react/components/Markdown.js +30 -27
  67. package/dist/react/components/McpUiSurfaceCard.js +2 -1
  68. package/dist/react/components/MessageList.d.ts +1 -1
  69. package/dist/react/components/MessageList.hooks.js +3 -2
  70. package/dist/react/components/MessageRow.d.ts +3 -0
  71. package/dist/react/components/MessageRow.js +32 -14
  72. package/dist/react/components/MessageRow.usage.js +3 -1
  73. package/dist/react/components/QuestionForm.js +4 -5
  74. package/dist/react/components/SentAttachment.js +4 -1
  75. package/dist/react/components/TodoCard.js +9 -1
  76. package/dist/react/components/ToolCard.d.ts +3 -1
  77. package/dist/react/components/ToolCard.js +84 -64
  78. package/dist/react/components/ToolCard.rules.d.ts +20 -0
  79. package/dist/react/components/ToolCard.rules.js +92 -0
  80. package/dist/react/components/composer-discovery.js +8 -8
  81. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
  82. package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
  83. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
  84. package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
  85. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
  86. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
  87. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
  88. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  89. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
  90. package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
  91. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
  92. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
  93. package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
  94. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
  95. package/dist/react/features/chat-pane/model-options.js +4 -2
  96. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  97. package/dist/react/features/chat-pane/styles.js +95 -5
  98. package/dist/react/features/chat-pane/types.d.ts +11 -0
  99. package/dist/react/hooks/assistant-chats.js +22 -8
  100. package/dist/react/hooks/useArtifactStream.js +2 -1
  101. package/dist/react/hooks/useComposer.js +2 -2
  102. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
  103. package/dist/react/hooks/useConversation.js +9 -4
  104. package/dist/react/hooks/useExtEventGroups.js +4 -2
  105. package/dist/react/hooks/useLatestOperation.js +2 -1
  106. package/dist/react/hooks/useQuestionForms.js +32 -22
  107. package/dist/react/hooks/useRunStream.js +4 -2
  108. package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
  109. package/dist/react/hooks/useUserTextGuard.js +3 -1
  110. package/dist/react/message-blocks.js +4 -2
  111. package/dist/react/styles/reference.css +9 -0
  112. package/dist/react/tool-call-display.d.ts +11 -0
  113. package/dist/react/tool-call-display.js +59 -2
  114. package/dist/server/durable-runs/recover.js +13 -5
  115. package/dist/server/run-finalizer/entry.js +1 -0
  116. package/dist/store/runtime/chat-history-store.js +9 -3
  117. package/dist/store/runtime/durable-run-store.js +34 -24
  118. package/dist/store/runtime/run-ledger.js +7 -4
  119. package/dist/store/sql/boundary.js +3 -1
  120. package/dist/store/sql/cursor.js +8 -3
  121. package/dist/store/sql/store.js +36 -28
  122. package/package.json +93 -88
  123. package/dist/core/browser/page-actions.d.ts +0 -121
  124. package/dist/core/browser/page-actions.js +0 -115
  125. package/dist/embed-widget/browser/page-actions.d.ts +0 -2
  126. package/dist/embed-widget/browser/page-actions.js +0 -3
@@ -74,7 +74,7 @@ function splitTableCells(line) {
74
74
  cells.push(tail);
75
75
  return cells;
76
76
  }
77
- const TABLE_ALIGN_CELL = /^:?-{1,}:?$/;
77
+ const TABLE_ALIGN_CELL = /^:?-+:?$/;
78
78
  // Every cell `TABLE_ALIGN_CELL` accepts starts and ends with `:` or `-`, so its
79
79
  // first + last character is always exactly one of these four keys.
80
80
  const COLUMN_ALIGN = { '::': 'center', '-:': 'right', ':-': 'left', '--': null };
@@ -117,7 +117,7 @@ function collectTableRows(lines, start) {
117
117
  }
118
118
  const ORDERED_ITEM = /^\s*\d+[.)]\s+/;
119
119
  const FENCE = /^```/;
120
- const HEADING = /^(#{1,3})\s+(.*)$/;
120
+ const HEADING = /^(#{1,3})\s+/;
121
121
  const RULE = /^(-{3,}|\*{3,}|_{3,})\s*$/;
122
122
  const QUOTE_LINE = /^>\s?/;
123
123
  const BULLET_ITEM = /^\s*[-*]\s+/;
@@ -139,11 +139,12 @@ function parseHeading(lines, i) {
139
139
  const heading = HEADING.exec(lines[i]);
140
140
  if (!heading)
141
141
  return null;
142
- // Both capture groups are mandatory (non-optional) in this pattern, so
143
- // whenever `heading` itself is truthy, `heading[1]`/`heading[2]` are
144
- // always matched strings (never `undefined`) — same dead-fallback
145
- // reasoning as the `lines[i]` accesses above.
146
- return { block: { kind: 'h', level: heading[1].length, text: heading[2] }, next: i + 1 };
142
+ const content = /^(.*)$/.exec(lines[i].slice(heading[0].length));
143
+ if (!content)
144
+ return null;
145
+ // Both captures are mandatory in their respective patterns, so each successful match has a
146
+ // defined group 1 — same dead-fallback reasoning as the `lines[i]` accesses above.
147
+ return { block: { kind: 'h', level: heading[1].length, text: content[1] }, next: i + 1 };
147
148
  }
148
149
  function parseRule(lines, i) {
149
150
  return RULE.test(lines[i]) ? { block: { kind: 'hr' }, next: i + 1 } : null;
@@ -268,13 +269,8 @@ function nextLooseListLine(lines, i) {
268
269
  }
269
270
  function isBlockStart(lines, i) {
270
271
  const line = lines[i];
271
- return (FENCE.test(line)
272
- || /^(#{1,3})\s+/.test(line)
273
- || RULE.test(line)
274
- || QUOTE_LINE.test(line)
275
- || BULLET_ITEM.test(line)
276
- || ORDERED_ITEM.test(line)
277
- || isTableStartAt(lines, i));
272
+ const simpleBlock = [FENCE, /^(#{1,3})\s+/, RULE, QUOTE_LINE, BULLET_ITEM, ORDERED_ITEM].some(pattern => pattern.test(line));
273
+ return simpleBlock || isTableStartAt(lines, i);
278
274
  }
279
275
  function renderBlock(block) {
280
276
  switch (block.kind) {
@@ -297,6 +293,7 @@ function renderBlock(block) {
297
293
  case 'hr':
298
294
  return _jsx("hr", {});
299
295
  }
296
+ return undefined;
300
297
  }
301
298
  // Fenced code block ('renderBlock' case 'code') plus its copy affordance. 'CopyCodeButton' below
302
299
  // sits in its own small toolbar row above the '<pre>', not overlaid on top of it: an overlay
@@ -343,7 +340,7 @@ function useTableOverflow() {
343
340
  // The wrap div renders unconditionally, so its ref is attached by the time this effect runs.
344
341
  const el = wrapRef.current;
345
342
  if (typeof ResizeObserver !== 'function')
346
- return;
343
+ return undefined;
347
344
  const check = () => setIsOverflowing(el.scrollWidth > el.clientWidth + TABLE_OVERFLOW_SLACK_PX);
348
345
  const observer = new ResizeObserver(check);
349
346
  observer.observe(el);
@@ -418,7 +415,7 @@ const MD_LINK_PARTS = /^\[([^\]\n]+)\]\((.+)\)$/u;
418
415
  const INLINE_RE = new RegExp(String.raw `(\x60[^\x60]+\x60)|(${MD_LINK})|(\*\*[^*]+\*\*)|(\*[^*]+\*|${UNDERSCORE_EM})|(https?:\/\/[^\s)]+)`, 'gu');
419
416
  // Only these targets become anchors; anything else (`javascript:`, `data:`, protocol-relative
420
417
  // `//host`) stays literal text, since the text is untrusted model output.
421
- const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
418
+ export const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
422
419
  // Every link opens in a new tab — an in-app path too, so following one never navigates the tab the
423
420
  // chat lives in (owner 2026-10-08). Only a `#` same-document anchor stays in place. ChatPane's
424
421
  // click guard (`useChatPaneNewTabLinks`) covers links this renderer does not draw.
@@ -441,17 +438,9 @@ function renderInline(text) {
441
438
  while (match !== null) {
442
439
  if (match.index > lastIndex)
443
440
  nodes.push(text.slice(lastIndex, match.index));
444
- const [whole, code, mdLink, bold, italic, link] = match;
445
- if (code)
446
- nodes.push(_jsx("code", { children: code.slice(1, -1) }, key++));
447
- else if (mdLink)
448
- nodes.push(renderMarkdownLink(mdLink, key++));
449
- else if (bold)
450
- nodes.push(_jsx("strong", { children: renderInline(bold.slice(2, -2)) }, key++));
451
- else if (italic)
452
- nodes.push(_jsx("em", { children: renderInline(italic.slice(1, -1)) }, key++));
453
- else if (link)
454
- nodes.push(_jsx("a", { href: link, ...NEW_TAB, children: link }, key++));
441
+ const whole = match[0];
442
+ nodes.push(renderInlineToken({ match, key }));
443
+ key += 1;
455
444
  // `whole` is `match[0]` — a successful `RegExpExecArray` always has a
456
445
  // defined (if possibly empty) full-match string at index 0, so this
457
446
  // fallback is unreachable (same dead-fallback reasoning as above).
@@ -462,4 +451,18 @@ function renderInline(text) {
462
451
  nodes.push(text.slice(lastIndex));
463
452
  return nodes;
464
453
  }
454
+ function renderInlineToken({ match, key }) {
455
+ const [, code, mdLink, bold, italic, link] = match;
456
+ if (code)
457
+ return _jsx("code", { children: code.slice(1, -1) }, key);
458
+ if (mdLink)
459
+ return renderMarkdownLink(mdLink, key);
460
+ if (bold)
461
+ return _jsx("strong", { children: renderInline(bold.slice(2, -2)) }, key);
462
+ if (italic)
463
+ return _jsx("em", { children: renderInline(italic.slice(1, -1)) }, key);
464
+ if (link)
465
+ return _jsx("a", { href: link, ...NEW_TAB, children: link }, key);
466
+ return null;
467
+ }
465
468
  //# sourceMappingURL=Markdown.js.map
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { trimChars } from '@jini-ai/core/primitives';
2
3
  /**
3
4
  * @module components/McpUiSurfaceCard
4
5
  *
@@ -98,7 +99,7 @@ function latestResourcesByUri(events) {
98
99
  function pendingMirrorHandle(uri) {
99
100
  // Never empty: only resources whose URI starts with `ui://` reach a card (`@jini-ai/ui/mcp-ui`'s
100
101
  // resource parser rejects every other scheme), so the sanitized handle always keeps at least `ui`.
101
- const sanitized = uri.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
102
+ const sanitized = trimChars({ text: uri.toLowerCase().replace(/[^a-z0-9]+/g, '-'), chars: '-' });
102
103
  return `mcp-ui-pending-${sanitized}`;
103
104
  }
104
105
  /** The one label a mirror region carries — what is pending, and what an agent could report or wait on. */
@@ -6,7 +6,7 @@ interface PendingPrompt {
6
6
  attachments?: ChatAttachment[];
7
7
  onCancel: () => void;
8
8
  }
9
- export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
9
+ export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking' | 'sendingLabel'> {
10
10
  messages: ChatMessage[];
11
11
  isStreaming?: boolean;
12
12
  /** Mirrors `useConversation().scrollIntent` — when `true`, this component scrolls to the bottom on mount/update. */
@@ -23,7 +23,8 @@ export function useConversationUsage({ messages }, _optional = {}) {
23
23
  if (old && JSON.stringify(old) === JSON.stringify(summary))
24
24
  next.set(id, old);
25
25
  }
26
- return previous.current = next;
26
+ previous.current = next;
27
+ return next;
27
28
  }, [messages]);
28
29
  }
29
30
  export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled, reducedMotion = prefersReducedMotion } = {}) {
@@ -86,7 +87,7 @@ export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolle
86
87
  useEffect(() => {
87
88
  const el = containerRef.current;
88
89
  if (!el || typeof ResizeObserver !== 'function')
89
- return;
90
+ return undefined;
90
91
  const observer = new ResizeObserver(() => {
91
92
  if (!stickToBottomRef.current)
92
93
  return;
@@ -73,6 +73,9 @@ export interface MessageRowProps {
73
73
  runStreaming?: boolean;
74
74
  /** Whether the server has yet to accept this reply's run (its user turn is still pending). */
75
75
  runSending?: boolean;
76
+ /** Host-localized text shown verbatim only while this run is active and sending.
77
+ * Omit to use the translated `Sending…` label. */
78
+ sendingLabel?: string;
76
79
  runSucceeded?: boolean;
77
80
  /** Whether this message's question-form (if any) is still the active/answerable one. */
78
81
  questionFormInteractive?: boolean;
@@ -71,7 +71,7 @@ import { Markdown } from './Markdown.js';
71
71
  import { ToolCard } from './ToolCard.js';
72
72
  import { QuestionForm } from './QuestionForm.js';
73
73
  import { ToolCallGroup } from './ToolCallGroup.js';
74
- import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
74
+ import { applyToolCallDisplay, retriedToolRowIds, toolRowsForDisplay } from '../tool-call-display.js';
75
75
  /**
76
76
  * True while an assistant message is still fully pending — no visible text
77
77
  * and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
@@ -140,6 +140,14 @@ function flatToolGroups({ message, rows, registry, displayBlocks }, { toolCallDi
140
140
  return flat.length > 0 ? [{ rows: flat, key: 'tools' }] : [];
141
141
  }
142
142
  /** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
143
+ /** A host status belongs to the run that emitted it, regardless of the current picker selection. */
144
+ function RunAccessNotice({ events }) {
145
+ const t = useT();
146
+ const notice = events?.find(event => event.kind === 'status' && event.label === 'full_cli_access');
147
+ if (notice?.kind !== 'status' || !notice.detail)
148
+ return null;
149
+ return _jsx("div", { className: "jini-chat-pane__notice", role: "note", children: t(notice.detail) });
150
+ }
143
151
  function ThinkingBlock({ text }) {
144
152
  const t = useT();
145
153
  if (!text.trim())
@@ -149,9 +157,9 @@ function ThinkingBlock({ text }) {
149
157
  function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
150
158
  return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
151
159
  }
152
- function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard }, _optional = {}) {
160
+ function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, sendingLabel, visibleContent, isAnsweredCard }, _optional = {}) {
153
161
  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, sending: runSending }) : null] });
162
+ 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, sendingLabel: sendingLabel }) : null] });
155
163
  }
156
164
  /** Stable empty list for a message with no attachments. */
157
165
  const NO_ATTACHMENTS = [];
@@ -178,7 +186,7 @@ function UserMessageRow({ message, renderAttachment }) {
178
186
  const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
179
187
  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] }));
180
188
  }
181
- function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
189
+ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
182
190
  const registry = useExtEventRendererRegistry();
183
191
  const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
184
192
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
@@ -198,7 +206,8 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
198
206
  });
199
207
  const footerUsage = showUsageLine ? usage.footer : undefined;
200
208
  const lastFinishedReply = !runInProgress && !usage.footer ? usage.replies.findLastIndex(value => value !== undefined) : -1;
201
- const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
209
+ const retriedRows = retriedToolRowIds({ rows: timeline.rows });
210
+ const renderToolCard = (row) => (_jsx(ToolCard, { retried: retriedRows.has(row.id), use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
202
211
  // Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
203
212
  const renderTools = (rows, key) => (toolCallDisplay !== 'show'
204
213
  ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
@@ -235,7 +244,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
235
244
  // arrives for it, instead of staying tombstoned for the message's whole lifetime.
236
245
  _jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
237
246
  };
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}`)));
247
+ const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => {
248
+ if (segment.kind === 'text')
249
+ return segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null;
250
+ return (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`));
251
+ });
239
252
  const renderBlock = (block) => {
240
253
  if (block.kind === 'text') {
241
254
  return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
@@ -243,7 +256,9 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
243
256
  if (block.kind === 'reply-end') {
244
257
  const reply = usage.replies[block.index];
245
258
  // A trailing input may have no reply: its predecessor's usage belongs above that input.
246
- return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key) : null;
259
+ if (!reply || !showUsageLine)
260
+ return null;
261
+ return _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key);
247
262
  }
248
263
  if (block.kind === 'tools')
249
264
  return renderTools(block.rows, block.key);
@@ -260,21 +275,23 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
260
275
  return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
261
276
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
262
277
  };
278
+ // The live footer follows the run as transcript sections split; its clock must not remount.
263
279
  const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
264
280
  ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
265
281
  : '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
282
  ? 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));
283
+ : (_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, sendingLabel, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, footer ? message.id : key));
268
284
  // The lossless fallback keeps the host's text intact; user receipts still own separate rows,
269
285
  // in their original receipt order, outside the assistant's actions and usage footer.
286
+ // Keep the same child-array nesting as the interleaved path so React preserves the footer.
270
287
  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)] });
288
+ return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), [renderAssistantSection({ section: null, key: message.id, footer: true })], extGroups.filter(group => group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME).map(renderExtGroup)] });
272
289
  // A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
273
290
  const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
274
291
  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 })) });
292
+ return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
293
+ ? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
294
+ : renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex }))] });
278
295
  }
279
296
  /** Preserve optional render-prop presence for custom extension renderers. */
280
297
  function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
@@ -302,13 +319,14 @@ function isUserSection({ section }, _options = {}) {
302
319
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
303
320
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
304
321
  * Kept a separate component so its one-second tick re-renders only this line, not the whole row.
322
+ * A host's already-localized sending label is used verbatim until acceptance.
305
323
  */
306
- function RunActivityLine({ events, active, isAnsweredCard, sending }) {
324
+ function RunActivityLine({ events, active, isAnsweredCard, sending, sendingLabel }) {
307
325
  const t = useT();
308
326
  const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
309
327
  // Until the server accepts the start the turn is still on its way: "Thinking…" there read as the
310
328
  // 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;
329
+ const label = active && sending ? sendingLabel ?? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
312
330
  if (label === null)
313
331
  return null;
314
332
  return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
@@ -44,7 +44,9 @@ export function usageLayout({ usage, interleaved, runInProgress }, { runStreamin
44
44
  * every keystroke restyle the whole page in long chats (2026-10-11). O(1).
45
45
  * @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
46
46
  export function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }, { showTotal = true } = {}) {
47
- const hasText = footer !== undefined && (footer.durationMs !== undefined || footer.costUsd !== undefined || (showTotal && footer.totalCostUsd !== undefined));
47
+ if (footer === undefined)
48
+ return false;
49
+ const hasText = footer.durationMs !== undefined || footer.costUsd !== undefined || (showTotal && footer.totalCostUsd !== undefined);
48
50
  return hasText && !terminalNotice && !pending && !runInProgress;
49
51
  }
50
52
  /** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
@@ -177,8 +177,8 @@ function DirectionCardsControl(ctx) {
177
177
  function TextControl({ q, value, locked, update, t }) {
178
178
  const shared = { className: 'qf-input', value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) };
179
179
  const props = questionTextInputProps({ question: q });
180
- if (props.type === 'password') {
181
- const { type: _type, ...secretProps } = props;
180
+ const { type, ...secretProps } = props;
181
+ if (type === 'password') {
182
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
183
  }
184
184
  return _jsx("input", { ...props, ...shared });
@@ -271,9 +271,8 @@ function collectFileSubmissions(form, fileAnswers) {
271
271
  if (q.type !== 'file')
272
272
  continue;
273
273
  const files = fileAnswers[q.id] ?? [];
274
- if (files.length === 0)
275
- continue;
276
- out.push({ questionId: q.id, questionLabel: q.label, files });
274
+ if (files.length > 0)
275
+ out.push({ questionId: q.id, questionLabel: q.label, files });
277
276
  }
278
277
  return out;
279
278
  }
@@ -9,6 +9,9 @@ import { useSentAttachmentThumbnail } from './SentAttachment.hooks.js';
9
9
  export function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }) {
10
10
  const t = useT();
11
11
  const { thumbnailUrl, onThumbnailError } = useSentAttachmentThumbnail({ attachment, hostRendered: renderAttachment !== undefined }, objectUrls === undefined ? {} : { objectUrls });
12
- return (_jsx("button", { type: "button", className: `jini-message-attachment-chip${thumbnailUrl ? ' is-thumbnail' : ''}`, onClick: () => onOpen(attachment), title: t('Open {name}', { name: attachment.name }), "aria-label": t('Open {name}', { name: attachment.name }), children: renderAttachment ? (renderAttachment(attachment)) : thumbnailUrl ? (_jsx("img", { className: "jini-message-attachment-thumb", src: thumbnailUrl, alt: "", onError: onThumbnailError })) : (attachment.name) }));
12
+ const fallback = thumbnailUrl
13
+ ? _jsx("img", { className: "jini-message-attachment-thumb", src: thumbnailUrl, alt: "", onError: onThumbnailError })
14
+ : attachment.name;
15
+ return (_jsx("button", { type: "button", className: `jini-message-attachment-chip${thumbnailUrl ? ' is-thumbnail' : ''}`, onClick: () => onOpen(attachment), title: t('Open {name}', { name: attachment.name }), "aria-label": t('Open {name}', { name: attachment.name }), children: renderAttachment ? renderAttachment(attachment) : fallback }));
13
16
  }
14
17
  //# sourceMappingURL=SentAttachment.js.map
@@ -18,7 +18,7 @@ export function TodoCard({ todos, runStreaming, onDismiss }) {
18
18
  const [overrideExpanded, setOverrideExpanded] = useState(null);
19
19
  const expanded = overrideExpanded ?? defaultExpanded;
20
20
  const showDismiss = Boolean(onDismiss) && allComplete;
21
- return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: todoLabel({ todo: inProgressTodo }) }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children: todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '◐' : todo.status === 'stopped' ? '!' : '○' }), _jsx("span", { className: "todo-text", children: todoLabel({ todo }) })] }, i))) }) }) })] }));
21
+ return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: todoLabel({ todo: inProgressTodo }) }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children: todoStatusGlyph({ status: todo.status }) }), _jsx("span", { className: "todo-text", children: todoLabel({ todo }) })] }, i))) }) }) })] }));
22
22
  }
23
23
  function todoProgress({ todos }, { runStreaming = false } = {}) {
24
24
  const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
@@ -32,4 +32,12 @@ function todoProgress({ todos }, { runStreaming = false } = {}) {
32
32
  function todoLabel({ todo }, _options = {}) {
33
33
  return todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content;
34
34
  }
35
+ function todoStatusGlyph({ status }) {
36
+ switch (status) {
37
+ case 'completed': return '✓';
38
+ case 'in_progress': return '◐';
39
+ case 'stopped': return '!';
40
+ default: return '○';
41
+ }
42
+ }
35
43
  //# sourceMappingURL=TodoCard.js.map
@@ -16,7 +16,9 @@ export interface ToolCardProps {
16
16
  projectFileNames?: Set<string>;
17
17
  /** Lifts a basename up to the host so it can focus the matching tab in its own file viewer. */
18
18
  onRequestOpenFile?: (name: string) => void;
19
+ /** A later call of the same tool in this turn succeeded (`retriedToolRowIds`): a failure shows a neutral "Retried" badge, not red. */
20
+ retried?: boolean;
19
21
  }
20
- export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }: ToolCardProps): import("react").JSX.Element | null;
22
+ export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile, retried }: ToolCardProps): import("react").JSX.Element | null;
21
23
  export {};
22
24
  //# sourceMappingURL=ToolCard.d.ts.map