@jini-ai/chat 0.5.0 → 0.5.2

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 (35) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/core/index.d.ts +1 -0
  3. package/dist/core/index.js +1 -0
  4. package/dist/core/surface-expiry.d.ts +42 -0
  5. package/dist/core/surface-expiry.js +42 -0
  6. package/dist/react/chat-pane.d.ts +2 -2
  7. package/dist/react/chat-pane.js +1 -1
  8. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +19 -2
  9. package/dist/react/components/AttachmentPreviewModal.hooks.js +53 -38
  10. package/dist/react/components/McpUiSurfaceCard.d.ts +7 -1
  11. package/dist/react/components/McpUiSurfaceCard.js +23 -38
  12. package/dist/react/components/MessageList.d.ts +11 -0
  13. package/dist/react/components/MessageList.hooks.d.ts +19 -0
  14. package/dist/react/components/MessageList.hooks.js +76 -0
  15. package/dist/react/components/MessageList.js +3 -71
  16. package/dist/react/components/MessageList.rules.d.ts +25 -0
  17. package/dist/react/components/MessageList.rules.js +23 -0
  18. package/dist/react/components/mcp-ui-surface-state.d.ts +27 -0
  19. package/dist/react/components/mcp-ui-surface-state.js +33 -0
  20. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  21. package/dist/react/features/chat-pane/components/ChatPane.js +6 -5
  22. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +54 -0
  23. package/dist/react/features/chat-pane/create-typed-answer-poster.js +46 -0
  24. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +17 -1
  25. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +120 -4
  26. package/dist/react/features/chat-pane/index.d.ts +2 -1
  27. package/dist/react/features/chat-pane/index.js +1 -0
  28. package/dist/react/features/chat-pane/rules.d.ts +56 -1
  29. package/dist/react/features/chat-pane/rules.js +71 -0
  30. package/dist/react/features/chat-pane/types.d.ts +34 -0
  31. package/dist/react/hooks/useSurfaceExpiry.d.ts +20 -0
  32. package/dist/react/hooks/useSurfaceExpiry.js +35 -0
  33. package/dist/react/index.d.ts +4 -2
  34. package/dist/react/index.js +2 -1
  35. package/package.json +8 -8
