@jini-ai/chat 0.6.6 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +129 -1
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +187 -98
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { CREDENTIAL_CARD_GUIDANCE, SECRET_REDACTED_NOTICE } from '../../core/user-text-redaction.js';
3
3
  import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
4
4
  /**
@@ -21,34 +21,46 @@ export const QuestionForm = forwardRef(function QuestionForm({ form: incomingFor
21
21
  const t = useT();
22
22
  const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
23
23
  const guard = useUserTextGuard({ ...(redactUserText ? { redactUserText } : {}), ...(onSecretRedacted ? { onSecretRedacted } : {}) });
24
+ const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
25
+ const state = useQuestionAnswers({ form, submittedAnswers, draftAnswers, locked }, { onDraftChange, onAnswerChange });
26
+ const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : state.answers;
27
+ const ready = answersReady(form, currentAnswers);
28
+ function handleSubmit() {
29
+ if (locked || !onSubmit)
30
+ return;
31
+ if (!ready)
32
+ return;
33
+ const safe = redactQuestionAnswers({ form, answers: state.answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
34
+ state.setAnswers(safe.answers);
35
+ const files = collectFileSubmissions(form, state.fileAnswers);
36
+ if (files.length > 0)
37
+ onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
38
+ else
39
+ onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
40
+ }
41
+ function handleSkipAll() {
42
+ if (locked || !onSubmit)
43
+ return;
44
+ const empty = {};
45
+ onSubmit(formatFormAnswers({ form: form, answers: empty }), empty);
46
+ }
47
+ useImperativeHandle(ref, () => ({ submit: handleSubmit, skipAll: handleSkipAll }));
48
+ useEffect(() => {
49
+ onReadyChange?.(!locked && ready);
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [onReadyChange, locked, ready]);
52
+ return (_jsxs("div", { className: `question-form${locked ? ' question-form-locked' : ''}`, "data-form-id": form.id, children: [_jsx(QuestionFormHead, { form: form, locked: locked }), _jsx("div", { className: "question-form-body", children: form.questions.map((q) => (_jsx(QuestionField, { q: q, value: currentAnswers[q.id], formId: form.id, locked: locked, t: t, ...state.handlers }, q.id))) }), guard.secretRedacted ? _jsx("div", { role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, hideInternalSubmit ? null : _jsx(QuestionFormFoot, { locked: locked, answered: submittedAnswers !== undefined, ready: ready, submitLabel: form.submitLabel, onSubmit: handleSubmit })] }));
53
+ });
54
+ /** Owns the editable answers plus the picked files; every edit is a no-op once the form is locked. */
55
+ function useQuestionAnswers({ form, submittedAnswers, draftAnswers, locked }, { onDraftChange, onAnswerChange } = {}) {
24
56
  const initial = useMemo(() => buildInitialState(form, submittedAnswers, draftAnswers), [form, submittedAnswers, draftAnswers]);
25
57
  const [answers, setAnswers] = useState(initial);
26
58
  const [fileAnswers, setFileAnswers] = useState({});
27
- const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
28
- const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : answers;
29
59
  useEffect(() => {
30
60
  setFileAnswers({});
31
61
  }, [form.id]);
32
62
  useEffect(() => {
33
- setAnswers((prev) => {
34
- let changed = false;
35
- const next = { ...prev };
36
- for (const q of form.questions) {
37
- if (next[q.id] !== undefined)
38
- continue;
39
- changed = true;
40
- if (submittedAnswers && submittedAnswers[q.id] !== undefined) {
41
- next[q.id] = canonicalizeQuestionValue(q, submittedAnswers[q.id]);
42
- }
43
- else if (q.defaultValue !== undefined) {
44
- next[q.id] = canonicalizeQuestionValue(q, q.defaultValue);
45
- }
46
- else {
47
- next[q.id] = emptyQuestionValue(q);
48
- }
49
- }
50
- return changed ? next : prev;
51
- });
63
+ setAnswers((prev) => backfillAnswers(prev, form, submittedAnswers));
52
64
  }, [form, submittedAnswers]);
53
65
  function update(id, value) {
54
66
  if (locked)
@@ -78,60 +90,111 @@ export const QuestionForm = forwardRef(function QuestionForm({ form: incomingFor
78
90
  const fixed = current.filter((entry) => questionValueIsKnown(q, entry));
79
91
  update(q.id, [...fixed, ...splitCustomEntries(raw)]);
80
92
  }
81
- function handleSubmit() {
82
- if (locked || !onSubmit)
83
- return;
84
- if (!ready)
85
- return;
86
- const safe = redactQuestionAnswers({ form, answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
87
- setAnswers(safe.answers);
88
- const files = collectFileSubmissions(form, fileAnswers);
89
- if (files.length > 0)
90
- onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
91
- else
92
- onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
93
- }
94
- function handleSkipAll() {
95
- if (locked || !onSubmit)
96
- return;
97
- const empty = {};
98
- onSubmit(formatFormAnswers({ form: form, answers: empty }), empty);
93
+ function selectFiles(q, list) {
94
+ const files = Array.from(list ?? []);
95
+ const names = files.map((file) => file.name);
96
+ setFileAnswers((current) => ({ ...current, [q.id]: files }));
97
+ update(q.id, q.multiple ? names : (names[0] ?? ''));
99
98
  }
99
+ return { answers, setAnswers, fileAnswers, handlers: { update, toggleCheckbox, updateCheckboxCustom, selectFiles } };
100
+ }
101
+ function answersReady(form, answers) {
100
102
  const withinSelectionLimits = form.questions.every((q) => {
101
103
  if (q.type !== 'checkbox' || q.maxSelections === undefined)
102
104
  return true;
103
- const v = currentAnswers[q.id];
105
+ const v = answers[q.id];
104
106
  return !Array.isArray(v) || v.length <= q.maxSelections;
105
107
  });
106
108
  const requiredAnswered = form.questions.every((q) => {
107
109
  if (q.required !== true)
108
110
  return true;
109
- const v = currentAnswers[q.id];
111
+ const v = answers[q.id];
110
112
  if (Array.isArray(v))
111
113
  return v.length > 0;
112
114
  return typeof v === 'string' && v.trim().length > 0;
113
115
  });
114
- const ready = withinSelectionLimits && requiredAnswered;
115
- useImperativeHandle(ref, () => ({ submit: handleSubmit, skipAll: handleSkipAll }));
116
- useEffect(() => {
117
- onReadyChange?.(!locked && ready);
118
- // eslint-disable-next-line react-hooks/exhaustive-deps
119
- }, [onReadyChange, locked, ready]);
120
- return (_jsxs("div", { className: `question-form${locked ? ' question-form-locked' : ''}`, "data-form-id": form.id, children: [_jsxs("div", { className: "question-form-head", children: [_jsx("span", { className: "question-form-icon", "aria-hidden": true, children: "?" }), _jsxs("div", { className: "question-form-titles", children: [_jsx("div", { className: "question-form-title", children: form.title }), form.description ? _jsx("div", { className: "question-form-desc", children: form.description }) : null] }), locked ? _jsx("span", { className: "question-form-pill", children: t('Answered') }) : null] }), _jsx("div", { className: "question-form-body", children: form.questions.map((q) => {
121
- const value = currentAnswers[q.id];
122
- return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, isSecretQuestion({ question: q }) ? _jsx("div", { className: "qf-help", children: t(CREDENTIAL_CARD_GUIDANCE) }) : null, q.type === 'radio' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${form.id}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) })) : null, q.type === 'radio' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'checkbox' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
123
- const arr = Array.isArray(value) ? value : [];
124
- const on = arr.includes(opt.value);
125
- const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections;
126
- return (_jsxs("label", { title: opt.description, className: `qf-chip${on ? ' qf-chip-on' : ''}${maxed ? ' qf-chip-disabled' : ''}`, children: [_jsx("input", { type: "checkbox", value: opt.value, checked: on, disabled: locked || maxed, "aria-label": opt.label, onChange: () => toggleCheckbox(q.id, opt.value, q.maxSelections) }), _jsx(OptionCopy, { option: opt })] }, opt.value));
127
- }) })) : null, q.type === 'checkbox' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customCheckboxValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => updateCheckboxCustom(q, next) })) : null, q.type === 'select' && q.options ? (_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] })) : null, q.type === 'select' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'text' ? _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'number' ? _jsx("input", { type: "number", className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'range' ? (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0) })] })) : null, q.type === 'date' || q.type === 'time' || q.type === 'datetime-local' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'color' ? _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'url' || q.type === 'email' || q.type === 'tel' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'file' ? (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => {
128
- const files = Array.from(e.target.files ?? []);
129
- const names = files.map((file) => file.name);
130
- setFileAnswers((current) => ({ ...current, [q.id]: files }));
131
- update(q.id, q.multiple ? names : (names[0] ?? ''));
132
- } }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })) : null, q.type === 'switch' ? (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })) : null, q.type === 'textarea' ? _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 ? (_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: form.id, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) })) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null] }, q.id));
133
- }) }), guard.secretRedacted ? _jsx("div", { role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, hideInternalSubmit ? null : (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: submittedAnswers ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: handleSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: form.submitLabel ?? t('Continue') })) : null] }))] }));
134
- });
116
+ return withinSelectionLimits && requiredAnswered;
117
+ }
118
+ function QuestionFormHead({ form, locked }) {
119
+ const t = useT();
120
+ return (_jsxs("div", { className: "question-form-head", children: [_jsx("span", { className: "question-form-icon", "aria-hidden": true, children: "?" }), _jsxs("div", { className: "question-form-titles", children: [_jsx("div", { className: "question-form-title", children: form.title }), form.description ? _jsx("div", { className: "question-form-desc", children: form.description }) : null] }), locked ? _jsx("span", { className: "question-form-pill", children: t('Answered') }) : null] }));
121
+ }
122
+ function QuestionFormFoot({ locked, answered, ready, submitLabel, onSubmit }) {
123
+ const t = useT();
124
+ return (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: answered ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: onSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: submitLabel ?? t('Continue') })) : null] }));
125
+ }
126
+ function QuestionField(ctx) {
127
+ const { q, t } = ctx;
128
+ const renderControl = QUESTION_CONTROLS[q.type];
129
+ return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, isSecretQuestion({ question: q }) ? _jsx("div", { className: "qf-help", children: t(CREDENTIAL_CARD_GUIDANCE) }) : null, renderControl ? renderControl(ctx) : null] }));
130
+ }
131
+ function stringValue(value) {
132
+ return typeof value === 'string' ? value : '';
133
+ }
134
+ /** A range with no usable answer yet (e.g. mid-stream) shows its minimum, matching `emptyQuestionValue`. */
135
+ function rangeValue(q, value) {
136
+ return typeof value === 'string' && value.trim() ? value : String(q.min ?? 0);
137
+ }
138
+ function customChoice({ q, t, locked }, value, onChange) {
139
+ if (!shouldRenderCustomChoice(q))
140
+ return null;
141
+ return _jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: value, placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: onChange });
142
+ }
143
+ function singleCustomChoice(ctx) {
144
+ return customChoice(ctx, customSingleValue(ctx.q, ctx.value), (next) => ctx.update(ctx.q.id, next));
145
+ }
146
+ function RadioControl(ctx) {
147
+ const { q, value, formId, locked, update } = ctx;
148
+ if (!q.options)
149
+ return null;
150
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${formId}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) }), singleCustomChoice(ctx)] }));
151
+ }
152
+ function CheckboxControl(ctx) {
153
+ const { q, value, locked, toggleCheckbox, updateCheckboxCustom } = ctx;
154
+ if (!q.options)
155
+ return null;
156
+ const arr = Array.isArray(value) ? value : [];
157
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
158
+ const on = arr.includes(opt.value);
159
+ const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections;
160
+ return (_jsxs("label", { title: opt.description, className: `qf-chip${on ? ' qf-chip-on' : ''}${maxed ? ' qf-chip-disabled' : ''}`, children: [_jsx("input", { type: "checkbox", value: opt.value, checked: on, disabled: locked || maxed, "aria-label": opt.label, onChange: () => toggleCheckbox(q.id, opt.value, q.maxSelections) }), _jsx(OptionCopy, { option: opt })] }, opt.value));
161
+ }) }), customChoice(ctx, customCheckboxValue(q, value), (next) => updateCheckboxCustom(q, next))] }));
162
+ }
163
+ function SelectControl(ctx) {
164
+ const { q, value, locked, t, update } = ctx;
165
+ if (!q.options)
166
+ return null;
167
+ return (_jsxs(_Fragment, { children: [_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] }), singleCustomChoice(ctx)] }));
168
+ }
169
+ function DirectionCardsControl(ctx) {
170
+ const { q, value, formId, locked, update } = ctx;
171
+ if (!q.cards || q.cards.length === 0)
172
+ return null;
173
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: formId, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) }), singleCustomChoice(ctx)] }));
174
+ }
175
+ /** date/time/datetime-local and url/email/tel share one plain input whose `type` is the question type. */
176
+ function TypedInputControl({ q, value, locked, update }) {
177
+ return _jsx("input", { type: q.type, className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) });
178
+ }
179
+ const QUESTION_CONTROLS = {
180
+ radio: RadioControl,
181
+ checkbox: CheckboxControl,
182
+ select: SelectControl,
183
+ 'direction-cards': DirectionCardsControl,
184
+ text: ({ q, value, locked, update }) => _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
185
+ number: ({ q, value, locked, update }) => _jsx("input", { type: "number", className: "qf-input", value: stringValue(value), placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
186
+ range: ({ q, value, locked, update }) => (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: rangeValue(q, value), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: rangeValue(q, value) })] })),
187
+ date: TypedInputControl,
188
+ time: TypedInputControl,
189
+ 'datetime-local': TypedInputControl,
190
+ url: TypedInputControl,
191
+ email: TypedInputControl,
192
+ tel: TypedInputControl,
193
+ color: ({ q, value, locked, update }) => _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
194
+ file: ({ q, value, locked, selectFiles }) => (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => selectFiles(q, e.target.files) }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })),
195
+ switch: ({ q, value, locked, update }) => (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })),
196
+ textarea: ({ q, value, locked, update }) => _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: stringValue(value), placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }),
197
+ };
135
198
  function OptionCopy({ option }) {
136
199
  return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));
