@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/ag-ui/entry.d.ts +33 -0
  3. package/dist/core/ag-ui/entry.js +80 -0
  4. package/dist/core/agentic/chat-capabilities.d.ts +2 -4
  5. package/dist/core/agentic/index.d.ts +4 -17
  6. package/dist/core/agentic/index.js +4 -17
  7. package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
  8. package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
  9. package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
  10. package/dist/core/assistant-client/ag-ui-transport.js +147 -0
  11. package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
  12. package/dist/core/assistant-client/attachment-liveness.js +93 -0
  13. package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
  14. package/dist/core/assistant-client/durable-run-subscription.js +96 -0
  15. package/dist/core/assistant-client/entry.d.ts +13 -0
  16. package/dist/core/assistant-client/entry.js +7 -0
  17. package/dist/core/assistant-client/messages.d.ts +73 -0
  18. package/dist/core/assistant-client/messages.js +68 -0
  19. package/dist/core/assistant-client/transport.d.ts +164 -0
  20. package/dist/core/assistant-client/transport.js +734 -0
  21. package/dist/core/browser/page-actions.d.ts +121 -0
  22. package/dist/core/browser/page-actions.js +113 -0
  23. package/dist/core/composer-history.d.ts +54 -0
  24. package/dist/core/composer-history.js +57 -0
  25. package/dist/core/durable-projection.d.ts +28 -0
  26. package/dist/core/durable-projection.js +71 -0
  27. package/dist/core/durable-runs/continuation.d.ts +18 -0
  28. package/dist/core/durable-runs/continuation.js +61 -0
  29. package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
  30. package/dist/core/durable-runs/ledger-contracts.js +2 -0
  31. package/dist/core/durable-runs/policies.d.ts +6 -0
  32. package/dist/core/durable-runs/policies.js +6 -0
  33. package/dist/core/durable-runs/ports.d.ts +123 -0
  34. package/dist/core/durable-runs/ports.js +2 -0
  35. package/dist/core/durable-runs/run-contracts.d.ts +17 -0
  36. package/dist/core/durable-runs/run-contracts.js +2 -0
  37. package/dist/core/durable-runs/run-lock.d.ts +4 -0
  38. package/dist/core/durable-runs/run-lock.js +4 -0
  39. package/dist/core/index.d.ts +9 -19
  40. package/dist/core/index.js +8 -19
  41. package/dist/core/messages.d.ts +2 -0
  42. package/dist/core/question-form/format.d.ts +0 -9
  43. package/dist/core/question-form/format.js +16 -2
  44. package/dist/core/question-form/normalize.js +1 -0
  45. package/dist/core/question-form/secret-answers.d.ts +38 -0
  46. package/dist/core/question-form/secret-answers.js +45 -0
  47. package/dist/core/question-form/types.d.ts +2 -0
  48. package/dist/core/question-form.d.ts +1 -0
  49. package/dist/core/question-form.js +1 -0
  50. package/dist/core/run-events/entry.d.ts +208 -16
  51. package/dist/core/run-events/entry.js +273 -39
  52. package/dist/core/transcript.d.ts +3 -2
  53. package/dist/core/transcript.js +19 -3
  54. package/dist/core/transport.d.ts +4 -0
  55. package/dist/core/user-text-redaction.d.ts +56 -0
  56. package/dist/core/user-text-redaction.js +89 -0
  57. package/dist/embed-widget/browser/page-actions.d.ts +1 -120
  58. package/dist/embed-widget/browser/page-actions.js +2 -112
  59. package/dist/embed-widget/react/embed.d.ts +1 -1
  60. package/dist/embed-widget/react/embed.js +1 -1
  61. package/dist/react/artifact-types.d.ts +9 -15
  62. package/dist/react/components/Composer.js +6 -0
  63. package/dist/react/components/MessageRow.js +24 -11
  64. package/dist/react/components/QuestionForm.d.ts +2 -1
  65. package/dist/react/components/QuestionForm.js +18 -12
  66. package/dist/react/components/QuestionsPanel.d.ts +3 -2
  67. package/dist/react/components/QuestionsPanel.js +2 -2
  68. package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
  69. package/dist/react/features/chat-pane/agent-tools.js +0 -1
  70. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
  71. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  72. package/dist/react/features/chat-pane/components/ChatPane.js +9 -4
  73. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
  74. package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
  75. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
  76. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +7 -1
  77. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
  78. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  79. package/dist/react/features/chat-pane/model-options.js +51 -0
  80. package/dist/react/features/chat-pane/rules.d.ts +3 -2
  81. package/dist/react/features/chat-pane/rules.js +12 -4
  82. package/dist/react/features/chat-pane/types.d.ts +30 -1
  83. package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
  84. package/dist/react/hooks/assistant-chats-contracts.js +2 -0
  85. package/dist/react/hooks/assistant-chats.d.ts +7 -0
  86. package/dist/react/hooks/assistant-chats.js +960 -0
  87. package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
  88. package/dist/react/hooks/attachment-preview-cache.js +1 -1
  89. package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
  90. package/dist/react/hooks/composer-draft-cache.js +2 -2
  91. package/dist/react/hooks/composer-history-storage.d.ts +8 -0
  92. package/dist/react/hooks/composer-history-storage.js +40 -0
  93. package/dist/react/hooks/fab-position.d.ts +90 -0
  94. package/dist/react/hooks/fab-position.js +271 -0
  95. package/dist/react/hooks/useComposer.d.ts +10 -1
  96. package/dist/react/hooks/useComposer.js +14 -2
  97. package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
  98. package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
  99. package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
  100. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
  101. package/dist/react/hooks/useConversation.d.ts +4 -1
  102. package/dist/react/hooks/useConversation.js +20 -14
  103. package/dist/react/hooks/useRunStream.js +12 -2
  104. package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
  105. package/dist/react/hooks/useUserTextGuard.js +19 -0
  106. package/dist/react/index.d.ts +14 -21
  107. package/dist/react/index.js +8 -0
  108. package/dist/server/durable-runs/accept.d.ts +25 -0
  109. package/dist/server/durable-runs/accept.js +21 -0
  110. package/dist/server/durable-runs/entry.d.ts +5 -0
  111. package/dist/server/durable-runs/entry.js +4 -0
  112. package/dist/server/durable-runs/recover.d.ts +5 -0
  113. package/dist/server/durable-runs/recover.js +147 -0
  114. package/dist/server/run-finalizer/ports.d.ts +2 -16
  115. package/dist/store/legacy/sqlite/messages.d.ts +24 -18
  116. package/dist/store/legacy/sqlite/messages.js +12 -2
  117. package/dist/store/pglite/index.d.ts +2 -1
  118. package/dist/store/postgres/index.d.ts +2 -1
  119. package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
  120. package/dist/store/runtime/chat-expiry-sweep.js +70 -0
  121. package/dist/store/runtime/chat-history-store.d.ts +20 -0
  122. package/dist/store/runtime/chat-history-store.js +50 -0
  123. package/dist/store/runtime/durable-run-store.d.ts +21 -0
  124. package/dist/store/runtime/durable-run-store.js +342 -0
  125. package/dist/store/runtime/entry.d.ts +7 -0
  126. package/dist/store/runtime/entry.js +6 -0
  127. package/dist/store/runtime/kernel.d.ts +29 -0
  128. package/dist/store/runtime/kernel.js +2 -0
  129. package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
  130. package/dist/store/runtime/run-guarded-chat-store.js +42 -0
  131. package/dist/store/runtime/run-ledger.d.ts +15 -0
  132. package/dist/store/runtime/run-ledger.js +206 -0
  133. package/dist/store/sql/boundary.d.ts +6 -0
  134. package/dist/store/sql/boundary.js +7 -1
  135. package/dist/store/sql/store.d.ts +3 -2
  136. package/dist/store/sql/store.js +9 -2
  137. package/dist/store/sqlite/index.d.ts +3 -2
  138. 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 { type CapabilityDef, type CapabilityInputSchema, type CapabilityRisk } from '@jini-ai/agentic';
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`. */
@@ -12,7 +12,6 @@
12
12
  * `@jini-ai/agentic` directly.
13
13
  */
14
14
  import { CHAT_CAPABILITIES } from '@jini-ai/chat';
15
- import {} from '@jini-ai/agentic';
16
15
  /**
17
16
  * The bounded, explicit set of chat-pane capabilities every transport surface hosts.
18
17
  *
@@ -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
- return (_jsxs("div", { className: "jini-runtime-models", children: [(selectedAgent.models?.length ?? 0) > 0 ? (_jsxs("label", { className: "jini-runtime-select", children: [_jsx("span", { children: t('Model') }), _jsx("select", { "aria-label": t('Model'), value: value.model ?? '', onChange: (event) => onChange({
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: selectedAgent.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({
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, conversationError, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, onSendAsNewMessage, t, }) {
77
- return (_jsxs(_Fragment, { children: [unavailable && connectingAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Connecting to the assistant…') })) : unavailable && scanningAgents ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Loading available CLIs') })) : unavailable ? (_jsx(ChatPaneNoUsableCliMessage, { executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t })) : null, conversationError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: conversationError.message })) : null, attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, workingDirectoryPending ? (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Checking working directory…') })) : workingDirectoryInvalid ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t('Working directory is unavailable.') })) : null, runtimeInventoryError && !connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, typedAnswerNotice ? (_jsxs("div", { className: "jini-chat-pane__error", role: "alert", children: [t(describeTypedAnswerNotice({ notice: typedAnswerNotice })), typedAnswerNotice === 'not-pending' ? (_jsxs(_Fragment, { children: [' ', _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: onSendAsNewMessage, children: t('Send as a new message') })] })) : null] })) : null] }));
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 = runtimeOptionLabel({ options: selectedAgent?.models, value: value.model, fallback: t('Default model') });
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
- const [queuedPrompt, setQueuedPrompt] = useState(null);
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 sendPrompt = useCallback(async (prompt) => {
268
- const trimmed = prompt.trim();
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
- setQueuedPrompt(prompt);
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
- setQueuedPrompt(prompt);
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
- setQueuedPrompt(null);
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 (queuedPrompt === null || sendBlocker !== null)
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
- setQueuedPrompt(null);
418
+ setQueuedTurn(null);
405
419
  return;
406
420
  }
407
- setQueuedPrompt(null);
408
- void sendPrompt(queuedPrompt);
409
- }, [options.conversationId, queuedPrompt, sendBlocker, sendPrompt]);
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
- setQueuedPrompt(null);
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
- const model = preferredOptionId(agent.models);
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.models, requested.model, defaults.model);
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',