@jini-ai/chat 0.5.1 → 0.5.3
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.
- package/CHANGELOG.md +24 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/run-activity.d.ts +12 -20
- package/dist/core/run-activity.js +59 -7
- package/dist/core/surface-expiry.d.ts +42 -0
- package/dist/core/surface-expiry.js +42 -0
- package/dist/core/tool-events.d.ts +34 -0
- package/dist/core/tool-events.js +61 -0
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/tools.js +1 -1
- package/dist/react/chat-pane.d.ts +2 -2
- package/dist/react/chat-pane.js +1 -1
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +19 -2
- package/dist/react/components/AttachmentPreviewModal.hooks.js +53 -38
- package/dist/react/components/Markdown.js +37 -6
- package/dist/react/components/McpUiSurfaceCard.d.ts +7 -1
- package/dist/react/components/McpUiSurfaceCard.js +23 -38
- package/dist/react/components/MessageList.d.ts +11 -0
- package/dist/react/components/MessageList.hooks.d.ts +19 -0
- package/dist/react/components/MessageList.hooks.js +82 -0
- package/dist/react/components/MessageList.js +3 -71
- package/dist/react/components/MessageList.rules.d.ts +43 -0
- package/dist/react/components/MessageList.rules.js +38 -0
- package/dist/react/components/MessageRow.js +3 -1
- package/dist/react/components/ToolCard.js +10 -7
- package/dist/react/components/mcp-ui-surface-state.d.ts +27 -0
- package/dist/react/components/mcp-ui-surface-state.js +33 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +6 -5
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +54 -0
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +46 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +17 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +153 -7
- package/dist/react/features/chat-pane/index.d.ts +2 -1
- package/dist/react/features/chat-pane/index.js +1 -0
- package/dist/react/features/chat-pane/rules.d.ts +59 -1
- package/dist/react/features/chat-pane/rules.js +74 -0
- package/dist/react/features/chat-pane/types.d.ts +34 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +28 -0
- package/dist/react/hooks/composer-draft-cache.js +56 -6
- package/dist/react/hooks/last-conversation-store.d.ts +58 -0
- package/dist/react/hooks/last-conversation-store.js +112 -0
- package/dist/react/hooks/useSurfaceExpiry.d.ts +20 -0
- package/dist/react/hooks/useSurfaceExpiry.js +35 -0
- package/dist/react/hooks/useToolTimeline.js +6 -3
- package/dist/react/index.d.ts +10 -2
- package/dist/react/index.js +7 -1
- package/package.json +7 -7
|
@@ -40,6 +40,13 @@ export interface ExtEventRenderProps {
|
|
|
40
40
|
/** Whether the owning run is still streaming — more events with this `name` may still arrive. */
|
|
41
41
|
runStreaming: boolean;
|
|
42
42
|
runSucceeded: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the owning run is waiting on the person — a card (form, confirm, sign-in) sits on a
|
|
45
|
+
* still-open tool call (`isAwaitingAnswer` in `core/run-activity.ts`). A "still working" notice
|
|
46
|
+
* should hide while this is true. Always `false` once the run stops streaming. Optional so a host
|
|
47
|
+
* that builds these props itself (tests, wrappers) keeps compiling; `MessageRow` always sets it.
|
|
48
|
+
*/
|
|
49
|
+
awaitingAnswer?: boolean;
|
|
43
50
|
/** The owning message's `ChatMessage.runId`, if known — a renderer that needs to send something back to this specific run (e.g. an A2UI action) needs this; omitted (`undefined`) for a message with no run association. */
|
|
44
51
|
runId: string | undefined;
|
|
45
52
|
/** See {@link ExtEventCall}. Absent when the group's first event arrived outside any tool call. */
|
|
@@ -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,7 +1,8 @@
|
|
|
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
|
+
import { readCachedAttachmentBatchId, writeCachedAttachmentBatchId, } from '../../../hooks/composer-draft-cache.js';
|
|
5
6
|
import { useComposer } from '../../../hooks/useComposer.js';
|
|
6
7
|
import { useConversation, } from '../../../hooks/useConversation.js';
|
|
7
8
|
import { useChatPaneWorkingDirectory, } from './useChatPaneWorkingDirectory.hooks.js';
|
|
@@ -18,6 +19,20 @@ function createAttachmentBatchId() {
|
|
|
18
19
|
return globalThis.crypto?.randomUUID?.()
|
|
19
20
|
?? `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Hands one typed answer to the host. A throw counts as `'failed'`: the host could not say whether
|
|
24
|
+
* anything was waiting, and the one outcome that must never follow is queueing the text as a new run.
|
|
25
|
+
*
|
|
26
|
+
* @complexity O(1) plus the host's own request.
|
|
27
|
+
*/
|
|
28
|
+
async function deliverTypedAnswerSafely({ deliver, text }) {
|
|
29
|
+
try {
|
|
30
|
+
return await deliver({ text });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 'failed';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
/** Module scope so each branch costs 1 cognitive point instead of 2 (nested one level inside
|
|
22
37
|
* `useChatPane` where it used to live as a ternary chain). */
|
|
23
38
|
function resolveChatPaneActivity(selectedAgent, conversation) {
|
|
@@ -61,13 +76,33 @@ export function useChatPane(options) {
|
|
|
61
76
|
const [activeUploadCount, setActiveUploadCount] = useState(0);
|
|
62
77
|
const [attachmentError, setAttachmentError] = useState(null);
|
|
63
78
|
const [queuedPrompt, setQueuedPrompt] = useState(null);
|
|
79
|
+
const [typedAnswerNotice, setTypedAnswerNotice] = useState(null);
|
|
80
|
+
// Set across the await of one typed-answer delivery so a second Enter cannot post the same text
|
|
81
|
+
// twice — the second post would find the question already answered and wrongly report it closed.
|
|
82
|
+
const deliveringTypedAnswerRef = useRef(false);
|
|
83
|
+
// The question (tool-use id) the current draft was typed for, or null. Set while a non-empty draft
|
|
84
|
+
// sits in the composer as a question waits, and kept after that question closes (answered
|
|
85
|
+
// elsewhere, expired, 409), so `send` cannot slip the draft into the queue as a new paid run.
|
|
86
|
+
// Released only by an empty draft, a delivery, `reset()`, or `sendAsNewMessage`.
|
|
87
|
+
const heldAnswerQuestionIdRef = useRef(null);
|
|
88
|
+
// The question the last delivery answered: typing more while its card is still drawn open (its
|
|
89
|
+
// tool_result has not streamed in yet) is a follow-up, not another answer to hold.
|
|
90
|
+
const answeredQuestionIdRef = useRef(null);
|
|
91
|
+
// Bumped by `reset()` and a conversation switch; a delivery that settles under a different value
|
|
92
|
+
// belongs to a composer that no longer exists and must neither clear the draft nor set a notice.
|
|
93
|
+
const typedAnswerGenerationRef = useRef(0);
|
|
64
94
|
// The conversation `queuedPrompt` was queued against. Compared with `options.conversationId` at
|
|
65
95
|
// flush time so a prompt queued behind a streaming run in one conversation can never be posted
|
|
66
96
|
// into a different one the caller switched to before that run finished.
|
|
67
97
|
const queuedConversationIdRef = useRef(undefined);
|
|
68
98
|
const mountedRef = useRef(true);
|
|
69
99
|
const attachmentGenerationRef = useRef(0);
|
|
70
|
-
|
|
100
|
+
// Every attachment staged for one turn must share one upload batch (the daemon refuses a turn
|
|
101
|
+
// spanning two). So a mount that will restore cached attachments starts in THEIR batch rather
|
|
102
|
+
// than a new one; see `readCachedAttachmentBatchId`.
|
|
103
|
+
const [initialAttachmentBatchId] = useState(() => readCachedAttachmentBatchId({ conversationId: options.conversationId }) ?? createAttachmentBatchId());
|
|
104
|
+
const attachmentBatchIdRef = useRef(initialAttachmentBatchId);
|
|
105
|
+
const batchConversationIdRef = useRef(options.conversationId);
|
|
71
106
|
const activeUploadsRef = useRef(new Set());
|
|
72
107
|
const [internalSelection, setInternalSelection] = useState(options.initialSelection ?? { agentId: '' });
|
|
73
108
|
const workingDirectoryState = useChatPaneWorkingDirectory(definedProps({ source: {
|
|
@@ -150,10 +185,51 @@ export function useChatPane(options) {
|
|
|
150
185
|
...(options.apiModeConfigured === undefined ? {} : { apiModeConfigured: options.apiModeConfigured }),
|
|
151
186
|
});
|
|
152
187
|
const canSend = sendBlocker === null && composer.canSubmit;
|
|
188
|
+
// Read after `await` in `answerWaitingQuestion`, where the render-time `composer.draft` is stale.
|
|
189
|
+
const latestDraftRef = useRef(composer.draft);
|
|
190
|
+
latestDraftRef.current = composer.draft;
|
|
191
|
+
const awaitedQuestionId = options.deliverTypedAnswer === undefined
|
|
192
|
+
? null
|
|
193
|
+
: findAwaitedTypedAnswerId({ messages: conversation.messages, toolName: options.deliverTypedAnswer.toolName });
|
|
194
|
+
const hasDraft = composer.draft.trim() !== '';
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!hasDraft) {
|
|
197
|
+
heldAnswerQuestionIdRef.current = null;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (awaitedQuestionId !== null && awaitedQuestionId !== answeredQuestionIdRef.current) {
|
|
201
|
+
heldAnswerQuestionIdRef.current = awaitedQuestionId;
|
|
202
|
+
}
|
|
203
|
+
}, [awaitedQuestionId, hasDraft]);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
typedAnswerGenerationRef.current += 1;
|
|
206
|
+
heldAnswerQuestionIdRef.current = null;
|
|
207
|
+
}, [options.conversationId]);
|
|
208
|
+
// A conversation switch without a remount: adopt the batch of the attachments the composer is
|
|
209
|
+
// about to restore for the new conversation, else start a fresh one so two conversations never
|
|
210
|
+
// share a batch directory (the agent is granted the whole directory, not just the claimed files).
|
|
211
|
+
// An id arriving where there was none is the same conversation finally getting a key — the
|
|
212
|
+
// composer keeps what is staged (see `useComposer`), so the batch stays too.
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const previousConversationId = batchConversationIdRef.current;
|
|
215
|
+
if (options.conversationId === previousConversationId)
|
|
216
|
+
return;
|
|
217
|
+
batchConversationIdRef.current = options.conversationId;
|
|
218
|
+
const restoredBatchId = readCachedAttachmentBatchId({ conversationId: options.conversationId });
|
|
219
|
+
if (restoredBatchId !== null) {
|
|
220
|
+
attachmentBatchIdRef.current = restoredBatchId;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (previousConversationId == null)
|
|
224
|
+
return;
|
|
225
|
+
attachmentBatchIdRef.current = createAttachmentBatchId();
|
|
226
|
+
}, [options.conversationId]);
|
|
153
227
|
const addAttachments = useCallback(async (files) => {
|
|
154
228
|
if (files.length === 0 || options.uploadAttachments === undefined)
|
|
155
229
|
return;
|
|
156
230
|
const generation = attachmentGenerationRef.current;
|
|
231
|
+
const batchId = attachmentBatchIdRef.current;
|
|
232
|
+
const conversationId = options.conversationId;
|
|
157
233
|
const controller = new AbortController();
|
|
158
234
|
activeUploadsRef.current.add(controller);
|
|
159
235
|
setActiveUploadCount(activeUploadsRef.current.size);
|
|
@@ -162,7 +238,7 @@ export function useChatPane(options) {
|
|
|
162
238
|
await uploadAttachmentBatch(files, {
|
|
163
239
|
upload: options.uploadAttachments,
|
|
164
240
|
signal: controller.signal,
|
|
165
|
-
batchId
|
|
241
|
+
batchId,
|
|
166
242
|
stillWanted: () => mountedRef.current
|
|
167
243
|
&& !controller.signal.aborted
|
|
168
244
|
&& generation === attachmentGenerationRef.current,
|
|
@@ -172,6 +248,9 @@ export function useChatPane(options) {
|
|
|
172
248
|
// `attachment-preview-cache.ts`'s module doc for why this is the only place that copy can
|
|
173
249
|
// ever be captured.
|
|
174
250
|
onAttachment: (attachment, file) => {
|
|
251
|
+
// Recorded before staging so the cache entry that persists this attachment carries its
|
|
252
|
+
// batch, which is what lets a later mount restore the batch along with the attachment.
|
|
253
|
+
writeCachedAttachmentBatchId({ conversationId, batchId });
|
|
175
254
|
composer.addAttachment(attachment);
|
|
176
255
|
if (file)
|
|
177
256
|
cacheAttachmentPreviewSource({ path: attachment.path, file });
|
|
@@ -184,7 +263,7 @@ export function useChatPane(options) {
|
|
|
184
263
|
setActiveUploadCount(activeUploadsRef.current.size);
|
|
185
264
|
}
|
|
186
265
|
}
|
|
187
|
-
}, [composer, options.uploadAttachments]);
|
|
266
|
+
}, [composer, options.conversationId, options.uploadAttachments]);
|
|
188
267
|
const sendPrompt = useCallback(async (prompt) => {
|
|
189
268
|
const trimmed = prompt.trim();
|
|
190
269
|
if (!trimmed)
|
|
@@ -220,10 +299,30 @@ export function useChatPane(options) {
|
|
|
220
299
|
sendBlocker,
|
|
221
300
|
workingDirectoryState.workingDirectory,
|
|
222
301
|
]);
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
302
|
+
const answerWaitingQuestion = useCallback(async ({ deliver, text }) => {
|
|
303
|
+
const generation = typedAnswerGenerationRef.current;
|
|
304
|
+
const submittedDraft = latestDraftRef.current;
|
|
305
|
+
const questionId = awaitedQuestionId;
|
|
306
|
+
deliveringTypedAnswerRef.current = true;
|
|
307
|
+
const outcome = await deliverTypedAnswerSafely({ deliver, text });
|
|
308
|
+
deliveringTypedAnswerRef.current = false;
|
|
309
|
+
if (!mountedRef.current || generation !== typedAnswerGenerationRef.current)
|
|
226
310
|
return;
|
|
311
|
+
// Anything but a delivery keeps the draft and says why — never the queue below, whose flush
|
|
312
|
+
// would post the answer as a brand-new (paid) run once this one ends.
|
|
313
|
+
if (outcome !== 'delivered') {
|
|
314
|
+
setTypedAnswerNotice(outcome);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
answeredQuestionIdRef.current = questionId;
|
|
318
|
+
heldAnswerQuestionIdRef.current = null;
|
|
319
|
+
// The textarea stays editable during delivery: clear only the text that went out, never a
|
|
320
|
+
// correction or next message typed while the host answered.
|
|
321
|
+
if (latestDraftRef.current === submittedDraft)
|
|
322
|
+
composer.setDraft('');
|
|
323
|
+
}, [awaitedQuestionId, composer]);
|
|
324
|
+
// The ordinary next-turn path: queue behind a running run, else send now.
|
|
325
|
+
const sendOrdinaryTurn = useCallback(async (prompt) => {
|
|
227
326
|
// Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
|
|
228
327
|
// staged attachments, which this turn still needs when it finally goes out.
|
|
229
328
|
if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
|
|
@@ -236,6 +335,48 @@ export function useChatPane(options) {
|
|
|
236
335
|
return;
|
|
237
336
|
await sendPrompt(prompt);
|
|
238
337
|
}, [canSend, composer, options.conversationId, sendBlocker, sendPrompt]);
|
|
338
|
+
const send = useCallback(async () => {
|
|
339
|
+
const prompt = composerPrompt(composer);
|
|
340
|
+
if (!prompt || deliveringTypedAnswerRef.current)
|
|
341
|
+
return;
|
|
342
|
+
setTypedAnswerNotice(null);
|
|
343
|
+
const deliver = options.deliverTypedAnswer;
|
|
344
|
+
if (deliver !== undefined && isTypedAnswerTurn({
|
|
345
|
+
blocker: sendBlocker,
|
|
346
|
+
attachmentCount: composer.attachments.length,
|
|
347
|
+
messages: conversation.messages,
|
|
348
|
+
toolName: deliver.toolName,
|
|
349
|
+
})) {
|
|
350
|
+
await answerWaitingQuestion({ deliver, text: prompt });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// Typed for a question that has since closed: the human meant an answer, so going on to the
|
|
354
|
+
// queue would start a paid run they never asked for. Attachments mark a new turn (a typed answer
|
|
355
|
+
// carries text only), the same rule `isTypedAnswerTurn` applies.
|
|
356
|
+
if (heldAnswerQuestionIdRef.current !== null && composer.attachments.length === 0) {
|
|
357
|
+
setTypedAnswerNotice('not-pending');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
await sendOrdinaryTurn(prompt);
|
|
361
|
+
}, [
|
|
362
|
+
answerWaitingQuestion,
|
|
363
|
+
composer,
|
|
364
|
+
conversation.messages,
|
|
365
|
+
options.deliverTypedAnswer,
|
|
366
|
+
sendBlocker,
|
|
367
|
+
sendOrdinaryTurn,
|
|
368
|
+
]);
|
|
369
|
+
const sendAsNewMessage = useCallback(async () => {
|
|
370
|
+
const prompt = composerPrompt(composer);
|
|
371
|
+
if (!prompt || deliveringTypedAnswerRef.current)
|
|
372
|
+
return;
|
|
373
|
+
setTypedAnswerNotice(null);
|
|
374
|
+
// Mark the question answered too, so the still-non-empty draft is not re-held by the effect
|
|
375
|
+
// while that question's card is drawn open.
|
|
376
|
+
answeredQuestionIdRef.current = heldAnswerQuestionIdRef.current ?? answeredQuestionIdRef.current;
|
|
377
|
+
heldAnswerQuestionIdRef.current = null;
|
|
378
|
+
await sendOrdinaryTurn(prompt);
|
|
379
|
+
}, [composer, sendOrdinaryTurn]);
|
|
239
380
|
const interruptSend = useCallback(() => {
|
|
240
381
|
const prompt = composerPrompt(composer);
|
|
241
382
|
if (!prompt)
|
|
@@ -277,6 +418,9 @@ export function useChatPane(options) {
|
|
|
277
418
|
conversation.setMessages(options.initialMessages ?? []);
|
|
278
419
|
composer.reset();
|
|
279
420
|
setAttachmentError(null);
|
|
421
|
+
setTypedAnswerNotice(null);
|
|
422
|
+
typedAnswerGenerationRef.current += 1;
|
|
423
|
+
heldAnswerQuestionIdRef.current = null;
|
|
280
424
|
// Without this, a prompt queued behind a streaming run survives `reset()` and — once
|
|
281
425
|
// `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
|
|
282
426
|
// the just-reset conversation instead of discarding it like the rest of this turn's state.
|
|
@@ -301,6 +445,8 @@ export function useChatPane(options) {
|
|
|
301
445
|
sendPrompt,
|
|
302
446
|
queuedPrompt,
|
|
303
447
|
cancelQueued,
|
|
448
|
+
typedAnswerNotice,
|
|
449
|
+
sendAsNewMessage,
|
|
304
450
|
interruptSend,
|
|
305
451
|
reset,
|
|
306
452
|
};
|
|
@@ -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';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChatMessage } from '@jini-ai/chat';
|
|
2
|
+
import type { ByokRuntimeSummary, ChatPaneAgent, ChatPaneAgentSelection, TypedAnswerNotice } from './types.js';
|
|
2
3
|
export declare function defaultChatPaneSelection({ agent }: {
|
|
3
4
|
agent: ChatPaneAgent;
|
|
4
5
|
}): ChatPaneAgentSelection;
|
|
@@ -71,6 +72,63 @@ export declare function describeChatPaneSendBlocker(blocker: ChatPaneSendBlocker
|
|
|
71
72
|
export declare function isChatPaneQueueableBlocker({ blocker }: {
|
|
72
73
|
blocker: ChatPaneSendBlocker | null;
|
|
73
74
|
}): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the run in flight is holding a question open for the human: the newest message is the
|
|
77
|
+
* assistant's, and it carries an interactive surface (`kind: 'ext'`, e.g. an MCP-UI card) that
|
|
78
|
+
* arrived inside a still-open call to the host's question tool (`toolName`). That is the one moment
|
|
79
|
+
* text typed into the composer is an ANSWER rather than a next turn — the tool is parked waiting on
|
|
80
|
+
* it, and queueing the text behind the run would only deliver it after the question had expired, as
|
|
81
|
+
* a new paid run.
|
|
82
|
+
*
|
|
83
|
+
* Only the question tool counts. Other tools park on cards too — a delete confirm, an approval —
|
|
84
|
+
* but they take no typed answer: the server routes typed text only to the question tool, so treating
|
|
85
|
+
* those as waiting would refuse the text with "no longer waiting" and drop it instead of queueing.
|
|
86
|
+
* A call is the question tool when ANY `tool_use` for its id names it, since the same call's
|
|
87
|
+
* `tool_use` can arrive more than once (the CLI re-emits it). A vendor's wrapper row
|
|
88
|
+
* (`execute_delegated_tool`) and the daemon's canonical row do NOT share an id — the wrapper carries
|
|
89
|
+
* the CLI's `toolu_…` id and the canonical row a fresh UUID from `@jini-ai/mcp`. The canonical row
|
|
90
|
+
* opens after its wrapper, so it is the newest open call when the surface arrives, and that is
|
|
91
|
+
* what attributes the surface to the question tool.
|
|
92
|
+
*
|
|
93
|
+
* A surface is attributed to the newest call still open when it arrived — the same rule
|
|
94
|
+
* `useExtEventGroups` uses to give a card its `call` — so an older call returning does not settle
|
|
95
|
+
* it. Once its call returns (answered, expired, abandoned), typing is a next turn again.
|
|
96
|
+
*
|
|
97
|
+
* @complexity O(e·c): e events in the newest message, c calls open at once (one or two in practice).
|
|
98
|
+
*/
|
|
99
|
+
export declare function isAwaitingTypedAnswer({ messages, toolName }: {
|
|
100
|
+
messages: readonly ChatMessage[];
|
|
101
|
+
toolName: string;
|
|
102
|
+
}): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The tool-use id of the question {@link isAwaitingTypedAnswer} sees waiting, or `null` — the newest
|
|
105
|
+
* one when several are open. The id lets the pane tell one question from the next: a draft typed
|
|
106
|
+
* for a question stays tied to THAT question after it closes (see `useChatPane`'s held answer).
|
|
107
|
+
*
|
|
108
|
+
* @complexity O(e·c), as {@link isAwaitingTypedAnswer}.
|
|
109
|
+
*/
|
|
110
|
+
export declare function findAwaitedTypedAnswerId({ messages, toolName }: {
|
|
111
|
+
messages: readonly ChatMessage[];
|
|
112
|
+
toolName: string;
|
|
113
|
+
}): string | null;
|
|
114
|
+
/**
|
|
115
|
+
* Whether a composer send is an answer to a waiting question rather than a next turn: a run is
|
|
116
|
+
* streaming ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a typed answer
|
|
117
|
+
* carries text only), and that run is holding the `toolName` question open
|
|
118
|
+
* ({@link isAwaitingTypedAnswer}).
|
|
119
|
+
*
|
|
120
|
+
* @complexity O(e·c), from {@link isAwaitingTypedAnswer}.
|
|
121
|
+
*/
|
|
122
|
+
export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages, toolName }: {
|
|
123
|
+
blocker: ChatPaneSendBlocker | null;
|
|
124
|
+
attachmentCount: number;
|
|
125
|
+
messages: readonly ChatMessage[];
|
|
126
|
+
toolName: string;
|
|
127
|
+
}): boolean;
|
|
128
|
+
/** The English copy (and i18n key) for why a typed answer was not sent. */
|
|
129
|
+
export declare function describeTypedAnswerNotice({ notice }: {
|
|
130
|
+
notice: TypedAnswerNotice;
|
|
131
|
+
}): string;
|
|
74
132
|
export declare function orderChatPaneAgents({ agents }: {
|
|
75
133
|
agents: readonly ChatPaneAgent[];
|
|
76
134
|
}): ChatPaneAgent[];
|