@jini-ai/chat 0.5.3 → 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 +11 -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/composer-history.d.ts +54 -0
- package/dist/core/composer-history.js +57 -0
- package/dist/core/durable-projection.d.ts +28 -0
- package/dist/core/durable-projection.js +71 -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 +9 -19
- package/dist/core/index.js +8 -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/transport.d.ts +4 -0
- 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/Composer.js +6 -0
- package/dist/react/components/MessageRow.js +24 -11
- 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 +9 -4
- 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 +7 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
- 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 +30 -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/composer-history-storage.d.ts +8 -0
- package/dist/react/hooks/composer-history-storage.js +40 -0
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useComposer.d.ts +10 -1
- package/dist/react/hooks/useComposer.js +14 -2
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
- package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +20 -14
- package/dist/react/hooks/useRunStream.js +12 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +14 -21
- package/dist/react/index.js +8 -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,5 +1,6 @@
|
|
|
1
|
+
import type { UserTextRedactionOptions } from '../../core/user-text-redaction.js';
|
|
1
2
|
import type { QuestionForm as QuestionFormType } from '../../core/index.js';
|
|
2
|
-
export interface QuestionsPanelProps {
|
|
3
|
+
export interface QuestionsPanelProps extends UserTextRedactionOptions {
|
|
3
4
|
form: QuestionFormType | null;
|
|
4
5
|
interactive: boolean;
|
|
5
6
|
/** Disables Continue/Skip while the turn is busy, without locking the form itself. */
|
|
@@ -9,5 +10,5 @@ export interface QuestionsPanelProps {
|
|
|
9
10
|
generating?: boolean;
|
|
10
11
|
onSubmit: (text: string, answers: Record<string, string | string[]>) => void;
|
|
11
12
|
}
|
|
12
|
-
export declare function QuestionsPanel({ form, interactive, submitDisabled, submittedAnswers, generating, onSubmit }: QuestionsPanelProps): import("react").JSX.Element | null;
|
|
13
|
+
export declare function QuestionsPanel({ form, interactive, submitDisabled, submittedAnswers, generating, onSubmit, redactUserText, onSecretRedacted }: QuestionsPanelProps): import("react").JSX.Element | null;
|
|
13
14
|
//# sourceMappingURL=QuestionsPanel.d.ts.map
|
|
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
import { useRef, useState } from 'react';
|
|
21
21
|
import { useT } from '../hooks/context.js';
|
|
22
22
|
import { QuestionForm } from './QuestionForm.js';
|
|
23
|
-
export function QuestionsPanel({ form, interactive, submitDisabled = false, submittedAnswers, generating = false, onSubmit }) {
|
|
23
|
+
export function QuestionsPanel({ form, interactive, submitDisabled = false, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
|
|
24
24
|
const t = useT();
|
|
25
25
|
const formRef = useRef(null);
|
|
26
26
|
const [ready, setReady] = useState(false);
|
|
@@ -28,6 +28,6 @@ export function QuestionsPanel({ form, interactive, submitDisabled = false, subm
|
|
|
28
28
|
return null;
|
|
29
29
|
const answered = submittedAnswers !== undefined;
|
|
30
30
|
const busy = submitDisabled || generating;
|
|
31
|
-
return (_jsxs("div", { className: "qp-panel", children: [_jsx(QuestionForm, { ref: formRef, form: form, interactive: interactive, ...(submittedAnswers !== undefined ? { submittedAnswers } : {}), hideInternalSubmit: true, onReadyChange: setReady, onSubmit: onSubmit }), !answered ? (_jsxs("div", { className: "qp-foot", children: [generating ? _jsx("span", { className: "qp-hint", children: t('Still generating…') }) : null, _jsx("button", { type: "button", className: "qp-skip", disabled: busy, onClick: () => formRef.current?.skipAll(), children: t('Skip') }), _jsx("button", { type: "button", className: "qp-continue primary", disabled: busy || !ready, onClick: () => formRef.current?.submit(), children: t('Continue') })] })) : null] }));
|
|
31
|
+
return (_jsxs("div", { className: "qp-panel", children: [_jsx(QuestionForm, { ref: formRef, form: form, interactive: interactive, ...(submittedAnswers !== undefined ? { submittedAnswers } : {}), hideInternalSubmit: true, ...(redactUserText ? { redactUserText } : {}), ...(onSecretRedacted ? { onSecretRedacted } : {}), onReadyChange: setReady, onSubmit: onSubmit }), !answered ? (_jsxs("div", { className: "qp-foot", children: [generating ? _jsx("span", { className: "qp-hint", children: t('Still generating…') }) : null, _jsx("button", { type: "button", className: "qp-skip", disabled: busy, onClick: () => formRef.current?.skipAll(), children: t('Skip') }), _jsx("button", { type: "button", className: "qp-continue primary", disabled: busy || !ready, onClick: () => formRef.current?.submit(), children: t('Continue') })] })) : null] }));
|
|
32
32
|
}
|
|
33
33
|
//# sourceMappingURL=QuestionsPanel.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CapabilityDef, CapabilityInputSchema, CapabilityRisk } from '@jini-ai/agentic';
|
|
2
2
|
/** @deprecated Use `CapabilityRisk` from `@jini-ai/agentic`. */
|
|
3
3
|
export type ChatPaneAgentToolRisk = CapabilityRisk;
|
|
4
4
|
/** @deprecated Use `CapabilityInputSchema` from `@jini-ai/agentic`. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { pickerModelValue, pickerCatalogNote, pickerModelOptions } from '../model-options.js';
|
|
2
3
|
import { createPortal } from 'react-dom';
|
|
3
4
|
import { AgentIcon, RemixIcon, SearchableModelSelect } from '@jini-ai/ui';
|
|
4
5
|
import { useT } from '../../../hooks/context.js';
|
|
@@ -30,11 +31,13 @@ function RuntimeAgentList({ orderedAgents, value, onChange, agentIconBasePath, t
|
|
|
30
31
|
function RuntimeModelSelects({ selectedAgent, value, onChange, t }) {
|
|
31
32
|
if (!selectedAgent)
|
|
32
33
|
return null;
|
|
33
|
-
|
|
34
|
+
const models = pickerModelOptions(selectedAgent);
|
|
35
|
+
const note = pickerCatalogNote(selectedAgent, t);
|
|
36
|
+
return (_jsxs("div", { className: "jini-runtime-models", children: [note ? _jsx("small", { children: note }) : null, models.length > 0 ? (_jsxs("label", { className: "jini-runtime-select", children: [_jsx("span", { children: t('Model') }), _jsxs("select", { "aria-label": t('Model'), value: pickerModelValue(selectedAgent, value.model), onChange: (event) => onChange({
|
|
34
37
|
agentId: value.agentId,
|
|
35
38
|
model: event.target.value,
|
|
36
39
|
...(value.reasoning ? { reasoning: value.reasoning } : {}),
|
|
37
|
-
}), children:
|
|
40
|
+
}), children: [_jsx("option", { value: "", disabled: true, children: t('Choose model') }), models.map((model) => (_jsx("option", { value: model.id, children: model.label }, model.id)))] })] })) : null, (selectedAgent.reasoningOptions?.length ?? 0) > 0 ? (_jsxs("label", { className: "jini-runtime-select", children: [_jsx("span", { children: t('Reasoning') }), _jsx("select", { "aria-label": t('Reasoning'), value: value.reasoning ?? '', onChange: (event) => onChange({
|
|
38
41
|
agentId: value.agentId,
|
|
39
42
|
...(value.model ? { model: value.model } : {}),
|
|
40
43
|
reasoning: event.target.value,
|
|
@@ -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, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
|
|
9
|
+
export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
|
|
10
10
|
//# sourceMappingURL=ChatPane.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { SECRET_REDACTED_NOTICE } from '../../../../core/user-text-redaction.js';
|
|
2
3
|
import { useEffect, useRef } from 'react';
|
|
3
4
|
import { WorkingDirPicker } from '@jini-ai/ui';
|
|
4
5
|
import { Composer } from '../../../components/Composer.js';
|
|
@@ -73,8 +74,8 @@ function ChatPaneNoUsableCliMessage({ executionMode, apiModeAvailable, onExecuti
|
|
|
73
74
|
}
|
|
74
75
|
/** The pane's stack of mutually-independent error/status banners — each condition is its own
|
|
75
76
|
* 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,
|
|
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,
|
|
77
|
+
function ChatPaneStatusMessages({ unavailable, scanningAgents, connectingAgents, executionMode, apiModeAvailable, onExecutionModeChange, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, onSendAsNewMessage, t, }) {
|
|
78
|
+
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, 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
79
|
}
|
|
79
80
|
function ChatPaneWorkingDirectoryBlock({ workingDirectoryAccess, workingDirectoryControlPlacement, pane, t, }) {
|
|
80
81
|
if (workingDirectoryAccess && workingDirectoryControlPlacement === 'below') {
|
|
@@ -178,7 +179,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
|
|
|
178
179
|
* @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
|
|
179
180
|
* @overallScore 100/100
|
|
180
181
|
*/
|
|
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
|
+
export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, runtimeAccess, runtimeStatusPollMs = 5_000, title, variant = 'workspace', initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
|
|
182
183
|
const t = useT();
|
|
183
184
|
const composerPlaceholder = useChatPaneComposerPlaceholder({ placeholder: placeholder, placeholders: placeholders });
|
|
184
185
|
const inventory = useChatPaneRuntimeInventory(definedProps({ source: {
|
|
@@ -201,10 +202,14 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
201
202
|
onSelectionChange,
|
|
202
203
|
runContext,
|
|
203
204
|
initialDraft,
|
|
205
|
+
composerHistoryScope,
|
|
206
|
+
composerHistoryStorage,
|
|
204
207
|
validateAttachments,
|
|
205
208
|
uploadAttachments,
|
|
206
209
|
onActivityChange,
|
|
207
210
|
onMessagesChange,
|
|
211
|
+
redactUserText,
|
|
212
|
+
onSecretRedacted,
|
|
208
213
|
workingDirectory,
|
|
209
214
|
initialWorkingDirectory,
|
|
210
215
|
onChangeWorkingDirectory,
|
|
@@ -263,6 +268,6 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
263
268
|
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 } }) })),
|
|
264
269
|
} });
|
|
265
270
|
const { rootRef, controlsRef } = useChatPaneControlsHeight();
|
|
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] }));
|
|
271
|
+
return (_jsxs("section", { className: chatPaneClassName(variant, className), style: style, "data-activity": pane.activity, ref: rootRef, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(header, title, pane.reset, t), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompt: pane.queuedPrompt === null ? null : { text: pane.queuedPrompt, onCancel: pane.cancelQueued }, ...(projectFileNames === undefined ? {} : { projectFileNames: new Set(projectFileNames) }) }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [_jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: pane.composer.setDraft, t: t }), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, conversationError: pane.conversation.error, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, onSendAsNewMessage: () => void pane.sendAsNewMessage(), t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: uploadAttachments, pane: pane, disabled: disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: attachmentAccept, workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, t: t })] })] }), footer] }));
|
|
267
272
|
}
|
|
268
273
|
//# sourceMappingURL=ChatPane.js.map
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* new run: 202 delivered it; 409 means nothing is waiting any more (answered, expired, or never
|
|
27
27
|
* open); anything else is a failure the human can retry. A rejection would collapse the last two.
|
|
28
28
|
*/
|
|
29
|
+
import { type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
29
30
|
import type { DeliverTypedAnswer } from './types.js';
|
|
30
|
-
export interface CreateTypedAnswerPosterOptions {
|
|
31
|
+
export interface CreateTypedAnswerPosterOptions extends UserTextRedactionOptions {
|
|
31
32
|
/** Path appended to `baseUrl`. Defaults to `/api/mcp-ui/tool-calls`, as for `createMcpUiToolCaller`. */
|
|
32
33
|
readonly path?: string;
|
|
33
34
|
/** Abandon a post after this many ms (reported as `'failed'`). Defaults to 30s. */
|
|
@@ -1,3 +1,32 @@
|
|
|
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 { redactUserText } from '../../../core/user-text-redaction.js';
|
|
1
30
|
/**
|
|
2
31
|
* The params key a typed answer travels under. Spelled here rather than imported because this
|
|
3
32
|
* browser package cannot depend on `@jini-ai/daemon`, which owns the canonical
|
|
@@ -28,6 +57,9 @@ export function createTypedAnswerPoster({ baseUrl, fetch, toolName }, options =
|
|
|
28
57
|
const endpoint = `${baseUrl.replace(/\/$/u, '')}${options.path ?? DEFAULT_PATH}`;
|
|
29
58
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
30
59
|
const deliver = async ({ text }) => {
|
|
60
|
+
const result = (options.redactUserText ?? redactUserText)({ text });
|
|
61
|
+
if (result.secretRedacted)
|
|
62
|
+
options.onSecretRedacted?.({ secretRedacted: true, count: result.count });
|
|
31
63
|
const controller = new AbortController();
|
|
32
64
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
33
65
|
const outcome = await fetch(endpoint, {
|
|
@@ -35,7 +67,7 @@ export function createTypedAnswerPoster({ baseUrl, fetch, toolName }, options =
|
|
|
35
67
|
// Same-origin cookie session: the human's own credentials authorize the answer.
|
|
36
68
|
credentials: 'same-origin',
|
|
37
69
|
headers: { ...options.headers, 'content-type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({ toolName, params: { [TYPED_ANSWER_PARAM]: text } }),
|
|
70
|
+
body: JSON.stringify({ toolName, params: { [TYPED_ANSWER_PARAM]: result.text } }),
|
|
39
71
|
signal: controller.signal,
|
|
40
72
|
}).then((response) => deliveryFor(response.status), () => 'failed');
|
|
41
73
|
clearTimeout(timer);
|
|
@@ -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,11 +1,13 @@
|
|
|
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';
|
|
5
|
+
import type { ComposerHistoryStoragePort } from '../../../../core/composer-history.js';
|
|
4
6
|
import type { ChatTransport } from '@jini-ai/chat';
|
|
5
7
|
import { type UseComposerResult } from '../../../hooks/useComposer.js';
|
|
6
8
|
import { type UseConversationResult } from '../../../hooks/useConversation.js';
|
|
7
9
|
import { type UseChatPaneWorkingDirectoryResult } from './useChatPaneWorkingDirectory.hooks.js';
|
|
8
|
-
export interface UseChatPaneOptions {
|
|
10
|
+
export interface UseChatPaneOptions extends UserTextRedactionOptions {
|
|
9
11
|
transport: ChatTransport;
|
|
10
12
|
agents: readonly ChatPaneAgent[];
|
|
11
13
|
initialMessages?: ChatMessage[];
|
|
@@ -15,6 +17,8 @@ export interface UseChatPaneOptions {
|
|
|
15
17
|
onSelectionChange?: (selection: ChatPaneAgentSelection) => void;
|
|
16
18
|
runContext?: ChatPaneRunContext;
|
|
17
19
|
initialDraft?: string;
|
|
20
|
+
composerHistoryScope?: string;
|
|
21
|
+
composerHistoryStorage?: ComposerHistoryStoragePort;
|
|
18
22
|
/** Forwarded verbatim to `useComposer`; see `validateAttachments` there for the contract. */
|
|
19
23
|
validateAttachments?: (attachments: readonly ChatAttachment[]) => Promise<readonly ChatAttachment[]>;
|
|
20
24
|
uploadAttachments?: (files: File[], options?: ChatPaneAttachmentUploadOptions) => Promise<ChatAttachment[]>;
|
|
@@ -34,6 +38,8 @@ export interface UseChatPaneOptions {
|
|
|
34
38
|
deliverTypedAnswer?: DeliverTypedAnswer;
|
|
35
39
|
}
|
|
36
40
|
export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
41
|
+
/** Show a localized non-secret notice when a paste was removed. */
|
|
42
|
+
secretRedacted: boolean;
|
|
37
43
|
conversation: UseConversationResult;
|
|
38
44
|
composer: UseComposerResult;
|
|
39
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.
|
|
@@ -114,20 +117,25 @@ export function useChatPane(options) {
|
|
|
114
117
|
const requestedSelection = options.selection ?? internalSelection;
|
|
115
118
|
const selection = useMemo(() => resolveChatPaneSelection({ agents: options.agents, requested: requestedSelection }), [options.agents, requestedSelection]);
|
|
116
119
|
const selectedAgent = options.agents.find((agent) => agent.id === selection.agentId);
|
|
117
|
-
const composer = useComposer(definedProps({ source: {
|
|
118
|
-
initialDraft: options.initialDraft,
|
|
119
|
-
initialAgent: selection,
|
|
120
|
-
conversationId: options.conversationId,
|
|
121
|
-
validateAttachments: options.validateAttachments,
|
|
122
|
-
} }));
|
|
123
120
|
const conversation = useConversation(definedProps({ source: {
|
|
124
121
|
transport: options.transport,
|
|
122
|
+
redactUserText: options.redactUserText,
|
|
123
|
+
onSecretRedacted: guard.notifyRedaction,
|
|
125
124
|
initialMessages: options.initialMessages,
|
|
126
125
|
conversationId: options.conversationId,
|
|
127
126
|
// Keys off an empty string, not `undefined` — `selection.agentId` is always a string (never
|
|
128
127
|
// absent), so only the falsy "no agent selected" case should omit the key.
|
|
129
128
|
agentId: selection.agentId || undefined,
|
|
130
129
|
} }));
|
|
130
|
+
const composer = useComposer(definedProps({ source: {
|
|
131
|
+
initialDraft: options.initialDraft,
|
|
132
|
+
historyMessages: conversation.messages,
|
|
133
|
+
historyScope: options.composerHistoryScope,
|
|
134
|
+
historyStorage: options.composerHistoryStorage,
|
|
135
|
+
initialAgent: selection,
|
|
136
|
+
conversationId: options.conversationId,
|
|
137
|
+
validateAttachments: options.validateAttachments,
|
|
138
|
+
} }));
|
|
131
139
|
const activity = resolveChatPaneActivity(selectedAgent, conversation);
|
|
132
140
|
useEffect(() => {
|
|
133
141
|
options.onActivityChange?.(activity);
|
|
@@ -172,6 +180,7 @@ export function useChatPane(options) {
|
|
|
172
180
|
options.onSelectionChange?.(validated);
|
|
173
181
|
}, [composer, options.agents, options.onSelectionChange]);
|
|
174
182
|
const sendBlocker = findChatPaneSendBlocker({
|
|
183
|
+
...(selection.model ? { model: selection.model } : {}),
|
|
175
184
|
selectedAgent,
|
|
176
185
|
isStreaming: conversation.isStreaming,
|
|
177
186
|
activeUploadCount,
|
|
@@ -264,8 +273,8 @@ export function useChatPane(options) {
|
|
|
264
273
|
}
|
|
265
274
|
}
|
|
266
275
|
}, [composer, options.conversationId, options.uploadAttachments]);
|
|
267
|
-
const
|
|
268
|
-
const trimmed =
|
|
276
|
+
const sendGuardedPrompt = useCallback(async ({ redaction }, _optional = {}) => {
|
|
277
|
+
const trimmed = redaction.text.trim();
|
|
269
278
|
if (!trimmed)
|
|
270
279
|
throw new Error('cannot send: the prompt is empty');
|
|
271
280
|
if (sendBlocker !== null) {
|
|
@@ -284,6 +293,7 @@ export function useChatPane(options) {
|
|
|
284
293
|
attachmentBatchIdRef.current = createAttachmentBatchId();
|
|
285
294
|
options.onActivityChange?.('queued');
|
|
286
295
|
await conversation.sendMessage(trimmed, definedProps({ source: {
|
|
296
|
+
secretRedaction: redaction.secretRedacted ? { secretRedacted: true, count: redaction.count } : undefined,
|
|
287
297
|
agentId: selection.agentId,
|
|
288
298
|
// Omitted when the array is EMPTY, not merely absent — an empty `attachments: []` would be a
|
|
289
299
|
// different (valid, present) value to the transport than "no attachments key at all".
|
|
@@ -299,12 +309,16 @@ export function useChatPane(options) {
|
|
|
299
309
|
sendBlocker,
|
|
300
310
|
workingDirectoryState.workingDirectory,
|
|
301
311
|
]);
|
|
312
|
+
const sendPrompt = useCallback(async (prompt) => {
|
|
313
|
+
await sendGuardedPrompt({ redaction: guard.redact({ text: prompt.trim() }, {}) }, {});
|
|
314
|
+
}, [guard.redact, sendGuardedPrompt]);
|
|
302
315
|
const answerWaitingQuestion = useCallback(async ({ deliver, text }) => {
|
|
316
|
+
const safeText = guard.redactText(text);
|
|
303
317
|
const generation = typedAnswerGenerationRef.current;
|
|
304
318
|
const submittedDraft = latestDraftRef.current;
|
|
305
319
|
const questionId = awaitedQuestionId;
|
|
306
320
|
deliveringTypedAnswerRef.current = true;
|
|
307
|
-
const outcome = await deliverTypedAnswerSafely({ deliver, text });
|
|
321
|
+
const outcome = await deliverTypedAnswerSafely({ deliver, text: safeText });
|
|
308
322
|
deliveringTypedAnswerRef.current = false;
|
|
309
323
|
if (!mountedRef.current || generation !== typedAnswerGenerationRef.current)
|
|
310
324
|
return;
|
|
@@ -320,21 +334,21 @@ export function useChatPane(options) {
|
|
|
320
334
|
// correction or next message typed while the host answered.
|
|
321
335
|
if (latestDraftRef.current === submittedDraft)
|
|
322
336
|
composer.setDraft('');
|
|
323
|
-
}, [awaitedQuestionId, composer]);
|
|
337
|
+
}, [awaitedQuestionId, composer, guard.redactText]);
|
|
324
338
|
// The ordinary next-turn path: queue behind a running run, else send now.
|
|
325
339
|
const sendOrdinaryTurn = useCallback(async (prompt) => {
|
|
326
340
|
// Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
|
|
327
341
|
// staged attachments, which this turn still needs when it finally goes out.
|
|
328
342
|
if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
|
|
329
343
|
queuedConversationIdRef.current = options.conversationId;
|
|
330
|
-
|
|
344
|
+
setQueuedTurn(guard.redact({ text: prompt }, {}));
|
|
331
345
|
composer.setDraft('');
|
|
332
346
|
return;
|
|
333
347
|
}
|
|
334
348
|
if (!canSend)
|
|
335
349
|
return;
|
|
336
350
|
await sendPrompt(prompt);
|
|
337
|
-
}, [canSend, composer, options.conversationId, sendBlocker, sendPrompt]);
|
|
351
|
+
}, [canSend, composer, options.conversationId, sendBlocker, sendPrompt, guard.redact]);
|
|
338
352
|
const send = useCallback(async () => {
|
|
339
353
|
const prompt = composerPrompt(composer);
|
|
340
354
|
if (!prompt || deliveringTypedAnswerRef.current)
|
|
@@ -382,31 +396,31 @@ export function useChatPane(options) {
|
|
|
382
396
|
if (!prompt)
|
|
383
397
|
return;
|
|
384
398
|
queuedConversationIdRef.current = options.conversationId;
|
|
385
|
-
|
|
399
|
+
setQueuedTurn(guard.redact({ text: prompt }, {}));
|
|
386
400
|
composer.setDraft('');
|
|
387
401
|
conversation.cancel();
|
|
388
|
-
}, [composer, conversation, options.conversationId]);
|
|
402
|
+
}, [composer, conversation, options.conversationId, guard.redact]);
|
|
389
403
|
const cancelQueued = useCallback(() => {
|
|
390
404
|
queuedConversationIdRef.current = undefined;
|
|
391
|
-
|
|
405
|
+
setQueuedTurn(null);
|
|
392
406
|
}, []);
|
|
393
407
|
// Flush on a FULLY clear blocker, not merely on streaming ending — see
|
|
394
408
|
// `isChatPaneQueueableBlocker`'s note about `findChatPaneSendBlocker`'s ordering. Clearing the
|
|
395
409
|
// queue slot BEFORE awaiting keeps a re-render from double-sending the same prompt.
|
|
396
410
|
useEffect(() => {
|
|
397
|
-
if (
|
|
411
|
+
if (queuedTurn === null || sendBlocker !== null)
|
|
398
412
|
return;
|
|
399
413
|
if (queuedConversationIdRef.current !== options.conversationId) {
|
|
400
414
|
// The conversation changed while this prompt waited behind a streaming run — sending it now
|
|
401
415
|
// would post it into a conversation the user never saw it queued against. Drop it instead
|
|
402
416
|
// (same as `cancelQueued`) rather than misrouting it.
|
|
403
417
|
queuedConversationIdRef.current = undefined;
|
|
404
|
-
|
|
418
|
+
setQueuedTurn(null);
|
|
405
419
|
return;
|
|
406
420
|
}
|
|
407
|
-
|
|
408
|
-
void
|
|
409
|
-
}, [options.conversationId,
|
|
421
|
+
setQueuedTurn(null);
|
|
422
|
+
void sendGuardedPrompt({ redaction: queuedTurn }, {});
|
|
423
|
+
}, [options.conversationId, queuedTurn, sendBlocker, sendGuardedPrompt]);
|
|
410
424
|
const reset = useCallback(() => {
|
|
411
425
|
attachmentGenerationRef.current += 1;
|
|
412
426
|
attachmentBatchIdRef.current = createAttachmentBatchId();
|
|
@@ -425,9 +439,10 @@ export function useChatPane(options) {
|
|
|
425
439
|
// `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
|
|
426
440
|
// the just-reset conversation instead of discarding it like the rest of this turn's state.
|
|
427
441
|
queuedConversationIdRef.current = undefined;
|
|
428
|
-
|
|
442
|
+
setQueuedTurn(null);
|
|
429
443
|
}, [composer, conversation, options.initialMessages]);
|
|
430
444
|
return {
|
|
445
|
+
secretRedacted: guard.secretRedacted,
|
|
431
446
|
conversation,
|
|
432
447
|
composer,
|
|
433
448
|
selection,
|
|
@@ -443,7 +458,7 @@ export function useChatPane(options) {
|
|
|
443
458
|
addAttachments,
|
|
444
459
|
send,
|
|
445
460
|
sendPrompt,
|
|
446
|
-
queuedPrompt,
|
|
461
|
+
queuedPrompt: queuedTurn?.text ?? null,
|
|
447
462
|
cancelQueued,
|
|
448
463
|
typedAnswerNotice,
|
|
449
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',
|