137
200
  }
@@ -145,22 +208,31 @@ function DirectionCardView({ card, formId, questionId, selected, disabled, onSel
145
208
  }
146
209
  function buildInitialState(form, submitted, draft) {
147
210
  const out = {};
211
+ for (const q of form.questions)
212
+ out[q.id] = initialQuestionValue(q, submitted, draft);
213
+ return out;
214
+ }
215
+ /** Submitted beats draft beats default beats empty; file drafts are never restored (the File objects are gone). */
216
+ function initialQuestionValue(q, submitted, draft) {
217
+ if (submitted?.[q.id] !== undefined)
218
+ return canonicalizeQuestionValue(q, submitted[q.id]);
219
+ if (draft?.[q.id] !== undefined && q.type !== 'file')
220
+ return canonicalizeQuestionValue(q, draft[q.id]);
221
+ if (q.defaultValue !== undefined)
222
+ return canonicalizeQuestionValue(q, q.defaultValue);
223
+ return emptyQuestionValue(q);
224
+ }
225
+ /** Questions that stream in after mount get their initial value; answers already set are kept. */
226
+ function backfillAnswers(prev, form, submitted) {
227
+ let changed = false;
228
+ const next = { ...prev };
148
229
  for (const q of form.questions) {
149
- if (submitted && submitted[q.id] !== undefined) {
150
- out[q.id] = canonicalizeQuestionValue(q, submitted[q.id]);
151
- continue;
152
- }
153
- if (draft && draft[q.id] !== undefined && q.type !== 'file') {
154
- out[q.id] = canonicalizeQuestionValue(q, draft[q.id]);
230
+ if (next[q.id] !== undefined)
155
231
  continue;
156
- }
157
- if (q.defaultValue !== undefined) {
158
- out[q.id] = canonicalizeQuestionValue(q, q.defaultValue);
159
- continue;
160
- }
161
- out[q.id] = emptyQuestionValue(q);
232
+ changed = true;
233
+ next[q.id] = initialQuestionValue(q, submitted, undefined);
162
234
  }
163
- return out;
235
+ return changed ? next : prev;
164
236
  }
165
237
  function draftSafeAnswers(form, answers) {
166
238
  const fileQuestionIds = new Set(form.questions.filter((q) => q.type === 'file').map((q) => q.id));
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  import { useRef, useState } from 'react';
21
21
  import { useT } from '../hooks/context.js';
22
22
  import { QuestionForm } from './QuestionForm.js';
23
- export function QuestionsPanel({ form, interactive, submitDisabled = false, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
23
+ export function QuestionsPanel({ form, interactive, submitDisabled, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
24
24
  const t = useT();
25
25
  const formRef = useRef(null);
26
26
  const [ready, setReady] = useState(false);
@@ -12,18 +12,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useState } from 'react';
13
13
  import { useT } from '../hooks/context.js';
14
14
  import { Icon } from './Icon.js';
15
- export function TodoCard({ todos, runStreaming = false, onDismiss }) {
15
+ export function TodoCard({ todos, runStreaming, onDismiss }) {
16
16
  const t = useT();
17
- const hasInProgress = todos.some((todo) => todo.status === 'in_progress');
18
- const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
19
- const defaultExpanded = todos.length > 0 && (hasInProgress || hasPending || runStreaming);
17
+ const { defaultExpanded, inProgressTodo, done, allComplete } = todoProgress({ todos }, { runStreaming });
20
18
  const [overrideExpanded, setOverrideExpanded] = useState(null);
21
19
  const expanded = overrideExpanded ?? defaultExpanded;
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))) }) }) })] }));
22
+ }
23
+ function todoProgress({ todos }, { runStreaming = false } = {}) {
24
+ const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
25
+ const defaultExpanded = todos.length > 0 && (hasPending || runStreaming);
22
26
  const inProgressTodo = todos.find((todo) => todo.status === 'in_progress');
23
27
  const completed = todos.filter((todo) => todo.status === 'completed').length;
24
28
  const done = todos.filter((todo) => todo.status === 'completed' || todo.status === 'in_progress').length;
25
29
  const allComplete = todos.length > 0 && completed === todos.length;
26
- const showDismiss = Boolean(onDismiss) && allComplete;
27
- 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: inProgressTodo.activeForm || inProgressTodo.content }) : 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: todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content })] }, i))) }) }) })] }));
30
+ return { defaultExpanded, inProgressTodo, done, allComplete };
31
+ }
32
+ function todoLabel({ todo }, _options = {}) {
33
+ return todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content;
28
34
  }
