@artooi/ag-ui-web-component 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +134 -36
  2. package/README.md +7 -7
  3. package/dist/ag-ui-web-component.bundle.js +145 -118
  4. package/dist/ag-ui-web-component.bundle.js.map +3 -3
  5. package/dist/constants.d.ts +65 -78
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +93 -126
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +24 -30
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/core/attachment.d.ts +9 -14
  12. package/dist/core/attachment.d.ts.map +1 -1
  13. package/dist/core/conversation_store.d.ts +20 -27
  14. package/dist/core/conversation_store.d.ts.map +1 -1
  15. package/dist/core/create_http_agent.d.ts +13 -15
  16. package/dist/core/create_http_agent.d.ts.map +1 -1
  17. package/dist/core/remote_conversation_store.d.ts +8 -9
  18. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  19. package/dist/core/run_index.d.ts +11 -20
  20. package/dist/core/run_index.d.ts.map +1 -1
  21. package/dist/core/transcribe_audio.d.ts +8 -8
  22. package/dist/core/transcribe_audio.d.ts.map +1 -1
  23. package/dist/core/upload_attachment.d.ts +15 -18
  24. package/dist/core/upload_attachment.d.ts.map +1 -1
  25. package/dist/core/utils.d.ts +4 -6
  26. package/dist/core/utils.d.ts.map +1 -1
  27. package/dist/dom/animations.d.ts +22 -30
  28. package/dist/dom/animations.d.ts.map +1 -1
  29. package/dist/dom/dom_driver.d.ts +7 -7
  30. package/dist/dom/native_setter.d.ts +2 -2
  31. package/dist/dom/native_setter.d.ts.map +1 -1
  32. package/dist/index.js +347 -401
  33. package/dist/index.js.map +2 -2
  34. package/dist/skills/fill_template.d.ts +4 -5
  35. package/dist/skills/fill_template.d.ts.map +1 -1
  36. package/dist/skills/parse_skills.d.ts.map +1 -1
  37. package/dist/skills/skill.d.ts +7 -8
  38. package/dist/skills/skill.d.ts.map +1 -1
  39. package/dist/tools/client_tool_registry.d.ts +2 -2
  40. package/dist/tools/page_action_tools.d.ts +7 -10
  41. package/dist/tools/page_action_tools.d.ts.map +1 -1
  42. package/dist/tools/page_state.d.ts +5 -8
  43. package/dist/tools/page_state.d.ts.map +1 -1
  44. package/dist/tools/route_map.d.ts +7 -10
  45. package/dist/tools/route_map.d.ts.map +1 -1
  46. package/dist/ui/approval_card.d.ts +15 -20
  47. package/dist/ui/approval_card.d.ts.map +1 -1
  48. package/dist/ui/attach_copy_buttons.d.ts +4 -10
  49. package/dist/ui/attach_copy_buttons.d.ts.map +1 -1
  50. package/dist/ui/attachment_chips.d.ts +13 -5
  51. package/dist/ui/attachment_chips.d.ts.map +1 -1
  52. package/dist/ui/attachment_tray.d.ts +6 -6
  53. package/dist/ui/attachment_tray.d.ts.map +1 -1
  54. package/dist/ui/checkpoint_menu.d.ts +7 -8
  55. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  56. package/dist/ui/confirmation_card.d.ts +10 -15
  57. package/dist/ui/confirmation_card.d.ts.map +1 -1
  58. package/dist/ui/question_card.d.ts +12 -15
  59. package/dist/ui/question_card.d.ts.map +1 -1
  60. package/dist/ui/relative_time.d.ts +5 -7
  61. package/dist/ui/relative_time.d.ts.map +1 -1
  62. package/dist/ui/render_markdown.d.ts +8 -8
  63. package/dist/ui/render_markdown.d.ts.map +1 -1
  64. package/dist/ui/resize_handle.d.ts +21 -34
  65. package/dist/ui/resize_handle.d.ts.map +1 -1
  66. package/dist/ui/run_notice.d.ts +5 -7
  67. package/dist/ui/run_notice.d.ts.map +1 -1
  68. package/dist/ui/skills_menu.d.ts +4 -5
  69. package/dist/ui/skills_menu.d.ts.map +1 -1
  70. package/dist/ui/styles.d.ts +1 -1
  71. package/dist/ui/styles.d.ts.map +1 -1
  72. package/dist/ui/thoughts_block.d.ts +9 -11
  73. package/dist/ui/thoughts_block.d.ts.map +1 -1
  74. package/dist/ui/thread_drawer.d.ts +6 -5
  75. package/dist/ui/thread_drawer.d.ts.map +1 -1
  76. package/dist/ui/tool_call_card.d.ts +17 -25
  77. package/dist/ui/tool_call_card.d.ts.map +1 -1
  78. package/dist/ui/ui_strings.d.ts +6 -12
  79. package/dist/ui/ui_strings.d.ts.map +1 -1
  80. package/dist/ui/voice_input.d.ts +10 -11
  81. package/dist/ui/voice_input.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/constants.ts +71 -80
  84. package/src/core/ag_ui_chat.ts +242 -292
  85. package/src/core/agui_client.ts +60 -71
  86. package/src/core/attachment.ts +9 -14
  87. package/src/core/conversation_store.ts +25 -33
  88. package/src/core/create_http_agent.ts +18 -22
  89. package/src/core/remote_conversation_store.ts +14 -15
  90. package/src/core/run_index.ts +14 -23
  91. package/src/core/transcribe_audio.ts +9 -10
  92. package/src/core/upload_attachment.ts +18 -21
  93. package/src/core/utils.ts +4 -6
  94. package/src/dom/animations.ts +33 -43
  95. package/src/dom/dom_driver.ts +7 -7
  96. package/src/dom/native_setter.ts +11 -12
  97. package/src/skills/fill_template.ts +4 -5
  98. package/src/skills/parse_skills.ts +3 -4
  99. package/src/skills/skill.ts +7 -8
  100. package/src/tools/client_tool_registry.ts +2 -2
  101. package/src/tools/page_action_tools.ts +12 -15
  102. package/src/tools/page_state.ts +5 -8
  103. package/src/tools/route_map.ts +15 -19
  104. package/src/ui/approval_card.ts +15 -20
  105. package/src/ui/attach_copy_buttons.ts +9 -18
  106. package/src/ui/attachment_chips.ts +19 -10
  107. package/src/ui/attachment_tray.ts +8 -7
  108. package/src/ui/checkpoint_menu.ts +7 -8
  109. package/src/ui/confirmation_card.ts +10 -15
  110. package/src/ui/question_card.ts +12 -15
  111. package/src/ui/relative_time.ts +5 -7
  112. package/src/ui/render_markdown.ts +25 -51
  113. package/src/ui/resize_handle.ts +25 -38
  114. package/src/ui/run_notice.ts +9 -12
  115. package/src/ui/skills_menu.ts +4 -5
  116. package/src/ui/styles.ts +122 -93
  117. package/src/ui/thoughts_block.ts +11 -13
  118. package/src/ui/thread_drawer.ts +6 -5
  119. package/src/ui/tool_call_card.ts +22 -32
  120. package/src/ui/ui_strings.ts +6 -12
  121. package/src/ui/voice_input.ts +10 -11
  122. package/src/version.ts +1 -1