@@ -1,80 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * @module MessageList
4
- *
5
- * Renders a conversation's messages via `<MessageRow>`, and — matching
6
- * `useConversation`'s `scrollIntent` flag (see
7
- * `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §4: "message array, optimistic
8
- * user message, scroll-intent flag") — auto-scrolls to the newest content
9
- * whenever `scrollIntent` is true, calling `onScrolled` once it has. Pure
10
- * DOM scroll-anchoring via a ref lives here (a presentational component),
11
- * not in the headless `useConversation` hook, per that hook's own
12
- * "transport-agnostic hooks never touch DOM" contract.
13
- */
14
- import { useEffect, useRef } from 'react';
15
2
  import { isTerminalRunStatus } from '../../core/index.js';
16
3
  import { useT } from '../hooks/context.js';
4
+ import { useMessageListAutoScroll } from './MessageList.hooks.js';
17
5
  import { MessageRow } from './MessageRow.js';
18
- /** Slack, in px, for treating "close enough to the bottom" as "at the bottom" — a fractional
19
- * scrollHeight/clientHeight rounding difference should not by itself stop the sticky-follow below. */
20
- const AT_BOTTOM_THRESHOLD_PX = 4;
21
- function isScrolledToBottom(el) {
22
- return el.scrollHeight - el.scrollTop - el.clientHeight <= AT_BOTTOM_THRESHOLD_PX;
23
- }
24
6
  export function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, pendingPrompt = null, }) {
25
7
  const t = useT();
26
- const containerRef = useRef(null);
27
- // Whether the transcript was scrolled to its bottom as of the last user scroll or programmatic
28
- // scroll-to-bottom — read by the ResizeObserver effect below, which only ever sees the DOM AFTER a
29
- // resize already happened and so cannot recompute "was this at the bottom" from current values.
30
- // Starts true: a freshly mounted pane opens on the latest turn, same as scrollIntent's own default.
31
- const stickToBottomRef = useRef(true);
32
- useEffect(() => {
33
- if (!scrollIntent)
34
- return;
35
- const el = containerRef.current;
36
- // Gated on `stickToBottomRef`, same as the ResizeObserver effect below: `scrollIntent` turns
37
- // true on EVERY streamed event (tool-call chatter included, not just a finished reply — see
38
- // `useConversation.applyRunToAssistantMessage`), so scrolling unconditionally here yanked a user
39
- // who had deliberately scrolled up to read history back to the bottom on the very next chunk
40
- // (owner-reported, 2026-08-30). `stickToBottomRef` already reflects the user's own last scroll
41
- // gesture via the `onScroll` handler below, captured BEFORE this content change, so it is safe
42
- // to trust here.
43
- if (el && stickToBottomRef.current) {
44
- el.scrollTop = el.scrollHeight;
45
- }
46
- onScrolled?.();
47
- // eslint-disable-next-line react-hooks/exhaustive-deps
48
- }, [scrollIntent, messages]);
49
- // Re-sticks to the bottom when a message ROW grows after mount with no accompanying `messages`
50
- // change — the exact case an embedded MCP-UI/A2UI surface hits: it mounts at an initial guessed
51
- // height (`preferredFrameSize` or `DEFAULT_INITIAL_HEIGHT`), then reports its real, often much
52
- // taller content height asynchronously via `ui/notifications/size-changed`, entirely inside that
53
- // surface's own React state (`McpUiHost`). The effect above never re-runs for that, so without this
54
- // a surface which grows after the one-shot scroll leaves its own action buttons — or, once it
55
- // resolves, the assistant's next reply — below the stale "bottom" with nothing to bring them back
56
- // into view. Observes each direct child (one per message row) rather than the scroll container
57
- // itself: the container's own box is held fixed by its flex/overflow layout, so only its CONTENT
58
- // grows, which `ResizeObserver` only reports for the element whose box is actually changing.
59
- // Re-created per `messages` change so a newly mounted row is observed too. Gated on
60
- // `stickToBottomRef` so a human who scrolled up to read history is never yanked back down by an
61
- // unrelated row resizing elsewhere in the transcript.
62
- useEffect(() => {
63
- const el = containerRef.current;
64
- if (!el || typeof ResizeObserver !== 'function')
65
- return;
66
- const observer = new ResizeObserver(() => {
67
- if (!stickToBottomRef.current)
68
- return;
69
- el.scrollTop = el.scrollHeight;
70
- });
71
- for (const child of Array.from(el.children))
72
- observer.observe(child);
73
- return () => observer.disconnect();
74
- }, [messages]);
75
- return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: (event) => {
76
- stickToBottomRef.current = isScrolledToBottom(event.currentTarget);
77
- }, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
8
+ const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
9
+ return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
78
10
  const isLast = message.id === messages[messages.length - 1]?.id;
79
11
  const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
80
12
  return (_jsx(MessageRow, { message: message, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file Pure scroll rules for `MessageList` (no DOM, no React), split out so the component file
3
+ * holds no logic.
4
+ */
5
+ import type { ChatMessage } from '../../core/index.js';
6
+ /** Slack, in px, for treating "close enough to the bottom" as "at the bottom" — a fractional
7
+ * scrollHeight/clientHeight rounding difference should not by itself stop the sticky-follow. */
8
+ export declare const AT_BOTTOM_THRESHOLD_PX = 4;
9
+ export declare function isScrolledToBottom(el: {
10
+ scrollHeight: number;
11
+ scrollTop: number;
12
+ clientHeight: number;
13
+ }): boolean;
14
+ /**
15
+ * Whether `next` starts a turn the user themself asked for, relative to `previous`: a user message
16
+ * that was not there before (a send — `useConversation.sendMessage` appends it optimistically), or
17
+ * an assistant message that newly entered `runStatus: 'queued'` (a retry, which resets the existing
18
+ * assistant message in place rather than appending one). Streamed chatter on an already-running
19
+ * turn is neither, so it never counts.
20
+ */
21
+ export declare function startsNewTurn({ previous, next }: {
22
+ previous: readonly ChatMessage[];
23
+ next: readonly ChatMessage[];
24
+ }): boolean;
25
+ //# sourceMappingURL=MessageList.rules.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Slack, in px, for treating "close enough to the bottom" as "at the bottom" — a fractional
2
+ * scrollHeight/clientHeight rounding difference should not by itself stop the sticky-follow. */
3
+ export const AT_BOTTOM_THRESHOLD_PX = 4;
4
+ export function isScrolledToBottom(el) {
5
+ return el.scrollHeight - el.scrollTop - el.clientHeight <= AT_BOTTOM_THRESHOLD_PX;
6
+ }
7
+ /**
8
+ * Whether `next` starts a turn the user themself asked for, relative to `previous`: a user message
9
+ * that was not there before (a send — `useConversation.sendMessage` appends it optimistically), or
10
+ * an assistant message that newly entered `runStatus: 'queued'` (a retry, which resets the existing
11
+ * assistant message in place rather than appending one). Streamed chatter on an already-running
12
+ * turn is neither, so it never counts.
13
+ */
14
+ export function startsNewTurn({ previous, next }) {
15
+ const previousById = new Map(previous.map((message) => [message.id, message]));
16
+ return next.some((message) => {
17
+ const before = previousById.get(message.id);
18
+ if (message.role === 'user')
19
+ return before === undefined;
20
+ return message.runStatus === 'queued' && before?.runStatus !== 'queued';
21
+ });
22
+ }
23
+ //# sourceMappingURL=MessageList.rules.js.map
@@ -0,0 +1,27 @@
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
+ import type { ExtEventCall } from '../ext-event-renderer-registry.js';
9
+ import type { useT } from '../hooks/context.js';
10
+ type Translate = ReturnType<typeof useT>;
11
+ /**
12
+ * Why a card can no longer be answered, or `undefined` while it still can: {@link settledLabel}'s
13
+ * answer first, so a card answered in time still reads "Answered", then the card's own deadline
14
+ * (`useSurfaceExpiry`). The deadline closes the card the moment it passes rather than when the
15
+ * server's expired result arrives, so a person never clicks a card nobody is waiting on.
16
+ *
17
+ * @complexity O(r) in the call result's length (one regex test).
18
+ */
19
+ export declare function closedSurfaceLabel({ t, call, runStreaming, documentsForUri, expired }: {
20
+ t: Translate;
21
+ call: ExtEventCall | undefined;
22
+ runStreaming: boolean;
23
+ documentsForUri: number;
24
+ expired: boolean;
25
+ }): string | undefined;
26
+ export {};
27
+ //# sourceMappingURL=mcp-ui-surface-state.d.ts.map
@@ -0,0 +1,33 @@
1
+ /** Words in a finished call's result that mean nobody answered in time. */
2
+ const UNANSWERED_RESULT = /\b(expired|abandoned|did not respond|run ended)\b/i;
3
+ /**
4
+ * Why a card can no longer be answered, or `undefined` while it still can.
5
+ *
6
+ * A held-open question (the tool shows the card and waits) is over once its tool call returns, and
7
+ * dead once its run ends without that return. Its first document stays on screen either way, so
8
+ * without this an expired, answered or orphaned question still looked answerable (host stuck-chat
9
+ * investigation, 2026-09-27). Only a card still showing that FIRST document closes: a tool that sent
10
+ * a follow-up document for the same `ui://` URI (an outcome after the answer) keeps showing it.
11
+ *
12
+ * The labels are read from the call's result text, so they are a best guess for a producer this
13
+ * package has not seen; the card is closed either way, which is the property that matters.
14
+ */
15
+ function settledLabel(t, call, runStreaming, documentsForUri) {
16
+ if (call === undefined || documentsForUri > 1)
17
+ return undefined;
18
+ if (call.result === undefined)
19
+ return runStreaming ? undefined : t('This question expired');
20
+ return UNANSWERED_RESULT.test(call.result.content) ? t('This question expired') : t('Answered');
21
+ }
22
+ /**
23
+ * Why a card can no longer be answered, or `undefined` while it still can: {@link settledLabel}'s
24
+ * answer first, so a card answered in time still reads "Answered", then the card's own deadline
25
+ * (`useSurfaceExpiry`). The deadline closes the card the moment it passes rather than when the
26
+ * server's expired result arrives, so a person never clicks a card nobody is waiting on.
27
+ *
28
+ * @complexity O(r) in the call result's length (one regex test).
29
+ */
30
+ export function closedSurfaceLabel({ t, call, runStreaming, documentsForUri, expired }) {
31
+ return settledLabel(t, call, runStreaming, documentsForUri) ?? (expired ? t('This question expired') : undefined);
32
+ }
33
+ //# sourceMappingURL=mcp-ui-surface-state.js.map
@@ -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, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, validateAttachments, 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, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, 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;
10
10
  //# sourceMappingURL=ChatPane.d.ts.map