29
35
  //# sourceMappingURL=TodoCard.js.map
@@ -2,11 +2,12 @@ import type { ReactNode } from 'react';
2
2
  import type { ToolDisplayRow } from '../tool-call-display.js';
3
3
  /**
4
4
  * One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
5
- * `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
6
- * `useToolCallGroup`.
5
+ * `toolCallDisplay: 'collapsed'` layout, or the compact failure notice in 'hidden'. The disclosure
6
+ * state lives in `useToolCallGroup`; copy uses the host's translator.
7
7
  */
8
- export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow }: {
8
+ export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow, failureOnly }: {
9
9
  rows: readonly Row[];
10
+ failureOnly?: boolean;
10
11
  renderRow: (row: Row) => ReactNode;
11
12
  }): import("react").JSX.Element;
12
13
  //# sourceMappingURL=ToolCallGroup.d.ts.map
@@ -1,13 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useToolCallGroup } from './ToolCallGroup.hooks.js';
3
+ import { useT } from '../hooks/context.js';
3
4
  import { Icon } from './Icon.js';
4
5
  /**
5
6
  * One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
6
- * `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
7
- * `useToolCallGroup`.
7
+ * `toolCallDisplay: 'collapsed'` layout, or the compact failure notice in 'hidden'. The disclosure
8
+ * state lives in `useToolCallGroup`; copy uses the host's translator.
8
9
  */
