@jini-ai/chat 0.6.5 → 0.6.7

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 (161) hide show
  1. package/CHANGELOG.md +12 -0
  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 +184 -97
  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 +35 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/policies.d.ts +1 -0
  23. package/dist/core/durable-runs/policies.js +1 -0
  24. package/dist/core/durable-runs/ports.d.ts +6 -2
  25. package/dist/core/index.d.ts +2 -0
  26. package/dist/core/index.js +2 -0
  27. package/dist/core/messages.d.ts +2 -0
  28. package/dist/core/partial-json.d.ts +6 -7
  29. package/dist/core/partial-json.js +81 -47
  30. package/dist/core/persistence/title.js +4 -2
  31. package/dist/core/question-answer.d.ts +23 -0
  32. package/dist/core/question-answer.js +66 -0
  33. package/dist/core/question-form/format.d.ts +6 -6
  34. package/dist/core/question-form/format.js +16 -17
  35. package/dist/core/question-form/normalize.d.ts +18 -0
  36. package/dist/core/question-form/normalize.js +112 -122
  37. package/dist/core/question-form/scan.js +141 -203
  38. package/dist/core/run-activity.js +62 -49
  39. package/dist/core/run-events/entry.d.ts +10 -1
  40. package/dist/core/run-events/entry.js +117 -87
  41. package/dist/core/todos.d.ts +1 -1
  42. package/dist/core/todos.js +60 -85
  43. package/dist/core/tool-events.js +14 -12
  44. package/dist/core/transcript.js +62 -16
  45. package/dist/core/turn-usage.d.ts +2 -1
  46. package/dist/core/turn-usage.js +56 -27
  47. package/dist/core/user-text-redaction.js +3 -0
  48. package/dist/core/util/manifest.js +77 -82
  49. package/dist/core/util/markdown-context.js +51 -53
  50. package/dist/core/util/parser.js +136 -144
  51. package/dist/core/util/pointer.js +16 -16
  52. package/dist/core/util/strip.js +57 -67
  53. package/dist/core/util/validate.js +15 -17
  54. package/dist/embed-widget/browser/session-store.js +21 -12
  55. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  56. package/dist/embed-widget/vanilla/index.js +81 -70
  57. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  58. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  59. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  60. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  61. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  62. package/dist/react/components/ChatSettingsDialog.js +72 -0
  63. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  64. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  65. package/dist/react/components/Composer.d.ts +5 -1
  66. package/dist/react/components/Composer.js +126 -82
  67. package/dist/react/components/ConversationList.js +25 -39
  68. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  69. package/dist/react/components/ConversationTitleEditor.js +54 -0
  70. package/dist/react/components/Icon.d.ts +1 -1
  71. package/dist/react/components/Icon.js +25 -45
  72. package/dist/react/components/Markdown.js +134 -123
  73. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  74. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  75. package/dist/react/components/MessageList.d.ts +13 -13
  76. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  77. package/dist/react/components/MessageList.hooks.js +19 -3
  78. package/dist/react/components/MessageList.js +34 -10
  79. package/dist/react/components/MessageRow.js +48 -32
  80. package/dist/react/components/MessageRow.usage.d.ts +5 -2
  81. package/dist/react/components/MessageRow.usage.js +20 -11
  82. package/dist/react/components/MidRunUserMessage.js +16 -3
  83. package/dist/react/components/QuestionForm.js +148 -76
  84. package/dist/react/components/QuestionsPanel.js +1 -1
  85. package/dist/react/components/TodoCard.js +12 -6
  86. package/dist/react/components/ToolCard.js +47 -40
  87. package/dist/react/components/composer-discovery.js +6 -1
  88. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  89. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  90. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  91. package/dist/react/ext-event-renderer-registry.js +5 -2
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  93. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  94. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  95. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  96. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  98. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  99. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  100. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  102. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  104. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  106. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  108. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  109. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  110. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  112. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  114. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  115. package/dist/react/features/chat-pane/model-options.d.ts +18 -1
  116. package/dist/react/features/chat-pane/model-options.js +49 -20
  117. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  118. package/dist/react/features/chat-pane/rules.js +84 -46
  119. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  120. package/dist/react/features/chat-pane/styles.js +156 -37
  121. package/dist/react/features/chat-pane/types.d.ts +17 -8
  122. package/dist/react/hooks/assistant-chats.js +12 -5
  123. package/dist/react/hooks/composer-draft-cache.js +116 -118
  124. package/dist/react/hooks/composer-history-storage.js +5 -5
  125. package/dist/react/hooks/fab-position.js +7 -4
  126. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  127. package/dist/react/hooks/useAnsweredCards.js +38 -11
  128. package/dist/react/hooks/useArtifactStream.js +28 -22
  129. package/dist/react/hooks/useComposer.d.ts +20 -0
  130. package/dist/react/hooks/useComposer.js +22 -9
  131. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  132. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  133. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  134. package/dist/react/hooks/useConversation.d.ts +7 -0
  135. package/dist/react/hooks/useConversation.js +23 -10
  136. package/dist/react/hooks/useExtEventGroups.js +39 -39
  137. package/dist/react/hooks/useQuestionForms.js +21 -26
  138. package/dist/react/hooks/useRunStream.js +10 -2
  139. package/dist/react/index.d.ts +7 -1
  140. package/dist/react/index.js +9 -0
  141. package/dist/react/message-blocks.d.ts +6 -4
  142. package/dist/react/message-blocks.js +94 -72
  143. package/dist/react/slots.d.ts +2 -2
  144. package/dist/react/tool-call-display.js +19 -17
  145. package/dist/server/durable-runs/recover.d.ts +1 -1
  146. package/dist/server/durable-runs/recover.js +139 -76
  147. package/dist/server/run-finalizer/entry.js +25 -15
  148. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  149. package/dist/store/legacy/sqlite/conversations.js +69 -98
  150. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  151. package/dist/store/legacy/sqlite/messages.js +113 -71
  152. package/dist/store/runtime/chat-history-store.js +9 -3
  153. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  154. package/dist/store/runtime/durable-run-store.js +165 -117
  155. package/dist/store/runtime/history-tools.js +118 -82
  156. package/dist/store/runtime/run-ledger.d.ts +4 -1
  157. package/dist/store/runtime/run-ledger.js +56 -16
  158. package/dist/store/sql/cursor.js +38 -18
  159. package/dist/store/sql/store.d.ts +18 -0
  160. package/dist/store/sql/store.js +46 -15
  161. package/package.json +12 -10