@@ -6,7 +6,7 @@ import { appendComposerDiscovery } from '../../../components/composer-discovery.
6
6
  import { MessageList } from '../../../components/MessageList.js';
7
7
  import { useT } from '../../../hooks/context.js';
8
8
  import { definedProps } from '../../../util/defined-props.js';
9
- import { isChatPaneApiModeConfigured } from '../rules.js';
9
+ import { describeTypedAnswerNotice, isChatPaneApiModeConfigured } from '../rules.js';
10
10
  import { useChatPane } from '../hooks/useChatPane.hooks.js';
11
11
  import { useChatPaneAgentControl } from '../hooks/useChatPaneAgentControl.hooks.js';
12
12
  import { useChatPaneComposerPlaceholder } from '../hooks/useChatPaneComposerPlaceholder.hooks.js';
@@ -73,8 +73,8 @@ function ChatPaneNoUsableCliMessage({ executionMode, apiModeAvailable, onExecuti
73
73
  }
74
74
  /** The pane's stack of mutually-independent error/status banners — each condition is its own
75
75
  * source of truth, so they are rendered as siblings rather than folded into one derived state. */
76
- function ChatPaneStatusMessages({ unavailable, scanningAgents, connectingAgents, executionMode, apiModeAvailable, onExecutionModeChange, conversationError, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, t, }) {
77
- return (_jsxs(_Fragment, { children: [unavailable && connectingAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Connecting to the assistant…') })) : unavailable && scanningAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Loading available CLIs') })) : unavailable ? (_jsx(ChatPaneNoUsableCliMessage, { executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t })) : null, conversationError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: conversationError.message })) : null, attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, workingDirectoryPending ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Checking working directory…') })) : workingDirectoryInvalid ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t('Working directory is unavailable.') })) : null, runtimeInventoryError && !connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null] }));
76
+ function ChatPaneStatusMessages({ unavailable, scanningAgents, connectingAgents, executionMode, apiModeAvailable, onExecutionModeChange, conversationError, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, onSendAsNewMessage, t, }) {
77
+ return (_jsxs(_Fragment, { children: [unavailable && connectingAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Connecting to the assistant…') })) : unavailable && scanningAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Loading available CLIs') })) : unavailable ? (_jsx(ChatPaneNoUsableCliMessage, { executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t })) : null, conversationError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: conversationError.message })) : null, attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, workingDirectoryPending ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Checking working directory…') })) : workingDirectoryInvalid ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t('Working directory is unavailable.') })) : null, runtimeInventoryError && !connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, typedAnswerNotice ? (_jsxs("div", { className: "jini-chat-pane__error", role: "alert", children: [t(describeTypedAnswerNotice({ notice: typedAnswerNotice })), typedAnswerNotice === 'not-pending' ? (_jsxs(_Fragment, { children: [' ', _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: onSendAsNewMessage, children: t('Send as a new message') })] })) : null] })) : null] }));
78
78
  }