9
- export function ToolCallGroup({ rows, renderRow }) {
10
+ export function ToolCallGroup({ rows, renderRow, failureOnly = false }) {
10
11
  const group = useToolCallGroup({ rows });
11
- return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [_jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: group.label }), _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
12
+ const t = useT();
13
+ return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [failureOnly ? _jsxs("span", { className: "jini-tool-group__label", children: [t('A step failed'), " "] }) : null, _jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: failureOnly ? t('Show') : group.label }), _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
12
14
  }
13
15
  //# sourceMappingURL=ToolCallGroup.js.map
@@ -27,14 +27,11 @@ import { Icon } from './Icon.js';
27
27
  import { TodoCard } from './TodoCard.js';
28
28
  /** A Jini registry tool id: dotted, lowercase, snake_case segments — `page.fill`, `daemon.db.vacuum`, `chat.send_message`. Never matches an agent-vendor tool name (`Bash`, `Write`, `mcp__jini__execute_delegated_tool`), which is the point — this is how a canonical delegated-tool event is told apart from everything else. */
29
29
  const JINI_TOOL_ID_PATTERN = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
30
- const SEARCH_TOOLS_NAMES = new Set(['search_tools', 'mcp__jini__search_tools']);
31
- const DESCRIBE_TOOL_NAMES = new Set(['describe_tool', 'mcp__jini__describe_tool']);
32
30
  export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }) {
33
31
  const registry = useToolRendererRegistry();
34
32
  const name = use.name;
35
- const isStreaming = runStreaming ?? false;
36
- const isSucceeded = runSucceeded ?? false;
37
- const content = renderToolCardBody(name, use, result, isStreaming, isSucceeded, { projectFileNames, onRequestOpenFile }, registry);
33
+ const card = { input: use.input, result: result, runStreaming: runStreaming ?? false, runSucceeded: runSucceeded ?? false };
34
+ const content = renderToolCardBody({ use, card, fileCtx: { projectFileNames, onRequestOpenFile }, registry });
38
35
  if (content === null)
39
36
  return null;
40
37
  // Tagged once here, at the single dispatch point every branch below funnels through, rather than
@@ -44,18 +41,42 @@ export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileN
44
41
  // ambiguous the way a repeated literal handle would be.
45
42
  return (_jsx("div", { "data-agent-element": `tool-call-${use.id}`, "data-agent-role": "region", "data-agent-label": `A ${humanizeToolId(name)} tool call the AI made`, children: content }));
46
43
  }
