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

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 (91) hide show
  1. package/dist/core/accepted-input.js +20 -11
  2. package/dist/core/ag-ui/entry.js +3 -1
  3. package/dist/core/agent-plugin-chat-state.js +3 -1
  4. package/dist/core/assistant-client/ag-ui-transport.js +1 -1
  5. package/dist/core/assistant-client/attachment-liveness.js +1 -1
  6. package/dist/core/assistant-client/durable-run-subscription.js +2 -1
  7. package/dist/core/assistant-client/transport.js +13 -7
  8. package/dist/core/assistant-content.js +2 -1
  9. package/dist/core/composer-history.js +2 -1
  10. package/dist/core/durable-projection.js +2 -1
  11. package/dist/core/durable-runs/continuation.js +2 -1
  12. package/dist/core/messages.d.ts +5 -1
  13. package/dist/core/partial-json.js +18 -9
  14. package/dist/core/persistence/title.d.ts +1 -1
  15. package/dist/core/persistence/title.js +25 -7
  16. package/dist/core/question-form/normalize.js +1 -1
  17. package/dist/core/question-form/scan.js +25 -15
  18. package/dist/core/question-form/secret-answers.js +2 -1
  19. package/dist/core/run-activity.js +8 -4
  20. package/dist/core/run-events/entry.js +6 -2
  21. package/dist/core/transcript.js +13 -6
  22. package/dist/core/turn-usage.js +7 -2
  23. package/dist/core/user-text-redaction.d.ts +2 -5
  24. package/dist/core/user-text-redaction.js +3 -3
  25. package/dist/core/util/manifest.js +13 -11
  26. package/dist/core/util/markdown-context.d.ts +14 -0
  27. package/dist/core/util/markdown-context.js +42 -4
  28. package/dist/core/util/markup-attributes.js +1 -1
  29. package/dist/core/util/parser.js +7 -7
  30. package/dist/core/util/pointer.js +22 -6
  31. package/dist/core/util/recover.js +5 -8
  32. package/dist/core/util/strip.js +12 -15
  33. package/dist/core/util/validate.js +30 -18
  34. package/dist/embed-widget/browser/lazy-loader.js +16 -12
  35. package/dist/embed-widget/react/dialog.hooks.js +2 -2
  36. package/dist/embed-widget/react/embed.js +1 -1
  37. package/dist/embed-widget/react/header.hooks.js +1 -1
  38. package/dist/embed-widget/transports/fetch-sse.js +2 -1
  39. package/dist/embed-widget/vanilla/index.js +19 -12
  40. package/dist/embed-widget/vanilla/state.js +4 -2
  41. package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
  42. package/dist/react/components/ChatSettingsDialog.js +18 -9
  43. package/dist/react/components/Composer.js +5 -4
  44. package/dist/react/components/ConversationList.js +5 -4
  45. package/dist/react/components/Markdown.js +29 -26
  46. package/dist/react/components/McpUiSurfaceCard.js +2 -1
  47. package/dist/react/components/MessageList.hooks.js +3 -2
  48. package/dist/react/components/MessageRow.js +23 -7
  49. package/dist/react/components/MessageRow.usage.js +3 -1
  50. package/dist/react/components/QuestionForm.js +4 -5
  51. package/dist/react/components/SentAttachment.js +4 -1
  52. package/dist/react/components/TodoCard.js +9 -1
  53. package/dist/react/components/ToolCard.js +8 -7
  54. package/dist/react/components/composer-discovery.js +8 -8
  55. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
  56. package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
  57. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +15 -5
  58. package/dist/react/features/chat-pane/components/ChatPane.js +4 -2
  59. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
  60. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
  61. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
  62. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +4 -2
  63. package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
  64. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
  65. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
  66. package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
  67. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
  68. package/dist/react/features/chat-pane/model-options.js +4 -2
  69. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  70. package/dist/react/features/chat-pane/styles.js +26 -3
  71. package/dist/react/features/chat-pane/types.d.ts +8 -0
  72. package/dist/react/hooks/assistant-chats.js +22 -8
  73. package/dist/react/hooks/useArtifactStream.js +2 -1
  74. package/dist/react/hooks/useComposer.js +2 -2
  75. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
  76. package/dist/react/hooks/useConversation.js +9 -4
  77. package/dist/react/hooks/useExtEventGroups.js +4 -2
  78. package/dist/react/hooks/useLatestOperation.js +2 -1
  79. package/dist/react/hooks/useQuestionForms.js +32 -22
  80. package/dist/react/hooks/useRunStream.js +4 -2
  81. package/dist/react/message-blocks.js +4 -2
  82. package/dist/react/styles/reference.css +9 -0
  83. package/dist/server/durable-runs/recover.js +13 -5
  84. package/dist/server/run-finalizer/entry.js +1 -0
  85. package/dist/store/runtime/chat-history-store.js +9 -3
  86. package/dist/store/runtime/durable-run-store.js +34 -24
  87. package/dist/store/runtime/run-ledger.js +7 -4
  88. package/dist/store/sql/boundary.js +3 -1
  89. package/dist/store/sql/cursor.js +8 -3
  90. package/dist/store/sql/store.js +36 -28
  91. package/package.json +15 -15