79
79
  function ChatPaneWorkingDirectoryBlock({ workingDirectoryAccess, workingDirectoryControlPlacement, pane, t, }) {
80
80
  if (workingDirectoryAccess && workingDirectoryControlPlacement === 'below') {
@@ -178,7 +178,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
178
178
  * @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
179
179
  * @overallScore 100/100
180
180
  */
181
- 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, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, validateAttachments, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
181
+ 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, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, 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, }) {
182
182
  const t = useT();
183
183
  const composerPlaceholder = useChatPaneComposerPlaceholder({ placeholder: placeholder, placeholders: placeholders });
184
184
  const inventory = useChatPaneRuntimeInventory(definedProps({ source: {
@@ -210,6 +210,7 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
210
210
  onChangeWorkingDirectory,
211
211
  workingDirectoryAccess,
212
212
  apiModeConfigured,
213
+ deliverTypedAnswer,
213
214
  } }));
214
215
  // Mirrors `Composer.tsx`'s own `draftRef`: `composerHandle.insertText` (below) is called from
215
216
  // OUTSIDE any render, so it cannot close over `pane.composer.draft` directly — that would freeze
@@ -262,6 +263,6 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
262
263
  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 } }) })),
263
264
  } });
264
265
  const { rootRef, controlsRef } = useChatPaneControlsHeight();
265
- 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 }), _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, 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] }));
266
+ 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 }), _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] }));
266
267
  }
267
268
  //# sourceMappingURL=ChatPane.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module create-typed-answer-poster