@@ -33,9 +33,9 @@ export interface ToolExecution {
33
33
  /**
34
34
  * Executes a frontend tool call.
35
35
  *
36
- * Returns the {@link ToolExecution} to post back to the agent, or ``null`` when
37
- * the call is not a frontend tool the host owns (a server-side tool the server
38
- * already executed the client must not re-run for it).
36
+ * Returns the {@link ToolExecution} to post back to the agent, or `null` when
37
+ * the call is not a frontend tool the host owns a server-side tool the server
38
+ * already executed, which the client must not re-run.
39
39
  */
40
40
  export type ExecuteTool = (call: AgUiToolCall) => Promise<ToolExecution | null>;
41
41
 
@@ -50,11 +50,10 @@ export type InterruptResponse = { status: "resolved"; payload?: unknown } | { st
50
50
  /**
51
51
  * Resolves the approval interrupts a run finished on, keyed by interrupt id.
52
52
  *
53
- * When a gated **server-side** tool defers instead of executing, the run
54
- * finishes on an AG-UI interrupt outcome; the host renders an approval card per
55
- * interrupt and returns each decision here, and the loop resumes the run with
56
- * the answers. Omit for agents that never gate server-side tools — an
57
- * unresolved interrupt then simply ends the loop.
53
+ * A gated server-side tool defers instead of executing, so the run finishes on
54
+ * an interrupt outcome; the host collects one decision per interrupt and the
55
+ * loop resumes with the answers. Omit for agents that never gate server-side
56
+ * tools an unresolved interrupt then ends the loop.
58
57
  */
59
58
  export type ResolveInterrupts = (
60
59
  interrupts: readonly Interrupt[],
@@ -67,9 +66,9 @@ export type ResolveInterrupts = (
67
66
  */
68
67
  export interface AgUiClientHandlers {
69
68
  onRunStart(): void;
70
- /** Fired on every streamed token; ``buffer`` is the full text so far. */
69
+ /** Fired on every streamed token; `buffer` is the full text so far. */
71
70
  onTextDelta(buffer: string): void;
72
- /** Fired when the assistant message completes; ``buffer`` is the final text. */
71
+ /** Fired when the assistant message completes; `buffer` is the final text. */
73
72
  onTextEnd(buffer: string): void;
74
73
  /** Fired when the agent finishes calling a tool (server- or frontend-side). */
75
74
  onToolCall(call: AgUiToolCall): void;
@@ -95,17 +94,15 @@ export interface AgUiClientHandlers {
95
94
  onError(message: string): void;
96
95
  /**
97
96
  * Fired when the user cancelled the run ({@link AgUiClient.cancel}) — the
98
- * deliberate-stop sibling of `onError`. Any partial assistant text already
99
- * streamed stays valid; the host should keep the bubble and show a muted
100
- * "stopped" affordance rather than an error. `onSettled` still follows.
97
+ * deliberate-stop sibling of `onError`. Partial assistant text already
98
+ * streamed stays valid, so the host keeps the bubble and shows a stopped
99
+ * affordance rather than an error. `onSettled` still follows.
101
100
  */
102
101
  onCancelled(): void;
103
102
  /**
104
- * Fired exactly once when the whole interaction settles after the run loop
105
- * ends for any reason (a server-only round, frontend-tool rounds exhausted,
106
- * a cancellation, or an error). The terminal guarantee that the UI returns
107
- * to rest (pending indicator cleared, input re-enabled) no matter how the
108
- * run finished.
103
+ * Fired exactly once when the interaction settles, however the run loop ended
104
+ * server-only round, rounds exhausted, cancellation, or error. The terminal
105
+ * guarantee that the UI returns to rest.
109
106
  */
110
107
  onSettled(): void;
111
108
  }
@@ -138,17 +135,15 @@ export interface AgUiClientConfig extends AgUiRunInputs {
138
135
  */
139
136
  onPersist?: (messages: readonly Message[]) => void;
140
137
  /**
141
- * Called whenever AG-UI shared state changes — the server streamed a
142
- * `STATE_SNAPSHOT` / `STATE_DELTA`, or {@link AgUiClient.setState} was
143
- * called. `@ag-ui/client` owns applying those events; this only forwards
144
- * the result so a host can react.
138
+ * Called whenever AG-UI shared state changes — a streamed `STATE_SNAPSHOT` /
139
+ * `STATE_DELTA`, or {@link AgUiClient.setState}. `@ag-ui/client` owns applying
140
+ * those events; this only forwards the result.
145
141
  */
146
142
  onStateChanged?: (state: Readonly<Record<string, unknown>>) => void;
147
143
  /**
148
- * Error text surfaced to {@link AgUiClientHandlers.onError} when a run's
149
- * stream closes without a terminal AG-UI event (`RUN_FINISHED`/`RUN_ERROR`)
150
- * a dropped connection. Defaults to `"Connection lost"`; the host passes its
151
- * localized string.
144
+ * Error text for {@link AgUiClientHandlers.onError} when a run's stream closes
145
+ * without a terminal AG-UI event. Defaults to `"Connection lost"`; the host
146
+ * passes its localized string.
152
147
  */
153
148
  connectionLostMessage?: string;
154
149
  }
@@ -182,10 +177,9 @@ export class AgUiClient {
182
177
  readonly #resolveInterrupts: ResolveInterrupts | null;
183
178
  readonly #onPersist: (messages: readonly Message[]) => void;
184
179
  /**
185
- * Message ids the server has already closed, so a reuse can be reported.
186
- *
187
- * Per client rather than per run: the merge happens across runs, which is the
188
- * case a per-run set would miss entirely.
180
+ * Message ids the server has already closed, so a reuse can be reported. Per
181
+ * client rather than per run — the merge happens across runs, which a per-run
182
+ * set would miss entirely.
189
183
  */
190
184
  readonly #closedMessageIds = new Set<string>();
191
185
  readonly #connectionLostMessage: string;
@@ -205,8 +199,8 @@ export class AgUiClient {
205
199
  const onStateChanged = config.onStateChanged;
206
200
  if (onStateChanged !== undefined) {
207
201
  // The agent applies STATE_SNAPSHOT / STATE_DELTA itself; subscribing is
208
- // how we learn the result rather than re-deriving it from the event
209
- // stream. Lives for the agent's lifetime, which is this client's.
202
+ // how the result is learned rather than re-derived from the event stream.
203
+ // Lives for the agent's lifetime, which is this client's.
210
204
  this.#agent.subscribe({
211
205
  onStateChanged: ({ state }) => {
212
206
  onStateChanged(state as Record<string, unknown>);
@@ -243,9 +237,8 @@ export class AgUiClient {
243
237
  * tools (bounded by {@link MAX_TOOL_ROUNDS}).
244
238
  *
245
239
  * `attachments` ride on the user message as a non-standard field so the
246
- * default client store round-trips them for history replay; the agent learns
247
- * the ids from the run context (the server's strict validation ignores the
248
- * unknown message field), then reads bytes via the `read_attachment` tool.
240
+ * default store round-trips them for history replay; see
241
+ * {@link messageAttachments}.
249
242
  */
250
243
  async send(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
251
244
  // Cast at the AG-UI boundary: `attachments` is a web-component augmentation
@@ -263,7 +256,7 @@ export class AgUiClient {
263
256
  /**
264
257
  * Resume the run loop after a navigating tool's result was supplied
265
258
  * post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
266
- * message it simply continues the conversation already in history.
259
+ * message; it continues the conversation already in history.
267
260
  */
268
261
  async resume(): Promise<void> {
269
262
  await this.#run();
@@ -309,21 +302,21 @@ export class AgUiClient {
309
302
  }
310
303
 
311
304
  #onCancelled(): void {
312
- // The truncated exchange (including any partial assistant text the agent
313
- // already applied) survives a reload.
305
+ // Persist so the truncated exchange, partial assistant text included,
306
+ // survives a reload.
314
307
  this.#onPersist(this.#agent.messages);
315
308
  this.#handlers.onCancelled();
316
309
  }
317
310
 
318
311
  async #runLoop(): Promise<void> {
319
- // Carries the resolved approval answers into the *next* run when a round
320
- // finished on a server-side-tool interrupt. Distinct from the public
321
- // resume() navigation-reload path (which continues an unfinished
322
- // frontend-tool round after a page load) — this stays inside one #run().
312
+ // Carries resolved approval answers into the next run when a round finished
313
+ // on a server-side-tool interrupt. Distinct from the public resume(), which
314
+ // continues an unfinished frontend-tool round after a page load; this stays
315
+ // inside one #run().
323
316
  let resume: ResumeEntry[] | undefined;
324
317
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
325
- // A cancel during the previous round's frontend-tool execution lands
326
- // here: the running handler completed, but no further round starts.
318
+ // A cancel during the previous round's tool execution lands here: the
319
+ // running handler completed, but no further round starts.
327
320
  if (this.#cancelled) {
328
321
  return;
329
322
  }
@@ -339,29 +332,28 @@ export class AgUiClient {
339
332
  await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
340
333
  resume = undefined;
341
334
  this.#onPersist(this.#agent.messages);
342
- // Cancelled mid-stream: the user said stop — don't execute the tool
343
- // calls collected before the abort.
335
+ // Cancelled mid-stream: don't execute the tool calls collected before the
336
+ // abort.
344
337
  if (this.#cancelled) {
345
338
  return;
346
339
  }
347
- // The stream resolved without RUN_FINISHED / RUN_ERROR: the transport
348
- // dropped mid-run. Surface it as an error so the UI doesn't rest silently
349
- // with a stuck pending indicator (caught by #run → onError).
340
+ // The stream resolved without RUN_FINISHED / RUN_ERROR, so the transport
341
+ // dropped mid-run. Surface it as an error (via #run) rather than resting
342
+ // silently with a stuck pending indicator.
350
343
  if (!runState.terminal) {
351
344
  throw new ConnectionLostError(this.#connectionLostMessage);
352
345
  }
353
- // RUN_ERROR is terminal: the agent already reported the failure via
354
- // onError. Don't execute the tool calls collected before it or start
355
- // another round — that would run into a broken context and surface a
356
- // confusing second error. Any pending tool card is swept at onSettled.
346
+ // RUN_ERROR is terminal and the agent already reported it via onError.
347
+ // Executing the calls collected before it, or starting another round,
348
+ // would run into a broken context and raise a confusing second error.
349
+ // Pending tool cards are swept at onSettled.
357
350
  if (runState.errored) {
358
351
  return;
359
352
  }
360
- // A gated server-side tool deferred instead of executing: the run finished
361
- // on an interrupt outcome. Ask the host to resolve each interrupt, then
362
- // re-enter the loop carrying the answers the follow-up run runs or denies
363
- // the tool (its result streams back as TOOL_CALL_RESULT). Takes precedence
364
- // over the frontend-tool sweep below: a server-side tool isn't ours to run.
353
+ // A gated server-side tool deferred, so the run finished on an interrupt
354
+ // outcome: resolve each interrupt, then re-enter carrying the answers so
355
+ // the follow-up run executes or denies the tool. Must precede the
356
+ // frontend-tool sweep below a server-side tool isn't ours to run.
365
357
  if (runState.interrupts.length > 0) {
366
358
  if (this.#resolveInterrupts === null) {
367
359
  return;
@@ -410,13 +402,10 @@ export class AgUiClient {
410
402
  h.onRunStart();
411
403
  },
412
404
  onTextMessageStartEvent({ event }) {
413
- // A server that reuses a message id gets its two answers merged into
414
- // one transcript entry, silently, and that merged entry is what gets
415
- // persisted. The protocol has no rule to enforce here and refusing the
416
- // event would be worse than the merge, so this warns and continues —
417
- // but it should not be silent, because the corruption outlives the
418
- // session and reads as a client bug. Found by a demo harness doing
419
- // exactly this.
405
+ // A reused message id merges two answers into one persisted transcript
406
+ // entry. The protocol has no rule to enforce and refusing the event
407
+ // would be worse than the merge, so warn and continue — but do not stay
408
+ // silent, since the corruption outlives the session.
420
409
  if (closed.has(event.messageId)) {
421
410
  console.warn(
422
411
  `<ag-ui-chat>: the server reused message id "${event.messageId}", which was ` +
@@ -448,9 +437,9 @@ export class AgUiClient {
448
437
  onActivitySnapshotEvent({ event }) {
449
438
  h.onActivity(event.activityType, event.content);
450
439
  },
451
- // Reasoning. `@ag-ui/client` already maps the deprecated
452
- // THINKING_* events onto these REASONING_* callbacks, so handling the
453
- // reasoning family alone covers both protocol versions.
440
+ // `@ag-ui/client` maps the deprecated THINKING_* events onto these
441
+ // REASONING_* callbacks, so the reasoning family alone covers both
442
+ // protocol versions.
454
443
  onReasoningStartEvent() {
455
444
  h.onReasoningStart();
456
445
  },
@@ -462,9 +451,9 @@ export class AgUiClient {
462
451
  },
463
452
  onRunFinishedEvent(params) {
464
453
  // RUN_FINISHED is terminal for both a normal finish and an interrupt.
465
- // Capturing the interrupts here (rather than reading the agent's
466
- // `pendingInterrupts` field afterwards) keeps the loop self-contained
467
- // and independent of that field's cross-run clearing semantics.
454
+ // Capture the interrupts here rather than reading the agent's
455
+ // `pendingInterrupts` afterwards, to stay independent of that field's
456
+ // cross-run clearing semantics.
468
457
  runState.terminal = true;
469
458
  if (params.outcome === "interrupt") {
470
459
  runState.interrupts = params.interrupts;
@@ -4,11 +4,9 @@ import type { Message } from "@ag-ui/core";
4
4
  * A durable, lightweight reference to one uploaded file — what an upload
5
5
  * returns and what rides on a sent message, never the bytes.
6
6
  *
7
- * Mirrors django-ag-ui's `AttachmentRef`: a file uploads out-of-band to the
7
+ * Mirrors django-ag-ui's `AttachmentRef`: the file uploads out-of-band to the
8
8
  * attachments endpoint, the server hands back this ref, and the agent reads the
9
- * actual content server-side via the `read_attachment` tool. Keeping the AG-UI
10
- * message stream free of base64 mirrors how the tool catalog keeps schemas off
11
- * the wire.
9
+ * content server-side via the `read_attachment` tool.
12
10
  */
13
11
  export interface AttachmentRef {
14
12
  /** Opaque, owner-scoped handle the server resolves back to bytes. */
@@ -26,17 +24,14 @@ export interface AttachmentRef {
26
24
  /**
27
25
  * The attachment refs a user message carries.
28
26
  *
29
- * Refs are stored on the user message as a non-standard `attachments` field: a
30
- * web-component augmentation that the default client store round-trips and
31
- * `@ag-ui/client` preserves through `addMessage` / `structuredClone`, so a
32
- * restored conversation re-renders its attachment chips. The server's strict
33
- * `RunAgentInput` validation ignores the unknown field — the model learns the
34
- * ids from the run context manifest instead.
27
+ * Refs ride on the message as a non-standard `attachments` field, which the
28
+ * default store round-trips and `@ag-ui/client` preserves through `addMessage`
29
+ * / `structuredClone`, so a restored conversation re-renders its chips. The
30
+ * server's strict `RunAgentInput` validation ignores the unknown field.
35
31
  *
36
- * The persisted array is untrusted (it can be hand-edited, truncated, or
37
- * corrupted in storage), so every entry is validated and malformed ones are
38
- * dropped a `null` or shapeless entry would otherwise throw in `iconFor` and
39
- * abort the whole history replay.
32
+ * Storage is untrusted hand-edited, truncated, or corrupted — so malformed
33
+ * entries are dropped here; a shapeless one would throw in `iconFor` and abort
34
+ * the whole history replay.
40
35
  */
41
36
  export function messageAttachments(message: Message): readonly AttachmentRef[] {
42
37
  const refs = (message as { attachments?: unknown }).attachments;
@@ -13,13 +13,11 @@ export interface NavigationCheckpoint {
13
13
  }
14
14
 
15
15
  /**
16
- * Lightweight metadata for one conversation — the thread-drawer row shape.
17
- *
18
- * Returned by {@link ClientConversationStore.listThreads} so the drawer can
19
- * render a list without loading message bodies. `title` defaults to a
20
- * truncation of the first user message (until an explicit rename); `preview`
21
- * is a one-line excerpt of the latest message; `updatedAt` is epoch ms of the
22
- * last change, used to order the list.
16
+ * Lightweight metadata for one conversation — the thread-drawer row shape, so
17
+ * the drawer renders without loading message bodies. `title` defaults to a
18
+ * truncation of the first user message until an explicit rename, `preview` is a
19
+ * one-line excerpt of the latest message, and `updatedAt` is epoch ms of the
20
+ * last change, which orders the list.
23
21
  */
24
22
  export interface ThreadMeta {
25
23
  readonly threadId: string;
@@ -32,16 +30,14 @@ export interface ThreadMeta {
32
30
  * Client-side persistence seam for the conversation and a pending-navigation
33
31
  * checkpoint, keyed by `thread_id`.
34
32
  *
35
- * The default {@link SessionStorageStore} keeps everything per-tab in
36
- * `sessionStorage`, so the chat survives the full page reloads of a
37
- * multi-page app. A host may inject a server-backed store instead (e.g. one
38
- * that rehydrates from a history endpoint); `loadMessages` and `listThreads`
39
- * are therefore async-friendly. The checkpoint methods stay synchronous — the
40
- * marker is a tiny local hint a server store can derive from history and no-op.
33
+ * The default {@link SessionStorageStore} keeps everything per-tab. A host may
34
+ * inject a server-backed store instead, which is why `loadMessages` and
35
+ * `listThreads` are async. The checkpoint methods stay synchronous: the marker
36
+ * is a small local hint a server store can derive from history and no-op.
41
37
  *
42
- * Thread enumeration (`listThreads` / `setActiveThread` / `renameThread`) backs
43
- * the chat-history drawer; "delete a thread" reuses {@link clear} and "new
44
- * chat" reuses {@link threadId} after clearing the active thread.
38
+ * Thread enumeration backs the chat-history drawer; deleting a thread reuses
39
+ * {@link clear} and "new chat" reuses {@link threadId} after clearing the
40
+ * active thread.
45
41
  */
46
42
  export interface ClientConversationStore {
47
43
  /** The active conversation id, generated and persisted on first read. */
@@ -86,18 +82,15 @@ interface StoredThread {
86
82
  /**
87
83
  * Default {@link ClientConversationStore}: per-tab `sessionStorage`.
88
84
  *
89
- * Survives full page reloads and same-tab navigation, clears on tab close
90
- * the right scope for an embedded agent's conversation in a multi-page app.
91
- * Tracks multiple threads per tab: the active id lives under one key, the
92
- * message history / checkpoint are namespaced by id, and a small index feeds
93
- * the drawer so it works with no server.
85
+ * Survives full page reloads and same-tab navigation, clears on tab close.
86
+ * Tracks multiple threads per tab: the active id under one key, message history
87
+ * and checkpoint namespaced by id, and a small index feeding the drawer with no
88
+ * server involved.
94
89
  *
95
- * An optional `namespace` scopes every key to one element (its `id`, else its
96
- * endpoint), so two `<ag-ui-chat>` instances — or two apps — on the same origin
97
- * keep separate active-thread pointers and drawer indexes instead of clobbering
98
- * each other. Constructing with a namespace migrates any pre-namespacing
99
- * (`ag-ui-chat:*`) keys into it once, so an existing conversation survives the
100
- * upgrade; the default empty namespace keeps the legacy origin-global keys.
90
+ * An optional `namespace` scopes every key to one element, so two
91
+ * `<ag-ui-chat>` instances on the same origin keep separate active-thread
92
+ * pointers and drawer indexes instead of clobbering each other. The default
93
+ * empty namespace keeps the origin-global keys; see {@link #migrateLegacyKeys}.
101
94
  */
102
95
  export class SessionStorageStore implements ClientConversationStore {
103
96
  readonly #root: string;
@@ -219,12 +212,11 @@ export class SessionStorageStore implements ClientConversationStore {
219
212
  }
220
213
 
221
214
  /**
222
- * One-time move of pre-namespacing (`ag-ui-chat:*`) keys into this instance's
223
- * namespace, so an existing conversation isn't orphaned by the upgrade. Only
224
- * this store's own keys move (thread pointer, drawer index, per-thread
225
- * messages/checkpoints) the element's `collapsed`/`theme` keys are left
226
- * alone. The first namespaced instance to mount adopts the legacy data; a
227
- * second namespace finds it gone and starts fresh.
215
+ * One-time move of un-namespaced `ag-ui-chat:*` keys into this instance's
216
+ * namespace, so an existing conversation isn't orphaned. Only this store's own
217
+ * keys move — the element's `collapsed` / `theme` keys are left alone. The
218
+ * first namespaced instance to mount adopts the data; a second namespace
219
+ * finds it gone and starts fresh.
228
220
  */
229
221
  #migrateLegacyKeys(): void {
230
222
  const legacyRoot = `${KEY_ROOT}:`;
@@ -8,20 +8,18 @@ export interface HttpAgentOptions {
8
8
  headers?: Record<string, string>;
9
9
  /**
10
10
  * Cookie policy for the run request, as `fetch`'s own `credentials` mode.
11
- * Unset leaves the browser default (`same-origin`) alone — which sends **no**
12
- * cookies when the agent endpoint is on a different origin (or a different
13
- * subdomain) from the page. A cookie-authenticated cross-origin deployment
14
- * needs `"include"` here, and a server that answers it with
15
- * `Access-Control-Allow-Credentials: true` and a concrete origin.
11
+ * Unset leaves the browser default (`same-origin`), which sends no cookies
12
+ * to an agent endpoint on a different origin or subdomain. A
13
+ * cookie-authenticated cross-origin deployment needs `"include"` here plus a
14
+ * server answering with `Access-Control-Allow-Credentials: true` and a
15
+ * concrete origin.
16
16
  */
17
17
  credentials?: RequestCredentials;
18
18
  /**
19
- * Live header source, re-read on **every** request. `HttpAgent` bakes the
20
- * static `headers` into its constructor and the element caches the agent
21
- * for the whole conversation so a rotated token (CSRF, short-lived JWT)
22
- * would otherwise never reach the agent endpoint and a long session 401s
23
- * mid-conversation. When set, the fetch wrapper overlays these values on
24
- * each call; `headers` still seeds the initial/static configuration.
19
+ * Live header source, re-read on every request, overlaid on `headers`.
20
+ * `HttpAgent` bakes static `headers` in at construction and the element
21
+ * caches the agent for the whole conversation, so rotating credentials (CSRF,
22
+ * short-lived JWT) must come through here.
25
23
  */
26
24
  getHeaders?: () => Record<string, string>;
27
25
  /** Stable conversation id, so the agent's runs share a thread. */
@@ -37,11 +35,11 @@ export interface HttpAgentOptions {
37
35
  }
38
36
 
39
37
  /**
40
- * Build an AG-UI {@link HttpAgent} pointed at ``endpoint``.
38
+ * Build an AG-UI {@link HttpAgent} pointed at `endpoint`.
41
39
  *
42
- * This is the default agent factory used by ``<ag-ui-chat>``. Tests and
43
- * advanced hosts override the element's ``agentFactory`` to inject a
44
- * different {@link AbstractAgent} (e.g. a fake, or a middleware-wrapped one).
40
+ * The default agent factory for `<ag-ui-chat>`; tests and advanced hosts
41
+ * override the element's `agentFactory` to inject a different
42
+ * {@link AbstractAgent}.
45
43
  */
46
44
  export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
47
45
  return new HttpAgent({
@@ -49,12 +47,10 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
49
47
  headers: options.headers ?? {},
50
48
  initialState: { ...(options.initialState ?? {}) },
51
49
  // HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
52
- // which would rebind the global `fetch` to the agent instance and trigger
53
- // "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
54
- // a free function with the correct receiver. The wrapper also overlays
55
- // `getHeaders()` per request, so header rotation (CSRF, short-lived JWT)
56
- // reaches the stream even though the agent instance is cached, and applies
57
- // the configured cookie policy — the agent's own config has no seam for it.
50
+ // rebinding the global `fetch` to the agent instance "Illegal invocation"
51
+ // in browsers. The wrapper keeps `fetch` a free call, and is also where the
52
+ // per-request `getHeaders()` overlay and the cookie policy go, the agent's
53
+ // own config having no seam for either.
58
54
  fetch: (url, init) => {
59
55
  const fresh = options.getHeaders?.();
60
56
  if (fresh === undefined) {
@@ -67,7 +63,7 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
67
63
  return fetch(url, withCredentials({ ...init, headers }, options.credentials));
68
64
  },
69
65
  // Spread conditionally: under `exactOptionalPropertyTypes` an explicit
70
- // `undefined` is not assignable to these optional config fields.
66
+ // `undefined` is not assignable to an optional field.
71
67
  ...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
72
68
  ...(options.initialMessages !== undefined
73
69
  ? { initialMessages: [...options.initialMessages] }
@@ -19,10 +19,9 @@ interface ServerThreadRow {
19
19
  type HeadersProvider = () => Record<string, string>;
20
20
 
21
21
  /**
22
- * Live cookie policy, read per request. A provider rather than a value because
23
- * the store is built once (on connect) and kept, while a host may configure the
24
- * element after inserting it a captured value would pin whatever was set
25
- * during that first frame.
22
+ * Live cookie policy, read per request. A provider rather than a value: the
23
+ * store is built once on connect, but a host may configure the element after
24
+ * inserting it, and a captured value would pin that first frame's setting.
26
25
  */
27
26
  type CredentialsProvider = () => RequestCredentials | undefined;
28
27
 
@@ -36,11 +35,11 @@ type CredentialsProvider = () => RequestCredentials | undefined;
36
35
  * - `PATCH <url><id>/` → rename (`{ "title": … }`);
37
36
  * - `DELETE <url><id>/` → delete.
38
37
  *
39
- * It wraps a local store (default {@link SessionStorageStore}) for the
40
- * client-only concerns — the active thread id, the navigation checkpoint, and a
41
- * message cache — and as the graceful fallback when a request fails. Rename and
42
- * delete apply **optimistically** (a small local overlay) so the drawer
43
- * reflects them at once, before the fire-and-forget server round-trip lands.
38
+ * Wraps a local store (default {@link SessionStorageStore}) for the client-only
39
+ * concerns — active thread id, navigation checkpoint, message cache — and as
40
+ * the fallback when a request fails. Rename and delete apply optimistically via
41
+ * a local overlay, so the drawer reflects them before the fire-and-forget
42
+ * round-trip lands.
44
43
  */
45
44
  export class RemoteConversationStore implements ClientConversationStore {
46
45
  readonly #url: string;
@@ -108,9 +107,9 @@ export class RemoteConversationStore implements ClientConversationStore {
108
107
  if (response === null || !response.ok) {
109
108
  return this.#local.loadMessages(threadId);
110
109
  }
111
- // A 200 whose body isn't the expected JSON (a proxy's HTML error page, a
112
- // truncated stream) must not throw an unhandled rejection that the caller's
113
- // `void #rehydrate()` swallows fall back to the local cache instead.
110
+ // A 200 whose body isn't JSON (a proxy's HTML error page, a truncated
111
+ // stream) must not throw an unhandled rejection that the caller's
112
+ // `void #rehydrate()` swallows; fall back to the local cache.
114
113
  const body = await this.#readJson<{ messages?: readonly Message[] }>(response);
115
114
  if (body === null) {
116
115
  return this.#local.loadMessages(threadId);
@@ -143,9 +142,9 @@ export class RemoteConversationStore implements ClientConversationStore {
143
142
  return {
144
143
  threadId: row.thread_id,
145
144
  title: this.#renamed.get(row.thread_id) ?? row.title,
146
- // `null` or an unparseable date both become `NaN` (Date.parse's own
147
- // signal), which `relativeTime` renders as a neutral label rather than
148
- // "~2950w ago" (epoch 0) or "NaNw ago".
145
+ // `null` and an unparseable date both become NaN, Date.parse's own
146
+ // signal, which `relativeTime` renders as a neutral label rather than an
147
+ // epoch-0 or NaN duration.
149
148
  updatedAt: row.updated_at === null ? Number.NaN : Date.parse(row.updated_at),
150
149
  preview: row.preview,
151
150
  };
@@ -13,12 +13,7 @@ export interface RunRow {
13
13
  /** Live header source, read per request so rotated tokens / CSRF reach the server. */
14
14
  type HeadersProvider = () => Record<string, string>;
15
15
 
16
- /**
17
- * Live cookie policy, read per request. A provider rather than a value because
18
- * the index is built once (on connect) and kept, while a host may configure the
19
- * element after inserting it — a captured value would pin whatever was set
20
- * during that first frame.
21
- */
16
+ /** Live cookie policy, read per request; see `RemoteConversationStore`'s note. */
22
17
  type CredentialsProvider = () => RequestCredentials | undefined;
23
18
 
24
19
  /**
@@ -29,17 +24,14 @@ type CredentialsProvider = () => RequestCredentials | undefined;
29
24
  *
30
25
  * - `GET <url>` → the user's runs, newest first.
31
26
  *
32
- * **Only `continuable` rows can be resumed.** The server reports whether a run
33
- * has a saved snapshot to seed from; a run that never reached a provider-valid
34
- * boundary has none, so resuming it would start from nothing. Callers should
35
- * offer the action only for those rows and treat the rest as informational — a
36
- * crashed run worth showing, not worth continuing. {@link continuable} filters
37
- * for exactly that.
27
+ * Only `continuable` rows can be resumed: the server reports whether a run has
28
+ * a saved snapshot to seed from, and a run that never reached a provider-valid
29
+ * boundary has none. Offer the action only for those rows ({@link continuable}
30
+ * filters them) and treat the rest as informational.
38
31
  *
39
- * **Resume and fork are siblings of the index**, not separate configuration.
40
- * django-ag-ui mounts all three under one prefix whenever a step store is
41
- * configured (`runs/`, `resume/<id>/`, `fork/<id>/`), so one URL locates them
42
- * all and there is no way to configure a half-working set.
32
+ * Resume and fork are siblings of the index rather than separate configuration
33
+ * django-ag-ui mounts `runs/`, `resume/<id>/` and `fork/<id>/` under one
34
+ * prefix so this one URL locates all three.
43
35
  */
44
36
  export class RunIndex {
45
37
  readonly #url: string;
@@ -57,10 +49,9 @@ export class RunIndex {
57
49
  }
58
50
 
59
51
  /**
60
- * The user's runs, or `[]` when the endpoint is unreachable or answers with
61
- * an error. A history affordance that cannot load is empty, never broken:
62
- * the caller renders its empty state rather than surfacing a transport fault
63
- * the user can do nothing about.
52
+ * The user's runs, or `[]` when the endpoint is unreachable or errors — the
53
+ * caller renders its empty state rather than a transport fault the user can
54
+ * do nothing about.
64
55
  */
65
56
  async list(): Promise<readonly RunRow[]> {
66
57
  try {
@@ -102,9 +93,9 @@ export class RunIndex {
102
93
  /**
103
94
  * `<mount>/<verb>/<runId>/`, derived from the index URL's own prefix.
104
95
  *
105
- * Built by string surgery on the trailing `runs/` rather than with `new URL`,
106
- * because the configured value may be root-relative (`/agent/runs/`) the
107
- * common case in a Django template and `new URL` needs an absolute base.
96
+ * String surgery on the trailing `runs/` rather than `new URL`, because the
97
+ * configured value may be root-relative (`/agent/runs/`), the common case in
98
+ * a Django template, and `new URL` needs an absolute base.
108
99
  */
109
100
  #sibling(verb: string, runId: string): string {
110
101
  const prefix = this.#url.slice(0, -"runs/".length);
@@ -2,10 +2,10 @@ import { withCredentials } from "./utils.js";
2
2
 
3
3
  /**
4
4
  * The composer's voice-transcription contract: take a recorded audio `Blob` and
5
- * resolve to the transcript text. The built-in handler is {@link transcribeAudio}
6
- * (multipart POST to `data-transcribe-url`); a host swaps in its own e.g. a
7
- * browser Web Speech adapter or a direct-to-provider call via
8
- * `AgUiChat.transcribeHandler`, without touching the mic button.
5
+ * resolve to the transcript text. The built-in handler is
6
+ * {@link transcribeAudio}; a host swaps in its own (a Web Speech adapter, a
7
+ * direct-to-provider call) via `AgUiChat.transcribeHandler` without touching
8
+ * the mic button.
9
9
  */
10
10
  export type TranscribeHandler = (audio: Blob) => Promise<string>;
11
11
 
@@ -27,15 +27,14 @@ export interface TranscribeOptions {
27
27
  /**
28
28
  * POST a recorded clip to the transcription endpoint and resolve to its text.
29
29
  *
30
- * The clip is sent as multipart under the `audio` field with the element's
31
- * `headers`, mirroring {@link uploadAttachment}; the server replies
32
- * `{ "text": "<transcript>" }`. A non-2xx response or a network error rejects so
33
- * the mic button can surface the failure.
30
+ * The clip goes as multipart under the `audio` field with the element's
31
+ * `headers`, mirroring {@link uploadAttachment}, and the server replies
32
+ * `{ "text": "<transcript>" }`. A non-2xx response or a network error rejects
33
+ * so the mic button can surface the failure.
34
34
  */
35
35
  export async function transcribeAudio(audio: Blob, options: TranscribeOptions): Promise<string> {
36
36
  const form = new FormData();
37
- // A filename hints the server/codec; the extension is cosmetic (the server
38
- // reads the blob's content type).
37
+ // The extension is cosmetic: the server reads the blob's content type.
39
38
  form.append("audio", audio, "recording.webm");
40
39
 
41
40
  const response = await fetch(