@jini-ai/chat 0.5.6 → 0.5.7

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 (48) hide show
  1. package/dist/core/durable-projection.d.ts +3 -1
  2. package/dist/core/durable-projection.js +40 -7
  3. package/dist/core/index.d.ts +1 -0
  4. package/dist/core/index.js +1 -0
  5. package/dist/core/run-events/entry.d.ts +12 -1
  6. package/dist/core/run-events/entry.js +17 -6
  7. package/dist/core/tool-call-detail.d.ts +36 -0
  8. package/dist/core/tool-call-detail.js +49 -0
  9. package/dist/core/turn-usage.d.ts +11 -0
  10. package/dist/core/turn-usage.js +17 -0
  11. package/dist/react/chat-pane.d.ts +2 -2
  12. package/dist/react/chat-pane.js +1 -1
  13. package/dist/react/components/Composer.d.ts +2 -0
  14. package/dist/react/components/Composer.js +5 -2
  15. package/dist/react/components/Markdown.js +6 -6
  16. package/dist/react/components/MessageList.d.ts +7 -12
  17. package/dist/react/components/MessageList.js +20 -2
  18. package/dist/react/components/MessageRow.d.ts +6 -1
  19. package/dist/react/components/MessageRow.js +10 -4
  20. package/dist/react/components/MidRunUserMessage.d.ts +27 -0
  21. package/dist/react/components/MidRunUserMessage.js +39 -0
  22. package/dist/react/components/ToolCard.js +33 -22
  23. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  24. package/dist/react/features/chat-pane/components/ChatPane.js +11 -3
  25. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
  26. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
  27. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  28. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +55 -6
  29. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
  30. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
  31. package/dist/react/features/chat-pane/index.d.ts +2 -1
  32. package/dist/react/features/chat-pane/index.js +1 -0
  33. package/dist/react/features/chat-pane/rules.d.ts +13 -0
  34. package/dist/react/features/chat-pane/rules.js +14 -0
  35. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  36. package/dist/react/features/chat-pane/styles.js +7 -0
  37. package/dist/react/features/chat-pane/types.d.ts +23 -0
  38. package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
  39. package/dist/react/hooks/composer-draft-cache.js +20 -0
  40. package/dist/react/hooks/useComposer.d.ts +14 -0
  41. package/dist/react/hooks/useComposer.js +78 -6
  42. package/dist/react/index.d.ts +4 -2
  43. package/dist/react/index.js +3 -1
  44. package/dist/server/durable-runs/recover.js +19 -11
  45. package/dist/server/run-finalizer/entry.js +2 -1
  46. package/dist/store/runtime/durable-run-store.js +5 -2
  47. package/dist/store/runtime/run-ledger.js +5 -2
  48. package/package.json +8 -8
@@ -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
  /**
3
3
  * @module ToolCard
4
4
  *
@@ -20,8 +20,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  * `AskUserQuestion`) falls through to `GenericCard`, which is a correct,
21
21
  * generic rendering for it.
22
22
  */
23
- import { useState } from 'react';
24
- import { formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
23
+ import { useId, useState } from 'react';
24
+ import { describeToolCallDetail, formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
25
25
  import { useT } from '../hooks/context.js';
26
26
  import { getToolRenderer } from '../tool-renderer-registry.js';
27
27
  import { Icon } from './Icon.js';
@@ -132,7 +132,6 @@ function primaryTarget(input) {
132
132
  */
133
133
  function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded }) {
134
134
  const t = useT();
135
- const [open, setOpen] = useState(false);
136
135
  // Wrapper form (only reached on a failed wrapper call, per ToolCard's suppression comment):
137
136
  // the real id and args are nested under {toolId, input}, not `input` itself.
138
137
  const wrapper = (input ?? {});
@@ -140,21 +139,19 @@ function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded })
140
139
  const args = isDelegatedWrapperToolName({ name: name }) ? wrapper.input : input;
141
140
  const target = primaryTarget(args);
142
141
  const isRunning = runStreaming && !result;
143
- return (_jsxs("div", { className: "op-card op-jini-tool", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: target }) : null, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsxs("div", { className: "op-card-detail", children: [_jsx("pre", { className: "op-command", children: JSON.stringify(args ?? {}) }), result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 2000) }) : null, _jsx(ToolResultMedia, { result: result })] }) }) }), _jsx(FileErrorDetail, { result: result })] }));
142
+ return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: target }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
144
143
  }