@@ -1,3 +1,4 @@
1
+ const ARIA_LABEL = 'aria-label';
1
2
  import { CHAT_BUBBLE_STYLE } from '../../contracts/chat-bubble-style.js';
2
3
  import { createVanillaChatController } from './state.js';
3
4
  import { isQueuedPageAction } from '../../core/browser/page-actions.js';
@@ -37,7 +38,7 @@ export function mountVanillaChat(args, options = {}) {
37
38
  const shell = node('div', 'jini-chat-vanilla');
38
39
  const fab = button('');
39
40
  fab.className = 'jini-chat-bubble';
40
- fab.setAttribute('aria-label', copy.fabLabel);
41
+ fab.setAttribute(ARIA_LABEL, copy.fabLabel);
41
42
  fab.setAttribute('aria-haspopup', 'dialog');
42
43
  fab.append(svgIcon({ document, path: 'M4 4h16v12H9l-5 4V4z' }));
43
44
  const panel = node('section', 'jini-chat-vanilla__panel');
@@ -45,12 +46,12 @@ export function mountVanillaChat(args, options = {}) {
45
46
  fab.setAttribute('aria-controls', panel.id);
46
47
  panel.setAttribute('role', 'dialog');
47
48
  panel.setAttribute('aria-modal', 'false');
48
- panel.setAttribute('aria-label', copy.title);
49
+ panel.setAttribute(ARIA_LABEL, copy.title);
49
50
  const header = node('header', 'jini-chat-vanilla__header');
50
51
  const title = node('strong', 'jini-chat-vanilla__title', copy.title);
51
52
  const reset = button(copy.reset);
52
53
  const close = button('');
53
- close.setAttribute('aria-label', copy.close);
54
+ close.setAttribute(ARIA_LABEL, copy.close);
54
55
  close.append(svgIcon({ document, path: 'M6 6l12 12M18 6L6 18' }));
55
56
  header.append(title, reset, close);
56
57
  const confirmation = node('div', 'jini-chat-vanilla__confirmation');
@@ -63,7 +64,7 @@ export function mountVanillaChat(args, options = {}) {
63
64
  log.setAttribute('role', 'log');
64
65
  log.setAttribute('aria-live', 'off');
65
66
  log.setAttribute('aria-relevant', 'additions text');
66
- log.setAttribute('aria-label', copy.title);
67
+ log.setAttribute(ARIA_LABEL, copy.title);
67
68
  log.tabIndex = 0;
68
69
  const status = node('div', 'jini-chat-vanilla__status');
69
70
  status.setAttribute('role', 'status');
@@ -81,20 +82,20 @@ export function mountVanillaChat(args, options = {}) {
81
82
  const form = node('form', 'jini-chat-vanilla__composer');
82
83
  const input = node('textarea', 'jini-chat-vanilla__input');
83
84
  input.placeholder = copy.placeholder;
84
- input.setAttribute('aria-label', copy.placeholder);
85
+ input.setAttribute(ARIA_LABEL, copy.placeholder);
85
86
  input.maxLength = 8000;
86
87
  input.rows = 2;
87
88
  const send = button(copy.send);
88
89
  send.type = 'submit';
89
90
  send.classList.add('jini-chat-vanilla__send');
90
- send.setAttribute('aria-label', copy.send);
91
+ send.setAttribute(ARIA_LABEL, copy.send);
91
92
  send.replaceChildren(svgIcon({ document, path: 'M5 4l15 8-15 8v-6l9-2-9-2V4z' }));
92
93
  const stop = button(copy.stop);
93
94
  form.append(input, send, stop);
94
95
  const greeting = node('p', 'jini-chat-vanilla__greeting', copy.greeting);
95
96
  const suggestions = node('div', 'jini-chat-vanilla__suggestions');
96
97
  if (copy.suggestionsLabel)
97
- suggestions.setAttribute('aria-label', copy.suggestionsLabel);
98
+ suggestions.setAttribute(ARIA_LABEL, copy.suggestionsLabel);
98
99
  const suggestionEntries = (args.suggestions ?? []).map(text => ({ text, button: button(text) }));
99
100
  const suggestionButtons = suggestionEntries.map(entry => entry.button);
100
101
  suggestions.append(...suggestionButtons);
@@ -180,11 +181,15 @@ export function mountVanillaChat(args, options = {}) {
180
181
  const moveFocus = ({ state, composerHadFocus, stopHadFocus }) => {
181
182
  if (!state.open)
182
183
  return;
183
- if (!wasOpen && initialized)
184
+ if (!wasOpen && initialized) {
184
185
  focusEntry({ busy: state.busy });
185
- else if (state.busy && composerHadFocus)
186
+ return;
187
+ }
188
+ if (state.busy && composerHadFocus) {
186
189
  stop.focus();
187
- else if (!state.busy && stopHadFocus)
190
+ return;
191
+ }
192
+ if (!state.busy && stopHadFocus)
188
193
  input.focus();
189
194
  };
190
195
  const renderFocus = ({ state, composerHadFocus, stopHadFocus, panelHadFocus }) => {
@@ -295,9 +300,11 @@ export function mountVanillaChat(args, options = {}) {
295
300
  if (!controller.getState({}).open || !panel.contains(document.activeElement))
296
301
  return;
297
302
  const key = event;
298
- if (key.key === 'Escape')
303
+ if (key.key === 'Escape') {
299
304
  onEscape({ key });
300
- else if (key.key === 'Tab')
305
+ return;
306
+ }
307
+ if (key.key === 'Tab')
301
308
  trapTab({ key });
302
309
  });
303
310
  listen(element, 'jini-chat-open', () => {
@@ -51,7 +51,8 @@ export function createVanillaChatController(args, _options = {}) {
51
51
  const content = text.trim();
52
52
  if (!content || state.busy)
53
53
  return false;
54
- const token = ++generation;
54
+ generation += 1;
55
+ const token = generation;
55
56
  const abort = args.createAbortController({});
56
57
  active = abort;
57
58
  const history = [...state.messages, { id: args.ids({}), role: 'user', content }];
@@ -80,8 +81,9 @@ export function createVanillaChatController(args, _options = {}) {
80
81
  active = null;
81
82
  events = final;
82
83
  state = { ...state, busy: false };
84
+ const completedStatus = abort.signal.aborted ? 'canceled' : 'succeeded';
83
85
  updateReply({ ...reply, content: assistantContentFromEvents({ events }), events: [...events],
84
- runStatus: failed ? 'failed' : abort.signal.aborted ? 'canceled' : 'succeeded' }, { persist: true });
86
+ runStatus: failed ? 'failed' : completedStatus }, { persist: true });
85
87
  if (!failed && !abort.signal.aborted)
86
88
  settleActions(events);
87
89
  };
@@ -154,7 +154,8 @@ export function useAttachmentPreviewModal({ attachment, onClose, navigation }, {
154
154
  const cachedFile = getAttachmentPreviewSource({ path: attachment.path });
155
155
  const attemptImage = !imageFailed && cachedFile !== undefined && looksLikeImageAttachment({ attachment: attachment }) && canCreateObjectUrl();
156
156
  const attemptText = !attemptImage && cachedFile !== undefined && looksLikeTextAttachment(attachment.name);
157
- const status = attemptImage ? 'image' : attemptText ? 'text' : 'unsupported';
157
+ const fallbackStatus = attemptText ? 'text' : 'unsupported';
158
+ const status = attemptImage ? 'image' : fallbackStatus;
158
159
  // Object URL: created/revoked in an effect (not `useMemo`) so a React 18 Strict Mode double-render
159
160
  // can never create two URLs and revoke only one — the cleanup below always pairs with the create
160
161
  // that produced the URL currently in state.
@@ -24,7 +24,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
24
24
  useLayoutEffect(() => {
25
25
  const dialog = dialogRef.current;
26
26
  if (!dialog || !required.open)
27
- return;
27
+ return undefined;
28
28
  openerRef.current = document.activeElement;
29
29
  if (typeof dialog.showModal === 'function') {
30
30
  if (!dialog.open)
@@ -47,7 +47,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
47
47
  // layout cleanups and would pull focus straight back to the closing dialog's × button.
48
48
  useEffect(() => {
49
49
  if (!required.open)
50
- return;
50
+ return undefined;
51
51
  return () => {
52
52
  const opener = openerRef.current;
53
53
  if (opener instanceof HTMLElement && opener.isConnected)
@@ -60,13 +60,22 @@ export function ChatSettingsDialog(required, _optional = {}) {
60
60
  if (event.target !== event.currentTarget)
61
61
  return;
62
62
  const { clientX: x, clientY: y } = event, box = event.currentTarget.getBoundingClientRect();
63
- if ((x === 0 && y === 0) || x < box.left || x > box.right || y < box.top || y > box.bottom)
63
+ const horizontalOutside = x < box.left || x > box.right;
64
+ const verticalOutside = y < box.top || y > box.bottom;
65
+ if ((x === 0 && y === 0) || horizontalOutside || verticalOutside)
64
66
  required.onClose();
65
- }, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(section => section.radio
66
- // One ⓘ for the whole choice, beside the legend (owner, 2026-10-18), not one per option. The
67
- // fieldset is named by the label span alone, so the tip's text never joins the group's name.
68
- ? _jsxs("fieldset", { className: "jini-chat-settings-group", "aria-labelledby": section.label ? section.labelId : undefined, "aria-describedby": [section.hint ? section.hintId : '', section.tooltip ? section.tooltipId : ''].filter(Boolean).join(' ') || undefined, children: [section.label && _jsxs("legend", { className: "jini-chat-settings-legend", children: [_jsx("span", { id: section.labelId, children: section.label }), section.tooltip && _jsx(InfoTip, { label: section.tooltip, descriptionId: section.tooltipId })] }), section.hint && _jsx("p", { id: section.hintId, className: "jini-chat-settings-description jini-chat-settings-description--group", children: section.hint }), _jsx(ChatSettingsSegmented, { name: section.name, items: section.items })] }, section.key)
69
- : _jsxs("fieldset", { className: "jini-chat-settings-group", children: [section.label && _jsx("legend", { className: "jini-chat-settings-legend", children: section.label }), section.items.map(item => _jsx(SettingSwitch, { label: item.label, checked: !!item.checked, disabled: item.disabled, onChange: item.change, description: item.hint, descriptionId: item.hintId, tooltip: item.tooltip }, item.key))] }, section.key))
70
- : _jsxs(_Fragment, { children: [_jsx("p", { className: "jini-chat-settings-note", children: vm.t('Permission changes apply to the next run.') }), vm.permissions.unavailableReason && _jsx("p", { className: "jini-chat-settings-note", children: vm.t(vm.permissions.unavailableReason) }), _jsx(SettingSwitch, { className: "jini-chat-settings-row--master", label: vm.master.label, checked: vm.master.checked, disabled: vm.master.disabled, onChange: checked => vm.master.onCheckedChange({ checked }), description: vm.master.description, descriptionId: vm.master.descriptionId }), _jsx("div", { className: "jini-chat-settings-capabilities", children: vm.capabilities.map(item => _jsx(SettingSwitch, { label: item.control.label, checked: item.control.checked, disabled: item.control.disabled, onChange: checked => item.control.onCheckedChange({ checked }), description: item.description, descriptionId: item.descriptionId }, item.key)) }), vm.permissions.busy && _jsx("p", { role: "status", className: "jini-chat-settings-note", children: vm.t('Saving…') }), vm.permissions.error && _jsx("p", { role: "alert", className: "jini-chat-settings-error", children: vm.permissions.error })] }) })] });
67
+ }, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(renderDisplaySection)
68
+ : renderPermissions(vm) })] });
69
+ }
70
+ function renderDisplaySection(section) {
71
+ if (section.radio)
72
+ return (
73
+ // One ⓘ for the whole choice, beside the legend (owner, 2026-10-18), not one per option. The
74
+ // fieldset is named by the label span alone, so the tip's text never joins the group's name.
75
+ _jsxs("fieldset", { className: "jini-chat-settings-group", "aria-labelledby": section.label ? section.labelId : undefined, "aria-describedby": [section.hint ? section.hintId : '', section.tooltip ? section.tooltipId : ''].filter(Boolean).join(' ') || undefined, children: [section.label && _jsxs("legend", { className: "jini-chat-settings-legend", children: [_jsx("span", { id: section.labelId, children: section.label }), section.tooltip && _jsx(InfoTip, { label: section.tooltip, descriptionId: section.tooltipId })] }), section.hint && _jsx("p", { id: section.hintId, className: "jini-chat-settings-description jini-chat-settings-description--group", children: section.hint }), _jsx(ChatSettingsSegmented, { name: section.name, items: section.items })] }, section.key));
76
+ return _jsxs("fieldset", { className: "jini-chat-settings-group", children: [section.label && _jsx("legend", { className: "jini-chat-settings-legend", children: section.label }), section.items.map(item => _jsx(SettingSwitch, { label: item.label, checked: !!item.checked, disabled: item.disabled, onChange: item.change, description: item.hint, descriptionId: item.hintId, tooltip: item.tooltip }, item.key))] }, section.key);
77
+ }
78
+ function renderPermissions(vm) {
79
+ return _jsxs(_Fragment, { children: [_jsx("p", { className: "jini-chat-settings-note", children: vm.t('Permission changes apply to the next run.') }), vm.permissions.unavailableReason && _jsx("p", { className: "jini-chat-settings-note", children: vm.t(vm.permissions.unavailableReason) }), _jsx(SettingSwitch, { className: "jini-chat-settings-row--master", label: vm.master.label, checked: vm.master.checked, disabled: vm.master.disabled, onChange: checked => vm.master.onCheckedChange({ checked }), description: vm.master.description, descriptionId: vm.master.descriptionId }), _jsx("div", { className: "jini-chat-settings-capabilities", children: vm.capabilities.map(item => _jsx(SettingSwitch, { label: item.control.label, checked: item.control.checked, disabled: item.control.disabled, onChange: checked => item.control.onCheckedChange({ checked }), description: item.description, descriptionId: item.descriptionId }, item.key)) }), vm.permissions.busy && _jsx("p", { role: "status", className: "jini-chat-settings-note", children: vm.t('Saving…') }), vm.permissions.error && _jsx("p", { role: "alert", className: "jini-chat-settings-error", children: vm.permissions.error })] });
71
80
  }
72
81
  //# sourceMappingURL=ChatSettingsDialog.js.map
@@ -114,11 +114,11 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
114
114
  */
115
115
  useEffect(() => {
116
116
  if (!anyPopoverOpen(popovers))
117
- return;
117
+ return undefined;
118
118
  const textarea = textareaRef.current;
119
119
  const composerRoot = textarea?.closest('.jini-composer');
120
120
  if (!composerRoot)
121
- return;
121
+ return undefined;
122
122
  function handleOutsideMouseDown(event) {
123
123
  const target = event.target;
124
124
  if (!target)
@@ -327,7 +327,7 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
327
327
  }, onKeyDown: handleKeyDown,
328
328
  // Draft persistence is debounced (see `DRAFT_PERSIST_DEBOUNCE_MS`); leaving the field is a
329
329
  // natural point to make it durable now.
330
- onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] }));
330
+ onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsxs("div", { className: "jini-composer-footer-trailing", children: [_jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] })] }));
331
331
  }