47
- function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx, registry) {
48
- const custom = registry.get({ name });
49
- if (custom) {
50
- try {
51
- const node = custom(toRenderProps({ use: use, result: result, runStreaming: isStreaming }, { runSucceeded: isSucceeded }));
52
- if (node !== undefined && node !== null && node !== false)
53
- return node;
54
- }
55
- catch (err) {
56
- console.error(`[ToolCard] custom renderer for "${name}" threw; falling back`, err);
57
- }
44
+ // Hardcoded family cards for tools shipped with this package, keyed by every name each tool goes
45
+ // by. A `Map`, not an object literal, so a tool named `constructor`/`toString` can never resolve
46
+ // to an inherited `Object.prototype` member.
47
+ const FAMILY_CARDS = new Map([
48
+ [['Write', 'write', 'create_file'], (card, fileCtx) => _jsx(FileWriteCard, { ...card, ctx: fileCtx })],
49
+ [['Edit', 'str_replace_edit'], (card, fileCtx) => _jsx(FileEditCard, { ...card, ctx: fileCtx })],
50
+ [['Read', 'read_file'], (card, fileCtx) => _jsx(FileReadCard, { ...card, ctx: fileCtx })],
51
+ [['Bash'], (card) => _jsx(BashCard, { ...card })],
52
+ [['Glob', 'list_files'], (card) => _jsx(GlobCard, { ...card })],
53
+ [['Grep'], (card) => _jsx(GrepCard, { ...card })],
54
+ [['WebFetch', 'web_fetch'], (card) => _jsx(WebFetchCard, { ...card })],
55
+ [['WebSearch', 'web_search'], (card) => _jsx(WebSearchCard, { ...card })],
56
+ [['search_tools', 'mcp__jini__search_tools'], (card) => _jsx(SearchToolsCard, { ...card })],
57
+ [['describe_tool', 'mcp__jini__describe_tool'], (card) => _jsx(DescribeToolCard, { ...card })],
58
+ ].flatMap(([names, render]) => names.map((name) => [name, render])));
59
+ /** A host-registered renderer's output, or `undefined` when none is registered, it renders nothing, or it throws (logged) — every one of those falls back to the built-in cards. */
60
+ function renderRegistered({ use, card, registry }) {
61
+ const custom = registry.get({ name: use.name });
62
+ if (!custom)
63
+ return undefined;
64
+ try {
65
+ const node = custom(toRenderProps({ use: use, result: card.result, runStreaming: card.runStreaming }, { runSucceeded: card.runSucceeded }));
66
+ if (node !== undefined && node !== null && node !== false)
67
+ return node;
68
+ }
69
+ catch (err) {
70
+ console.error(`[ToolCard] custom renderer for "${use.name}" threw; falling back`, err);
58
71
  }
72
+ return undefined;
73
+ }
74
+ function renderToolCardBody(args) {
75
+ const custom = renderRegistered(args);
76
+ if (custom !== undefined)
77
+ return custom;
78
+ const { use, card, fileCtx } = args;
79
+ const name = use.name;
59
80
  // `execute_delegated_tool` is a transport wrapper: `@jini-ai/daemon`'s `delegated-tool-bridge.ts`
60
81
  // always emits a second, canonical `tool_use` named after the real Jini tool id (`page.fill`,
61
82
  // not `execute_delegated_tool`) alongside it — before calling `ToolExecutor`, so it exists even
@@ -68,37 +89,23 @@ function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx
68
89
  // `useToolTimeline`: `foldDelegatedWrapperCalls` drops it in favour of that canonical row. This
69
90
  // check still covers a host that renders `<ToolCard>` straight from raw events.)
70
91
  // Covers the read-only gateway (`execute_readonly_delegated_tool`) too.
71
- if (isDelegatedWrapperToolName({ name: name }) && result && !result.isError)
92
+ if (isDelegatedWrapperToolName({ name: name }) && card.result && !card.result.isError)
72
93
  return null;
73
94
  if (isTodoWriteToolName({ name: name }))
74
- return _jsx(TodoCard, { todos: parseTodoWriteInput({ input: use.input }), runStreaming: isStreaming });
75
- if (name === 'Write' || name === 'write' || name === 'create_file')
76
- return _jsx(FileWriteCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
77
- if (name === 'Edit' || name === 'str_replace_edit')
78
- return _jsx(FileEditCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
79
- if (name === 'Read' || name === 'read_file')
80
- return _jsx(FileReadCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
81
- if (name === 'Bash')
82
- return _jsx(BashCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
83
- if (name === 'Glob' || name === 'list_files')
84
- return _jsx(GlobCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
85
- if (name === 'Grep')
86
- return _jsx(GrepCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
87
- if (name === 'WebFetch' || name === 'web_fetch')
88
- return _jsx(WebFetchCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
89
- if (name === 'WebSearch' || name === 'web_search')
90
- return _jsx(WebSearchCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
95
+ return _jsx(TodoCard, { todos: parseTodoWriteInput({ input: use.input }), runStreaming: card.runStreaming });
96
+ // Jini tool ids are dotted and wrapper names are `execute_*delegated_tool`, so neither ever
97
+ // collides with a family name — the search/describe cards can sit in the same table as the rest
98
+ // even though they are looked up ahead of the Jini-id check below.
99
+ const family = FAMILY_CARDS.get(name);
100
+ if (family)
101
+ return family(card, fileCtx);
91
102
  if (JINI_TOOL_ID_PATTERN.test(name) || isDelegatedWrapperToolName({ name: name })) {
92
103
  // The unsuppressed (failed-wrapper) case falls through here too: `name` is still
93
104
  // `execute_delegated_tool`, so `jiniToolIdFromInput` recovers the real id from
94
105
  // `{toolId, input}` instead of `DelegatedToolCard` receiving its own wrapper name as the title.
95
- return _jsx(DelegatedToolCard, { name: name, input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
106
+ return _jsx(DelegatedToolCard, { name: name, ...card });
96
107
  }
97
- if (SEARCH_TOOLS_NAMES.has(name))
98
- return _jsx(SearchToolsCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
99
- if (DESCRIBE_TOOL_NAMES.has(name))
100
- return _jsx(DescribeToolCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
101
- return _jsx(GenericCard, { name: name, input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
108
+ return _jsx(GenericCard, { name: name, ...card });
102
109
  }
103
110
  /** `"page.fill"` -> `"Page Fill"`, `"daemon.db.vacuum"` -> `"Daemon Db Vacuum"`. Cosmetic only — never used to make a routing decision. */
104
111
  function humanizeToolId(id) {
@@ -9,8 +9,13 @@ export function parseComposerSlashQuery({ draft }) {
9
9
  const match = SLASH_QUERY_RE.exec(draft);
10
10
  if (!match)
11
11
  return null;
12
+ // The `as string` casts are guaranteed by SLASH_QUERY_RE itself, not assumed: group 1 is a bare
13
+ // `*` capture outside any optional group, so it always participates (possibly as ''); group 3 is
14
+ // `*` inside the same optional group as group 2, so it participates whenever a separator does.
15
+ // `noUncheckedIndexedAccess` cannot see either guarantee, and the `?? ''` fallbacks that used to
16
+ // satisfy it were unreachable branches.
12
17
  const [, command, separator, rest] = match;
13
- return { command: command ?? '', argument: separator === undefined ? null : (rest ?? '') };
18
+ return { command: command, argument: separator === undefined ? null : rest };
14
19
  }
15
20
  /**
16
21
  * Once a separator has been typed, only an item whose declared `command` exactly equals the typed
@@ -1,10 +1,3 @@
1
- /**
2
- * @module components/mcp-ui-surface-state
3
- *
4
- * Whether an MCP-UI card in a chat transcript is closed, and with which label. Kept out of
5
- * `McpUiSurfaceCard.tsx` so the component only renders; moved here from that file 2026-10-04 when
6
- * the card's own deadline (`useSurfaceExpiry`) became a second reason to close it.
7
- */
8
1
  import type { McpUiToolCallHandler } from '@jini-ai/ui/mcp-ui';
9
2
  import type { ExtEventCall } from '../ext-event-renderer-registry.js';
10
3
  import type { useT } from '../hooks/context.js';
@@ -35,14 +28,16 @@ export declare function closedSurfaceLabel({ t, call, runStreaming, documentsFor
35
28
  export declare function isDeliveredAnswer(result: unknown): boolean;
36
29
  /**
37
30
  * Wraps a card's tool executor so a delivered answer is reported once it resolves. The result is
38
- * passed through unchanged, and a rejected call reports nothing (the dialog shows its error and can
39
- * be answered again).
31
+ * passed through unchanged; onAnswerText receives readable choice text when available. A rejected call
32
+ * reports nothing (the dialog shows its error and can be answered again).
40
33
  *
41
- * @complexity O(1) per call, plus `onToolCall`'s own cost.
34
+ * @complexity O(p + s*m) answer-label mapping, plus `onToolCall`'s own cost (see questionAnswerText).
42
35
  */
43
- export declare function reportDeliveredAnswer({ onToolCall, onAnswered }: {
36
+ export declare function reportDeliveredAnswer({ onToolCall, onAnswered, onAnswerText, input }: {
44
37
  onToolCall: McpUiToolCallHandler;
45
38
  onAnswered: () => void;
39
+ onAnswerText?: ((answerText: string) => void) | undefined;
40
+ input?: unknown;
46
41
  }): McpUiToolCallHandler;
47
42
  export {};
48
43
  //# sourceMappingURL=mcp-ui-surface-state.d.ts.map