145
144
  function SearchToolsCard({ input, result, runStreaming, runSucceeded }) {
146
145
  const t = useT();
147
- const [open, setOpen] = useState(false);
148
146
  const obj = (input ?? {});
149
147
  const isRunning = runStreaming && !result;
150
- return (_jsxs("div", { className: "op-card op-jini-tool", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Search tools') }), obj.query ? _jsx("span", { className: "op-meta", children: obj.query }) : null, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("div", { className: "op-card-detail", children: result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 2000) }) : null }) }) }), _jsx(FileErrorDetail, { result: result })] }));
148
+ return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Search tools') }), obj.query ? _jsx("span", { className: "op-meta", children: obj.query }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
151
149
  }
152
150
  function DescribeToolCard({ input, result, runStreaming, runSucceeded }) {
153
151
  const t = useT();
154
- const [open, setOpen] = useState(false);
155
152
  const obj = (input ?? {});
156
153
  const isRunning = runStreaming && !result;
157
- return (_jsxs("div", { className: "op-card op-jini-tool", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Describe tool') }), obj.id ? _jsx("span", { className: "op-meta", children: obj.id }) : null, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("div", { className: "op-card-detail", children: result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 2000) }) : null }) }) }), _jsx(FileErrorDetail, { result: result })] }));
154
+ return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Describe tool') }), obj.id ? _jsx("span", { className: "op-meta", children: obj.id }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
158
155
  }
159
156
  function OpenInTabButton({ filePath, ctx }) {
160
157
  const t = useT();
@@ -174,68 +171,82 @@ function OpenInTabButton({ filePath, ctx }) {
174
171
  }
175
172
  function FileWriteCard({ input, result, runStreaming, runSucceeded, ctx }) {
176
173
  const t = useT();
177
- const [open, setOpen] = useState(false);
178
174
  const obj = (input ?? {});
179
175
  const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
180
176
  // See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
181
177
  const baseName = file.split('/').pop();
182
178
  const lines = typeof obj.content === 'string' ? obj.content.split('\n').length : null;
183
179
  const isRunning = runStreaming && !result;
184
- return (_jsxs("div", { className: "op-card op-file", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Write') }), _jsxs("span", { className: "op-meta", children: [baseName, lines !== null ? ` · ${t('{n} lines', { n: lines })}` : ''] }), _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }) }) }), _jsx(FileErrorDetail, { result: result })] }));
180
+ return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Write') }), _jsxs("span", { className: "op-meta", children: [baseName, lines !== null ? ` · ${t('{n} lines', { n: lines })}` : ''] })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
185
181
  }
186
182
  function FileEditCard({ input, result, runStreaming, runSucceeded, ctx }) {
187
183
  const t = useT();
188
- const [open, setOpen] = useState(false);
189
184
  const obj = (input ?? {});
190
185
  const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
191
186
  // See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
192
187
  const baseName = file.split('/').pop();
193
188
  const editCount = Array.isArray(obj.edits) ? obj.edits.length : 1;
194
189
  const isRunning = runStreaming && !result;
195
- return (_jsxs("div", { className: "op-card op-file", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Edit') }), _jsxs("span", { className: "op-meta", children: [baseName, " \u00B7 ", editCount, " ", editCount === 1 ? t('change') : t('changes')] }), _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }) }) }), _jsx(FileErrorDetail, { result: result })] }));
190
+ return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Edit') }), _jsxs("span", { className: "op-meta", children: [baseName, " \u00B7 ", editCount, " ", editCount === 1 ? t('change') : t('changes')] })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
196
191
  }
197
192
  function FileReadCard({ input, result, runStreaming, runSucceeded, ctx }) {
198
193
  const t = useT();
199
- const [open, setOpen] = useState(false);
200
194
  const obj = (input ?? {});
201
195
  const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
202
196
  // See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
203
197
  const baseName = file.split('/').pop();
204
198
  const isRunning = runStreaming && !result;
205
- return (_jsxs("div", { className: "op-card op-file", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Read') }), _jsx("span", { className: "op-meta", children: baseName }), _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }) }) }), _jsx(FileErrorDetail, { result: result })] }));
199
+ return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Read') }), _jsx("span", { className: "op-meta", children: baseName })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
206
200
  }
207
201
  function BashCard({ input, result, runStreaming, runSucceeded }) {
208
202
  const t = useT();
209
203
  const obj = (input ?? {});
210
204
  const command = obj.command ?? '';
211
205
  const desc = obj.description;
212
- const [open, setOpen] = useState(false);
213
206
  const isRunning = runStreaming && !result;
214
- return (_jsxs("div", { className: "op-card op-bash", children: [_jsxs("button", { type: "button", className: "op-card-head", onClick: () => setOpen((o) => !o), children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsxs("div", { className: "op-card-detail", children: [_jsx("pre", { className: "op-command", children: truncate(command, 400) }), result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 4000) }) : null] }) }) })] }));
207
+ return (_jsx(ExpandableCard, { className: "op-bash", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children: _jsxs("div", { className: "op-card-detail", children: [_jsx("pre", { className: "op-command", children: truncate(command, 400) }), result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 4000) }) : null] }) }));
215
208
  }
216
209
  function GlobCard({ input, result, runStreaming, runSucceeded }) {
217
210
  const t = useT();
218
211
  const obj = (input ?? {});
219
- return (_jsx("div", { className: "op-card op-search", children: _jsxs("div", { className: "op-card-head", children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search files') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '*', obj.path ? ` in ${obj.path}` : ''] })] }) }));
212
+ return (_jsx(ExpandableCard, { className: "op-search", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search files') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '*', obj.path ? ` in ${obj.path}` : ''] })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
220
213
  }
221
214
  function GrepCard({ input, result, runStreaming, runSucceeded }) {
222
215
  const t = useT();
223
216
  const obj = (input ?? {});
224
- return (_jsx("div", { className: "op-card op-search", children: _jsxs("div", { className: "op-card-head", children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search content') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '', obj.path ? ` in ${obj.path}` : ''] })] }) }));
217
+ return (_jsx(ExpandableCard, { className: "op-search", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search content') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '', obj.path ? ` in ${obj.path}` : ''] })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
225
218
  }
226
219
  function WebFetchCard({ input, result, runStreaming, runSucceeded }) {
227
220
  const t = useT();
228
221
  const obj = (input ?? {});
229
- return (_jsx("div", { className: "op-card op-web", children: _jsxs("div", { className: "op-card-head", children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Fetch') }), _jsx("span", { className: "op-meta", children: obj.url ?? '' })] }) }));
222
+ return (_jsx(ExpandableCard, { className: "op-web", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Fetch') }), _jsx("span", { className: "op-meta", children: obj.url ?? '' })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
230
223
  }
231
224
  function WebSearchCard({ input, result, runStreaming, runSucceeded }) {
232
225
  const t = useT();
233
226
  const obj = (input ?? {});
234
- return (_jsx("div", { className: "op-card op-web", children: _jsxs("div", { className: "op-card-head", children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search') }), _jsx("span", { className: "op-meta", children: obj.query ?? '' })] }) }));
227
+ return (_jsx(ExpandableCard, { className: "op-web", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search') }), _jsx("span", { className: "op-meta", children: obj.query ?? '' })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
235
228
  }
229
+ /** Any tool this package has no family card for — an MCP tool, or a card tool such as `assistant_ask_choice` whose form renders elsewhere in the turn. The row still opens to the full call. */
236
230
  function GenericCard({ name, input, result, runStreaming, runSucceeded }) {
237
231
  const summary = describeInput(input);
238
- return (_jsxs("div", { className: "op-card op-generic", children: [_jsxs("div", { className: "op-card-head", children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: truncate(summary, 200) }) : null] }), _jsx(ToolResultMedia, { result: result })] }));
232
+ return (_jsx(ExpandableCard, { className: "op-generic", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: truncate(summary, 200) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
233
+ }
234
+ /**
235
+ * The one row shell every card shares: a real `<button>` head (Enter/Space toggle it) that reports
236
+ * `aria-expanded` and points `aria-controls` at its panel, and a panel that stays mounted while
237
+ * collapsed — a `page.*` DOM reader sees the detail through `textContent` without anyone clicking.
238
+ * `after` renders outside the panel, for content that must stay visible collapsed (an error, media).
239
+ */
240
+ function ExpandableCard({ className, head, after, children }) {
241
+ const [open, setOpen] = useState(false);
242
+ const panelId = useId();
243
+ return (_jsxs("div", { className: `op-card ${className}`, children: [_jsxs("button", { type: "button", className: "op-card-head", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen((o) => !o), children: [head, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { id: panelId, className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: children }) }), after] }));
244
+ }
245
+ /** The call's input, its result (or why there is none), and any extra `children` — see `describeToolCallDetail` for the redaction and formatting rules. */
246
+ function ToolCallDetail({ input, result, runStreaming, runSucceeded, children }) {
247
+ const t = useT();
248
+ const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded });
249
+ return (_jsxs("div", { className: "op-card-detail", children: [detail.inputText !== null ? _jsx("pre", { className: "op-command op-input", children: detail.inputText }) : null, detail.outputText !== null ? _jsx("pre", { className: "op-output", children: detail.outputText }) : null, detail.noteKey !== null ? _jsx("p", { className: "op-detail-note", children: t(detail.noteKey) }) : null, children] }));
239
250
  }
240
251
  function ResultBadge({ result, runStreaming, runSucceeded }) {
241
252
  const t = useT();
@@ -6,5 +6,5 @@ import type { ChatPaneProps } from '../types.js';
6
6
  * @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
7
7
  * @overallScore 100/100
8
8
  */
9
- export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
9
+ export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, deliverMidRunMessage, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
10
10
  //# sourceMappingURL=ChatPane.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { SECRET_REDACTED_NOTICE } from '../../../../core/user-text-redaction.js';
3
- import { useEffect, useRef } from 'react';
3
+ import { useEffect, useMemo, useRef } from 'react';
4
4
  import { WorkingDirPicker } from '@jini-ai/ui';
5
5
  import { Composer } from '../../../components/Composer.js';
6
6
  import { appendComposerDiscovery } from '../../../components/composer-discovery.js';
@@ -12,6 +12,7 @@ import { useChatPane } from '../hooks/useChatPane.hooks.js';
12
12
  import { useChatPaneAgentControl } from '../hooks/useChatPaneAgentControl.hooks.js';
13
13
  import { useChatPaneComposerPlaceholder } from '../hooks/useChatPaneComposerPlaceholder.hooks.js';
14
14
  import { useChatPaneControlsHeight } from '../hooks/useChatPaneControlsHeight.hooks.js';
15
+ import { useChatPaneNewTabLinks } from '../hooks/useChatPaneNewTabLinks.hooks.js';
15
16
  import { useChatPaneFileDrop, } from '../hooks/useChatPaneFileDrop.hooks.js';
16
17
  import { useChatPaneRuntimeInventory, } from '../hooks/useChatPaneRuntimeInventory.hooks.js';
17
18
  import { CHAT_PANE_STYLES } from '../styles.js';
@@ -179,7 +180,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
179
180
  * @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
180
181
  * @overallScore 100/100
181
182
  */
182
- export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, runtimeAccess, runtimeStatusPollMs = 5_000, title, variant = 'workspace', initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
183
+ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, runtimeAccess, runtimeStatusPollMs = 5_000, title, variant = 'workspace', initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, deliverMidRunMessage, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
183
184
  const t = useT();
184
185
  const composerPlaceholder = useChatPaneComposerPlaceholder({ placeholder: placeholder, placeholders: placeholders });
185
186
  const inventory = useChatPaneRuntimeInventory(definedProps({ source: {
@@ -216,6 +217,7 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
216
217
  workingDirectoryAccess,
217
218
  apiModeConfigured,
218
219
  deliverTypedAnswer,
220
+ deliverMidRunMessage,
219
221
  } }));
220
222
  // Mirrors `Composer.tsx`'s own `draftRef`: `composerHandle.insertText` (below) is called from
221
223
  // OUTSIDE any render, so it cannot close over `pane.composer.draft` directly — that would freeze
@@ -268,6 +270,12 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
268
270
  footerAccessories: (_jsx(AgentRuntimePicker, { agents: runtimeView.agents, value: pane.selection, onChange: pane.setSelection, ...definedProps({ source: { onRescan: runtimeView.rescanAgents } }), scanning: runtimeView.scanningAgents, daemonOnline: runtimeView.daemonOnline, placement: runtimePickerPlacement, executionMode: executionMode, apiModeAvailable: apiModeAvailable, ...definedProps({ source: { onExecutionModeChange, byokRuntime, onByokModelChange } }) })),
269
271
  } });
270
272
  const { rootRef, controlsRef } = useChatPaneControlsHeight();
271
- return (_jsxs("section", { className: chatPaneClassName(variant, className), style: style, "data-activity": pane.activity, ref: rootRef, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(header, title, pane.reset, t), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompt: pane.queuedPrompt === null ? null : { text: pane.queuedPrompt, onCancel: pane.cancelQueued }, ...(projectFileNames === undefined ? {} : { projectFileNames: new Set(projectFileNames) }) }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [_jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: pane.composer.setDraft, t: t }), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, conversationError: pane.conversation.error, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, onSendAsNewMessage: () => void pane.sendAsNewMessage(), t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: uploadAttachments, pane: pane, disabled: disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: attachmentAccept, workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, t: t })] })] }), footer] }));
273
+ const openLinkInNewTab = useChatPaneNewTabLinks();
274
+ // `MessageList` is memoized so a composer keystroke does not re-render the transcript; these two
275
+ // props would otherwise be fresh objects on every render and defeat it.
276
+ const { queuedPrompt, cancelQueued } = pane;
277
+ const pendingPrompt = useMemo(() => (queuedPrompt === null ? null : { text: queuedPrompt, onCancel: cancelQueued }), [queuedPrompt, cancelQueued]);
278
+ const projectFileNameSet = useMemo(() => (projectFileNames === undefined ? undefined : new Set(projectFileNames)), [projectFileNames]);
279
+ return (_jsxs("section", { className: chatPaneClassName(variant, className), style: style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(header, title, pane.reset, t), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompt: pendingPrompt, ...(projectFileNameSet === undefined ? {} : { projectFileNames: projectFileNameSet }) }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [_jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: pane.composer.setDraft, t: t }), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, conversationError: pane.conversation.error, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, onSendAsNewMessage: () => void pane.sendAsNewMessage(), t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: uploadAttachments, pane: pane, disabled: disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: attachmentAccept, workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, t: t })] })] }), footer] }));
272
280
  }
273
281
  //# sourceMappingURL=ChatPane.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @module create-mid-run-message-poster
3
+ *
4
+ * The client half of a mid-run message: a ready-made {@link DeliverMidRunMessage} that posts text the
5
+ * human sent while a run streams to that run's live agent. Pass it as `ChatPane`'s
6
+ * `deliverMidRunMessage`.
7
+ *
8
+ * ```tsx
9
+ * <ChatPane deliverMidRunMessage={createMidRunMessagePoster({ baseUrl: '', fetch })} />
10
+ * ```
11
+ *
12
+ * Sibling of `create-typed-answer-poster.ts`. The route is `@jini-ai/daemon`'s
13
+ * `POST /api/runs/:runId/messages`: `202` when the agent took the text into its running turn, `409`
14
+ * with `details.delivery` when it did not.
15
+ *
16
+ * ## Why it resolves an outcome instead of throwing
17
+ *
18
+ * The pane acts differently on each outcome and must never drop the text: `'unsupported'` stops the
19
+ * run and sends the text as the next turn; `'not-running'` and `'failed'` queue it behind the run,
20
+ * which is already ending (or may be — never cut short a run the host could not reach). A 404 means
21
+ * the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
22
+ * too: no live process to steer, but an interrupt still works.
23
+ */
24
+ import { type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
25
+ import type { DeliverMidRunMessage } from './types.js';
26
+ export interface CreateMidRunMessagePosterOptions extends UserTextRedactionOptions {
27
+ /** Abandon a post after this many ms (reported as `'failed'`). Defaults to 30s. */
28
+ readonly timeoutMs?: number;
29
+ /** Extra headers per request (e.g. a CSRF token). `content-type` is always set and cannot be overridden. */
30
+ readonly headers?: Readonly<Record<string, string>>;
31
+ }
32
+ /**
33
+ * Builds a {@link DeliverMidRunMessage} that POSTs `{text}` to `${baseUrl}/api/runs/:runId/messages`.
34
+ *
35
+ * @param deps.baseUrl - Origin to call; `''` for same-origin.
36
+ * @param deps.fetch - The fetch to use (the browser's, in production).
37
+ * @returns Resolves to the outcome; never rejects.
38
+ * @complexity O(1) — one request per call.
39
+ */
40
+ export declare function createMidRunMessagePoster({ baseUrl, fetch }: {
41
+ baseUrl: string;
42
+ fetch: typeof globalThis.fetch;
43
+ }, options?: CreateMidRunMessagePosterOptions): DeliverMidRunMessage;
44
+ //# sourceMappingURL=create-mid-run-message-poster.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module create-mid-run-message-poster
3
+ *
4
+ * The client half of a mid-run message: a ready-made {@link DeliverMidRunMessage} that posts text the
5
+ * human sent while a run streams to that run's live agent. Pass it as `ChatPane`'s
6
+ * `deliverMidRunMessage`.
7
+ *
8
+ * ```tsx
9
+ * <ChatPane deliverMidRunMessage={createMidRunMessagePoster({ baseUrl: '', fetch })} />
10
+ * ```
11
+ *
12
+ * Sibling of `create-typed-answer-poster.ts`. The route is `@jini-ai/daemon`'s
13
+ * `POST /api/runs/:runId/messages`: `202` when the agent took the text into its running turn, `409`
14
+ * with `details.delivery` when it did not.
15
+ *
16
+ * ## Why it resolves an outcome instead of throwing
17
+ *
18
+ * The pane acts differently on each outcome and must never drop the text: `'unsupported'` stops the
19
+ * run and sends the text as the next turn; `'not-running'` and `'failed'` queue it behind the run,
20
+ * which is already ending (or may be — never cut short a run the host could not reach). A 404 means
21
+ * the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
22
+ * too: no live process to steer, but an interrupt still works.
23
+ */
24
+ import { redactUserText } from '../../../core/user-text-redaction.js';
25
+ const DEFAULT_TIMEOUT_MS = 30_000;
26
+ /** Reads `error.details.delivery` off a 409 body; anything unreadable counts as `'unsupported'`. */
27
+ async function conflictDelivery(response) {
28
+ const body = await response.json().catch(() => null);
29
+ const delivery = body?.error?.details?.delivery;
30
+ return delivery === 'not-running' ? 'not-running' : 'unsupported';
31
+ }
32
+ /** Maps the route's response onto an outcome. Only 202 is a delivery. */
33
+ async function deliveryFor(response) {
34
+ if (response.status === 202)
35
+ return 'delivered';
36
+ if (response.status === 409)
37
+ return conflictDelivery(response);
38
+ return response.status === 404 ? 'unsupported' : 'failed';
39
+ }
40
+ /**
41
+ * Builds a {@link DeliverMidRunMessage} that POSTs `{text}` to `${baseUrl}/api/runs/:runId/messages`.
42
+ *
43
+ * @param deps.baseUrl - Origin to call; `''` for same-origin.
44
+ * @param deps.fetch - The fetch to use (the browser's, in production).
45
+ * @returns Resolves to the outcome; never rejects.
46
+ * @complexity O(1) — one request per call.
47
+ */
48
+ export function createMidRunMessagePoster({ baseUrl, fetch }, options = {}) {
49
+ const origin = baseUrl.replace(/\/$/u, '');
50
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
51
+ return async ({ runId, text }) => {
52
+ const result = (options.redactUserText ?? redactUserText)({ text });
53
+ if (result.secretRedacted)
54
+ options.onSecretRedacted?.({ secretRedacted: true, count: result.count });
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
57
+ const outcome = await fetch(`${origin}/api/runs/${encodeURIComponent(runId)}/messages`, {
58
+ method: 'POST',
59
+ // Same-origin cookie session: the human's own credentials authorize steering their run.
60
+ credentials: 'same-origin',
61
+ headers: { ...options.headers, 'content-type': 'application/json' },
62
+ body: JSON.stringify({ text: result.text }),
63
+ signal: controller.signal,
64
+ }).then(deliveryFor, () => 'failed');
65
+ clearTimeout(timer);
66
+ return outcome;
67
+ };
68
+ }
69
+ //# sourceMappingURL=create-mid-run-message-poster.js.map
@@ -1,6 +1,6 @@
1
1
  import type { UserTextRedactionOptions } from '../../../../core/user-text-redaction.js';
2
2
  import { type ChatPaneSendBlocker } from '../rules.js';
3
- import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
3
+ import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverMidRunMessage, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
4
4
  import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
5
5
  import type { ComposerHistoryStoragePort } from '../../../../core/composer-history.js';
6
6
  import type { ChatTransport } from '@jini-ai/chat';
@@ -36,6 +36,8 @@ export interface UseChatPaneOptions extends UserTextRedactionOptions {
36
36
  apiModeConfigured?: boolean;
37
37
  /** See `ChatPaneProps.deliverTypedAnswer`. Omitted keeps today's queue-while-streaming behavior. */
38
38
  deliverTypedAnswer?: DeliverTypedAnswer;
39
+ /** See `ChatPaneProps.deliverMidRunMessage`. Omitted keeps today's queue-while-streaming behavior. */
40
+ deliverMidRunMessage?: DeliverMidRunMessage;
39
41
  }
40
42
  export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
41
43
  /** Show a localized non-secret notice when a paste was removed. */
@@ -1,6 +1,6 @@
1
1
  import { useUserTextGuard } from '../../../hooks/useUserTextGuard.js';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
3
+ import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, findMidRunMessageRunId, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
4
4
  import { definedProps } from '../../../util/defined-props.js';
5
5
  import { cacheAttachmentPreviewSource } from '../../../hooks/attachment-preview-cache.js';
6
6
  import { readCachedAttachmentBatchId, writeCachedAttachmentBatchId, } from '../../../hooks/composer-draft-cache.js';
@@ -34,6 +34,20 @@ async function deliverTypedAnswerSafely({ deliver, text }) {
34
34
  return 'failed';
35
35
  }
36
36
  }
37
+ /**
38
+ * Hands one mid-run message to the host. A throw counts as `'failed'`, which queues the text — the
39
+ * host could not say whether the run took it, and a message must never be dropped.
40
+ *
41
+ * @complexity O(1) plus the host's own request.
42
+ */
43
+ async function deliverMidRunMessageSafely({ deliver, runId, text }) {
44
+ try {
45
+ return await deliver({ runId, text });
46
+ }
47
+ catch {
48
+ return 'failed';
49
+ }
50
+ }
37
51
  /** Module scope so each branch costs 1 cognitive point instead of 2 (nested one level inside
38
52
  * `useChatPane` where it used to live as a ternary chain). */
39
53
  function resolveChatPaneActivity(selectedAgent, conversation) {
@@ -344,20 +358,55 @@ export function useChatPane(options) {
344
358
  if (latestDraftRef.current === submittedDraft)
345
359
  composer.setDraft('');
346
360
  }, [awaitedQuestionId, composer, guard.redactText]);
347
- // The ordinary next-turn path: queue behind a running run, else send now.
361
+ // A text-only message sent while a run streams goes INTO that run when the host can deliver it.
362
+ // The draft clears before the await so the human sees it go out; whatever the outcome, the text
363
+ // ends up somewhere — in the run, or queued as the next turn (after stopping the run when the
364
+ // agent cannot take input mid-run). Never cancel on 'not-running'/'failed': the run is ending.
365
+ const sendIntoRunningTurn = useCallback(async ({ deliver, runId, turn }) => {
366
+ queuedConversationIdRef.current = options.conversationId;
367
+ composer.setDraft('');
368
+ const outcome = await deliverMidRunMessageSafely({ deliver, runId, text: turn.text });
369
+ if (outcome === 'delivered' || !mountedRef.current)
370
+ return;
371
+ setQueuedTurn(turn);
372
+ if (outcome === 'unsupported')
373
+ conversation.cancel();
374
+ }, [composer, conversation, options.conversationId]);
375
+ // The ordinary next-turn path: into the running run, else queue behind it, else send now.
348
376
  const sendOrdinaryTurn = useCallback(async (prompt) => {
349
- // Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
350
- // staged attachments, which this turn still needs when it finally goes out.
351
377
  if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
378
+ const turn = guard.redact({ text: prompt }, {});
379
+ const deliver = options.deliverMidRunMessage;
380
+ const runId = deliver === undefined ? null : findMidRunMessageRunId({
381
+ blocker: sendBlocker,
382
+ attachmentCount: composer.attachments.length,
383
+ messages: conversation.messages,
384
+ });
385
+ if (deliver !== undefined && runId !== null) {
386
+ await sendIntoRunningTurn({ deliver, runId, turn });
387
+ return;
388
+ }
389
+ // Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
390
+ // staged attachments, which this turn still needs when it finally goes out.
352
391
  queuedConversationIdRef.current = options.conversationId;
353
- setQueuedTurn(guard.redact({ text: prompt }, {}));
392
+ setQueuedTurn(turn);
354
393
  composer.setDraft('');
355
394
  return;
356
395
  }
357
396
  if (!canSend)
358
397
  return;
359
398
  await sendPrompt(prompt);
360
- }, [canSend, composer, options.conversationId, sendBlocker, sendPrompt, guard.redact]);
399
+ }, [
400
+ canSend,
401
+ composer,
402
+ conversation.messages,
403
+ options.conversationId,
404
+ options.deliverMidRunMessage,
405
+ sendBlocker,
406
+ sendIntoRunningTurn,
407
+ sendPrompt,
408
+ guard.redact,
409
+ ]);
361
410
  const send = useCallback(async () => {
362
411
  const prompt = composerPrompt(composer);
363
412
  if (!prompt || deliveringTypedAnswerRef.current)
@@ -0,0 +1,25 @@
1
+ import type { MouseEvent as ReactMouseEvent } from 'react';
2
+ /**
3
+ * Decides whether a click inside the pane should open a link in a new tab.
4
+ *
5
+ * @param input.event - The click, native (React's `nativeEvent` or a DOM listener's own).
6
+ * @param input.root - The pane root; an anchor outside it (one wrapping the whole pane) is ignored.
7
+ * @returns The anchor's resolved absolute URL to open, or `null` to leave the click to the browser.
8
+ * @complexity O(d) for `closest` over the target's ancestor depth d.
9
+ */
10
+ export declare function newTabLinkHref({ event, root }: {
11
+ event: MouseEvent;
12
+ root: Element;
13
+ }): string | null;
14
+ /**
15
+ * The pane root's `onClickCapture` handler: opens qualifying link clicks in a new tab and cancels
16
+ * the in-tab navigation. See this module's doc for what qualifies.
17
+ *
18
+ * @param _required - Reserved; no required inputs.
19
+ * @param options.openInNewTab - Opener port; defaults to `window.open(url, '_blank', 'noopener,noreferrer')`.
20
+ * @returns A stable click-capture handler.
21
+ */
22
+ export declare function useChatPaneNewTabLinks(_required?: Record<string, never>, { openInNewTab }?: {
23
+ openInNewTab?: (url: string) => void;
24
+ }): (event: ReactMouseEvent<HTMLElement>) => void;
25
+ //# sourceMappingURL=useChatPaneNewTabLinks.hooks.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @module useChatPaneNewTabLinks
3
+ *
4
+ * Every link inside the chat pane opens in a new tab, so following one never navigates the tab the
5
+ * conversation lives in (owner 2026-10-08: "every link"). One capture-phase click guard on the
6
+ * pane's root absorbs every link source at once — markdown in messages, tool cards, host-registered
7
+ * ext-event cards, A2UI surfaces, the header slot — instead of each renderer setting `target` on its
8
+ * own anchors and the next new card forgetting to.
9
+ *
10
+ * Capture phase, then `preventDefault()`: the guard runs before any anchor's own `onClick`, so a
11
+ * router `<Link>` (React Router, TanStack Router — both skip their in-app navigation once
12
+ * `defaultPrevented` is set) does not ALSO navigate the chat's tab after the new tab opens.
13
+ *
14
+ * Left to the browser: modified or non-primary clicks (the browser already opens those however the
15
+ * user asked), anchors that already say `target="_blank"`, `#` same-document anchors, `download`
16
+ * links, `role="button"` anchors (an action, not a link), and any scheme other than http(s) —
17
+ * `mailto:`/`tel:` hand off to another app without navigating the tab anyway.
18
+ *
19
+ * MCP-UI Views render inside a sandboxed iframe, out of this guard's reach; their proxy page
20
+ * forwards link clicks as `ui/open-link` (`@jini-ai/ui/mcp-ui`'s sandbox proxy) instead.
21
+ */
22
+ import { useCallback } from 'react';
23
+ const NEW_TAB_PROTOCOLS = new Set(['http:', 'https:']);
24
+ /** Opens `url` in a new tab with no opener or referrer — `rel="noopener noreferrer"` for `window.open`. */
25
+ function openInNewTabDefault(url) {
26
+ window.open(url, '_blank', 'noopener,noreferrer');
27
+ }
28
+ /**
29
+ * Decides whether a click inside the pane should open a link in a new tab.
30
+ *
31
+ * @param input.event - The click, native (React's `nativeEvent` or a DOM listener's own).
32
+ * @param input.root - The pane root; an anchor outside it (one wrapping the whole pane) is ignored.
33
+ * @returns The anchor's resolved absolute URL to open, or `null` to leave the click to the browser.
34
+ * @complexity O(d) for `closest` over the target's ancestor depth d.
35
+ */
36
+ export function newTabLinkHref({ event, root }) {
37
+ if (event.defaultPrevented || event.button !== 0)
38
+ return null;
39
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
40
+ return null;
41
+ const target = event.target;
42
+ if (!(target instanceof Element))
43
+ return null;
44
+ const link = target.closest('a[href]');
45
+ if (!(link instanceof HTMLAnchorElement) || !root.contains(link))
46
+ return null;
47
+ if (link.target === '_blank' || link.hasAttribute('download') || link.getAttribute('role') === 'button')
48
+ return null;
49
+ if (link.getAttribute('href').startsWith('#'))
50
+ return null;
51
+ return NEW_TAB_PROTOCOLS.has(link.protocol) ? link.href : null;
52
+ }
53
+ /**
54
+ * The pane root's `onClickCapture` handler: opens qualifying link clicks in a new tab and cancels
55
+ * the in-tab navigation. See this module's doc for what qualifies.
56
+ *
57
+ * @param _required - Reserved; no required inputs.
58
+ * @param options.openInNewTab - Opener port; defaults to `window.open(url, '_blank', 'noopener,noreferrer')`.
59
+ * @returns A stable click-capture handler.
60
+ */
61
+ export function useChatPaneNewTabLinks(_required = {}, { openInNewTab = openInNewTabDefault } = {}) {
62
+ return useCallback((event) => {
63
+ const href = newTabLinkHref({ event: event.nativeEvent, root: event.currentTarget });
64
+ if (href === null)
65
+ return;
66
+ event.preventDefault();
67
+ openInNewTab(href);
68
+ }, [openInNewTab]);
69
+ }
70
+ //# sourceMappingURL=useChatPaneNewTabLinks.hooks.js.map
@@ -1,8 +1,9 @@
1
- export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, } from './types.js';
1
+ export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, DeliverMidRunMessage, DeliverTypedAnswer, MidRunMessageDelivery, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, } from './types.js';
2
2
  export { CHAT_PANE_AGENT_TOOLS, type ChatPaneAgentToolDef, type ChatPaneAgentToolInputSchema, type ChatPaneAgentToolRisk, } from './agent-tools.js';
3
3
  export { defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, } from './rules.js';
4
4
  export { createDaemonAttachmentUploader, type CreateDaemonAttachmentUploaderOptions, } from './create-daemon-attachment-uploader.js';
5
5
  export { createMcpUiToolCaller, type CreateMcpUiToolCallerOptions, type McpUiToolCallRequest, } from './create-mcp-ui-tool-caller.js';
6
+ export { createMidRunMessagePoster, type CreateMidRunMessagePosterOptions, } from './create-mid-run-message-poster.js';
6
7
  export { createTypedAnswerPoster, type CreateTypedAnswerPosterOptions, } from './create-typed-answer-poster.js';
7
8
  export { useChatPane, type UseChatPaneOptions, type UseChatPaneResult, } from './hooks/useChatPane.hooks.js';
8
9
  export { useChatPaneAgentControl, type UseChatPaneAgentControlOptions, } from './hooks/useChatPaneAgentControl.hooks.js';
@@ -2,6 +2,7 @@ export { CHAT_PANE_AGENT_TOOLS, } from './agent-tools.js';
2
2
  export { defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, } from './rules.js';
3
3
  export { createDaemonAttachmentUploader, } from './create-daemon-attachment-uploader.js';
4
4
  export { createMcpUiToolCaller, } from './create-mcp-ui-tool-caller.js';
5
+ export { createMidRunMessagePoster, } from './create-mid-run-message-poster.js';
5
6
  export { createTypedAnswerPoster, } from './create-typed-answer-poster.js';
6
7
  export { useChatPane, } from './hooks/useChatPane.hooks.js';
7
8
  export { useChatPaneAgentControl, } from './hooks/useChatPaneAgentControl.hooks.js';
@@ -126,6 +126,19 @@ export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages,
126
126
  messages: readonly ChatMessage[];
127
127
  toolName: string;
128
128
  }): boolean;
129
+ /**
130
+ * The run a composer send can go INTO instead of queueing behind, or `null`: a run is streaming
131
+ * ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a mid-run message carries text
132
+ * only — the live agent's stdin takes no files), and the newest message is that run's assistant
133
+ * turn with a known `runId`.
134
+ *
135
+ * @complexity O(1).
136
+ */
137
+ export declare function findMidRunMessageRunId({ blocker, attachmentCount, messages }: {
138
+ blocker: ChatPaneSendBlocker | null;
139
+ attachmentCount: number;
140
+ messages: readonly ChatMessage[];
141
+ }): string | null;
129
142
  /** The English copy (and i18n key) for why a typed answer was not sent. */
130
143
  export declare function describeTypedAnswerNotice({ notice }: {
131
144
  notice: TypedAnswerNotice;