332
332
  function anyPopoverOpen({ discoveryMenuOpen, slashOpen, workdirOpen }) {
333
333
  return discoveryMenuOpen || slashOpen || workdirOpen;
@@ -380,8 +380,9 @@ function ComposerWorkdirControl({ workingDirectory, onChangeWorkingDirectory, on
380
380
  if (e.key === 'Enter') {
381
381
  e.preventDefault();
382
382
  onConfirm();
383
+ return;
383
384
  }
384
- else if (e.key === 'Escape') {
385
+ if (e.key === 'Escape') {
385
386
  e.preventDefault();
386
387
  onClose();
387
388
  }
@@ -105,7 +105,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
105
105
  // nothing on every document event.
106
106
  useEffect(() => {
107
107
  if (!open)
108
- return;
108
+ return undefined;
109
109
  // The root div is committed before any effect runs, so it is never null here. Capturing it
110
110
  // (rather than reading the ref per event) keeps a late event during unmount, after React has
111
111
  // detached the ref but before this cleanup runs, measured against the real node.
@@ -135,10 +135,10 @@ export function ConversationList({ conversations: savedConversations, activeConv
135
135
  }, [open]);
136
136
  useEffect(() => {
137
137
  if (!onSearch)
138
- return;
138
+ return undefined;
139
139
  if (!deferredQuery) {
140
140
  setServerResults(null);
141
- return;
141
+ return undefined;
142
142
  }
143
143
  // `cancelled` guards the classic out-of-order response: a slow query for "a" resolving after
144
144
  // a fast one for "abc" would otherwise overwrite the newer, correct results.
@@ -190,6 +190,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
190
190
  });
191
191
  }, [confirmDelete, onDelete, t]);
192
192
  const active = conversations.find((c) => c.id === activeConversationId) ?? null;
193
+ const emptyLabel = () => conversations.length === 0 ? emptyState ?? t('No conversations yet') : t('No conversations match');
193
194
  return (_jsxs("div", { className: "jini-conv", ref: rootRef, children: [_jsx("button", { type: "button", className: `jini-conv-trigger${open ? ' jini-conv-trigger-open' : ''}`, "aria-haspopup": "menu", "aria-expanded": open, ...conversationTriggerAttributes({ activeTitle: active?.title, t }, { label: triggerLabel, nativeTitle }), "data-testid": "conversation-trigger", onClick: () => setOpen((v) => !v), children: _jsx(Icon, { name: "comment", size: 16 }) }), open ? (_jsxs("div", { className: "jini-conv-menu", role: "menu", "data-testid": "conversation-menu", children: [_jsxs("div", { className: "jini-conv-head", children: [_jsxs("span", { className: "jini-conv-heading", children: [t('Conversations'), _jsx("span", { className: "jini-conv-count", "data-testid": "conversation-count", children: conversations.length })] }), _jsxs("button", { type: "button", className: "jini-conv-new", disabled: createDisabled, "data-testid": "conversation-new",
194
195
  // `disabled` alone guards this: a disabled button never dispatches a click.
195
196
  onClick: () => {
@@ -199,7 +200,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
199
200
  // the remaining gap between "switcher closed" and "the host actually knows".
200
201
  runConversationListHostEffect('onCreate', () => onCreate());
201
202
  setOpen(false);
202
- }, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0 ? (_jsx("div", { className: "jini-conv-empty", children: emptyState ?? t('No conversations yet') })) : visible.length === 0 ? (_jsx("div", { className: "jini-conv-empty", children: t('No conversations match') })) : (visible.map((item) => (_jsxs("div", { className: `jini-conv-item${item.id === activeConversationId ? ' jini-conv-item-active' : ''}`, "data-testid": `conversation-item-${item.id}`, children: [editingId === item.id ? (_jsx(ConversationTitleEditor, { title: rawTitles.get(item.id) ?? null, "data-testid": `conversation-rename-${item.id}`, onCommit: (next) => commitRename(item.id, next), onCancel: () => setEditingId(null) })) : (_jsxs("button", { type: "button", className: "jini-conv-item-select", "data-testid": `conversation-select-${item.id}`, title: t('Double-click to rename'), onClick: () => {
203
+ }, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0 || visible.length === 0 ? (_jsx("div", { className: "jini-conv-empty", children: emptyLabel() })) : (visible.map((item) => (_jsxs("div", { className: `jini-conv-item${item.id === activeConversationId ? ' jini-conv-item-active' : ''}`, "data-testid": `conversation-item-${item.id}`, children: [editingId === item.id ? (_jsx(ConversationTitleEditor, { title: rawTitles.get(item.id) ?? null, "data-testid": `conversation-rename-${item.id}`, onCommit: (next) => commitRename(item.id, next), onCancel: () => setEditingId(null) })) : (_jsxs("button", { type: "button", className: "jini-conv-item-select", "data-testid": `conversation-select-${item.id}`, title: t('Double-click to rename'), onClick: () => {
203
204
  cancelPendingSelect();
204
205
  pendingSelectRef.current = setTimeout(() => {
205
206
  pendingSelectRef.current = null;
@@ -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);
@@ -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. */
@@ -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;
@@ -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())
@@ -235,7 +243,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
235
243
  // arrives for it, instead of staying tombstoned for the message's whole lifetime.
236
244
  _jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
237
245
  };
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}`)));
246
+ const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => {
247
+ if (segment.kind === 'text')
248
+ return segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null;
249
+ return (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`));
250
+ });
239
251
  const renderBlock = (block) => {
240
252
  if (block.kind === 'text') {
241
253
  return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
@@ -243,7 +255,9 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
243
255
  if (block.kind === 'reply-end') {
244
256
  const reply = usage.replies[block.index];
245
257
  // 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;
258
+ if (!reply || !showUsageLine)
259
+ return null;
260
+ return _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key);
247
261
  }
248
262
  if (block.kind === 'tools')
249
263
  return renderTools(block.rows, block.key);
@@ -260,21 +274,23 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
260
274
  return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
261
275
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
262
276
  };
277
+ // The live footer follows the run as transcript sections split; its clock must not remount.
263
278
  const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
264
279
  ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
265
280
  : '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
281
  ? 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));
282
+ : (_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 })] }, footer ? message.id : key));
268
283
  // The lossless fallback keeps the host's text intact; user receipts still own separate rows,
269
284
  // in their original receipt order, outside the assistant's actions and usage footer.
285
+ // Keep the same child-array nesting as the interleaved path so React preserves the footer.
270
286
  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)] });
287
+ 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
288
  // A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
273
289
  const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
274
290
  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 })) });
291
+ return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
292
+ ? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
293
+ : renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex }))] });
278
294
  }
279
295
  /** Preserve optional render-prop presence for custom extension renderers. */
280
296
  function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
@@ -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