@jini-ai/chat 0.5.4 → 0.5.5
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 +4 -0
- package/dist/core/ag-ui/entry.d.ts +33 -0
- package/dist/core/ag-ui/entry.js +80 -0
- package/dist/core/agentic/chat-capabilities.d.ts +2 -4
- package/dist/core/agentic/index.d.ts +4 -17
- package/dist/core/agentic/index.js +4 -17
- package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
- package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
- package/dist/core/assistant-client/ag-ui-transport.js +147 -0
- package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
- package/dist/core/assistant-client/attachment-liveness.js +93 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
- package/dist/core/assistant-client/durable-run-subscription.js +96 -0
- package/dist/core/assistant-client/entry.d.ts +13 -0
- package/dist/core/assistant-client/entry.js +7 -0
- package/dist/core/assistant-client/messages.d.ts +73 -0
- package/dist/core/assistant-client/messages.js +68 -0
- package/dist/core/assistant-client/transport.d.ts +164 -0
- package/dist/core/assistant-client/transport.js +734 -0
- package/dist/core/browser/page-actions.d.ts +121 -0
- package/dist/core/browser/page-actions.js +113 -0
- package/dist/core/durable-runs/continuation.d.ts +18 -0
- package/dist/core/durable-runs/continuation.js +61 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
- package/dist/core/durable-runs/ledger-contracts.js +2 -0
- package/dist/core/durable-runs/policies.d.ts +6 -0
- package/dist/core/durable-runs/policies.js +6 -0
- package/dist/core/durable-runs/ports.d.ts +123 -0
- package/dist/core/durable-runs/ports.js +2 -0
- package/dist/core/durable-runs/run-contracts.d.ts +17 -0
- package/dist/core/durable-runs/run-contracts.js +2 -0
- package/dist/core/durable-runs/run-lock.d.ts +4 -0
- package/dist/core/durable-runs/run-lock.js +4 -0
- package/dist/core/index.d.ts +6 -19
- package/dist/core/index.js +6 -19
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/question-form/format.d.ts +0 -9
- package/dist/core/question-form/format.js +16 -2
- package/dist/core/question-form/normalize.js +1 -0
- package/dist/core/question-form/secret-answers.d.ts +38 -0
- package/dist/core/question-form/secret-answers.js +45 -0
- package/dist/core/question-form/types.d.ts +2 -0
- package/dist/core/question-form.d.ts +1 -0
- package/dist/core/question-form.js +1 -0
- package/dist/core/run-events/entry.d.ts +208 -16
- package/dist/core/run-events/entry.js +273 -39
- package/dist/core/transcript.d.ts +3 -2
- package/dist/core/transcript.js +19 -3
- package/dist/core/user-text-redaction.d.ts +56 -0
- package/dist/core/user-text-redaction.js +89 -0
- package/dist/embed-widget/browser/page-actions.d.ts +1 -120
- package/dist/embed-widget/browser/page-actions.js +2 -112
- package/dist/embed-widget/react/embed.d.ts +1 -1
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/react/artifact-types.d.ts +9 -15
- package/dist/react/components/QuestionForm.d.ts +2 -1
- package/dist/react/components/QuestionForm.js +18 -12
- package/dist/react/components/QuestionsPanel.d.ts +3 -2
- package/dist/react/components/QuestionsPanel.js +2 -2
- package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
- package/dist/react/features/chat-pane/agent-tools.js +0 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +5 -2
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +29 -17
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +51 -0
- package/dist/react/features/chat-pane/rules.d.ts +3 -2
- package/dist/react/features/chat-pane/rules.js +12 -4
- package/dist/react/features/chat-pane/types.d.ts +25 -1
- package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
- package/dist/react/hooks/assistant-chats-contracts.js +2 -0
- package/dist/react/hooks/assistant-chats.d.ts +7 -0
- package/dist/react/hooks/assistant-chats.js +960 -0
- package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.js +1 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
- package/dist/react/hooks/composer-draft-cache.js +2 -2
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +14 -12
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +12 -21
- package/dist/react/index.js +7 -0
- package/dist/server/durable-runs/accept.d.ts +25 -0
- package/dist/server/durable-runs/accept.js +21 -0
- package/dist/server/durable-runs/entry.d.ts +5 -0
- package/dist/server/durable-runs/entry.js +4 -0
- package/dist/server/durable-runs/recover.d.ts +5 -0
- package/dist/server/durable-runs/recover.js +147 -0
- package/dist/server/run-finalizer/ports.d.ts +2 -16
- package/dist/store/legacy/sqlite/messages.d.ts +24 -18
- package/dist/store/legacy/sqlite/messages.js +12 -2
- package/dist/store/pglite/index.d.ts +2 -1
- package/dist/store/postgres/index.d.ts +2 -1
- package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
- package/dist/store/runtime/chat-expiry-sweep.js +70 -0
- package/dist/store/runtime/chat-history-store.d.ts +20 -0
- package/dist/store/runtime/chat-history-store.js +50 -0
- package/dist/store/runtime/durable-run-store.d.ts +21 -0
- package/dist/store/runtime/durable-run-store.js +342 -0
- package/dist/store/runtime/entry.d.ts +7 -0
- package/dist/store/runtime/entry.js +6 -0
- package/dist/store/runtime/kernel.d.ts +29 -0
- package/dist/store/runtime/kernel.js +2 -0
- package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
- package/dist/store/runtime/run-guarded-chat-store.js +42 -0
- package/dist/store/runtime/run-ledger.d.ts +15 -0
- package/dist/store/runtime/run-ledger.js +206 -0
- package/dist/store/sql/boundary.d.ts +6 -0
- package/dist/store/sql/boundary.js +7 -1
- package/dist/store/sql/store.d.ts +3 -2
- package/dist/store/sql/store.js +9 -2
- package/dist/store/sqlite/index.d.ts +3 -2
- package/package.json +75 -10
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pickerModelLabel } from '../model-options.js';
|
|
1
2
|
/**
|
|
2
3
|
* All of `AgentRuntimePicker`'s behavior, so the component itself is only markup.
|
|
3
4
|
*
|
|
@@ -146,12 +147,12 @@ export function useAgentRuntimePicker({ agents, value, placement, t, executionMo
|
|
|
146
147
|
const [position, setPosition] = useState();
|
|
147
148
|
const triggerRef = useRef(null);
|
|
148
149
|
const popoverRef = useRef(null);
|
|
149
|
-
const orderedAgents = useMemo(() => orderChatPaneAgents({ agents: agents.filter((agent) => agent.available !== false) }), [agents]);
|
|
150
|
+
const orderedAgents = useMemo(() => orderChatPaneAgents({ agents: agents.filter((agent) => agent.available !== false && agent.id !== 'gemini') }), [agents]);
|
|
150
151
|
const selectedAgent = orderedAgents.find((agent) => agent.id === value.agentId);
|
|
151
152
|
// `selectedAgent` stays resolved in BOTH modes on purpose — the CLI selection is still the stored
|
|
152
153
|
// one, and switching back to Local CLI must not have silently lost it. What changes in `'api'` is
|
|
153
154
|
// only what gets DESCRIBED, because none of the CLI's labels are true of an API turn.
|
|
154
|
-
const localModelLabel =
|
|
155
|
+
const localModelLabel = pickerModelLabel(selectedAgent, value.model, t);
|
|
155
156
|
const byok = byokSummaryText({ byokRuntime: byokRuntime, t: t });
|
|
156
157
|
const isApi = executionMode === 'api';
|
|
157
158
|
const modelLabel = isApi ? byok.modelLabel : localModelLabel;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserTextRedactionOptions } from '../../../../core/user-text-redaction.js';
|
|
1
2
|
import { type ChatPaneSendBlocker } from '../rules.js';
|
|
2
3
|
import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
|
|
3
4
|
import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
|
|
@@ -6,7 +7,7 @@ import type { ChatTransport } from '@jini-ai/chat';
|
|
|
6
7
|
import { type UseComposerResult } from '../../../hooks/useComposer.js';
|
|
7
8
|
import { type UseConversationResult } from '../../../hooks/useConversation.js';
|
|
8
9
|
import { type UseChatPaneWorkingDirectoryResult } from './useChatPaneWorkingDirectory.hooks.js';
|
|
9
|
-
export interface UseChatPaneOptions {
|
|
10
|
+
export interface UseChatPaneOptions extends UserTextRedactionOptions {
|
|
10
11
|
transport: ChatTransport;
|
|
11
12
|
agents: readonly ChatPaneAgent[];
|
|
12
13
|
initialMessages?: ChatMessage[];
|
|
@@ -37,6 +38,8 @@ export interface UseChatPaneOptions {
|
|
|
37
38
|
deliverTypedAnswer?: DeliverTypedAnswer;
|
|
38
39
|
}
|
|
39
40
|
export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
41
|
+
/** Show a localized non-secret notice when a paste was removed. */
|
|
42
|
+
secretRedacted: boolean;
|
|
40
43
|
conversation: UseConversationResult;
|
|
41
44
|
composer: UseComposerResult;
|
|
42
45
|
selection: ChatPaneAgentSelection;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useUserTextGuard } from '../../../hooks/useUserTextGuard.js';
|
|
1
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
3
|
import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
|
|
3
4
|
import { definedProps } from '../../../util/defined-props.js';
|
|
@@ -73,9 +74,11 @@ async function uploadAttachmentBatch(files, effects) {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
export function useChatPane(options) {
|
|
77
|
+
const guard = useUserTextGuard(options);
|
|
76
78
|
const [activeUploadCount, setActiveUploadCount] = useState(0);
|
|
77
79
|
const [attachmentError, setAttachmentError] = useState(null);
|
|
78
|
-
|
|
80
|
+
// Keep the signal with sanitized queue text; re-redacting it later cannot detect the original paste.
|
|
81
|
+
const [queuedTurn, setQueuedTurn] = useState(null);
|
|
79
82
|
const [typedAnswerNotice, setTypedAnswerNotice] = useState(null);
|
|
80
83
|
// Set across the await of one typed-answer delivery so a second Enter cannot post the same text
|
|
81
84
|
// twice — the second post would find the question already answered and wrongly report it closed.
|
|
@@ -116,6 +119,8 @@ export function useChatPane(options) {
|
|
|
116
119
|
const selectedAgent = options.agents.find((agent) => agent.id === selection.agentId);
|
|
117
120
|
const conversation = useConversation(definedProps({ source: {
|
|
118
121
|
transport: options.transport,
|
|
122
|
+
redactUserText: options.redactUserText,
|
|
123
|
+
onSecretRedacted: guard.notifyRedaction,
|
|
119
124
|
initialMessages: options.initialMessages,
|
|
120
125
|
conversationId: options.conversationId,
|
|
121
126
|
// Keys off an empty string, not `undefined` — `selection.agentId` is always a string (never
|
|
@@ -175,6 +180,7 @@ export function useChatPane(options) {
|
|
|
175
180
|
options.onSelectionChange?.(validated);
|
|
176
181
|
}, [composer, options.agents, options.onSelectionChange]);
|
|
177
182
|
const sendBlocker = findChatPaneSendBlocker({
|
|
183
|
+
...(selection.model ? { model: selection.model } : {}),
|
|
178
184
|
selectedAgent,
|
|
179
185
|
isStreaming: conversation.isStreaming,
|
|
180
186
|
activeUploadCount,
|
|
@@ -267,8 +273,8 @@ export function useChatPane(options) {
|
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
}, [composer, options.conversationId, options.uploadAttachments]);
|
|
270
|
-
const
|
|
271
|
-
const trimmed =
|
|
276
|
+
const sendGuardedPrompt = useCallback(async ({ redaction }, _optional = {}) => {
|
|
277
|
+
const trimmed = redaction.text.trim();
|
|
272
278
|
if (!trimmed)
|
|
273
279
|
throw new Error('cannot send: the prompt is empty');
|
|
274
280
|
if (sendBlocker !== null) {
|
|
@@ -287,6 +293,7 @@ export function useChatPane(options) {
|
|
|
287
293
|
attachmentBatchIdRef.current = createAttachmentBatchId();
|
|
288
294
|
options.onActivityChange?.('queued');
|
|
289
295
|
await conversation.sendMessage(trimmed, definedProps({ source: {
|
|
296
|
+
secretRedaction: redaction.secretRedacted ? { secretRedacted: true, count: redaction.count } : undefined,
|
|
290
297
|
agentId: selection.agentId,
|
|
291
298
|
// Omitted when the array is EMPTY, not merely absent — an empty `attachments: []` would be a
|
|
292
299
|
// different (valid, present) value to the transport than "no attachments key at all".
|
|
@@ -302,12 +309,16 @@ export function useChatPane(options) {
|
|
|
302
309
|
sendBlocker,
|
|
303
310
|
workingDirectoryState.workingDirectory,
|
|
304
311
|
]);
|
|
312
|
+
const sendPrompt = useCallback(async (prompt) => {
|
|
313
|
+
await sendGuardedPrompt({ redaction: guard.redact({ text: prompt.trim() }, {}) }, {});
|
|
314
|
+
}, [guard.redact, sendGuardedPrompt]);
|
|
305
315
|
const answerWaitingQuestion = useCallback(async ({ deliver, text }) => {
|
|
316
|
+
const safeText = guard.redactText(text);
|
|
306
317
|
const generation = typedAnswerGenerationRef.current;
|
|
307
318
|
const submittedDraft = latestDraftRef.current;
|
|
308
319
|
const questionId = awaitedQuestionId;
|
|
309
320
|
deliveringTypedAnswerRef.current = true;
|
|
310
|
-
const outcome = await deliverTypedAnswerSafely({ deliver, text });
|
|
321
|
+
const outcome = await deliverTypedAnswerSafely({ deliver, text: safeText });
|
|
311
322
|
deliveringTypedAnswerRef.current = false;
|
|
312
323
|
if (!mountedRef.current || generation !== typedAnswerGenerationRef.current)
|
|
313
324
|
return;
|
|
@@ -323,21 +334,21 @@ export function useChatPane(options) {
|
|
|
323
334
|
// correction or next message typed while the host answered.
|
|
324
335
|
if (latestDraftRef.current === submittedDraft)
|
|
325
336
|
composer.setDraft('');
|
|
326
|
-
}, [awaitedQuestionId, composer]);
|
|
337
|
+
}, [awaitedQuestionId, composer, guard.redactText]);
|
|
327
338
|
// The ordinary next-turn path: queue behind a running run, else send now.
|
|
328
339
|
const sendOrdinaryTurn = useCallback(async (prompt) => {
|
|
329
340
|
// Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
|
|
330
341
|
// staged attachments, which this turn still needs when it finally goes out.
|
|
331
342
|
if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
|
|
332
343
|
queuedConversationIdRef.current = options.conversationId;
|
|
333
|
-
|
|
344
|
+
setQueuedTurn(guard.redact({ text: prompt }, {}));
|
|
334
345
|
composer.setDraft('');
|
|
335
346
|
return;
|
|
336
347
|
}
|
|
337
348
|
if (!canSend)
|
|
338
349
|
return;
|
|
339
350
|
await sendPrompt(prompt);
|
|
340
|
-
}, [canSend, composer, options.conversationId, sendBlocker, sendPrompt]);
|
|
351
|
+
}, [canSend, composer, options.conversationId, sendBlocker, sendPrompt, guard.redact]);
|
|
341
352
|
const send = useCallback(async () => {
|
|
342
353
|
const prompt = composerPrompt(composer);
|
|
343
354
|
if (!prompt || deliveringTypedAnswerRef.current)
|
|
@@ -385,31 +396,31 @@ export function useChatPane(options) {
|
|
|
385
396
|
if (!prompt)
|
|
386
397
|
return;
|
|
387
398
|
queuedConversationIdRef.current = options.conversationId;
|
|
388
|
-
|
|
399
|
+
setQueuedTurn(guard.redact({ text: prompt }, {}));
|
|
389
400
|
composer.setDraft('');
|
|
390
401
|
conversation.cancel();
|
|
391
|
-
}, [composer, conversation, options.conversationId]);
|
|
402
|
+
}, [composer, conversation, options.conversationId, guard.redact]);
|
|
392
403
|
const cancelQueued = useCallback(() => {
|
|
393
404
|
queuedConversationIdRef.current = undefined;
|
|
394
|
-
|
|
405
|
+
setQueuedTurn(null);
|
|
395
406
|
}, []);
|
|
396
407
|
// Flush on a FULLY clear blocker, not merely on streaming ending — see
|
|
397
408
|
// `isChatPaneQueueableBlocker`'s note about `findChatPaneSendBlocker`'s ordering. Clearing the
|
|
398
409
|
// queue slot BEFORE awaiting keeps a re-render from double-sending the same prompt.
|
|
399
410
|
useEffect(() => {
|
|
400
|
-
if (
|
|
411
|
+
if (queuedTurn === null || sendBlocker !== null)
|
|
401
412
|
return;
|
|
402
413
|
if (queuedConversationIdRef.current !== options.conversationId) {
|
|
403
414
|
// The conversation changed while this prompt waited behind a streaming run — sending it now
|
|
404
415
|
// would post it into a conversation the user never saw it queued against. Drop it instead
|
|
405
416
|
// (same as `cancelQueued`) rather than misrouting it.
|
|
406
417
|
queuedConversationIdRef.current = undefined;
|
|
407
|
-
|
|
418
|
+
setQueuedTurn(null);
|
|
408
419
|
return;
|
|
409
420
|
}
|
|
410
|
-
|
|
411
|
-
void
|
|
412
|
-
}, [options.conversationId,
|
|
421
|
+
setQueuedTurn(null);
|
|
422
|
+
void sendGuardedPrompt({ redaction: queuedTurn }, {});
|
|
423
|
+
}, [options.conversationId, queuedTurn, sendBlocker, sendGuardedPrompt]);
|
|
413
424
|
const reset = useCallback(() => {
|
|
414
425
|
attachmentGenerationRef.current += 1;
|
|
415
426
|
attachmentBatchIdRef.current = createAttachmentBatchId();
|
|
@@ -428,9 +439,10 @@ export function useChatPane(options) {
|
|
|
428
439
|
// `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
|
|
429
440
|
// the just-reset conversation instead of discarding it like the rest of this turn's state.
|
|
430
441
|
queuedConversationIdRef.current = undefined;
|
|
431
|
-
|
|
442
|
+
setQueuedTurn(null);
|
|
432
443
|
}, [composer, conversation, options.initialMessages]);
|
|
433
444
|
return {
|
|
445
|
+
secretRedacted: guard.secretRedacted,
|
|
434
446
|
conversation,
|
|
435
447
|
composer,
|
|
436
448
|
selection,
|
|
@@ -446,7 +458,7 @@ export function useChatPane(options) {
|
|
|
446
458
|
addAttachments,
|
|
447
459
|
send,
|
|
448
460
|
sendPrompt,
|
|
449
|
-
queuedPrompt,
|
|
461
|
+
queuedPrompt: queuedTurn?.text ?? null,
|
|
450
462
|
cancelQueued,
|
|
451
463
|
typedAnswerNotice,
|
|
452
464
|
sendAsNewMessage,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { I18nAdapter } from '../../slots.js';
|
|
2
|
+
import type { ChatPaneAgent, ChatPaneAgentOption } from './types.js';
|
|
3
|
+
export declare function isConcretePickerModel(id: string): boolean;
|
|
4
|
+
/** Aliases are selectable only as their evidence-backed concrete ID. Never render a Default row. */
|
|
5
|
+
export declare function pickerModelOptions(agent: ChatPaneAgent): ChatPaneAgentOption[];
|
|
6
|
+
export declare function configuredPickerModel(agent: ChatPaneAgent): string | undefined;
|
|
7
|
+
export declare function pickerModelLabel(agent: ChatPaneAgent | undefined, requested?: string, t?: I18nAdapter['t']): string;
|
|
8
|
+
export declare function pickerModelValue(agent: ChatPaneAgent, requested?: string): string;
|
|
9
|
+
export declare function pickerCatalogNote(agent: ChatPaneAgent, t?: I18nAdapter['t']): string | undefined;
|
|
10
|
+
//# sourceMappingURL=model-options.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Model names and timestamps are metadata; only surrounding picker copy is translated.
|
|
2
|
+
const passthrough = key => key;
|
|
3
|
+
export function isConcretePickerModel(id) {
|
|
4
|
+
const token = id.slice(id.lastIndexOf('/') + 1);
|
|
5
|
+
return !/^(?:opus|sonnet|haiku|fable|best|latest|claude|gemini|gpt|codex|swe)(?:\[.*\])?$/i.test(token) && !/^(default|auto|adaptive|smart|deep|rush|ultimate|premium|balanced|fast|grok-build|vercel-ai-gateway)$/i.test(token) && !/(?:^|[-/])(gateway|latest)$/i.test(id);
|
|
6
|
+
}
|
|
7
|
+
/** Aliases are selectable only as their evidence-backed concrete ID. Never render a Default row. */
|
|
8
|
+
export function pickerModelOptions(agent) {
|
|
9
|
+
if (agent.supportsConcreteModelSelection === false)
|
|
10
|
+
return [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const options = (agent.models || []).flatMap((row) => {
|
|
13
|
+
const id = row.resolvedId || (row.identityKind === 'routing-mode' || row.identityKind === 'alias' ? undefined : row.id);
|
|
14
|
+
if (!id || !isConcretePickerModel(id) || seen.has(id))
|
|
15
|
+
return [];
|
|
16
|
+
seen.add(id);
|
|
17
|
+
return [{ ...row, id, label: row.label, identityKind: 'concrete' }];
|
|
18
|
+
});
|
|
19
|
+
const resolution = agent.defaultModelResolution;
|
|
20
|
+
// A host may publish a resolved default outside its visible catalog. Preserve its concrete ID.
|
|
21
|
+
if (resolution?.status === 'resolved' && isConcretePickerModel(resolution.id) && !seen.has(resolution.id))
|
|
22
|
+
options.unshift({ id: resolution.id, label: resolution.id, identityKind: 'concrete' });
|
|
23
|
+
return options;
|
|
24
|
+
}
|
|
25
|
+
export function configuredPickerModel(agent) {
|
|
26
|
+
if (agent.supportsConcreteModelSelection === false)
|
|
27
|
+
return undefined;
|
|
28
|
+
const resolution = agent.defaultModelResolution;
|
|
29
|
+
return resolution?.status === 'resolved' && isConcretePickerModel(resolution.id) ? resolution.id : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function pickerModelLabel(agent, requested, t = passthrough) {
|
|
32
|
+
if (!agent)
|
|
33
|
+
return t('Choose model');
|
|
34
|
+
const id = !requested || requested === 'default' ? configuredPickerModel(agent) : requested;
|
|
35
|
+
if (!id || !isConcretePickerModel(id))
|
|
36
|
+
return t('Choose model');
|
|
37
|
+
return pickerModelOptions(agent).find((row) => row.id === id)?.label || id;
|
|
38
|
+
}
|
|
39
|
+
export function pickerModelValue(agent, requested) {
|
|
40
|
+
const id = requested && requested !== 'default' ? requested : configuredPickerModel(agent);
|
|
41
|
+
return id && pickerModelOptions(agent).some((row) => row.id === id) ? id : '';
|
|
42
|
+
}
|
|
43
|
+
export function pickerCatalogNote(agent, t = passthrough) {
|
|
44
|
+
const catalog = agent.modelCatalog;
|
|
45
|
+
if (catalog?.freshness === 'stale')
|
|
46
|
+
return `${t('Cached')} · ${catalog.fetchedAt}`;
|
|
47
|
+
if (catalog?.freshness === 'offline-fallback' || !catalog && agent.models)
|
|
48
|
+
return t('Offline fallback');
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=model-options.js.map
|
|
@@ -14,9 +14,10 @@ export declare function resolveChatPaneSelection({ agents, requested }: {
|
|
|
14
14
|
* precondition (`composer.canSubmit`), not a property of the pane. An agent-driven send supplies
|
|
15
15
|
* its own prompt and must still clear every blocker below.
|
|
16
16
|
*/
|
|
17
|
-
export type ChatPaneSendBlocker = 'no-agent-selected' | 'agent-unavailable' | 'streaming' | 'uploads-pending' | 'working-directory-pending' | 'working-directory-invalid' | 'working-directory-error';
|
|
17
|
+
export type ChatPaneSendBlocker = 'no-agent-selected' | 'agent-unavailable' | 'model-unresolved' | 'streaming' | 'uploads-pending' | 'working-directory-pending' | 'working-directory-invalid' | 'working-directory-error';
|
|
18
18
|
export interface ChatPaneSendability {
|
|
19
|
-
readonly selectedAgent: Pick<ChatPaneAgent, 'available'> | undefined;
|
|
19
|
+
readonly selectedAgent: Pick<ChatPaneAgent, 'available' | 'modelCatalog' | 'supportsConcreteModelSelection'> | undefined;
|
|
20
|
+
readonly model?: string;
|
|
20
21
|
readonly isStreaming: boolean;
|
|
21
22
|
readonly activeUploadCount: number;
|
|
22
23
|
readonly workingDirectoryPending: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configuredPickerModel, pickerModelOptions, isConcretePickerModel } from './model-options.js';
|
|
1
2
|
function preferredOptionId(options) {
|
|
2
3
|
return options?.find((option) => option.id === 'default')?.id ?? options?.[0]?.id;
|
|
3
4
|
}
|
|
@@ -7,7 +8,9 @@ function validatedOptionId(options, requested, fallback) {
|
|
|
7
8
|
return fallback;
|
|
8
9
|
}
|
|
9
10
|
export function defaultChatPaneSelection({ agent }) {
|
|
10
|
-
|
|
11
|
+
// An unresolved native route requires an intentional concrete choice. Legacy hosts without
|
|
12
|
+
// structured resolution retain their first-concrete-entry behavior.
|
|
13
|
+
const model = configuredPickerModel(agent) || (agent.defaultModelResolution?.status === 'unresolved' ? undefined : pickerModelOptions(agent)[0]?.id);
|
|
11
14
|
const reasoning = preferredOptionId(agent.reasoningOptions);
|
|
12
15
|
return {
|
|
13
16
|
agentId: agent.id,
|
|
@@ -16,17 +19,19 @@ export function defaultChatPaneSelection({ agent }) {
|
|
|
16
19
|
};
|
|
17
20
|
}
|
|
18
21
|
export function resolveChatPaneSelection({ agents, requested }) {
|
|
19
|
-
const requestedAgent = agents.find((agent) => agent.id === requested.agentId && agent.available !== false);
|
|
20
|
-
const agent = requestedAgent ?? agents.find((candidate) => candidate.available !== false);
|
|
22
|
+
const requestedAgent = agents.find((agent) => agent.id === requested.agentId && agent.id !== 'gemini' && agent.available !== false);
|
|
23
|
+
const agent = requestedAgent ?? agents.find((candidate) => candidate.id !== 'gemini' && candidate.available !== false);
|
|
21
24
|
if (!agent)
|
|
22
25
|
return { agentId: '' };
|
|
23
26
|
const defaults = defaultChatPaneSelection({ agent: agent });
|
|
24
27
|
if (agent !== requestedAgent)
|
|
25
28
|
return defaults;
|
|
26
29
|
const model = requested.model
|
|
30
|
+
&& isConcretePickerModel(requested.model)
|
|
27
31
|
&& agent.supportsCustomModel
|
|
32
|
+
&& agent.supportsConcreteModelSelection !== false
|
|
28
33
|
? requested.model
|
|
29
|
-
: validatedOptionId(agent
|
|
34
|
+
: validatedOptionId(pickerModelOptions(agent), requested.model, defaults.model);
|
|
30
35
|
const reasoning = validatedOptionId(agent.reasoningOptions, requested.reasoning, defaults.reasoning);
|
|
31
36
|
return {
|
|
32
37
|
agentId: agent.id,
|
|
@@ -64,6 +69,8 @@ export function findChatPaneSendBlocker(state) {
|
|
|
64
69
|
else if (state.selectedAgent.available === false) {
|
|
65
70
|
return 'agent-unavailable';
|
|
66
71
|
}
|
|
72
|
+
if (!state.apiModeConfigured && state.selectedAgent && (state.selectedAgent.modelCatalog || state.selectedAgent.supportsConcreteModelSelection === false) && (!state.model || !isConcretePickerModel(state.model) || state.selectedAgent.supportsConcreteModelSelection === false))
|
|
73
|
+
return 'model-unresolved';
|
|
67
74
|
if (state.isStreaming)
|
|
68
75
|
return 'streaming';
|
|
69
76
|
if (state.activeUploadCount > 0)
|
|
@@ -79,6 +86,7 @@ export function findChatPaneSendBlocker(state) {
|
|
|
79
86
|
const SEND_BLOCKER_MESSAGES = {
|
|
80
87
|
'no-agent-selected': 'no agent is selected',
|
|
81
88
|
'agent-unavailable': 'the selected agent is unavailable',
|
|
89
|
+
'model-unresolved': 'choose a concrete model before sending',
|
|
82
90
|
streaming: 'a run is already streaming — cancel it first',
|
|
83
91
|
'uploads-pending': 'attachment uploads are still in flight',
|
|
84
92
|
'working-directory-pending': 'the working directory is still being validated',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
1
2
|
import type { CSSProperties, ReactNode, RefObject } from 'react';
|
|
2
3
|
import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
|
|
3
4
|
import type { ComposerSlots } from '../../slots.js';
|
|
@@ -6,6 +7,8 @@ import type { ChatTransport, RunContext } from '@jini-ai/chat';
|
|
|
6
7
|
export interface ChatPaneAgentOption {
|
|
7
8
|
id: string;
|
|
8
9
|
label: string;
|
|
10
|
+
identityKind?: 'concrete' | 'alias' | 'routing-mode';
|
|
11
|
+
resolvedId?: string;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* Browser-safe runtime inventory consumed by the chat pane. This is
|
|
@@ -19,8 +22,29 @@ export interface ChatPaneAgent {
|
|
|
19
22
|
version?: string | null;
|
|
20
23
|
authStatus?: 'ok' | 'missing' | 'unknown';
|
|
21
24
|
models?: readonly ChatPaneAgentOption[];
|
|
25
|
+
modelCatalog?: {
|
|
26
|
+
source: 'cli' | 'rpc' | 'provider-api' | 'cli-cache' | 'config-file' | 'offline-fallback';
|
|
27
|
+
freshness: 'fresh' | 'stale' | 'offline-fallback';
|
|
28
|
+
fetchedAt: string;
|
|
29
|
+
expiresAt: string;
|
|
30
|
+
coverage: 'account' | 'provider' | 'configured' | 'observed';
|
|
31
|
+
launchFingerprint: string;
|
|
32
|
+
};
|
|
33
|
+
defaultModelResolution?: {
|
|
34
|
+
status: 'resolved';
|
|
35
|
+
id: string;
|
|
36
|
+
selectionId?: string;
|
|
37
|
+
source: string;
|
|
38
|
+
resolvedAt: string;
|
|
39
|
+
launchFingerprint: string;
|
|
40
|
+
} | {
|
|
41
|
+
status: 'unresolved';
|
|
42
|
+
selectionId?: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
};
|
|
22
45
|
reasoningOptions?: readonly ChatPaneAgentOption[];
|
|
23
46
|
supportsCustomModel?: boolean;
|
|
47
|
+
supportsConcreteModelSelection?: boolean;
|
|
24
48
|
diagnostic?: string;
|
|
25
49
|
/**
|
|
26
50
|
* Whether this runtime can receive external MCP servers (host/Jini tools) at all — mirrors
|
|
@@ -143,7 +167,7 @@ export type DeliverTypedAnswer = ((input: {
|
|
|
143
167
|
}) => Promise<TypedAnswerDelivery>) & {
|
|
144
168
|
readonly toolName: string;
|
|
145
169
|
};
|
|
146
|
-
export interface ChatPaneProps {
|
|
170
|
+
export interface ChatPaneProps extends UserTextRedactionOptions {
|
|
147
171
|
transport: ChatTransport;
|
|
148
172
|
agents?: readonly ChatPaneAgent[];
|
|
149
173
|
/** Optional host effects for package-owned inventory, rescan, and health polling. */
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ChatMessage } from "../../core/messages.js";
|
|
2
|
+
import type { AssistantConversation } from "../../core/assistant-client/messages.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file What `useAssistantChats` needs from the outside world, as an interface rather than a set of
|
|
5
|
+
* module imports.
|
|
6
|
+
*
|
|
7
|
+
* The hook used to import `lib/assistant-chats`'s functions directly, which meant the only way to
|
|
8
|
+
* test it was to stub the global `fetch` and assert on the URLs and methods that came out — the
|
|
9
|
+
* tests described HTTP when what they meant to describe was "which conversation did this message
|
|
10
|
+
* get written to". Worse, behaviour that has nothing to do with transport (the write retry and its
|
|
11
|
+
* backoff) could only be exercised by hand-building `Response` objects with the right status codes.
|
|
12
|
+
*
|
|
13
|
+
* Declaring the seam here follows the `useX(dependencies)` / `useWiredX()` pair this workspace
|
|
14
|
+
* already uses throughout `@jini-ai/ui` (see `features/html-viewer/react/hooks/usePresentMode.ts`
|
|
15
|
+
* for the canonical shape, and `foundry/docs/jini-port/skills/fixing-open-design-web.md` for the
|
|
16
|
+
* rules). `ports.ts` declares, `assistant-chats-dependencies.ts` binds, and nothing else imports the
|
|
17
|
+
* real provider.
|
|
18
|
+
*
|
|
19
|
+
* `AssistantConversation` is imported here `import type`, which is erased at compile time — so this
|
|
20
|
+
* file still has no runtime dependency on `lib/assistant-chats`, and `dependencies.ts` remains the
|
|
21
|
+
* single place the real implementation is reached.
|
|
22
|
+
*
|
|
23
|
+
* ## What is deliberately NOT in this port
|
|
24
|
+
*
|
|
25
|
+
* `persistableMessages` and `HttpError`. The first is a pure rule — no I/O, no host — and per the
|
|
26
|
+
* pattern a hook imports rules directly rather than having them injected; making it swappable would
|
|
27
|
+
* let a fake quietly change which messages count as settled, which is the one thing every test here
|
|
28
|
+
* needs to hold still. The second is the error type an implementation is expected to throw for a
|
|
29
|
+
* non-2xx response, and it is part of this contract even though it is not a method: {@link
|
|
30
|
+
* AssistantChatsPort.saveMessage}'s retry classifies failures by it.
|
|
31
|
+
*/
|
|
32
|
+
export interface AssistantChatsPort {
|
|
33
|
+
listConversations(required: Record<string, never>, optional: Record<string, never>): Promise<AssistantConversation[]>;
|
|
34
|
+
/** `firstMessage` seeds the server-side title heuristic; omit it for an untitled empty chat. */
|
|
35
|
+
createConversation(required: {
|
|
36
|
+
firstMessage?: string;
|
|
37
|
+
}, optional: Record<string, never>): Promise<AssistantConversation>;
|
|
38
|
+
renameConversation(required: {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
}, optional: Record<string, never>): Promise<void>;
|
|
42
|
+
deleteConversation(required: {
|
|
43
|
+
id: string;
|
|
44
|
+
}, optional: Record<string, never>): Promise<void>;
|
|
45
|
+
loadMessages(required: {
|
|
46
|
+
conversationId: string;
|
|
47
|
+
}, optional: Record<string, never>): Promise<ChatMessage[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Writes one message.
|
|
50
|
+
*
|
|
51
|
+
* Expected to reject with an `HttpError` for a non-2xx response and with anything else for a
|
|
52
|
+
* transport failure — the hook retries the second kind, and the first only for 429/5xx. An
|
|
53
|
+
* implementation that swallows failures into a resolved promise silently disables the retry.
|
|
54
|
+
*/
|
|
55
|
+
saveMessage(required: {
|
|
56
|
+
conversationId: string;
|
|
57
|
+
message: ChatMessage;
|
|
58
|
+
}, optional: Record<string, never>): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface UseAssistantChats {
|
|
61
|
+
conversations: AssistantConversation[];
|
|
62
|
+
activeId: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* `ChatPane`'s `key`. Deliberately NOT `activeId`.
|
|
65
|
+
*
|
|
66
|
+
* Remounting the pane is correct for a user-initiated switch and destructive at any other time: it
|
|
67
|
+
* re-seeds from `initialMessages` and discards whatever the pane currently holds. `activeId` now
|
|
68
|
+
* also changes when an untitled pane silently adopts a freshly created conversation mid-run (see
|
|
69
|
+
* `onMessagesChange`), and re-keying on that would wipe the reply being streamed. So the two are
|
|
70
|
+
* separate: this changes only on `select`/`create`/`remove`, `activeId` tracks where writes go.
|
|
71
|
+
*/
|
|
72
|
+
paneKey: string;
|
|
73
|
+
/** Messages to seed the pane with. Changes identity only on a real conversation switch. */
|
|
74
|
+
initialMessages: ChatMessage[];
|
|
75
|
+
select: (required: {
|
|
76
|
+
id: string;
|
|
77
|
+
}, optional?: Record<string, never>) => void;
|
|
78
|
+
create: (required: Record<string, never>, optional?: Record<string, never>) => Promise<void>;
|
|
79
|
+
remove: (required: {
|
|
80
|
+
id: string;
|
|
81
|
+
}, optional?: Record<string, never>) => Promise<void>;
|
|
82
|
+
rename: (required: {
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
}, optional?: Record<string, never>) => Promise<void>;
|
|
86
|
+
onMessagesChange: (required: {
|
|
87
|
+
messages: ChatMessage[];
|
|
88
|
+
}, optional?: Record<string, never>) => void;
|
|
89
|
+
/**
|
|
90
|
+
* This pane's conversation id, adopting one if none is active yet. Awaited by
|
|
91
|
+
* `assistant-transport.ts`'s `startRun` so turn 1's run carries the identity its agent-CLI session
|
|
92
|
+
* id gets filed under — see the hook's own implementation doc for the amnesia defect that needs it.
|
|
93
|
+
* Resolves `null` (never rejects) when creation fails.
|
|
94
|
+
*/
|
|
95
|
+
ensureConversationId: (required: Record<string, never>, optional?: Record<string, never>) => Promise<string | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Writes one user turn durably, awaited by `assistant-transport.ts`'s `startRun` BEFORE it
|
|
98
|
+
* dispatches the run — see the implementation's own doc for the defect (a failed run's user
|
|
99
|
+
* message reaching no agent and no later prompt). Never rejects; a failed write releases the id
|
|
100
|
+
* so the delta-driven `flush` can still pick it up.
|
|
101
|
+
*/
|
|
102
|
+
persistUserTurn: (required: {
|
|
103
|
+
conversationId: string;
|
|
104
|
+
message: ChatMessage;
|
|
105
|
+
}, optional?: Record<string, never>) => Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
export interface AssistantChatsClientPorts {
|
|
108
|
+
readonly statusOf: (required: {
|
|
109
|
+
error: unknown;
|
|
110
|
+
}, optional: Record<string, never>) => number | undefined;
|
|
111
|
+
readonly sleep: (required: {
|
|
112
|
+
delayMs: number;
|
|
113
|
+
}, optional: Record<string, never>) => Promise<void>;
|
|
114
|
+
readonly reportPermanent: (required: {
|
|
115
|
+
conversationId: string;
|
|
116
|
+
messageId: string;
|
|
117
|
+
error: unknown;
|
|
118
|
+
}, optional: Record<string, never>) => void;
|
|
119
|
+
readonly requestRunPrefixes: readonly string[];
|
|
120
|
+
}
|
|
121
|
+
export type SaveOutcome = "saved" | "exhausted" | "permanent" | "missing";
|
|
122
|
+
export type AttemptResult = {
|
|
123
|
+
done: true;
|
|
124
|
+
outcome: SaveOutcome;
|
|
125
|
+
} | {
|
|
126
|
+
done: false;
|
|
127
|
+
};
|
|
128
|
+
export interface SaveFailureInput {
|
|
129
|
+
error: unknown;
|
|
130
|
+
conversationId: string;
|
|
131
|
+
message: ChatMessage;
|
|
132
|
+
attempt: number;
|
|
133
|
+
isDisposed: () => boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface AssistantChatsClient {
|
|
136
|
+
giveUpOutcome(required: {
|
|
137
|
+
error: unknown;
|
|
138
|
+
}, optional?: Record<string, never>): SaveOutcome;
|
|
139
|
+
handleSaveFailure(required: SaveFailureInput, optional?: Record<string, never>): Promise<AttemptResult>;
|
|
140
|
+
attemptSave(required: Omit<SaveFailureInput, "error"> & {
|
|
141
|
+
port: AssistantChatsPort;
|
|
142
|
+
}, optional?: Record<string, never>): Promise<AttemptResult>;
|
|
143
|
+
summarizeFlushOutcomes(required: {
|
|
144
|
+
results: readonly {
|
|
145
|
+
message: ChatMessage;
|
|
146
|
+
outcome: SaveOutcome;
|
|
147
|
+
}[];
|
|
148
|
+
isConversationStillActive: boolean;
|
|
149
|
+
}, optional?: Record<string, never>): {
|
|
150
|
+
idsToRelease: string[];
|
|
151
|
+
shouldRefresh: boolean;
|
|
152
|
+
};
|
|
153
|
+
resolveRememberedConversation(required: {
|
|
154
|
+
remembered: string | null;
|
|
155
|
+
conversations: readonly AssistantConversation[];
|
|
156
|
+
listFailed: boolean;
|
|
157
|
+
}, optional?: Record<string, never>): "restore" | "forget" | "keep";
|
|
158
|
+
useAssistantChats(required: {
|
|
159
|
+
port: AssistantChatsPort;
|
|
160
|
+
}, optional?: {
|
|
161
|
+
lastConversation?: import("./last-conversation-store.js").LastConversationStore;
|
|
162
|
+
}): UseAssistantChats;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=assistant-chats-contracts.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AssistantChatsClientPorts, AssistantChatsClient } from "./assistant-chats-contracts.js";
|
|
2
|
+
/** Bind effects once; the returned hook captures its conversation port at mount.
|
|
3
|
+
* Existing race guards and retry ordering remain in the original lexical lifecycle. */
|
|
4
|
+
export declare function createAssistantChatsClient({ ports }: {
|
|
5
|
+
ports: AssistantChatsClientPorts;
|
|
6
|
+
}, _options?: Record<string, never>): AssistantChatsClient;
|
|
7
|
+
//# sourceMappingURL=assistant-chats.d.ts.map
|