3
+ *
4
+ * The client half of a typed answer: a ready-made {@link DeliverTypedAnswer} that posts text the
5
+ * human typed into the composer to the MCP-UI tool-call route, for the question a running agent is
6
+ * holding open. Pass it as `ChatPane`'s `deliverTypedAnswer`.
7
+ *
8
+ * ```tsx
9
+ * <ChatPane deliverTypedAnswer={createTypedAnswerPoster({ baseUrl: '', fetch, toolName: 'ask_choice' })} />
10
+ * ```
11
+ *
12
+ * Sibling of `create-mcp-ui-tool-caller.ts`, and the same wire shape: `{toolName, params}` to the
13
+ * same route. The difference is what the params carry and what the caller needs back.
14
+ *
15
+ * ## Why the body names no exchange
16
+ *
17
+ * A human typing into the composer has never seen an exchange id; the only client-side copy lives
18
+ * inside the surface's own (model-influenced, sandboxed) HTML. So the body carries only the tool and
19
+ * the text under {@link TYPED_ANSWER_PARAM}, and the server resolves which open exchange — for that
20
+ * tool and the request's own authenticated principal — the answer is for (`@jini-ai/daemon`'s
21
+ * `findTypedAnswerTarget`).
22
+ *
23
+ * ## Why it resolves an outcome instead of throwing
24
+ *
25
+ * The pane must tell three cases apart and act differently on each, never by queueing the text as a
26
+ * new run: 202 delivered it; 409 means nothing is waiting any more (answered, expired, or never
27
+ * open); anything else is a failure the human can retry. A rejection would collapse the last two.
28
+ */
29
+ import type { DeliverTypedAnswer } from './types.js';
30
+ export interface CreateTypedAnswerPosterOptions {
31
+ /** Path appended to `baseUrl`. Defaults to `/api/mcp-ui/tool-calls`, as for `createMcpUiToolCaller`. */
32
+ readonly path?: string;
33
+ /** Abandon a post after this many ms (reported as `'failed'`). Defaults to 30s. */
34
+ readonly timeoutMs?: number;
35
+ /** Extra headers per request (e.g. a CSRF token). `content-type` is always set and cannot be overridden. */
36
+ readonly headers?: Readonly<Record<string, string>>;
37
+ }
38
+ /**
39
+ * Builds a {@link DeliverTypedAnswer} that POSTs `{toolName, params: {__typedAnswer: text}}`.
40
+ *
41
+ * @param deps.baseUrl - Origin to call; `''` for same-origin.
42
+ * @param deps.fetch - The fetch to use (the browser's, in production).
43
+ * @param deps.toolName - The host's question tool id — the server only routes typed answers to a
44
+ * tool that opted in, so this package cannot pick it.
45
+ * @returns Resolves to the outcome; never rejects. Carries `toolName`, which also tells the pane
46
+ * which tool's open card is a question awaiting typed text.
47
+ * @complexity O(1) — one request per call.
48
+ */
49
+ export declare function createTypedAnswerPoster({ baseUrl, fetch, toolName }: {
50
+ baseUrl: string;
51
+ fetch: typeof globalThis.fetch;
52
+ toolName: string;
53
+ }, options?: CreateTypedAnswerPosterOptions): DeliverTypedAnswer;
54
+ //# sourceMappingURL=create-typed-answer-poster.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The params key a typed answer travels under. Spelled here rather than imported because this
3
+ * browser package cannot depend on `@jini-ai/daemon`, which owns the canonical
4
+ * `SURFACE_TYPED_ANSWER_PARAM`; `@jini-ai/mcp`'s ask-choice tool keeps its own copy for the same
5
+ * reason. All three must agree.
6
+ */
7
+ const TYPED_ANSWER_PARAM = '__typedAnswer';
8
+ const DEFAULT_PATH = '/api/mcp-ui/tool-calls';
9
+ const DEFAULT_TIMEOUT_MS = 30_000;
10
+ /** Maps the route's status onto an outcome. Only 202 is a delivery; a 200 would be a fresh execution. */
11
+ function deliveryFor(status) {
12
+ if (status === 202)
13
+ return 'delivered';
14
+ return status === 409 ? 'not-pending' : 'failed';
15
+ }
16
+ /**
17
+ * Builds a {@link DeliverTypedAnswer} that POSTs `{toolName, params: {__typedAnswer: text}}`.
18
+ *
19
+ * @param deps.baseUrl - Origin to call; `''` for same-origin.
20
+ * @param deps.fetch - The fetch to use (the browser's, in production).
21
+ * @param deps.toolName - The host's question tool id — the server only routes typed answers to a
22
+ * tool that opted in, so this package cannot pick it.
23
+ * @returns Resolves to the outcome; never rejects. Carries `toolName`, which also tells the pane
24
+ * which tool's open card is a question awaiting typed text.
25
+ * @complexity O(1) — one request per call.
26
+ */
27
+ export function createTypedAnswerPoster({ baseUrl, fetch, toolName }, options = {}) {
28
+ const endpoint = `${baseUrl.replace(/\/$/u, '')}${options.path ?? DEFAULT_PATH}`;
29
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
30
+ const deliver = async ({ text }) => {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
33
+ const outcome = await fetch(endpoint, {
34
+ method: 'POST',
35
+ // Same-origin cookie session: the human's own credentials authorize the answer.
36
+ credentials: 'same-origin',
37
+ headers: { ...options.headers, 'content-type': 'application/json' },
38
+ body: JSON.stringify({ toolName, params: { [TYPED_ANSWER_PARAM]: text } }),
39
+ signal: controller.signal,
40
+ }).then((response) => deliveryFor(response.status), () => 'failed');
41
+ clearTimeout(timer);
42
+ return outcome;
43
+ };
44
+ return Object.assign(deliver, { toolName });
45
+ }
46
+ //# sourceMappingURL=create-typed-answer-poster.js.map
@@ -1,5 +1,5 @@
1
1
  import { type ChatPaneSendBlocker } from '../rules.js';
