@jini-ai/chat 0.5.4 → 0.5.5

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