@jini-ai/chat 0.6.6 → 0.6.10

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 (160) hide show
  1. package/CHANGELOG.md +129 -1
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +187 -98
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
@@ -1,3 +1,4 @@
1
+ import { readAgentPluginChatState, captureAgentPluginUsage } from '../agent-plugin-chat-state.js';
1
2
  import { buildTranscript, latestUserPromptFromHistory } from "../transcript.js";
2
3
  import * as runEvents from "../run-events/entry.js";
3
4
  /** Bind transport effects once; in-flight controllers belong to this client, never the package.
@@ -34,8 +35,6 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
34
35
  * accumulated here) — chat-core's own `ChatMessage.events` array is what concatenates them into one
35
36
  * growing message, so accumulating twice would double the text.
36
37
  */
37
- const RUNS_URL = ports.runsUrl;
38
- const BYOK_TURN_URL = ports.byokTurnUrl;
39
38
  /** Distinguishes a BYOK-run id (client-minted, no server-side run record) from a daemon-run id
40
39
  * (server-minted, reattachable) wherever a bare `runId` string is all a `ChatTransport` method
41
40
  * receives — see `stopRun`/`fetchRunStatus`/`reattachRun` below for why the distinction matters. */
@@ -73,11 +72,18 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
73
72
  * Bounded deliberately: each run is billed, and an unbounded transcript grows the input cost of
74
73
  * every subsequent turn in a conversation that has no natural end. {@link MAX_TRANSCRIPT_TURNS}
75
74
  * keeps a long-running chat from silently becoming the most expensive thing in the product.
75
+ * Failed turns are labeled outcomes so another agent can explain and recover from them.
76
76
  */
77
77
  function runPrompt(history) {
78
- // `historyForTranscript` first, THEN the cap: dropping failed rows before slicing means the 40
79
- // turns that survive are 40 real ones, not 40 slots some of which are rows the agent never wrote.
80
- const recent = historyForTranscript(history).slice(-MAX_TRANSCRIPT_TURNS);
78
+ const recent = history.slice(-MAX_TRANSCRIPT_TURNS).map(message => {
79
+ if (message.role !== 'assistant' || isAnsweredAssistantTurn(message))
80
+ return message;
81
+ const detail = message.events?.filter(event => event.kind === 'status')
82
+ .map(event => [event.label, event.detail].filter(Boolean).join(': ')).join('\n').slice(-4000);
83
+ // Reserve space for the failure reason even when partial output fills the transcript cap.
84
+ const outcome = [message.content.slice(0, 6000), detail].filter(Boolean).join('\n\n');
85
+ return { ...message, content: `[${message.agentName ?? message.agentId ?? 'Assistant'} run ${message.runStatus}; not a completed answer]\n${outcome || 'No assistant reply was produced.'}` };
86
+ });
81
87
  return buildTranscript({ history: recent });
82
88
  }
83
89
  /**
@@ -97,7 +103,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
97
103
  * `events_json` is the sole durable record of why a run died (exit code, signal, the CLI's own
98
104
  * stderr) once the daemon's in-memory event log is gone, and the pane renders it on reload — so
99
105
  * dropping the row trades a history bug for a forensics hole. The skip belongs here, on the read
100
- * side. See {@link historyForTranscript} and `assistant-chats.ts`'s `persistableMessages`.
106
+ * side. The transcript's hidden-tool mode likewise filters only recovered tool failures and keeps
107
+ * unrecovered failures behind a compact disclosure; it never deletes this durable evidence.
108
+ * See {@link historyForTranscript} and `assistant-chats.ts`'s `persistableMessages`.
101
109
  */