@@ -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
  /**
@@ -205,7 +211,49 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
205
211
  /** The durable message owns recovery and terminal state; this subscription follows replacement
206
212
  * attempts without writing a browser-inferred failure into history. */
207
213
  function subscribeToRun(runId, handlers, signal) {
208
- ports.followRun({ runId, handlers, ...(signal === undefined ? {} : { signal }) }, {});
214
+ ports.followRun({ runId, handlers: capturePluginUsage(handlers), ...(signal === undefined ? {} : { signal }) }, {});
215
+ }
216
+ /** The well-formed plugin usage an `agent_plugins` extension event carries, or `undefined` for any
217
+ * other event — the one validity rule both live events and checkpoint replays are captured by. */
218
+ function pluginUsageOf({ event }, _optional) {
219
+ if (event.kind !== 'ext' || event.name !== 'agent_plugins')
220
+ return undefined;
221
+ const usage = event.data;
222
+ return typeof usage?.conversationId === 'string' && usage.state && Array.isArray(usage.state.excluded) ? usage : undefined;
223
+ }
224
+ /** Capture before notifying the UI so answers received after reload persist for the next turn.
225
+ * Checkpoint panes skip live deltas to avoid duplicate text, so capture their saved events here
226
+ * too. Keep this wrapper as the single capture owner for both delivery contracts.
227
+ * @complexity O(e + s) per checkpoint; O(s) per live event, in saved events and plugin-state size.
228
+ */
229
+ function capturePluginUsage(handlers) {
230
+ const captured = new Map();
231
+ const capture = (event) => {
232
+ const usage = pluginUsageOf({ event }, {});
233
+ if (!usage)
234
+ return;
235
+ const state = JSON.stringify(usage.state);
236
+ if (captured.get(usage.conversationId) === state)
237
+ return;
238
+ captured.set(usage.conversationId, state);
239
+ captureAgentPluginUsage({ event });
240
+ };
241
+ return {
242
+ ...handlers,
243
+ onEvent: event => { capture(event); handlers.onEvent(event); },
244
+ ...(handlers.onCheckpoint ? { onCheckpoint: (message) => {
245
+ // Historical answers must not roll the current state backwards on every poll.
246
+ const latest = new Map();
247
+ for (const event of message.events ?? []) {
248
+ const usage = pluginUsageOf({ event }, {});
249
+ if (usage)
250
+ latest.set(usage.conversationId, event);
251
+ }
252
+ for (const event of latest.values())
253
+ capture(event);
254
+ handlers.onCheckpoint(message);
255
+ } } : {}),
256
+ };
209
257
  }