2
- import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess } from '../types.js';
2
+ import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
3
3
  import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
4
4
  import type { ChatTransport } from '@jini-ai/chat';
5
5
  import { type UseComposerResult } from '../../../hooks/useComposer.js';
@@ -30,6 +30,8 @@ export interface UseChatPaneOptions {
30
30
  * behavior: no selected agent always blocks sending.
31
31
  */
32
32
  apiModeConfigured?: boolean;
33
+ /** See `ChatPaneProps.deliverTypedAnswer`. Omitted keeps today's queue-while-streaming behavior. */
34
+ deliverTypedAnswer?: DeliverTypedAnswer;
33
35
  }
34
36
  export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
35
37
  conversation: UseConversationResult;
@@ -76,6 +78,20 @@ export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
76
78
  queuedPrompt: string | null;
77
79
  /** Drops the queued prompt without ever sending it. */
78
80
  cancelQueued: () => void;
81
+ /**
82
+ * Why the last typed answer was NOT sent, or `null`. Set when `deliverTypedAnswer` reports the
83
+ * question no longer waiting or could not be reached, and when {@link send} holds back an answer
84
+ * whose question has since closed; the draft stays in the composer. Cleared by the next send and
85
+ * by `reset()`.
86
+ */
87
+ typedAnswerNotice: TypedAnswerNotice | null;
88
+ /**
89
+ * Sends the composer draft as an ordinary next turn (queued behind a running run, as {@link send}
90
+ * does) even though it was typed as an answer to a question that has since closed. The one way
91
+ * past {@link send}'s hold on such a draft other than clearing it: the human chose a new, possibly
92
+ * paid, run on purpose.
93
+ */
94
+ sendAsNewMessage: () => Promise<void>;
79
95
  /**
80
96
  * Cancels the run in flight and sends the composer draft as soon as it stops — the modifier-key
81
97
  * counterpart to {@link send}, which queues behind the run instead of ending it. Implemented as
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { describeChatPaneSendBlocker, findChatPaneSendBlocker, isChatPaneQueueableBlocker, resolveChatPaneSelection, } from '../rules.js';
2
+ import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
3
3
  import { definedProps } from '../../../util/defined-props.js';
4
4
  import { cacheAttachmentPreviewSource } from '../../../hooks/attachment-preview-cache.js';
5
5
  import { useComposer } from '../../../hooks/useComposer.js';
@@ -18,6 +18,20 @@ function createAttachmentBatchId() {
18
18
  return globalThis.crypto?.randomUUID?.()
19
19
  ?? `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
20
20
  }
21
+ /**
22
+ * Hands one typed answer to the host. A throw counts as `'failed'`: the host could not say whether
23
+ * anything was waiting, and the one outcome that must never follow is queueing the text as a new run.
24
+ *
25
+ * @complexity O(1) plus the host's own request.
26
+ */
27
+ async function deliverTypedAnswerSafely({ deliver, text }) {
28
+ try {
29
+ return await deliver({ text });
30
+ }
31
+ catch {
32
+ return 'failed';
33
+ }
34
+ }
21
35
  /** Module scope so each branch costs 1 cognitive point instead of 2 (nested one level inside
22
36
  * `useChatPane` where it used to live as a ternary chain). */
