@jini-ai/chat 0.6.17 → 0.6.21
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 +15 -0
- package/dist/core/accepted-input.d.ts +10 -2
- package/dist/core/accepted-input.js +87 -33
- package/dist/core/assistant-client/durable-run-subscription.js +6 -0
- package/dist/core/assistant-client/entry.d.ts +1 -1
- package/dist/core/assistant-client/entry.js +1 -1
- package/dist/core/assistant-client/messages.d.ts +21 -0
- package/dist/core/assistant-client/messages.js +31 -0
- package/dist/core/checkpoint-interval.d.ts +25 -0
- package/dist/core/checkpoint-interval.js +38 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +27 -1
- package/dist/core/durable-runs/ports.d.ts +25 -1
- package/dist/core/events.d.ts +6 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/run-events/entry.js +15 -2
- package/dist/core/surface-recovery.d.ts +19 -0
- package/dist/core/surface-recovery.js +26 -0
- package/dist/core/tool-events.d.ts +11 -3
- package/dist/core/tool-events.js +75 -43
- package/dist/core/turn-usage.d.ts +1 -1
- package/dist/core/turn-usage.js +5 -3
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +14 -1
- package/dist/embed-widget/browser/frontend-session-bridge.js +120 -47
- package/dist/react/components/Composer.d.ts +3 -3
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +2 -1
- package/dist/react/components/ConversationTitleEditor.js +2 -1
- package/dist/react/components/ExtEventErrorBoundary.js +2 -1
- package/dist/react/components/McpUiSurfaceCard.js +12 -5
- package/dist/react/components/MessageList.js +2 -1
- package/dist/react/components/MidRunUserMessage.js +1 -1
- package/dist/react/components/ToolCard.js +3 -2
- package/dist/react/components/ToolCard.rules.js +2 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -3
- package/dist/react/features/chat-pane/components/ChatPane.js +1 -1
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.d.ts +15 -2
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +19 -3
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +5 -4
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +17 -9
- package/dist/react/features/chat-pane/hooks/useChatPaneFileDrop.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +20 -3
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +22 -3
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +3 -1
- package/dist/react/features/chat-pane/surface-recovery-stream.d.ts +12 -0
- package/dist/react/features/chat-pane/surface-recovery-stream.js +47 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +6 -6
- package/dist/react/hooks/composer-draft-cache.js +18 -17
- package/dist/react/hooks/useComposer.js +52 -4
- package/dist/react/hooks/useConversation.js +41 -22
- package/dist/react/hooks/useRunStream.js +2 -1
- package/dist/react/hooks/useSurfaceRecovery.d.ts +23 -0
- package/dist/react/hooks/useSurfaceRecovery.js +84 -0
- package/dist/react/styles/reference.css +4 -5
- package/dist/server/durable-runs/recover.js +3 -3
- package/dist/store/ports.d.ts +17 -3
- package/dist/store/runtime/chat-expiry-sweep.js +3 -1
- package/dist/store/runtime/chat-history-store.js +6 -0
- package/dist/store/runtime/checkpoint-delta.d.ts +35 -0
- package/dist/store/runtime/checkpoint-delta.js +75 -0
- package/dist/store/runtime/durable-run-store.js +92 -25
- package/dist/store/runtime/events-json.d.ts +17 -0
- package/dist/store/runtime/events-json.js +19 -0
- package/dist/store/runtime/run-guarded-chat-store.js +8 -3
- package/dist/store/runtime/run-ledger.d.ts +2 -2
- package/dist/store/runtime/run-ledger.js +120 -45
- package/dist/store/sql/read-version.d.ts +15 -0
- package/dist/store/sql/read-version.js +21 -0
- package/dist/store/sql/store.d.ts +9 -0
- package/dist/store/sql/store.js +89 -4
- package/dist/store/sqlite/schema.d.ts +2 -2
- package/dist/store/sqlite/schema.js +4 -0
- package/package.json +12 -12
|
@@ -49,7 +49,7 @@ function MidRunInput({ input, queued, transcriptRow }) {
|
|
|
49
49
|
const t = useT();
|
|
50
50
|
const attachments = input.attachments ?? [];
|
|
51
51
|
const gallery = useAttachmentGallery({ attachments });
|
|
52
|
-
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
52
|
+
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-chat-pane__status jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { formatErrorForLog } from '@jini-ai/core';
|
|
2
3
|
const RUNNING_TITLE_CLASS = ' shimmer-text';
|
|
3
4
|
/**
|
|
4
5
|
* @module ToolCard
|
|
@@ -79,7 +80,7 @@ function renderRegistered({ use, card, registry }) {
|
|
|
79
80
|
return node;
|
|
80
81
|
}
|
|
81
82
|
catch (err) {
|
|
82
|
-
console.error(`[ToolCard] custom renderer for "${use.name}" threw; falling back`, err);
|
|
83
|
+
console.error(`[ToolCard] custom renderer for "${use.name}" threw; falling back`, formatErrorForLog({ error: err }));
|
|
83
84
|
}
|
|
84
85
|
return undefined;
|
|
85
86
|
}
|
|
@@ -124,7 +125,7 @@ function renderToolCardBody(args) {
|
|
|
124
125
|
* The head every Jini/MCP/unknown tool row shares: status, "<Humanized Name>" (announced as
|
|
125
126
|
* "Tool call · <Humanized Name>"), and the
|
|
126
127
|
* target ({@link toolRowTarget}). One place, so a tool id the family table doesn't know (`content_post_update`,
|
|
127
|
-
* `
|
|
128
|
+
* `mcp__acme__theme_list_files`) still reads as a label and never as its raw name and JSON args.
|
|
128
129
|
*/
|
|
129
130
|
function ToolRowHead({ name, input, result, runStreaming, runSucceeded }) {
|
|
130
131
|
const t = useT();
|
|
@@ -48,7 +48,8 @@ function resultName(content) {
|
|
|
48
48
|
const obj = parsed;
|
|
49
49
|
if (isErrorResult(obj))
|
|
50
50
|
return undefined;
|
|
51
|
-
|
|
51
|
+
// The nested level gets the same error check: `{ result: { error, name: 'NotFoundError' } }` is still a failure.
|
|
52
|
+
const nested = Object.values(obj).filter((v) => v != null && typeof v === 'object' && !Array.isArray(v) && !isErrorResult(v));
|
|
52
53
|
return firstReadable(obj, RESULT_NAME_KEYS) ?? nested.map((v) => firstReadable(v, RESULT_NAME_KEYS)).find(Boolean);
|
|
53
54
|
}
|
|
54
55
|
/** A batch member's own target: a readable string member, or an object member's summary key. O(k). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { pickerModelValue, pickerCatalogNote, pickerModelOptions, shortModelLabel } from '../model-options.js';
|
|
2
|
+
import { friendlyModelLabel, pickerModelValue, pickerCatalogNote, pickerModelOptions, shortModelLabel } from '../model-options.js';
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { AgentIcon, RemixIcon, SearchableModelSelect } from '@jini-ai/ui';
|
|
@@ -62,9 +62,11 @@ function RuntimeAgentRow({ agent, active, noteId, onChange, agentIconBasePath, t
|
|
|
62
62
|
function RuntimeBarMenus({ selectedAgent, value, onChange, modelLabel, t }) {
|
|
63
63
|
if (!selectedAgent)
|
|
64
64
|
return null;
|
|
65
|
-
|
|
65
|
+
// Raw catalog ids (an offline fallback list's rows) read as friendly names in the bar and its list.
|
|
66
|
+
const models = pickerModelOptions(selectedAgent).map((row) => ({ ...row, label: friendlyModelLabel({ label: row.label }) }));
|
|
66
67
|
const efforts = selectedAgent.reasoningOptions ?? [];
|
|
67
|
-
|
|
68
|
+
const modelName = friendlyModelLabel({ label: modelLabel });
|
|
69
|
+
return (_jsxs(_Fragment, { children: [models.length > 0 ? (_jsx(RuntimeDropUp, { label: t('Model'), valueLabel: modelName,
|
|
68
70
|
// The agent icon sits right beside it, so the bar drops the vendor word and caps the rest.
|
|
69
71
|
triggerLabel: shortModelLabel({ label: modelLabel, agentId: selectedAgent.id }), value: pickerModelValue(selectedAgent, value.model), options: models, note: pickerCatalogNote(selectedAgent, t), width: 260, onSelect: (model) => onChange({
|
|
70
72
|
agentId: value.agentId,
|
|
@@ -225,7 +225,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
|
|
|
225
225
|
// rather than refusing it, so the operator can keep writing while the agent thinks.
|
|
226
226
|
disabled: disabled || unavailable,
|
|
227
227
|
// The one blocker `send()` handles itself. Every other refusal still greys out the button.
|
|
228
|
-
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', ...definedProps({ source: { sendDisabledReason, sendDisabledReasonId: sendDisabledReason ? sendDisabledReasonId : undefined } }), running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel,
|
|
228
|
+
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', ...definedProps({ source: { sendDisabledReason, sendDisabledReasonId: sendDisabledReason ? sendDisabledReasonId : undefined } }), running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel, ...definedProps({ source: { placeholder } }), slots: { ...slots, footerAccessories: _jsxs(_Fragment, { children: [sendDisabledReason ? _jsx("small", { id: sendDisabledReasonId, className: "jini-composer-send-blocker", children: sendDisabledReason }) : null, slots.footerAccessories] }) }, ...resolveComposerAttachmentPicker(uploadAttachments, pane, attachmentAccept), ...resolveComposerWorkingDirectory(workingDirectoryAccess, workingDirectoryControlPlacement, pane) }), _jsx(ChatPaneWorkingDirectoryBlock, { workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, pane: pane, t: t })] }));
|
|
229
229
|
}
|
|
230
230
|
const EMPTY_SUGGESTIONS = [];
|
|
231
231
|
const CHAT_PANE_DEFAULTS = {
|
|
@@ -34,7 +34,20 @@
|
|
|
34
34
|
* single-use token minted into the dialog is what proves a human saw it. Adding a client-side
|
|
35
35
|
* allow-list would suggest a protection that is not real.
|
|
36
36
|
*/
|
|
37
|
-
import type { McpUiToolCallHandler } from '@jini-ai/ui/mcp-ui';
|
|
37
|
+
import type { McpUiToolCall, McpUiToolCallHandler } from '@jini-ai/ui/mcp-ui';
|
|
38
|
+
import type { SurfaceEmission } from '@jini-ai/core';
|
|
39
|
+
import type { SurfaceRecoveryInfo } from '../../../core/surface-recovery.js';
|
|
40
|
+
export interface SurfaceRecoveryCallbacks {
|
|
41
|
+
readonly onSurface: (emission: SurfaceEmission) => void;
|
|
42
|
+
readonly signal: AbortSignal;
|
|
43
|
+
}
|
|
44
|
+
/** Callable as before; lifecycle controllers can also restore without replaying browser input. */
|
|
45
|
+
export type RecoverableMcpUiToolCaller = McpUiToolCallHandler & {
|
|
46
|
+
restore(required: SurfaceRecoveryInfo & SurfaceRecoveryCallbacks, options?: Record<string, never>): Promise<unknown>;
|
|
47
|
+
answer(required: {
|
|
48
|
+
call: McpUiToolCall;
|
|
49
|
+
} & SurfaceRecoveryCallbacks, options?: Record<string, never>): Promise<unknown>;
|
|
50
|
+
};
|
|
38
51
|
export interface CreateMcpUiToolCallerOptions {
|
|
39
52
|
/**
|
|
40
53
|
* Path appended to `baseUrl`. Defaults to `/api/mcp-ui/tool-calls`.
|
|
@@ -69,5 +82,5 @@ export interface McpUiToolCallRequest {
|
|
|
69
82
|
export declare function createMcpUiToolCaller({ baseUrl, fetch }: {
|
|
70
83
|
baseUrl: string;
|
|
71
84
|
fetch: typeof globalThis.fetch;
|
|
72
|
-
}, options?: CreateMcpUiToolCallerOptions):
|
|
85
|
+
}, options?: CreateMcpUiToolCallerOptions): RecoverableMcpUiToolCaller;
|
|
73
86
|
//# sourceMappingURL=create-mcp-ui-tool-caller.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSurfaceRecoveryStream } from './surface-recovery-stream.js';
|
|
1
2
|
const DEFAULT_PATH = '/api/mcp-ui/tool-calls';
|
|
2
3
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
4
|
function errorTextFrom(body, status) {
|
|
@@ -35,7 +36,7 @@ function timeoutAware(error, timeoutMs) {
|
|
|
35
36
|
: error;
|
|
36
37
|
}
|
|
37
38
|
/** One POST of a confirmed action and its parsed answer; a non-2xx response rejects via {@link errorFrom}. */
|
|
38
|
-
async function postToolCall({ call, endpoint, headers, signal, fetch }) {
|
|
39
|
+
async function postToolCall({ call, endpoint, headers, signal, fetch, onSurface, exchangeId, restore }) {
|
|
39
40
|
// `{name, arguments}` on the MCP-UI side becomes `{toolName, params}` on the wire: `arguments`
|
|
40
41
|
// is a reserved word in a JS function scope and reads badly in a server handler, and the
|
|
41
42
|
// renamed pair matches what the reference implementation's redemption route already accepts.
|
|
@@ -48,9 +49,12 @@ async function postToolCall({ call, endpoint, headers, signal, fetch }) {
|
|
|
48
49
|
// Same-origin cookie session is the usual authenticator for the human's second step.
|
|
49
50
|
credentials: 'same-origin',
|
|
50
51
|
headers: { ...headers, 'content-type': 'application/json' },
|
|
51
|
-
body: JSON.stringify(request),
|
|
52
|
+
body: JSON.stringify({ ...request, ...(restore ? { exchangeId, restore } : {}) }),
|
|
52
53
|
signal,
|
|
53
54
|
});
|
|
55
|
+
if (response.ok && response.headers.get('content-type')?.includes('application/x-ndjson')) {
|
|
56
|
+
return readSurfaceRecoveryStream({ response, onSurface });
|
|
57
|
+
}
|
|
54
58
|
const raw = await response.text();
|
|
55
59
|
let parsed = raw;
|
|
56
60
|
if (raw.length > 0) {
|
|
@@ -83,12 +87,24 @@ export function createMcpUiToolCaller({ baseUrl, fetch }, options = {}) {
|
|
|
83
87
|
const path = options.path ?? DEFAULT_PATH;
|
|
84
88
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
85
89
|
const endpoint = `${baseUrl.replace(/\/$/u, '')}${path}`;
|
|
86
|
-
|
|
90
|
+
const answer = async (call) => {
|
|
87
91
|
const controller = new AbortController();
|
|
88
92
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
93
|
return postToolCall({ call, endpoint, headers: options.headers, signal: controller.signal, fetch })
|
|
90
94
|
.catch((error) => { throw timeoutAware(error, timeoutMs); })
|
|
91
95
|
.finally(() => clearTimeout(timer));
|
|
92
96
|
};
|
|
97
|
+
return Object.assign(answer, {
|
|
98
|
+
// Recovery streams deliberately have no reading-time deadline. The controller cancels on
|
|
99
|
+
// unmount; the server's executing-call lease handles run cancellation independently.
|
|
100
|
+
restore: (required) => postToolCall({
|
|
101
|
+
call: { name: required.toolId, arguments: {} }, endpoint, headers: options.headers, fetch,
|
|
102
|
+
signal: required.signal, onSurface: required.onSurface, exchangeId: required.exchangeId, restore: true,
|
|
103
|
+
}),
|
|
104
|
+
answer: (required) => postToolCall({
|
|
105
|
+
call: required.call, endpoint, headers: options.headers, fetch,
|
|
106
|
+
signal: required.signal, onSurface: required.onSurface,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
93
109
|
}
|
|
94
110
|
//# sourceMappingURL=create-mcp-ui-tool-caller.js.map
|
|
@@ -115,10 +115,11 @@ export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
|
115
115
|
*/
|
|
116
116
|
sendAsNewMessage: () => Promise<void>;
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* queue-then-cancel rather than cancel-then-send
|
|
121
|
-
* that never
|
|
118
|
+
* @deprecated Use send and let Composer apply sendDisabled to keyboard shortcuts.
|
|
119
|
+
* Legacy name for the modifier-key send shortcut; uses {@link send} and keeps the run going.
|
|
120
|
+
* Previously queue-then-cancel rather than cancel-then-send kept both paths on one flush so a
|
|
121
|
+
* cancel that never landed could not strand the prompt. Sharing send now also preserves that
|
|
122
|
+
* queue ownership, while a capable run reads the input after its current step.
|
|
122
123
|
*/
|
|
123
124
|
interruptSend: () => void;
|
|
124
125
|
reset: () => void;
|
|
@@ -262,6 +262,20 @@ export function useChatPane(options) {
|
|
|
262
262
|
queued, next: { text: input.text, secretRedacted: input.secretRedaction?.secretRedacted ?? false,
|
|
263
263
|
count: input.secretRedaction?.count ?? 0 },
|
|
264
264
|
}), null), [queuedInputs]);
|
|
265
|
+
const releaseStateRef = useRef({ streaming: conversation.isStreaming, queued: queuedInputs.length > 0 });
|
|
266
|
+
releaseStateRef.current = { streaming: conversation.isStreaming, queued: queuedInputs.length > 0 };
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (typeof window === 'undefined')
|
|
269
|
+
return;
|
|
270
|
+
const onReleaseCheck = (event) => {
|
|
271
|
+
const { streaming, queued } = releaseStateRef.current;
|
|
272
|
+
// Read mutable operation locks at dispatch time, including the gap before React renders.
|
|
273
|
+
if (streaming || queued || activeUploadsRef.current.size > 0 || startingPromptRef.current || deliveringTypedAnswerRef.current)
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
};
|
|
276
|
+
window.addEventListener('jini:release-check', onReleaseCheck);
|
|
277
|
+
return () => window.removeEventListener('jini:release-check', onReleaseCheck);
|
|
278
|
+
}, []);
|
|
265
279
|
// Release consumed text/files locally as well as hiding them; next-turn fallback uses this same projection.
|
|
266
280
|
useEffect(() => {
|
|
267
281
|
if (queuedInputs.length !== storedQueuedInputs.length)
|
|
@@ -742,15 +756,9 @@ export function useChatPane(options) {
|
|
|
742
756
|
guard.clearNotice();
|
|
743
757
|
await sendOrdinaryTurn(prompt);
|
|
744
758
|
}, [composer, guard.clearNotice, sendOrdinaryTurn]);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return;
|
|
749
|
-
guard.clearNotice();
|
|
750
|
-
enqueueTurn({ turn: guard.redact({ text: prompt }, {}) });
|
|
751
|
-
composer.setDraft('');
|
|
752
|
-
conversation.cancel();
|
|
753
|
-
}, [composer, conversation, enqueueTurn, guard.clearNotice, guard.redact]);
|
|
759
|
+
// A send shortcut must honor the same mid-run delivery contract as the Send button.
|
|
760
|
+
// Unsupported agents wait behind the current reply; only the explicit Stop action cancels it.
|
|
761
|
+
const interruptSend = useCallback(() => { void send(); }, [send]);
|
|
754
762
|
/** Abort only uploads owned by the removed inputs; late completions must not refill the composer. */
|
|
755
763
|
const discardQueuedUploads = useCallback(({ id }, _options = {}) => {
|
|
756
764
|
for (const [controller, owner] of queuedUploadOwnersRef.current) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatErrorForLog } from '@jini-ai/core';
|
|
1
2
|
import { useCallback, } from 'react';
|
|
2
3
|
import { useFileDropTarget } from '@jini-ai/ui';
|
|
3
4
|
function carriesFiles(event) {
|
|
@@ -12,11 +13,11 @@ function runOnFilesHostEffect(onFiles, files) {
|
|
|
12
13
|
result = onFiles(files);
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
|
-
console.error('[@jini-ai/chat] useChatPaneFileDrop onFiles host effect failed:', error);
|
|
16
|
+
console.error('[@jini-ai/chat] useChatPaneFileDrop onFiles host effect failed:', formatErrorForLog({ error }));
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
19
|
void Promise.resolve(result).catch((error) => {
|
|
19
|
-
console.error('[@jini-ai/chat] useChatPaneFileDrop onFiles host effect failed:', error);
|
|
20
|
+
console.error('[@jini-ai/chat] useChatPaneFileDrop onFiles host effect failed:', formatErrorForLog({ error }));
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
@@ -100,13 +100,30 @@ export function useRuntimeDropUp({ width }) {
|
|
|
100
100
|
const checked = menu?.querySelector(`${MENU_ITEMS}[aria-checked="true"]`);
|
|
101
101
|
(checked ?? menu?.querySelector(MENU_ITEMS))?.focus();
|
|
102
102
|
}, [open, position]);
|
|
103
|
+
// Escape is also handled here, on the menu and the trigger themselves: the document listener
|
|
104
|
+
// above never hears a keydown that something between them and the document stopped (demo
|
|
105
|
+
// practice run, 2026-11-02: Escape left the model picker open). preventDefault marks it consumed,
|
|
106
|
+
// so a host's own Escape (the admin dock closing) leaves this press to the menu.
|
|
107
|
+
const closeOnOwnEscape = useCallback((event) => {
|
|
108
|
+
if (event.key !== 'Escape')
|
|
109
|
+
return false;
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
closeToTrigger();
|
|
112
|
+
return true;
|
|
113
|
+
}, [closeToTrigger]);
|
|
103
114
|
const handleTriggerKeyDown = useCallback((event) => {
|
|
104
|
-
if (open
|
|
115
|
+
if (open) {
|
|
116
|
+
closeOnOwnEscape(event);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
|
|
105
120
|
return;
|
|
106
121
|
event.preventDefault();
|
|
107
122
|
setOpen(true);
|
|
108
|
-
}, [open]);
|
|
123
|
+
}, [closeOnOwnEscape, open]);
|
|
109
124
|
const handleMenuKeyDown = useCallback((event) => {
|
|
125
|
+
if (closeOnOwnEscape(event))
|
|
126
|
+
return;
|
|
110
127
|
if (event.key === 'Tab') {
|
|
111
128
|
setOpen(false);
|
|
112
129
|
return;
|
|
@@ -123,7 +140,7 @@ export function useRuntimeDropUp({ width }) {
|
|
|
123
140
|
return;
|
|
124
141
|
event.preventDefault();
|
|
125
142
|
items[next]?.focus();
|
|
126
|
-
}, []);
|
|
143
|
+
}, [closeOnOwnEscape]);
|
|
127
144
|
const choose = useCallback((apply) => {
|
|
128
145
|
apply();
|
|
129
146
|
closeToTrigger();
|
|
@@ -12,6 +12,16 @@ export declare function configuredPickerModel(agent: ChatPaneAgent): string | un
|
|
|
12
12
|
export declare function pickerModelLabel(agent: ChatPaneAgent | undefined, requested?: string, t?: I18nAdapter['t']): string;
|
|
13
13
|
export declare function pickerModelValue(agent: ChatPaneAgent, requested?: string): string;
|
|
14
14
|
export declare function pickerCatalogNote(agent: ChatPaneAgent, t?: I18nAdapter['t']): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* A catalog row's display name: a bare raw id ("claude-opus-5-5", which offline fallback lists carry)
|
|
17
|
+
* reads as "Claude Opus 5.5"; a human label is kept as written. Still exact: a date or context suffix
|
|
18
|
+
* stays, and a routed id ("anthropic/…") is left whole, since its prefix names a different model.
|
|
19
|
+
* @example friendlyModelLabel({ label: 'claude-haiku-4-5-20251001' }) // "Claude Haiku 4.5 20251001"
|
|
20
|
+
* @complexity O(n) in the label length. Pure.
|
|
21
|
+
*/
|
|
22
|
+
export declare function friendlyModelLabel({ label }: {
|
|
23
|
+
label: string;
|
|
24
|
+
}, _options?: Record<string, never>): string;
|
|
15
25
|
/**
|
|
16
26
|
* The composer bar's compact model name: the catalog label (or a prettified raw id when the catalog
|
|
17
27
|
* has no label), minus the leading vendor word the agent's icon already conveys, capped with "…".
|
|
@@ -35,6 +35,10 @@ export function pickerCatalogNote(agent, t = passthrough) {
|
|
|
35
35
|
const catalog = agent.modelCatalog;
|
|
36
36
|
if (catalog?.freshness === 'stale')
|
|
37
37
|
return `${t('Cached')} · ${catalog.fetchedAt}`;
|
|
38
|
+
// While the host is still probing, the live list has not failed yet: the rows are the
|
|
39
|
+
// cached/fallback list the inventory contract says to show meanwhile, not an offline verdict.
|
|
40
|
+
if (agent.discoveryPending)
|
|
41
|
+
return undefined;
|
|
38
42
|
if (catalog?.freshness === 'offline-fallback' || !catalog && agent.models)
|
|
39
43
|
return t('Offline fallback');
|
|
40
44
|
return undefined;
|
|
@@ -49,21 +53,36 @@ const BAR_VENDOR_WORDS = {
|
|
|
49
53
|
// A bare catalog id ("claude-opus-5-5", "gpt-6-astra[1m]", "anthropic/claude-sonnet-5") rather than a display name.
|
|
50
54
|
const RAW_MODEL_ID = /^(?:[a-z0-9._-]+\/)*[a-z0-9][a-z0-9._-]*(?:\[[^\]]*\])?$/;
|
|
51
55
|
const BRAND_CASING = { gpt: 'GPT' };
|
|
52
|
-
function prettifyRawModelId(id) {
|
|
56
|
+
function prettifyRawModelId(id, { keepDates = false } = {}) {
|
|
53
57
|
// A routing prefix ("anthropic/") is the full name's job; the bar names only the model.
|
|
54
58
|
// The caller proved RAW_MODEL_ID, so the first '[' can only start its trailing context suffix.
|
|
55
|
-
const tokens = id.slice(id.lastIndexOf('/') + 1).split('[', 1)[0].split(/[-_]+/).filter((token) => token && !/^\d{8}$/.test(token));
|
|
59
|
+
const tokens = id.slice(id.lastIndexOf('/') + 1).split('[', 1)[0].split(/[-_]+/).filter((token) => token && (keepDates || !/^\d{8}$/.test(token)));
|
|
56
60
|
const words = [];
|
|
57
61
|
for (const token of tokens) {
|
|
58
62
|
const previous = words[words.length - 1];
|
|
59
63
|
// Dash-separated version digits read as one dotted version: "5-5" → "5.5".
|
|
60
|
-
|
|
64
|
+
// A kept 8-digit date stays its own word, never a version part.
|
|
65
|
+
if (/^\d{1,7}$/.test(token) && previous && /^\d+(?:\.\d+)*$/.test(previous))
|
|
61
66
|
words[words.length - 1] = `${previous}.${token}`;
|
|
62
67
|
else
|
|
63
68
|
words.push(BRAND_CASING[token] ?? token.charAt(0).toUpperCase() + token.slice(1));
|
|
64
69
|
}
|
|
65
70
|
return words;
|
|
66
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A catalog row's display name: a bare raw id ("claude-opus-5-5", which offline fallback lists carry)
|
|
74
|
+
* reads as "Claude Opus 5.5"; a human label is kept as written. Still exact: a date or context suffix
|
|
75
|
+
* stays, and a routed id ("anthropic/…") is left whole, since its prefix names a different model.
|
|
76
|
+
* @example friendlyModelLabel({ label: 'claude-haiku-4-5-20251001' }) // "Claude Haiku 4.5 20251001"
|
|
77
|
+
* @complexity O(n) in the label length. Pure.
|
|
78
|
+
*/
|
|
79
|
+
export function friendlyModelLabel({ label }, _options = {}) {
|
|
80
|
+
if (!RAW_MODEL_ID.test(label) || label.includes('/'))
|
|
81
|
+
return label;
|
|
82
|
+
const suffix = label.indexOf('[');
|
|
83
|
+
const words = prettifyRawModelId(label, { keepDates: true }).join(' ');
|
|
84
|
+
return suffix === -1 ? words : `${words} ${label.slice(suffix)}`;
|
|
85
|
+
}
|
|
67
86
|
/**
|
|
68
87
|
* The composer bar's compact model name: the catalog label (or a prettified raw id when the catalog
|
|
69
88
|
* has no label), minus the leading vendor word the agent's icon already conveys, capped with "…".
|