210
258
  /** Client-minted, not server-minted — see module doc's path-2 section: a BYOK run has no server-side
211
259
  * run record to name it. Prefixed so `stopRun`/`fetchRunStatus`/`reattachRun` below can tell a
@@ -293,6 +341,56 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
293
341
  ctx.finish();
294
342
  }
295
343
  }
344
+ /**
345
+ * The provider-bound history of one BYOK turn: only turns the agent actually answered, never an
346
+ * empty one, each carrying its redaction signal.
347
+ */
348
+ function byokMessages({ history }, _optional) {
349
+ // `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
350
+ // hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
351
+ // that partial output would be sent to the provider as if it were the assistant's answer. This is
352
+ // the same defect as on the Local CLI path, reachable by a second route.
353
+ return historyForTranscript(history)
354
+ .filter((message) => message.content.trim().length > 0)
355
+ .map((message) => ({ role: message.role, content: message.content,
356
+ ...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
357
+ }));
358
+ }
359
+ /** The credential block of a BYOK turn; an empty base URL and an unset token cap are omitted. */
360
+ function byokCredentials({ byok }, _optional) {
361
+ return {
362
+ protocol: byok.protocol,
363
+ apiKey: byok.apiKey,
364
+ ...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
365
+ model: byok.model,
366
+ ...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
367
+ };
368
+ }
369
+ /** The JSON body `POST byokTurnUrl` carries for one turn. */
370
+ function byokTurnBody({ input, byok }, _optional) {
371
+ return {
372
+ messages: byokMessages({ history: input.history }, {}),
373
+ ...attachmentIdsField(input),
374
+ ...conversationIdField(input),
375
+ agentPluginState: input.context?.["agentPluginState"],
376
+ userPrompt: input.context?.["userPrompt"],
377
+ ...pluginRefIdsField(input),
378
+ // The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
379
+ // in front of the newest message so a BYOK turn knows the open page and where drawings go.
380
+ ...pageContextField(input),
381
+ byok: byokCredentials({ byok }, {}),
382
+ };
383
+ }
384
+ /**
385
+ * The error for a refused request: `<action> (<status>)`, plus up to 300 characters of the
386
+ * response body when it has one — shared by the BYOK start, Local CLI start and cancel refusals.
387
+ */
388
+ async function refusalError({ action, response }, _optional) {
389
+ // A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
390
+ // rejected the request" into "the client crashed reading the rejection".
391
+ const detail = await response.text().catch(() => "");
392
+ return new Error(`${action} (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
393
+ }
296
394
  /**
297
395
  * The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
298
396
  * `EventSource`/reattach. See module doc's path-2 section for why this shape differs from
@@ -307,35 +405,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
307
405
  const controller = new AbortController();
308
406
  byokAbortControllers.set(runId, controller);
309
407
  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
408
  let response;
320
409
  try {
321
- response = await ports.fetch(BYOK_TURN_URL, {
410
+ response = await ports.fetch(ports.byokTurnUrl, {
322
411
  method: "POST",
323
412
  headers: { "Content-Type": "application/json" },
324
413
  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
- }),
414
+ body: JSON.stringify(byokTurnBody({ input, byok }, {})),
339
415
  signal: controller.signal,
340
416
  });
341
417
  }
@@ -345,8 +421,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
345
421
  }
346
422
  if (!response.ok || !response.body) {
347
423
  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)}` : ""}`);
424
+ throw await refusalError({ action: "BYOK turn failed to start", response }, {});
350
425
  }
351
426
  const collected = [];
352
427
  let settled = false;
@@ -484,10 +559,14 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
484
559
  * adopt a conversation because `input.context` named none (the first turn of a chat). Omitted by
485
560
  * every caller that already has an id on `input.context`, and by this function's own unit tests —
486
561
  * in which case the `context` field is read exactly as before.
562
+ * The bounded `coldStartContext` is the complete prompt, including the current request. The
563
+ * daemon selects it instead of the resume tail when native resume is unavailable.
487
564
  */
488
565
  function buildLocalCliContextRef(input, prompt, resolvedConversationId) {
489
566
  return {
490
567
  prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
568
+ // Select one complete transcript after binding; concatenating it with the tail repeats turns.
569
+ coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
491
570
  // The transcript is already sanitized; retain its signal across the daemon JSON boundary.
492
571
  ...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
493
572
  assistantMessageId: input.assistantMessageId,
@@ -496,6 +575,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
496
575
  ...reasoningField(input),
497
576
  ...attachmentIdsField(input),
498
577
  ...pluginRefIdsField(input),
578
+ agentPluginState: input.context?.["agentPluginState"],
579
+ userPrompt: input.context?.["userPrompt"],
499
580
  ...conversationIdField(input, resolvedConversationId),
500
581
  ...pageContextField(input),
501
582
  };
@@ -523,27 +604,28 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
523
604
  const adopted = await ensureConversationId({}, {}).catch(() => null);
524
605
  return typeof adopted === "string" && adopted.length > 0 ? adopted : undefined;
525
606
  }
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.
607
+ /** Send a resumed agent only its unseen conversation tail, retaining other agents' outcomes.
608
+ * Legacy rows without agent identity retain the existing unanswered-user delivery contract.
609
+ * @complexity O(n) to find the last answered turn; transcript rendering is capped at 40 turns.
535
610
  */
536
611
  function resolveLocalCliPrompt(input, resumeCapableAgentIds) {
537
612
  const carriesOwnMemory = input.agentId !== undefined && resumeCapableAgentIds?.has(input.agentId) === true;
613
+ if (!carriesOwnMemory)
614
+ return runPrompt(input.history);
615
+ let lastOwnAnswer = -1;
616
+ input.history.forEach((message, index) => {
617
+ if (message.role === 'assistant' && (!message.agentId || message.agentId === input.agentId) && isAnsweredAssistantTurn(message))
618
+ lastOwnAnswer = index;
619
+ });
620
+ const unseen = input.history.slice(lastOwnAnswer + 1);
621
+ const crossedAgents = unseen.some(message => message.role === 'assistant' && message.agentId && message.agentId !== input.agentId);
538
622
  // BEHAVIOR CHANGE (2026-09-11, Defect 2): the resume-capable branch was `latestUserPrompt` — the
539
623
  // single newest user turn, on the assumption that everything before it is already inside the
540
624
  // CLI's own session. That assumption holds only for turns the agent actually answered; a turn
541
625
  // whose run died before the CLI read its prompt is in neither place, and nothing ever sent it
542
626
  // 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);
627
+ // differs only when there is genuinely something unanswered to catch up on.
628
+ return crossedAgents ? runPrompt(unseen) : undeliveredUserPrompt(input.history);
547
629
  }
548
630
  /**
549
631
  * Writes the newest user turn to durable storage before its run is dispatched, and never fails the
@@ -562,44 +644,73 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
562
644
  * two, and a retry of the same content is free. `useAssistantChats`'s own `persistUserTurn` also
563
645
  * marks the id written, so in practice `flush` skips it entirely.
564
646
  *
565
- * @param input - `startRun`'s own input; the newest `role: "user"` message in its history is what
566
- * gets written.
647
+ * @param message - The newest `role: "user"` message in `startRun`'s history — what gets written.
648
+ * `startRun` has already refused a history without one, so it is always present here.
567
649
  * @param conversationId - The already-resolved conversation (see `resolveRunConversationId`), or
568
650
  * `undefined` when adoption failed — there is no row to write into in that case.
569
651
  * @param persistUserTurn - `AssistantTransportOptions.persistUserTurn`, or `undefined`
570
652
  * for a transport built without it (every test and any non-dock consumer).
571
653
  * @returns Nothing, always. The `.catch` keeps a failed browser PUT from refusing the user's turn;
572
654
  * 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.
655
+ * @complexity O(1) plus one PUT.
574
656
  */
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)
657
+ async function persistUserTurnBeforeDispatch({ message, conversationId }, { persistUserTurn }) {
658
+ // Pending composer input is persisted by atomic run acceptance. An earlier PUT could save a
659
+ // rejected (409) draft as a user turn; confirmed/legacy callers retain their existing ordering.
660
+ if (message.inputPending || conversationId === undefined || persistUserTurn === undefined)
581
661
  return;
582
662
  await persistUserTurn({ conversationId, message }, {}).catch(() => undefined);
583
663
  }
664
+ /** The newest `role: "user"` message in a history, or `undefined` when it has none.
665
+ * @complexity O(n) in history length. */
666
+ function newestUserMessage({ history }, _optional) {
667
+ return [...history].reverse().find((message) => message.role === "user");
668
+ }
584
669
  /**
585
670
  * Turns `startRun`'s Local CLI `POST /api/runs` response into either a thrown, detailed error or a
586
671
  * subscribed run — the two outcomes once the request lands. Pulled out of `startRun` (2026-09-04,
587
672
  * 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.
673
+ * `.text().catch(() => "")` is load-bearing, not decorative — see {@link refusalError}'s inline comment.
589
674
  */
590
675
  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
- }
676
+ if (!response.ok)
677
+ throw await refusalError({ action: "agent run failed to start", response }, {});
597
678
  const { run, messageId, conversationId } = (await response.json());
598
679
  if (messageId && conversationId)
599
680
  ports.bindRun({ runId: run.id, binding: { messageId, conversationId } }, {});
600
681
  subscribeToRun(run.id, handlers, signal);
601
682
  return { runId: run.id };
602
683
  }
684
+ /**
685
+ * The Local CLI path of `startRun`: durable user turn, then acceptance, then the followed run.
686
+ *
687
+ * @param input - `startRun`'s input with its conversation identity already merged into `context`.
688
+ * @param userMessage - The newest user message; acceptance carries it verbatim.
689
+ * @param conversationId - The resolved conversation, or `undefined` when adoption failed.
690
+ */
691
+ async function startLocalCliRun({ input, handlers, userMessage, conversationId }, { options }) {
692
+ // Local CLI path below. `resolveLocalCliPrompt`'s own doc has the full contract for why an
693
+ // unresolved/absent agentId fails open to the full transcript rather than the bare message.
694
+ const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
695
+ // Also before the POST, and for the same reason it is awaited rather than fired off: the
696
+ // user's turn must be durable before a run that can fail to deliver it exists at all
697
+ // (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
698
+ // adoption produces. BYOK and AG-UI share the identity above; neither writes a durable
699
+ // user turn ahead of dispatch; those transports retain their own persistence behavior.
700
+ await persistUserTurnBeforeDispatch({ message: userMessage, conversationId }, { persistUserTurn: options.persistUserTurn });
701
+ // Acceptance commits the question and stub together even if the separate browser PUT fails
702
+ // or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
703
+ const base = buildLocalCliContextRef(input, prompt, conversationId);
704
+ const contextRef = { ...base, userMessage };
705
+ const response = await ports.acceptRun({
706
+ ...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
707
+ requestSignal: input.cancelSignal ?? input.signal,
708
+ body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
709
+ messageId: base.assistantMessageId,
710
+ ...(conversationId === undefined ? {} : { conversationId }),
711
+ }, {});
712
+ return finishLocalCliRun(response, handlers, input.signal);
713
+ }
603
714
  function createAssistantTransport(options = {}) {
604
715
  return {
605
716
  async startRun(input, handlers) {
@@ -608,9 +719,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
608
719
  // prompt asks the agent to reply to itself. Shared by both paths below, and reused (rather than
609
720
  // recomputed) by the Local CLI branch's own resume-capable check further down.
610
721
  const latestUserPrompt = latestUserPromptFromHistory({ history: input.history });
611
- if (!latestUserPrompt) {
722
+ const userMessage = newestUserMessage({ history: input.history }, {});
723
+ if (!latestUserPrompt || userMessage === undefined) {
612
724
  throw new Error("no user message to send");
613
725
  }
726
+ // Adopt identity before either runtime branch so first-turn decisions survive a reload.
727
+ const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
728
+ input = { ...input, context: { ...input.context, ...(conversationId ? { conversationId, agentPluginState: readAgentPluginChatState({ conversationId }) } : {}), userPrompt: latestUserPrompt } };
614
729
  const executionConfig = options.getExecutionConfig?.({}, {});
615
730
  // Dispatches on MODE alone, not on whether a key is typed in this browser right now
616
731
  // (2026-08-05). `executionConfig.byok.apiKey` is write-only server-side — it is empty on
@@ -623,7 +738,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
623
738
  // level down — the route's own 400 `"no usable BYOK credential..."` — which is a real,
624
739
  // actionable answer instead of the mode picker silently refusing to try.
625
740
  if (executionConfig?.mode === "byok") {
626
- return startByokRun(ports.projectInput({ input }, {}), handlers, executionConfig.byok);
741
+ return startByokRun(ports.projectInput({ input }, {}), capturePluginUsage(handlers), executionConfig.byok);
627
742
  }
628
743
  // AG-UI canary path (ADR-059) — a host-supplied toggle, independent of `executionConfig.mode`
629
744
  // (Jini's own closed union, never extended for this). Checked AFTER the byok branch above:
@@ -633,35 +748,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
633
748
  if (options.getAgUiEnabled?.({}, {})) {
634
749
  // `historyForTranscript` here rather than inside `startAgUiRun`: that module importing this
635
750
  // 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 }, {});
751
+ return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers: capturePluginUsage(handlers) }, {});
637
752
  }
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);
753
+ return startLocalCliRun({ input, handlers, userMessage, conversationId }, { options });
665
754
  },
666
755
  async reattachRun(runId, handlers, options) {
667
756
  // A BYOK run has no server-side record to reattach to (module doc's path-2 section) — the
@@ -688,7 +777,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
688
777
  return null;
689
778
  if (ports.agUi.isAgUiRunId({ runId }, {}))
690
779
  return ports.agUi.fetchAgUiRunStatus({}, {});
691
- const response = await ports.fetch(`${RUNS_URL}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
780
+ const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
692
781
  if (!response.ok)
693
782
  return null;
694
783
  const { run } = (await response.json());
@@ -704,7 +793,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
704
793
  if (ports.agUi.isAgUiRunId({ runId }, {})) {
705
794
  return ports.agUi.stopAgUiRun({ runId }, {});
706
795
  }
707
- const response = await ports.fetch(`${RUNS_URL}/${encodeURIComponent(runId)}/cancel`, {
796
+ const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}/cancel`, {
708
797
  method: "POST",
709
798
  headers: { "Content-Type": "application/json" },
710
799
  credentials: "same-origin",
@@ -715,10 +804,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
715
804
  return;
716
805
  // `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
717
806
  // 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
- }
807
+ if (!response.ok)
808
+ throw await refusalError({ action: "cancelling agent run failed", response }, {});
722
809
  },
723
810
  };
724
811
  }
@@ -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