23
37
  function resolveChatPaneActivity(selectedAgent, conversation) {
@@ -61,6 +75,21 @@ export function useChatPane(options) {
61
75
  const [activeUploadCount, setActiveUploadCount] = useState(0);
62
76
  const [attachmentError, setAttachmentError] = useState(null);
63
77
  const [queuedPrompt, setQueuedPrompt] = useState(null);
78
+ const [typedAnswerNotice, setTypedAnswerNotice] = useState(null);
79
+ // Set across the await of one typed-answer delivery so a second Enter cannot post the same text
80
+ // twice — the second post would find the question already answered and wrongly report it closed.
81
+ const deliveringTypedAnswerRef = useRef(false);
82
+ // The question (tool-use id) the current draft was typed for, or null. Set while a non-empty draft
83
+ // sits in the composer as a question waits, and kept after that question closes (answered
84
+ // elsewhere, expired, 409), so `send` cannot slip the draft into the queue as a new paid run.
85
+ // Released only by an empty draft, a delivery, `reset()`, or `sendAsNewMessage`.
86
+ const heldAnswerQuestionIdRef = useRef(null);
87
+ // The question the last delivery answered: typing more while its card is still drawn open (its
88
+ // tool_result has not streamed in yet) is a follow-up, not another answer to hold.
89
+ const answeredQuestionIdRef = useRef(null);
90
+ // Bumped by `reset()` and a conversation switch; a delivery that settles under a different value
91
+ // belongs to a composer that no longer exists and must neither clear the draft nor set a notice.
92
+ const typedAnswerGenerationRef = useRef(0);
64
93
  // The conversation `queuedPrompt` was queued against. Compared with `options.conversationId` at
65
94
  // flush time so a prompt queued behind a streaming run in one conversation can never be posted
66
95
  // into a different one the caller switched to before that run finished.
@@ -150,6 +179,26 @@ export function useChatPane(options) {
150
179
  ...(options.apiModeConfigured === undefined ? {} : { apiModeConfigured: options.apiModeConfigured }),
151
180
  });
152
181
  const canSend = sendBlocker === null && composer.canSubmit;
182
+ // Read after `await` in `answerWaitingQuestion`, where the render-time `composer.draft` is stale.
183
+ const latestDraftRef = useRef(composer.draft);
184
+ latestDraftRef.current = composer.draft;
185
+ const awaitedQuestionId = options.deliverTypedAnswer === undefined
186
+ ? null
187
+ : findAwaitedTypedAnswerId({ messages: conversation.messages, toolName: options.deliverTypedAnswer.toolName });
188
+ const hasDraft = composer.draft.trim() !== '';
189
+ useEffect(() => {
190
+ if (!hasDraft) {
191
+ heldAnswerQuestionIdRef.current = null;
192
+ return;
193
+ }
194
+ if (awaitedQuestionId !== null && awaitedQuestionId !== answeredQuestionIdRef.current) {
195
+ heldAnswerQuestionIdRef.current = awaitedQuestionId;
196
+ }
197
+ }, [awaitedQuestionId, hasDraft]);
198
+ useEffect(() => {
199
+ typedAnswerGenerationRef.current += 1;
200
+ heldAnswerQuestionIdRef.current = null;
201
+ }, [options.conversationId]);
153
202
  const addAttachments = useCallback(async (files) => {
154
203
  if (files.length === 0 || options.uploadAttachments === undefined)
155
204
  return;
@@ -220,10 +269,30 @@ export function useChatPane(options) {
220
269
  sendBlocker,
221
270
  workingDirectoryState.workingDirectory,
222
271
  ]);
223
- const send = useCallback(async () => {
224
- const prompt = composerPrompt(composer);
225
- if (!prompt)
272
+ const answerWaitingQuestion = useCallback(async ({ deliver, text }) => {
273
+ const generation = typedAnswerGenerationRef.current;
274
+ const submittedDraft = latestDraftRef.current;
275
+ const questionId = awaitedQuestionId;
276
+ deliveringTypedAnswerRef.current = true;
277
+ const outcome = await deliverTypedAnswerSafely({ deliver, text });
278
+ deliveringTypedAnswerRef.current = false;
279
+ if (!mountedRef.current || generation !== typedAnswerGenerationRef.current)
226
280
  return;
281
+ // Anything but a delivery keeps the draft and says why — never the queue below, whose flush
282
+ // would post the answer as a brand-new (paid) run once this one ends.
283
+ if (outcome !== 'delivered') {
284
+ setTypedAnswerNotice(outcome);
285
+ return;
286
+ }
287
+ answeredQuestionIdRef.current = questionId;
288
+ heldAnswerQuestionIdRef.current = null;
289
+ // The textarea stays editable during delivery: clear only the text that went out, never a
290
+ // correction or next message typed while the host answered.
291
+ if (latestDraftRef.current === submittedDraft)
292
+ composer.setDraft('');
293
+ }, [awaitedQuestionId, composer]);
294
+ // The ordinary next-turn path: queue behind a running run, else send now.
295
+ const sendOrdinaryTurn = useCallback(async (prompt) => {
227
296
  // Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
228
297
  // staged attachments, which this turn still needs when it finally goes out.
229
298
  if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
@@ -236,6 +305,48 @@ export function useChatPane(options) {
236
305
  return;
237
306
  await sendPrompt(prompt);
238
307
  }, [canSend, composer, options.conversationId, sendBlocker, sendPrompt]);
308
+ const send = useCallback(async () => {
309
+ const prompt = composerPrompt(composer);
310
+ if (!prompt || deliveringTypedAnswerRef.current)
311
+ return;
312
+ setTypedAnswerNotice(null);
313
+ const deliver = options.deliverTypedAnswer;
314
+ if (deliver !== undefined && isTypedAnswerTurn({
315
+ blocker: sendBlocker,
316
+ attachmentCount: composer.attachments.length,
317
+ messages: conversation.messages,
318
+ toolName: deliver.toolName,
319
+ })) {
320
+ await answerWaitingQuestion({ deliver, text: prompt });
321
+ return;
322
+ }
323
+ // Typed for a question that has since closed: the human meant an answer, so going on to the
324
+ // queue would start a paid run they never asked for. Attachments mark a new turn (a typed answer
325
+ // carries text only), the same rule `isTypedAnswerTurn` applies.
326
+ if (heldAnswerQuestionIdRef.current !== null && composer.attachments.length === 0) {
327
+ setTypedAnswerNotice('not-pending');
328
+ return;
329
+ }
330
+ await sendOrdinaryTurn(prompt);
331
+ }, [
332
+ answerWaitingQuestion,
333
+ composer,
334
+ conversation.messages,
335
+ options.deliverTypedAnswer,
336
+ sendBlocker,
337
+ sendOrdinaryTurn,
338
+ ]);
339
+ const sendAsNewMessage = useCallback(async () => {
340
+ const prompt = composerPrompt(composer);
341
+ if (!prompt || deliveringTypedAnswerRef.current)
342
+ return;
343
+ setTypedAnswerNotice(null);
344
+ // Mark the question answered too, so the still-non-empty draft is not re-held by the effect
345
+ // while that question's card is drawn open.
346
+ answeredQuestionIdRef.current = heldAnswerQuestionIdRef.current ?? answeredQuestionIdRef.current;
347
+ heldAnswerQuestionIdRef.current = null;
348
+ await sendOrdinaryTurn(prompt);
349
+ }, [composer, sendOrdinaryTurn]);
239
350
  const interruptSend = useCallback(() => {
240
351
  const prompt = composerPrompt(composer);
241
352
  if (!prompt)
@@ -277,6 +388,9 @@ export function useChatPane(options) {
277
388
  conversation.setMessages(options.initialMessages ?? []);
278
389
  composer.reset();
279
390
  setAttachmentError(null);
391
+ setTypedAnswerNotice(null);
392
+ typedAnswerGenerationRef.current += 1;
393
+ heldAnswerQuestionIdRef.current = null;
280
394
  // Without this, a prompt queued behind a streaming run survives `reset()` and — once
281
395
  // `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
282
396
  // the just-reset conversation instead of discarding it like the rest of this turn's state.
@@ -301,6 +415,8 @@ export function useChatPane(options) {
301
415
  sendPrompt,
302
416
  queuedPrompt,
303
417
  cancelQueued,
418
+ typedAnswerNotice,
419
+ sendAsNewMessage,
304
420
  interruptSend,
305
421
  reset,
306
422
  };
@@ -1,8 +1,9 @@
1
- export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, RuntimePickerPlacement, } from './types.js';
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';
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 { createTypedAnswerPoster, type CreateTypedAnswerPosterOptions, } from './create-typed-answer-poster.js';
6
7
  export { useChatPane, type UseChatPaneOptions, type UseChatPaneResult, } from './hooks/useChatPane.hooks.js';
7
8
  export { useChatPaneAgentControl, type UseChatPaneAgentControlOptions, } from './hooks/useChatPaneAgentControl.hooks.js';
8
9
  export { useChatPaneWorkingDirectory, type UseChatPaneWorkingDirectoryOptions, type UseChatPaneWorkingDirectoryResult, } from './hooks/useChatPaneWorkingDirectory.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 { createTypedAnswerPoster, } from './create-typed-answer-poster.js';
5
6
  export { useChatPane, } from './hooks/useChatPane.hooks.js';
6
7
  export { useChatPaneAgentControl, } from './hooks/useChatPaneAgentControl.hooks.js';
7
8
  export { useChatPaneWorkingDirectory, } from './hooks/useChatPaneWorkingDirectory.hooks.js';