102
110
  function isAnsweredAssistantTurn(message) {
103
111
  return message.runStatus === undefined || message.runStatus === "succeeded";
@@ -205,7 +213,49 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
205
213
  /** The durable message owns recovery and terminal state; this subscription follows replacement
206
214
  * attempts without writing a browser-inferred failure into history. */
207
215
  function subscribeToRun(runId, handlers, signal) {
208
- ports.followRun({ runId, handlers, ...(signal === undefined ? {} : { signal }) }, {});
216
+ ports.followRun({ runId, handlers: capturePluginUsage(handlers), ...(signal === undefined ? {} : { signal }) }, {});
217
+ }
218
+ /** The well-formed plugin usage an `agent_plugins` extension event carries, or `undefined` for any
219
+ * other event — the one validity rule both live events and checkpoint replays are captured by. */
220
+ function pluginUsageOf({ event }, _optional) {
221
+ if (event.kind !== 'ext' || event.name !== 'agent_plugins')
222
+ return undefined;
223
+ const usage = event.data;
224
+ return typeof usage?.conversationId === 'string' && usage.state && Array.isArray(usage.state.excluded) ? usage : undefined;
225
+ }
226
+ /** Capture before notifying the UI so answers received after reload persist for the next turn.
227
+ * Checkpoint panes skip live deltas to avoid duplicate text, so capture their saved events here
228
+ * too. Keep this wrapper as the single capture owner for both delivery contracts.
229
+ * @complexity O(e + s) per checkpoint; O(s) per live event, in saved events and plugin-state size.
230
+ */
231
+ function capturePluginUsage(handlers) {
232
+ const captured = new Map();
233
+ const capture = (event) => {
234
+ const usage = pluginUsageOf({ event }, {});
235
+ if (!usage)
236
+ return;
237
+ const state = JSON.stringify(usage.state);
238
+ if (captured.get(usage.conversationId) === state)
239
+ return;
240
+ captured.set(usage.conversationId, state);
241
+ captureAgentPluginUsage({ event });
242
+ };
243
+ return {
244
+ ...handlers,
245
+ onEvent: event => { capture(event); handlers.onEvent(event); },
246
+ ...(handlers.onCheckpoint ? { onCheckpoint: (message) => {
247
+ // Historical answers must not roll the current state backwards on every poll.
248
+ const latest = new Map();
249
+ for (const event of message.events ?? []) {
250
+ const usage = pluginUsageOf({ event }, {});
251
+ if (usage)
252
+ latest.set(usage.conversationId, event);
253
+ }
254
+ for (const event of latest.values())
255
+ capture(event);
256
+ handlers.onCheckpoint(message);
257
+ } } : {}),
258
+ };
209
259
  }
210
260
  /** Client-minted, not server-minted — see module doc's path-2 section: a BYOK run has no server-side
211
261
  * run record to name it. Prefixed so `stopRun`/`fetchRunStatus`/`reattachRun` below can tell a
@@ -293,6 +343,56 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
293
343
  ctx.finish();
294
344
  }
295
345
  }
346
+ /**
347
+ * The provider-bound history of one BYOK turn: only turns the agent actually answered, never an
348
+ * empty one, each carrying its redaction signal.
349
+ */
350
+ function byokMessages({ history }, _optional) {
351
+ // `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
352
+ // hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
353
+ // that partial output would be sent to the provider as if it were the assistant's answer. This is
354
+ // the same defect as on the Local CLI path, reachable by a second route.
355
+ return historyForTranscript(history)
356
+ .filter((message) => message.content.trim().length > 0)
357
+ .map((message) => ({ role: message.role, content: message.content,
358
+ ...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
359
+ }));
360
+ }
361
+ /** The credential block of a BYOK turn; an empty base URL and an unset token cap are omitted. */
362
+ function byokCredentials({ byok }, _optional) {
363
+ return {
364
+ protocol: byok.protocol,
365
+ apiKey: byok.apiKey,
366
+ ...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
367
+ model: byok.model,
368
+ ...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
369
+ };
370
+ }
371
+ /** The JSON body `POST byokTurnUrl` carries for one turn. */
372
+ function byokTurnBody({ input, byok }, _optional) {
373
+ return {
374
+ messages: byokMessages({ history: input.history }, {}),
375
+ ...attachmentIdsField(input),
376
+ ...conversationIdField(input),
377
+ agentPluginState: input.context?.["agentPluginState"],
378
+ userPrompt: input.context?.["userPrompt"],
379
+ ...pluginRefIdsField(input),
380
+ // The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
381
+ // in front of the newest message so a BYOK turn knows the open page and where drawings go.
382
+ ...pageContextField(input),
383
+ byok: byokCredentials({ byok }, {}),
384
+ };
385
+ }
386
+ /**
387
+ * The error for a refused request: `<action> (<status>)`, plus up to 300 characters of the
388
+ * response body when it has one — shared by the BYOK start, Local CLI start and cancel refusals.
389
+ */
390
+ async function refusalError({ action, response }, _optional) {
391
+ // A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
392
+ // rejected the request" into "the client crashed reading the rejection".
393
+ const detail = await response.text().catch(() => "");
394
+ return new Error(`${action} (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
395
+ }
296
396
  /**
297
397
  * The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
298
398
  * `EventSource`/reattach. See module doc's path-2 section for why this shape differs from
@@ -307,35 +407,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
307
407
  const controller = new AbortController();
308
408
  byokAbortControllers.set(runId, controller);
309
409
  input.signal?.addEventListener("abort", () => controller.abort());
310
- // `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
311
- // hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
312
- // that partial output would be sent to the provider as if it were the assistant's answer. This is
313
- // the same defect as on the Local CLI path, reachable by a second route.
314
- const messages = historyForTranscript(input.history)
315
- .filter((message) => message.content.trim().length > 0)
316
- .map((message) => ({ role: message.role, content: message.content,
317
- ...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
318
- }));
319
410
  let response;
320
411
  try {
321
- response = await ports.fetch(BYOK_TURN_URL, {
412
+ response = await ports.fetch(ports.byokTurnUrl, {
322
413
  method: "POST",
323
414
  headers: { "Content-Type": "application/json" },
324
415
  credentials: "same-origin",
325
- body: JSON.stringify({
326
- messages,
327
- ...attachmentIdsField(input),
328
- // The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
329
- // in front of the newest message so a BYOK turn knows the open page and where drawings go.
330
- ...pageContextField(input),
331
- byok: {
332
- protocol: byok.protocol,
333
- apiKey: byok.apiKey,
334
- ...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
335
- model: byok.model,
336
- ...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
337
- },
338
- }),
416
+ body: JSON.stringify(byokTurnBody({ input, byok }, {})),
339
417
  signal: controller.signal,
340
418
  });
341
419
  }
@@ -345,8 +423,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
345
423
  }
346
424
  if (!response.ok || !response.body) {
347
425
  byokAbortControllers.delete(runId);
348
- const detail = await response.text().catch(() => "");
349
- throw new Error(`BYOK turn failed to start (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
426
+ throw await refusalError({ action: "BYOK turn failed to start", response }, {});
350
427
  }
351
428
  const collected = [];
352
429
  let settled = false;
@@ -484,10 +561,14 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
484
561
  * adopt a conversation because `input.context` named none (the first turn of a chat). Omitted by
485
562
  * every caller that already has an id on `input.context`, and by this function's own unit tests —
486
563
  * in which case the `context` field is read exactly as before.
564
+ * The bounded `coldStartContext` is the complete prompt, including the current request. The
565
+ * daemon selects it instead of the resume tail when native resume is unavailable.
487
566
  */
488
567
  function buildLocalCliContextRef(input, prompt, resolvedConversationId) {
489
568
  return {
490
569
  prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
570
+ // Select one complete transcript after binding; concatenating it with the tail repeats turns.
571
+ coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
491
572
  // The transcript is already sanitized; retain its signal across the daemon JSON boundary.
492
573
  ...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
493
574
  assistantMessageId: input.assistantMessageId,
@@ -496,6 +577,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
496
577
  ...reasoningField(input),
497
578
  ...attachmentIdsField(input),
498
579
  ...pluginRefIdsField(input),
580
+ agentPluginState: input.context?.["agentPluginState"],
581
+ userPrompt: input.context?.["userPrompt"],
499
582
  ...conversationIdField(input, resolvedConversationId),
500
583
  ...pageContextField(input),
501
584
  };
@@ -523,27 +606,28 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
523
606
  const adopted = await ensureConversationId({}, {}).catch(() => null);
524
607
  return typeof adopted === "string" && adopted.length > 0 ? adopted : undefined;
525
608
  }
526
- /**
527
- * Resolves the prompt string `startRun`'s Local CLI branch sends: the full rendered transcript
528
- * ({@link runPrompt}) by default, or only what the agent has not already been given
529
- * ({@link undeliveredUserPrompt}) when `input.agentId` is present in `resumeCapableAgentIds` — see
530
- * `getResumeCapableAgentIds`'s own doc on {@link AssistantTransportOptions} for the full
531
- * contract this mirrors. Pulled out of `startRun` (2026-09-04, complexity pass) as its own pure
532
- * step. `options.getResumeCapableAgentIds?.({}, {})` is still called at the same point in `startRun` as
533
- * before — only the "which agentIds carries own memory" branch itself moves here, so the "read fresh
534
- * on every call" convention that doc argues for is unchanged.
609
+ /** Send a resumed agent only its unseen conversation tail, retaining other agents' outcomes.
610
+ * Legacy rows without agent identity retain the existing unanswered-user delivery contract.
611
+ * @complexity O(n) to find the last answered turn; transcript rendering is capped at 40 turns.
535
612
  */
536
613
  function resolveLocalCliPrompt(input, resumeCapableAgentIds) {
537
614
  const carriesOwnMemory = input.agentId !== undefined && resumeCapableAgentIds?.has(input.agentId) === true;
615
+ if (!carriesOwnMemory)
616
+ return runPrompt(input.history);
617
+ let lastOwnAnswer = -1;
618
+ input.history.forEach((message, index) => {
619
+ if (message.role === 'assistant' && (!message.agentId || message.agentId === input.agentId) && isAnsweredAssistantTurn(message))
620
+ lastOwnAnswer = index;
621
+ });
622
+ const unseen = input.history.slice(lastOwnAnswer + 1);
623
+ const crossedAgents = unseen.some(message => message.role === 'assistant' && message.agentId && message.agentId !== input.agentId);
538
624
  // BEHAVIOR CHANGE (2026-09-11, Defect 2): the resume-capable branch was `latestUserPrompt` — the
539
625
  // single newest user turn, on the assumption that everything before it is already inside the
540
626
  // CLI's own session. That assumption holds only for turns the agent actually answered; a turn
541
627
  // whose run died before the CLI read its prompt is in neither place, and nothing ever sent it
542
628
  // again. `undeliveredUserPrompt` returns the identical single string in the ordinary case and
543
- // differs only when there is genuinely something unanswered to catch up on. The
544
- // `latestUserPrompt` parameter this used to take went with it: `startRun` still computes that
545
- // string for its own send guard, but this branch has no use for it any more.
546
- return carriesOwnMemory ? undeliveredUserPrompt(input.history) : runPrompt(input.history);
629
+ // differs only when there is genuinely something unanswered to catch up on.
630
+ return crossedAgents ? runPrompt(unseen) : undeliveredUserPrompt(input.history);
547
631
  }
548
632
  /**
549
633
  * Writes the newest user turn to durable storage before its run is dispatched, and never fails the
@@ -562,44 +646,73 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
562
646
  * two, and a retry of the same content is free. `useAssistantChats`'s own `persistUserTurn` also
563
647
  * marks the id written, so in practice `flush` skips it entirely.
564
648
  *
565
- * @param input - `startRun`'s own input; the newest `role: "user"` message in its history is what
566
- * gets written.
649
+ * @param message - The newest `role: "user"` message in `startRun`'s history — what gets written.
650
+ * `startRun` has already refused a history without one, so it is always present here.
567
651
  * @param conversationId - The already-resolved conversation (see `resolveRunConversationId`), or
568
652
  * `undefined` when adoption failed — there is no row to write into in that case.
569
653
  * @param persistUserTurn - `AssistantTransportOptions.persistUserTurn`, or `undefined`
570
654
  * for a transport built without it (every test and any non-dock consumer).
571
655
  * @returns Nothing, always. The `.catch` keeps a failed browser PUT from refusing the user's turn;
572
656
  * durable acceptance is now responsible for committing its copy before dispatch.
573
- * @complexity O(n) in history length for the newest-user-turn scan, plus one PUT.
657
+ * @complexity O(1) plus one PUT.
574
658
  */
575
- async function persistUserTurnBeforeDispatch(input, conversationId, persistUserTurn) {
576
- if (conversationId === undefined || persistUserTurn === undefined)
577
- return;
578
- const history = input.history;
579
- const message = [...history].reverse().find((candidate) => candidate.role === "user");
580
- if (message === undefined)
659
+ async function persistUserTurnBeforeDispatch({ message, conversationId }, { persistUserTurn }) {
660
+ // Pending composer input is persisted by atomic run acceptance. An earlier PUT could save a
661
+ // rejected (409) draft as a user turn; confirmed/legacy callers retain their existing ordering.
662
+ if (message.inputPending || conversationId === undefined || persistUserTurn === undefined)
581
663
  return;
582
664
  await persistUserTurn({ conversationId, message }, {}).catch(() => undefined);
583
665
  }
666
+ /** The newest `role: "user"` message in a history, or `undefined` when it has none.
667
+ * @complexity O(n) in history length. */
668
+ function newestUserMessage({ history }, _optional) {
669
+ return [...history].reverse().find((message) => message.role === "user");
670
+ }
584
671
  /**
585
672
  * Turns `startRun`'s Local CLI `POST /api/runs` response into either a thrown, detailed error or a
586
673
  * subscribed run — the two outcomes once the request lands. Pulled out of `startRun` (2026-09-04,
587
674
  * complexity pass) as its own step: `response.ok` in, `{runId}` or a thrown `Error` out. The
588
- * `.text().catch(() => "")` is load-bearing, not decorative — see the inline comment below.
675
+ * `.text().catch(() => "")` is load-bearing, not decorative — see {@link refusalError}'s inline comment.
589
676
  */
590
677
  async function finishLocalCliRun(response, handlers, signal) {
591
- if (!response.ok) {
592
- // A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
593
- // rejected the request" into "the client crashed reading the rejection".
594
- const detail = await response.text().catch(() => "");
595
- throw new Error(`agent run failed to start (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
596
- }
678
+ if (!response.ok)
679
+ throw await refusalError({ action: "agent run failed to start", response }, {});
597
680
  const { run, messageId, conversationId } = (await response.json());
598
681
  if (messageId && conversationId)
599
682
  ports.bindRun({ runId: run.id, binding: { messageId, conversationId } }, {});
600
683
  subscribeToRun(run.id, handlers, signal);
601
684
  return { runId: run.id };
602
685
  }
686
+ /**
687
+ * The Local CLI path of `startRun`: durable user turn, then acceptance, then the followed run.
688
+ *
689
+ * @param input - `startRun`'s input with its conversation identity already merged into `context`.
690
+ * @param userMessage - The newest user message; acceptance carries it verbatim.
691
+ * @param conversationId - The resolved conversation, or `undefined` when adoption failed.
692
+ */
693
+ async function startLocalCliRun({ input, handlers, userMessage, conversationId }, { options }) {
694
+ // Local CLI path below. `resolveLocalCliPrompt`'s own doc has the full contract for why an
695
+ // unresolved/absent agentId fails open to the full transcript rather than the bare message.
696
+ const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
697
+ // Also before the POST, and for the same reason it is awaited rather than fired off: the
698
+ // user's turn must be durable before a run that can fail to deliver it exists at all
699
+ // (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
700
+ // adoption produces. BYOK and AG-UI share the identity above; neither writes a durable
701
+ // user turn ahead of dispatch; those transports retain their own persistence behavior.
702
+ await persistUserTurnBeforeDispatch({ message: userMessage, conversationId }, { persistUserTurn: options.persistUserTurn });
703
+ // Acceptance commits the question and stub together even if the separate browser PUT fails
704
+ // or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
705
+ const base = buildLocalCliContextRef(input, prompt, conversationId);
706
+ const contextRef = { ...base, userMessage };
707
+ const response = await ports.acceptRun({
708
+ ...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
709
+ requestSignal: input.cancelSignal ?? input.signal,
710
+ body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
711
+ messageId: base.assistantMessageId,
712
+ ...(conversationId === undefined ? {} : { conversationId }),
713
+ }, {});
714
+ return finishLocalCliRun(response, handlers, input.signal);
715
+ }
603
716
  function createAssistantTransport(options = {}) {
604
717
  return {
605
718
  async startRun(input, handlers) {
@@ -608,9 +721,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
608
721
  // prompt asks the agent to reply to itself. Shared by both paths below, and reused (rather than
609
722
  // recomputed) by the Local CLI branch's own resume-capable check further down.
610
723
  const latestUserPrompt = latestUserPromptFromHistory({ history: input.history });
611
- if (!latestUserPrompt) {
724
+ const userMessage = newestUserMessage({ history: input.history }, {});
725
+ if (!latestUserPrompt || userMessage === undefined) {
612
726
  throw new Error("no user message to send");
613
727
  }
728
+ // Adopt identity before either runtime branch so first-turn decisions survive a reload.
729
+ const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
730
+ input = { ...input, context: { ...input.context, ...(conversationId ? { conversationId, agentPluginState: readAgentPluginChatState({ conversationId }) } : {}), userPrompt: latestUserPrompt } };
614
731
  const executionConfig = options.getExecutionConfig?.({}, {});
615
732
  // Dispatches on MODE alone, not on whether a key is typed in this browser right now
616
733
  // (2026-08-05). `executionConfig.byok.apiKey` is write-only server-side — it is empty on
@@ -623,7 +740,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
623
740
  // level down — the route's own 400 `"no usable BYOK credential..."` — which is a real,
624
741
  // actionable answer instead of the mode picker silently refusing to try.
625
742
  if (executionConfig?.mode === "byok") {
626
- return startByokRun(ports.projectInput({ input }, {}), handlers, executionConfig.byok);
743
+ return startByokRun(ports.projectInput({ input }, {}), capturePluginUsage(handlers), executionConfig.byok);
627
744
  }
628
745
  // AG-UI canary path (ADR-059) — a host-supplied toggle, independent of `executionConfig.mode`
629
746
  // (Jini's own closed union, never extended for this). Checked AFTER the byok branch above:
@@ -633,35 +750,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
633
750
  if (options.getAgUiEnabled?.({}, {})) {
634
751
  // `historyForTranscript` here rather than inside `startAgUiRun`: that module importing this
635
752
  // one would be a cycle, and this is its only caller. Same rule as the BYOK and Local CLI paths.
636
- return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers }, {});
753
+ return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers: capturePluginUsage(handlers) }, {});
637
754
  }
638
- // Local CLI path below. `resolveLocalCliPrompt`'s own doc has the full contract for why an
639
- // unresolved/absent agentId fails open to the full transcript rather than the bare message.
640
- const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
641
- // Awaited BEFORE the POST, and only on this branch: session resume is a daemon-run concept, so
642
- // neither the BYOK nor the AG-UI path above has anything to file a session id under. See
643
- // `resolveRunConversationId` and `AssistantTransportOptions.ensureConversationId`.
644
- const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
645
- // Also before the POST, and for the same reason it is awaited rather than fired off: the
646
- // user's turn must be durable before a run that can fail to deliver it exists at all
647
- // (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
648
- // adoption produces. Known gap, not an oversight: the BYOK and AG-UI branches above return
649
- // before this point and resolve no conversation id of their own, so neither writes a durable
650
- // user turn ahead of dispatch — closing that needs those paths to adopt a conversation first.
651
- await persistUserTurnBeforeDispatch(input, conversationId, options.persistUserTurn);
652
- const userMessage = [...input.history].reverse().find((message) => message.role === "user");
653
- // Acceptance commits the question and stub together even if the separate browser PUT fails
654
- // or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
655
- const base = buildLocalCliContextRef(input, prompt, conversationId);
656
- const contextRef = userMessage ? { ...base, userMessage } : base;
657
- const response = await ports.acceptRun({
658
- ...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
659
- requestSignal: input.cancelSignal ?? input.signal,
660
- body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
661
- messageId: base.assistantMessageId,
662
- ...(conversationId === undefined ? {} : { conversationId }),
663
- }, {});
664
- return finishLocalCliRun(response, handlers, input.signal);
755
+ return startLocalCliRun({ input, handlers, userMessage, conversationId }, { options });
665
756
  },
666
757
  async reattachRun(runId, handlers, options) {
667
758
  // A BYOK run has no server-side record to reattach to (module doc's path-2 section) — the
@@ -688,7 +779,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
688
779
  return null;
689
780
  if (ports.agUi.isAgUiRunId({ runId }, {}))
690
781
  return ports.agUi.fetchAgUiRunStatus({}, {});
691
- const response = await ports.fetch(`${RUNS_URL}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
782
+ const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
692
783
  if (!response.ok)
693
784
  return null;
694
785
  const { run } = (await response.json());
@@ -704,7 +795,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
704
795
  if (ports.agUi.isAgUiRunId({ runId }, {})) {
705
796
  return ports.agUi.stopAgUiRun({ runId }, {});
706
797
  }
707
- const response = await ports.fetch(`${RUNS_URL}/${encodeURIComponent(runId)}/cancel`, {
798
+ const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}/cancel`, {
708
799
  method: "POST",
709
800
  headers: { "Content-Type": "application/json" },
710
801
  credentials: "same-origin",
@@ -715,10 +806,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
715
806
  return;
716
807
  // `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
717
808
  // like a successful one while the run kept executing. `useRunStream` logs a rejection here.
718
- if (!response.ok) {
719
- const detail = await response.text().catch(() => "");
720
- throw new Error(`cancelling agent run failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
721
- }
809
+ if (!response.ok)
810
+ throw await refusalError({ action: "cancelling agent run failed", response }, {});
722
811
  },
723
812
  };
724
813
  }
@@ -19,9 +19,10 @@ import type { AgentEvent } from './events.js';
19
19
  export declare const ASSISTANT_STEP_SEPARATOR = "\n\n";
20
20
  /**
21
21
  * A turn's text, with a paragraph break between runs separated by a tool call or card.
22
+ * A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
22
23
  *
23
24
  * @param events - The turn's events, in arrival order.
24
- * @complexity O(total text length).
25
+ * @complexity O(n + total text length) time and O(n) space for n events.
25
26
  */
26
27
  export declare function assistantContentFromEvents({ events }: {
27
28
  events: readonly AgentEvent[] | undefined;
@@ -1,3 +1,4 @@
1
+ import { dedupeToolUsesById } from './tool-events.js';
1
2
  /** What separates two text runs that a tool call or card sat between. */
2
3
  export const ASSISTANT_STEP_SEPARATOR = '\n\n';
3
4
  function separatorBefore(out, text) {
@@ -7,25 +8,22 @@ function separatorBefore(out, text) {
7
8
  }
8
9
  /**
9
10
  * A turn's text, with a paragraph break between runs separated by a tool call or card.
11
+ * A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
10
12
  *
11
13
  * @param events - The turn's events, in arrival order.
12
- * @complexity O(total text length).
14
+ * @complexity O(n + total text length) time and O(n) space for n events.
13
15
  */
14
16
  export function assistantContentFromEvents({ events }) {
15
17
  let out = '';
16
18
  let stepBoundary = false;
17
- for (const ev of events ?? []) {
18
- if (ev.kind === 'text') {
19
- if (ev.text.length === 0)
20
- continue;
21
- if (stepBoundary)
22
- out += separatorBefore(out, ev.text);
23
- out += ev.text;
24
- stepBoundary = false;
25
- }
26
- else if (ev.kind === 'tool_use' || ev.kind === 'ext') {
19
+ for (const ev of dedupeToolUsesById({ events: events ? [...events] : undefined })) {
20
+ if (ev.kind === 'tool_use' || ev.kind === 'ext') {
27
21
  stepBoundary = true;
28
22
  }
23
+ else if (ev.kind === 'text' && ev.text.length > 0) {
24
+ out += (stepBoundary ? separatorBefore(out, ev.text) : '') + ev.text;
25
+ stepBoundary = false;
26
+ }
29
27
  }
30
28
  return out;
31
29
  }
@@ -41,8 +41,10 @@ export function findTargetElement({ title, root, selection }) {
41
41
  return null;
42
42
  const candidates = Array.from(root.querySelectorAll(selection.candidateSelector))
43
43
  .filter(el => !el.closest(selection.excludedAncestorSelector));
44
- return candidates.find(el => (el.textContent ?? '').trim() === needle)
45
- ?? candidates.find(el => (el.textContent ?? '').trim().toLowerCase() === needle.toLowerCase()) ?? null;
44
+ // `textContent` is null only for documents and doctypes; every Element the selector yields has a string.
45
+ const text = (el) => el.textContent.trim();
46
+ return candidates.find(el => text(el) === needle)
47
+ ?? candidates.find(el => text(el).toLowerCase() === needle.toLowerCase()) ?? null;
46
48
  }
47
49
  /**
48
50
  * Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
@@ -7,11 +7,15 @@ export interface ComposerHistoryState {
7
7
  export interface ComposerHistoryStoragePort {
8
8
  read: (required: {
9
9
  scope: string;
10
- }, optional: {}) => Promise<readonly string[]>;
10
+ }, optional: {
11
+ cap?: number;
12
+ }) => Promise<readonly string[]>;
11
13
  write: (required: {
12
14
  scope: string;
13
15
  entries: readonly string[];
14
- }, optional: {}) => Promise<void>;
16
+ }, optional: {
17
+ cap?: number;
18
+ }) => Promise<void>;
15
19
  }
16
20
  export interface ComposerHistoryKeyInput {
17
21
  key: string;
@@ -40,15 +44,18 @@ export declare function mergeComposerHistory({ recent, current }: {
40
44
  cap?: number;
41
45
  }): string[];
42
46
  export declare function createComposerHistoryState(_required: {}, _optional?: {}): ComposerHistoryState;
43
- export declare function composerHistoryKeyAction(input: ComposerHistoryKeyInput, _optional?: {}): 'previous' | 'next' | 'restore' | null;
44
- export declare function transitionComposerHistory({ state, action, text, entries }: {
45
- state: ComposerHistoryState;
46
- action: 'previous' | 'next' | 'restore' | 'edit' | 'reset';
47
- text: string;
48
- entries: readonly string[];
49
- }, _optional?: {}): {
47
+ type ComposerHistoryKeyAction = 'previous' | 'next' | 'restore';
48
+ type ComposerHistoryTransition = {
50
49
  state: ComposerHistoryState;
51
50
  text: string;
52
51
  handled: boolean;
53
52
  };
53
+ export declare function composerHistoryKeyAction(input: ComposerHistoryKeyInput, _optional?: {}): ComposerHistoryKeyAction | null;
54
+ export declare function transitionComposerHistory({ state, action, text, entries }: {
55
+ state: ComposerHistoryState;
56
+ action: ComposerHistoryKeyAction | 'edit' | 'reset';
57
+ text: string;
58
+ entries: readonly string[];
59
+ }, _optional?: {}): ComposerHistoryTransition;
60
+ export {};
54
61
  //# sourceMappingURL=composer-history.d.ts.map
@@ -20,34 +20,46 @@ export function mergeComposerHistory({ recent, current }, { cap = 100 } = {}) {
20
20
  export function createComposerHistoryState(_required, _optional = {}) {
21
21
  return { entries: [], index: null, draft: null };
22
22
  }
23
+ function hasModifierOrPopup({ input }, _optional = {}) {
24
+ return Boolean(input.shiftKey || input.ctrlKey || input.altKey || input.metaKey || input.isComposing || input.popupOpen);
25
+ }
26
+ /**
27
+ * Whether the selection sits on the first/last line. A selection spanning a newline is on
28
+ * neither, because text[0, end) and text[start, ∞) both contain the selected newline.
29
+ */
30
+ function caretBoundaries({ input }, _optional = {}) {
31
+ const start = Math.min(input.selectionStart, input.selectionEnd);
32
+ const end = Math.max(input.selectionStart, input.selectionEnd);
33
+ return {
34
+ firstLine: !input.text.slice(0, end).includes('\n') && input.firstLine !== false,
35
+ lastLine: !input.text.slice(start).includes('\n') && input.lastLine !== false,
36
+ };
37
+ }
23
38
  export function composerHistoryKeyAction(input, _optional = {}) {
24
- if (input.shiftKey || input.ctrlKey || input.altKey || input.metaKey || input.isComposing || input.popupOpen)
39
+ if (hasModifierOrPopup({ input }))
25
40
  return null;
26
41
  if (input.key === 'Escape')
27
42
  return 'restore';
28
- const start = Math.min(input.selectionStart, input.selectionEnd);
29
- const end = Math.max(input.selectionStart, input.selectionEnd);
30
- if (input.text.slice(start, end).includes('\n'))
31
- return null;
32
- const firstLine = !input.text.slice(0, end).includes('\n') && input.firstLine !== false;
33
- const lastLine = !input.text.slice(start).includes('\n') && input.lastLine !== false;
34
- if (input.key === 'ArrowUp' && firstLine)
43
+ const lines = caretBoundaries({ input });
44
+ if (input.key === 'ArrowUp' && lines.firstLine)
35
45
  return 'previous';
36
- if (input.key === 'ArrowDown' && lastLine)
46
+ if (input.key === 'ArrowDown' && lines.lastLine)
37
47
  return 'next';
38
48
  return null;
39
49
  }
50
+ function startBrowsing({ state, action, text, entries }, _optional = {}) {
51
+ if (action !== 'previous' || entries.length === 0)
52
+ return { state, text, handled: false };
53
+ const snapshot = [...entries];
54
+ const index = snapshot.length - 1;
55
+ return { state: { entries: snapshot, index, draft: text }, text: snapshot[index], handled: true };
56
+ }
40
57
  export function transitionComposerHistory({ state, action, text, entries }, _optional = {}) {
41
58
  if (action === 'edit' || action === 'reset') {
42
59
  return { state: createComposerHistoryState({}), text, handled: false };
43
60
  }
44
- if (state.index === null) {
45
- if (action !== 'previous' || entries.length === 0)
46
- return { state, text, handled: false };
47
- const snapshot = [...entries];
48
- const index = snapshot.length - 1;
49
- return { state: { entries: snapshot, index, draft: text }, text: snapshot[index], handled: true };
50
- }
61
+ if (state.index === null)
62
+ return startBrowsing({ state, action, text, entries });
51
63
  if (action === 'restore' || (action === 'next' && state.index === state.entries.length - 1)) {
52
64
  return { state: createComposerHistoryState({}), text: state.draft ?? '', handled: true };
53
65
  }
@@ -0,0 +1,3 @@
1
+ /** Maximum length of a user-supplied conversation title, in UTF-16 code units (HTML maxLength). */
2
+ export declare const MAX_CONVERSATION_TITLE_LENGTH = 50;
3
+ //# sourceMappingURL=conversation-title.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Maximum length of a user-supplied conversation title, in UTF-16 code units (HTML maxLength). */
2
+ export const MAX_CONVERSATION_TITLE_LENGTH = 50;
3
+ //# sourceMappingURL=conversation-title.js.map