@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
@@ -119,14 +119,13 @@ export interface UnreadDetail {
119
119
  * Attributes read once while connecting, to decide what chrome exists at all.
120
120
  *
121
121
  * Changing one afterwards is silently ignored: the tray, the mic, the skills
122
- * menu and the header icon are built (or not) during connect, and no later read
123
- * revisits the decision. Observed only so the element can say so — see
124
- * `attributeChangedCallback`.
122
+ * menu and the header icon are built during connect and no later read revisits
123
+ * the decision. Observed only so `attributeChangedCallback` can warn.
125
124
  *
126
- * Deliberately excludes the attributes that *are* re-read per use, where a late
127
- * change works and a warning would be wrong: `data-runs-url`,
128
- * `data-page-actions`, `data-text-animation`, `data-tool-display`, `endpoint`,
129
- * and the CSS-reactive `theme` / `collapsed`.
125
+ * Excludes the attributes that are re-read per use, where a late change works
126
+ * and a warning would be wrong: `data-runs-url`, `data-page-actions`,
127
+ * `data-text-animation`, `data-tool-display`, `endpoint`, and the CSS-reactive
128
+ * `theme` / `collapsed`.
130
129
  */
131
130
  const CONNECT_TIME_ATTRIBUTES = [
132
131
  "data-attachments-url",
@@ -169,43 +168,36 @@ const THEME_KEY = "ag-ui-chat:theme";
169
168
  * `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
170
169
  *
171
170
  * Owns the Shadow DOM shell (header, scrolling message list, input row),
172
- * builds an {@link AgUiClient} on first send (via the overridable
173
- * {@link agentFactory}), and renders streaming assistant text plus tool-call
171
+ * builds an {@link AgUiClient} on first send via the overridable
172
+ * {@link agentFactory}, and renders streaming assistant text plus tool-call
174
173
  * activity. Emits a {@link SUBMIT_EVENT} for host visibility.
175
174
  *
176
- * The per-run frontend tool catalog and context are supplied by
177
- * {@link getTools} / {@link getContext}, which later phases (the tool
178
- * registry, DOM driver) populate.
175
+ * The per-run frontend tool catalog and context come from {@link getTools} and
176
+ * {@link getContext}.
179
177
  */
180
178
  export class AgUiChat extends HTMLElement {
181
179
  /** Agent factory; override to inject a custom or fake agent (tests). */
182
180
  agentFactory: AgentFactory = createHttpAgent;
183
181
 
184
182
  /**
185
- * Static extra HTTP headers, sent with **every** request this element makes
186
- * the agent run, the thread index and its messages, the tool and skill
187
- * catalogs, the run index, uploads and transcription.
183
+ * Static extra HTTP headers, sent with every request this element makes: the
184
+ * agent run, the thread index and its messages, the tool and skill catalogs,
185
+ * the run index, uploads and transcription.
188
186
  *
189
- * Right for values fixed for the element's lifetime. A credential that
190
- * rotates (a short-lived JWT, a re-issued CSRF token) belongs in
191
- * {@link getHeaders} instead: this is read at request time, but only a
192
- * re-assignment updates it, so a token captured here is pinned until the host
193
- * remembers to assign again.
187
+ * For values fixed for the element's lifetime. A rotating credential belongs
188
+ * in {@link getHeaders} instead — only a re-assignment updates this, so a
189
+ * token captured here is pinned until the host assigns again.
194
190
  */
195
191
  headers: Record<string, string> = {};
196
192
 
197
193
  /**
198
- * Live header source, consulted immediately before every request — the way to
199
- * supply rotating credentials.
194
+ * Live header source, called afresh immediately before every request — the
195
+ * way to supply rotating credentials, with nothing to re-assign or keep in
196
+ * sync.
200
197
  *
201
- * Set it to a function and each request calls it afresh: a token refreshed by
202
- * the host between two requests reaches the second one, with nothing to
203
- * re-assign and nothing to keep in sync.
204
- *
205
- * Composes with {@link headers} rather than replacing it: the two are merged
206
- * per key with `getHeaders()` winning, so a static `X-Client` and a rotating
207
- * `Authorization` can be configured independently and neither silently drops
208
- * the other.
198
+ * Composes with {@link headers} rather than replacing it: merged per key with
199
+ * `getHeaders()` winning, so a static `X-Client` and a rotating
200
+ * `Authorization` are configured independently and neither drops the other.
209
201
  */
210
202
  getHeaders: (() => Record<string, string>) | null = null;
211
203
 
@@ -222,40 +214,34 @@ export class AgUiChat extends HTMLElement {
222
214
 
223
215
  /**
224
216
  * When true, the built-in `ask_user` frontend tool is offered to the agent:
225
- * calling it renders an inline question card (radio choices and/or a free-text
226
- * field) and returns the user's answer. Off by default — like the other
227
- * built-in tool groups (route / page-action), it is opt-in so it doesn't
228
- * change the advertised catalog until a host asks for it.
217
+ * calling it renders an inline question card and returns the user's answer.
218
+ * Off by default, like the other built-in tool groups, so the advertised
219
+ * catalog does not change until a host asks for it.
229
220
  */
230
221
  askUser = false;
231
222
 
232
223
  /**
233
- * Optional full replacement for the `ask_user` question UI. When set, calling
234
- * `ask_user` invokes this instead of the built-in inline card: the host
235
- * renders whatever it likes (a native modal, a framework component, …) and
236
- * resolves with the answer. Unset (default) uses the built-in
237
- * {@link requestQuestion} card — style that via the `strings` override and the
238
- * `question*` CSS `::part()`s. Requires {@link askUser} to be enabled.
224
+ * Optional full replacement for the `ask_user` question UI, resolving with
225
+ * the answer; the same seam as {@link approvalRenderer}, styled via `strings`
226
+ * and the `question*` `::part()`s when left unset. Requires {@link askUser}.
239
227
  */
240
228
  askUserRenderer: QuestionRenderer | null = null;
241
229
 
242
230
  /**
243
- * Optional full replacement for the server-side-tool approval UI. When set, an
244
- * approval interrupt invokes this instead of the built-in inline approval
245
- * card: the host renders whatever it likes and resolves `true` to approve /
246
- * `false` to deny. Unset (default) uses the built-in {@link requestApproval}
247
- * card style that via the `strings` override and the `approval*` CSS
248
- * `::part()`s. The gate itself is enabled server-side; this only changes how
249
- * the decision is collected.
231
+ * Optional full replacement for the server-side-tool approval UI: an approval
232
+ * interrupt invokes this instead of the built-in {@link requestApproval}
233
+ * card, resolving `true` to approve or `false` to deny. Style the built-in
234
+ * card via `strings` and the `approval*` `::part()`s instead. The gate itself
235
+ * is enabled server-side; this only changes how the decision is collected.
250
236
  */
251
237
  approvalRenderer: ApprovalRenderer | null = null;
252
238
 
253
239
  /**
254
- * Optional per-call confirmation predicate. When set, it is authoritative:
255
- * given a tool name + args it decides whether *this* call needs confirmation
256
- * (so one tool can be instant for some args and confirmed for others — what a
257
- * static `x-destructive` flag can't express). When unset, the `x-destructive`
258
- * schema flag is used. `autoConfirm` short-circuits both.
240
+ * Optional per-call confirmation predicate. When set it is authoritative,
241
+ * deciding from the tool name and args whether this particular call needs
242
+ * confirmation — so one tool can be instant for some args and confirmed for
243
+ * others, which a static `x-destructive` flag cannot express. When unset the
244
+ * `x-destructive` flag decides. `autoConfirm` short-circuits both.
259
245
  */
260
246
  confirmPredicate:
261
247
  | ((toolName: string, args: Record<string, unknown>) => boolean | Promise<boolean>)
@@ -277,14 +263,14 @@ export class AgUiChat extends HTMLElement {
277
263
  ];
278
264
 
279
265
  /**
280
- * Per-run context provider. Defaults to the compact page map (when a
281
- * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
282
- * plus a one-line manifest of the files attached to the message being sent,
283
- * so the agent knows which `read_attachment` ids are available.
266
+ * Per-run context provider. Defaults to the compact page map, when a
267
+ * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on.
268
+ *
269
+ * Attachments are deliberately not restated here: the server derives its own
270
+ * manifest from the refs riding the messages.
284
271
  */
285
272
  getContext: () => Context[] = () => [
286
273
  ...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
287
- ...this.#attachmentContext(),
288
274
  ];
289
275
 
290
276
  /**
@@ -315,22 +301,18 @@ export class AgUiChat extends HTMLElement {
315
301
 
316
302
  /**
317
303
  * How attached files are uploaded. `null` (default) uses the built-in
318
- * multipart `POST` to `data-attachments-url`. Set a custom
319
- * {@link UploadHandler} `(file, onProgress) => Promise<AttachmentRef>` to
320
- * swap the transport (e.g. a `tus-js-client` resumable adapter or
321
- * direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
322
- * wire (refs are transport-agnostic). When set, the 📎 affordance appears even
323
- * with no `data-attachments-url`; the handler owns its own endpoint + headers.
304
+ * multipart `POST` to `data-attachments-url`; a custom {@link UploadHandler}
305
+ * swaps the transport without changing the tray, the chips, or the AG-UI
306
+ * wire. When set, the 📎 affordance appears even with no
307
+ * `data-attachments-url`, and the handler owns its own endpoint and headers.
324
308
  */
325
309
  uploadHandler: UploadHandler | null = null;
326
310
 
327
311
  /**
328
312
  * How recorded voice clips are transcribed. `null` (default) POSTs the clip
329
- * to `data-transcribe-url` (django-ag-ui's `TranscribeView`). Set a custom
330
- * {@link TranscribeHandler} `(audio: Blob) => Promise<string>` to swap the
331
- * transport (a different STT endpoint, a browser Web Speech adapter) without
332
- * touching the mic button. When set, the 🎤 affordance appears even with no
333
- * `data-transcribe-url`.
313
+ * to `data-transcribe-url`; a custom {@link TranscribeHandler} swaps the
314
+ * transport without touching the mic button. When set, the 🎤 affordance
315
+ * appears even with no `data-transcribe-url`.
334
316
  */
335
317
  transcribeHandler: TranscribeHandler | null = null;
336
318
 
@@ -352,12 +334,10 @@ export class AgUiChat extends HTMLElement {
352
334
  skillContext: () => Record<string, unknown> = () => ({});
353
335
 
354
336
  /**
355
- * Friendly display labels for tool-call cards, keyed by tool name (e.g.
356
- * `{ list_projects: "Search projects" }`). Used as a fallback when a tool has
357
- * no `x-summary` in its own schema chiefly **server-side tools** (drf-mcp,
358
- * `@tool` registry), whose schema never reaches the browser (AG-UI streams
359
- * only the tool-call name). Client tools should prefer `x-summary` on their
360
- * schema; this map is the seam for everything else.
337
+ * Friendly display labels for tool-call cards, keyed by tool name. The
338
+ * fallback when a tool has no `x-summary` in its own schema, which chiefly
339
+ * means server-side tools: AG-UI streams only the tool-call name, so their
340
+ * schema never reaches the browser. Client tools should prefer `x-summary`.
361
341
  */
362
342
  toolSummaries: Record<string, string> = {};
363
343
 
@@ -431,9 +411,6 @@ export class AgUiChat extends HTMLElement {
431
411
  /** Whether the element is currently in the DOM; gates the connect-time warning. */
432
412
  #connected = false;
433
413
 
434
- /** Refs attached to the message currently being sent (the context manifest). */
435
- #runAttachments: readonly AttachmentRef[] = [];
436
-
437
414
  #client: AgUiClient | null = null;
438
415
  // Seed for the next client. Once one exists it owns the live value (the
439
416
  // agent applies STATE_SNAPSHOT / STATE_DELTA into it), so this is only the
@@ -540,13 +517,10 @@ export class AgUiChat extends HTMLElement {
540
517
  * Continue `runId` as a **new** run, seeded server-side from its snapshot.
541
518
  *
542
519
  * Uses a short-lived agent pointed at the resume / fork endpoint and seeded
543
- * with **no** history, so the request carries only the turn typed here the
544
- * contract those endpoints assume, since the server supplies the prior turns
545
- * from the snapshot and re-sending them would duplicate. Building a separate
546
- * agent makes that structural: the main agent keeps its own history, and
547
- * "only the new turn" can't be got wrong by forgetting to clear it. The
548
- * fresh `run_id` the endpoints also require comes free — a new agent mints
549
- * one per run.
520
+ * with no history, because those endpoints supply the prior turns from the
521
+ * snapshot and re-sending them would duplicate. A separate agent makes that
522
+ * structural the main agent keeps its own history and mints the fresh
523
+ * `run_id` the endpoints also require.
550
524
  *
551
525
  * Handlers are the element's own, so the continuation streams into the same
552
526
  * transcript the user is looking at.
@@ -627,12 +601,11 @@ export class AgUiChat extends HTMLElement {
627
601
  return;
628
602
  }
629
603
  // Everything else here is read once, in connectedCallback, to build chrome
630
- // that then exists (or does not). Setting it afterwards is silently
631
- // ignored, and the symptom is an affordance that simply never appears
632
- // which reads as a broken component rather than a mis-timed assignment.
633
- // This is the common React/Vue shape: the element mounts on the first
634
- // render pass and the framework patches attributes in on the next one.
635
- // Observed purely so this can be said out loud.
604
+ // that then exists or does not. A later change is silently ignored and the
605
+ // symptom is an affordance that never appears, which reads as a broken
606
+ // component rather than a mis-timed assignment — the common React/Vue shape,
607
+ // where the element mounts on one render pass and attributes are patched in
608
+ // on the next. Observed purely so this can be said out loud.
636
609
  if (previous === value || !this.#connected) {
637
610
  return;
638
611
  }
@@ -657,10 +630,8 @@ export class AgUiChat extends HTMLElement {
657
630
  * when the server streams `STATE_SNAPSHOT` / `STATE_DELTA`. Assigning seeds
658
631
  * the next run; reading returns whatever the agent last applied.
659
632
  *
660
- * Listen for {@link STATE_EVENT} to react to server-driven changes.
661
- *
662
- * Not to be confused with {@link registerPageState}, which exposes host
663
- * state to the agent as ordinary *tools*.
633
+ * Listen for {@link STATE_EVENT} to react to server-driven changes. Distinct
634
+ * from {@link registerPageState}, which exposes host state as ordinary tools.
664
635
  */
665
636
  get sharedState(): Readonly<Record<string, unknown>> {
666
637
  return this.#client?.state ?? this.#sharedState;
@@ -681,11 +652,9 @@ export class AgUiChat extends HTMLElement {
681
652
  }
682
653
 
683
654
  /**
684
- * @deprecated Renamed to {@link registerPageState}. The old name read as
685
- * AG-UI shared-state sync (`STATE_SNAPSHOT` / `STATE_DELTA`), which this
686
- * component does not implement — these are ordinary client tools over host
687
- * page state. Behaviour is unchanged; this alias will be removed in a future
688
- * major.
655
+ * @deprecated Renamed to {@link registerPageState} the old name read as
656
+ * AG-UI shared-state sync. Behaviour is unchanged; the alias will be removed
657
+ * in a future major.
689
658
  */
690
659
  registerStateHook(binding: PageState): void {
691
660
  this.registerPageState(binding);
@@ -762,10 +731,9 @@ export class AgUiChat extends HTMLElement {
762
731
  /**
763
732
  * The built-in `ask_user` frontend tool, or `[]` when {@link askUser} is off.
764
733
  *
765
- * A generic "ask the user a typed question" primitive: the agent calls it, the
766
- * client executes it locally via the normal frontend-tool path (rendering a
767
- * {@link requestQuestion} card), and the chosen/typed answer flows back as the
768
- * tool result — no new protocol, reusing the machinery already in place.
734
+ * The agent calls it, the client executes it locally through the normal
735
+ * frontend-tool path by rendering a {@link requestQuestion} card, and the
736
+ * answer flows back as the tool result. No new protocol.
769
737
  */
770
738
  #askUserTool(): ClientTool[] {
771
739
  if (!this.askUser) {
@@ -855,19 +823,18 @@ export class AgUiChat extends HTMLElement {
855
823
  * `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
856
824
  * the `credentials` attribute, so markup embeds can set it without script.
857
825
  *
858
- * `null` (the default) leaves the browser's default of `same-origin` in
859
- * place. That default sends **no cookies at all** when the endpoints live on
860
- * a different origin from the page app.example.com calling
861
- * api.example.com is cross-origin and the request goes out anonymously
862
- * rather than failing, so the symptom is a 401 from a server that looks
863
- * correctly configured. A cookie-authenticated cross-origin deployment wants
864
- * `"include"`, plus `Access-Control-Allow-Credentials: true` and a concrete
865
- * (non-wildcard) `Access-Control-Allow-Origin` on the server.
826
+ * `null` (the default) leaves the browser's `same-origin` default in place,
827
+ * which sends no cookies at all to an endpoint on a different origin — and
828
+ * the request goes out anonymously rather than failing, so the symptom is a
829
+ * 401 from a server that looks correctly configured. A cookie-authenticated
830
+ * cross-origin deployment wants `"include"`, plus
831
+ * `Access-Control-Allow-Credentials: true` and a concrete, non-wildcard
832
+ * `Access-Control-Allow-Origin` on the server.
866
833
  *
867
834
  * Read per request, so a late assignment applies to everything after it.
868
- * `"omit"` cannot be honoured by the built-in **upload** transport, which is
869
- * an `XMLHttpRequest` and only has a two-state cookie switch; every other
870
- * endpoint honours all three modes.
835
+ * `"omit"` cannot be honoured by the built-in upload transport, an
836
+ * `XMLHttpRequest` with only a two-state cookie switch; every other endpoint
837
+ * honours all three modes.
871
838
  */
872
839
  get credentials(): RequestCredentials | null {
873
840
  const attr = this.getAttribute("credentials");
@@ -1004,25 +971,21 @@ export class AgUiChat extends HTMLElement {
1004
971
  * The catalog requests the element issues on startup: the tool labels
1005
972
  * (`data-tools-url`) and the backend skills (`data-skills-url`).
1006
973
  *
1007
- * Deliberately one microtask behind `connectedCallback`. A host that
1008
- * configures the element through a framework ref necessarily does so *after*
1009
- * inserting the node React attaches refs and runs layout effects in the
1010
- * same commit as the insertion, but strictly afterwards so a request issued
1011
- * from `connectedCallback` itself goes out before `headers`,
1012
- * {@link getHeaders} or {@link credentials} exist, and comes back 401 in a
1013
- * way that reads as a server fault rather than a mis-timed assignment. A
1014
- * microtask lands after that commit and still before paint.
974
+ * Deliberately one microtask behind `connectedCallback`. A framework ref is
975
+ * attached after the node is inserted but within the same commit, so a
976
+ * request issued from `connectedCallback` itself goes out before `headers`,
977
+ * {@link getHeaders} or {@link credentials} exist and 401s in a way that
978
+ * reads as a server fault. A microtask lands after that commit, still before
979
+ * paint.
980
+ *
981
+ * It is not a fix for configuration arriving later than the commit (a passive
982
+ * effect, an awaited token fetch): configure before insertion, or call
983
+ * {@link reload}, since a longer timer would hide that race rather than close
984
+ * it.
1015
985
  *
1016
- * Two things it is **not**. It is not a fix for configuration that arrives
1017
- * later than the commit (a passive `useEffect`, an awaited token fetch):
1018
- * configure before insertion (`createElement` configure `append`) or call
1019
- * {@link reload} once configured, because a longer timer would hide that race
1020
- * rather than close it. And it deliberately excludes the *history* replay,
1021
- * which stays in `connectedCallback`: the replay renders into the transcript,
1022
- * so deferring it lets a `sendMessage()` issued in the same task land first
1023
- * and the replay then duplicate it. The thread history is therefore the one
1024
- * request that can still go out before a ref is attached — {@link reload}
1025
- * covers it.
986
+ * The history replay stays in `connectedCallback` on purpose. It renders into
987
+ * the transcript, so deferring it would let a `sendMessage()` in the same
988
+ * task land first and be duplicated by the replay.
1026
989
  */
1027
990
  #startup(): void {
1028
991
  // An element can be inserted and removed inside one task (a discarded
@@ -1040,15 +1003,13 @@ export class AgUiChat extends HTMLElement {
1040
1003
  * the backend skill catalog and the thread's history — with the transport
1041
1004
  * configuration as it stands now.
1042
1005
  *
1043
- * This is the answer for a host that can only configure the element after the
1044
- * fact (a token fetched in a passive effect, an async auth handshake): the
1045
- * startup requests already went out with whatever was set then, and this says
1046
- * "try again, properly authenticated" without removing and re-inserting the
1047
- * node.
1006
+ * For a host that can only configure the element after the fact (a token
1007
+ * fetched in a passive effect, an async auth handshake), this re-issues the
1008
+ * startup requests authenticated, without removing and re-inserting the node.
1048
1009
  *
1049
- * A reload, not a merge the in-flight run is cancelled and the transcript
1050
- * is rebuilt from the persisted history, so anything streamed since is
1051
- * dropped. Call it once, when configuration lands; not between turns.
1010
+ * A reload, not a merge: the in-flight run is cancelled and the transcript is
1011
+ * rebuilt from persisted history, so anything streamed since is dropped. Call
1012
+ * it once, when configuration lands, not between turns.
1052
1013
  */
1053
1014
  async reload(): Promise<void> {
1054
1015
  this.#cancelRun();
@@ -1058,12 +1019,10 @@ export class AgUiChat extends HTMLElement {
1058
1019
  }
1059
1020
 
1060
1021
  /**
1061
- * Tear down live resources when the element leaves the DOM (a removed node, a
1062
- * client-side route swap): cancel the in-flight run so its SSE stream closes,
1063
- * abort any in-flight uploads so they don't orphan server-side files, and
1064
- * release the mic so the browser's recording indicator clears. Without this a
1065
- * removed `<ag-ui-chat>` leaks a streaming request, uploads, and a live
1066
- * `MediaRecorder`.
1022
+ * Tear down live resources when the element leaves the DOM: cancel the
1023
+ * in-flight run so its stream closes, abort in-flight uploads so they do not
1024
+ * orphan server-side files, and release the mic so the browser's recording
1025
+ * indicator clears. Without this a removed element leaks all three.
1067
1026
  */
1068
1027
  disconnectedCallback(): void {
1069
1028
  this.#connected = false;
@@ -1075,14 +1034,10 @@ export class AgUiChat extends HTMLElement {
1075
1034
  /**
1076
1035
  * Read an opt-in flag attribute the way HTML reads a boolean attribute.
1077
1036
  *
1078
- * Present means on bare (`data-prompt-chips`), empty
1079
- * (`data-prompt-chips=""`) or any value except the literal `"false"`. These
1080
- * were compared against the string `"true"`, so writing the attribute bare —
1081
- * the spelling every native boolean attribute uses, and the one a reader
1082
- * reaches for first — silently *disabled* the feature it names, with nothing
1083
- * to indicate why the chips never appeared.
1084
- *
1085
- * `="false"` still turns it off, so an explicit opt-out keeps working.
1037
+ * Present means on: bare (`data-prompt-chips`), empty (`=""`), or any value
1038
+ * except the literal `"false"`. Comparing against `"true"` instead would make
1039
+ * the bare spelling every native boolean attribute uses silently disable the
1040
+ * feature it names. `="false"` still turns it off.
1086
1041
  */
1087
1042
  #flag(name: string): boolean {
1088
1043
  const value = this.getAttribute(name);
@@ -1242,22 +1197,6 @@ export class AgUiChat extends HTMLElement {
1242
1197
  });
1243
1198
  }
1244
1199
 
1245
- /** The one-line manifest of the message's attachments, for the run context. */
1246
- #attachmentContext(): Context[] {
1247
- if (this.#runAttachments.length === 0) {
1248
- return [];
1249
- }
1250
- const lines = this.#runAttachments.map(
1251
- (ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`,
1252
- );
1253
- return [
1254
- {
1255
- description: "Files the user attached to this message",
1256
- value: `${lines.join("\n")}\nUse the read_attachment tool with an id to read a file's contents.`,
1257
- },
1258
- ];
1259
- }
1260
-
1261
1200
  /**
1262
1201
  * When `data-threads-url` is set, route thread enumeration / load / rename /
1263
1202
  * delete through that server endpoint (wrapping the current store as the
@@ -1351,21 +1290,16 @@ export class AgUiChat extends HTMLElement {
1351
1290
  /**
1352
1291
  * Act on a picked skill.
1353
1292
  *
1354
- * A skill that ships no `prompt` is **server-resolved**: the catalog carries
1355
- * only its name and label, and picking it sends the bare `/name` token for
1356
- * the agent to expandfrom the harness `Skills` capability, or from the
1357
- * server's own instructions. That is the shape to prefer, because the prompt
1358
- * then never reaches the browser at all: a skill is often where a project's
1359
- * internal workflow is written down most plainly, and a catalog endpoint is a
1360
- * plain GET.
1293
+ * A skill with no `prompt` is server-resolved: picking it sends the bare
1294
+ * `/name` token for the agent to expand, so the wording never reaches the
1295
+ * browser. Prefer that shapea skill often states a project's internal
1296
+ * workflow most plainly, and a catalog endpoint is a plain GET.
1361
1297
  *
1362
- * A skill that does carry a `prompt` keeps the older behaviour the client
1363
- * fills its `{placeholder}`s from the page and sends (or pre-fills) the text.
1364
- * Right for a user-facing convenience, and for placeholders only the page can
1365
- * supply.
1298
+ * A skill carrying a `prompt` has the client fill its `{placeholder}`s from
1299
+ * the page instead, which is right for placeholders only the page can supply.
1366
1300
  *
1367
- * Either way a pick now **sends**, rather than parking text in the composer
1368
- * for a second click; `sendImmediately: false` opts back into pre-filling.
1301
+ * Either way a pick sends; `sendImmediately: false` opts into pre-filling the
1302
+ * composer instead.
1369
1303
  */
1370
1304
  #applySkill(skill: Skill): void {
1371
1305
  if (skill.prompt === undefined) {
@@ -1499,18 +1433,13 @@ export class AgUiChat extends HTMLElement {
1499
1433
  }
1500
1434
 
1501
1435
  /**
1502
- * Which edges the layout is holding still, by measuring rather than guessing.
1436
+ * Which edges the layout is holding still, by measuring rather than guessing:
1437
+ * nudge the size by a pixel, see which edges stayed put, and undo. One forced
1438
+ * reflow per drag.
1503
1439
  *
1504
- * A resize has to be computed from the edge that does not move, and which
1505
- * edge that is belongs to the **host's layout**, not to `placement`: a
1506
- * floating panel is pinned bottom-right, while an embedded one goes wherever
1507
- * the page's own CSS puts it — flex-start, flex-end, a grid cell. Mapping
1508
- * placement to a corner got this wrong for any host that right-aligns the
1509
- * element, and the symptom is bad enough to read as a broken control: the
1510
- * panel shrinks when dragged outward, travelling by its opposite corner.
1511
- *
1512
- * So: nudge the size by a pixel, see which edges stayed put, and undo. One
1513
- * forced reflow per drag, which is cheap next to being wrong.
1440
+ * `placement` cannot answer this an embedded panel goes wherever the page's
1441
+ * CSS puts it and see {@link createResizeHandle} for why guessing produces
1442
+ * a visibly broken control.
1514
1443
  */
1515
1444
  #measureAnchor(): ResizeAnchor {
1516
1445
  const before = this.getBoundingClientRect();
@@ -1549,14 +1478,13 @@ export class AgUiChat extends HTMLElement {
1549
1478
  /**
1550
1479
  * Write a dragged size onto the host, on the axes this placement leaves free.
1551
1480
  *
1552
- * Writing the custom property rather than inline `width` / `height` does
1553
- * **not** by itself leave placement in charge an inline custom property
1554
- * still outranks a `:host([placement=…])` rule setting the same property, so
1555
- * a height dragged while floating capped a docked sidebar that had asked for
1556
- * `100vh`. The cascade cannot arbitrate this; the axis check has to.
1557
- *
1558
- * So the rule is explicit: a placement owns the axes it fixes, and a
1559
- * persisted size is only ever applied to the ones it does not.
1481
+ * Writing the custom property rather than inline `width` / `height` does not
1482
+ * by itself leave placement in charge: an inline custom property still
1483
+ * outranks a `:host([placement=…])` rule setting the same property, so a
1484
+ * height dragged while floating would cap a docked sidebar asking for
1485
+ * `100vh`. The cascade cannot arbitrate this, so the axis check must — a
1486
+ * placement owns the axes it fixes, and a persisted size is applied only to
1487
+ * the ones it leaves free.
1560
1488
  */
1561
1489
  #applySize(size: ResizeSize): void {
1562
1490
  const axis = this.#resizeAxis();
@@ -1638,12 +1566,10 @@ export class AgUiChat extends HTMLElement {
1638
1566
  * Open the thread-history drawer: the imperative route to the control that
1639
1567
  * renders as `::part(history-button)`.
1640
1568
  *
1641
- * A host that hides `::part(header)` to render its own title bar hides the
1642
- * history, new-chat and collapse buttons with it and thread switching then
1643
- * has no route at all, because those controls live inside the header. Each of
1644
- * them has a method, so a host chrome can rebuild the set: this one,
1645
- * {@link openCheckpoints}, {@link newChat}, {@link toggleCollapsed} and
1646
- * {@link toggleTheme}.
1569
+ * A host that hides `::part(header)` for its own title bar hides the history,
1570
+ * new-chat and collapse buttons with it. Each has a method so that chrome can
1571
+ * be rebuilt: this one, {@link openCheckpoints}, {@link newChat},
1572
+ * {@link toggleCollapsed} and {@link toggleTheme}.
1647
1573
  */
1648
1574
  openThreads(): void {
1649
1575
  void this.#refreshDrawer();
@@ -1687,7 +1613,6 @@ export class AgUiChat extends HTMLElement {
1687
1613
  this.#toolCards.clear();
1688
1614
  this.#serverSettled.clear();
1689
1615
  this.#initialMessages = [];
1690
- this.#runAttachments = [];
1691
1616
  this.#attachTray?.clear();
1692
1617
  // Keep the empty-state region; everything else clears.
1693
1618
  this.#messages.replaceChildren(this.#emptyWrap);
@@ -1760,23 +1685,17 @@ export class AgUiChat extends HTMLElement {
1760
1685
  /**
1761
1686
  * Notice a previous run that never produced a response.
1762
1687
  *
1763
- * {@link AgUiClient.send} persists the user's message *before* starting the
1764
- * run, so a transcript whose last entry is that user message means nothing
1765
- * ever came back: the page navigated or reloaded mid-run, the tab closed, or
1766
- * the process died. No extra persistence is needed to detect it — the shape
1767
- * of the transcript already says so, which is why this needs no
1768
- * {@link ClientConversationStore} method and no `pagehide` listener (neither
1769
- * of which fires on a crash or a force-quit anyway).
1688
+ * {@link AgUiClient.send} persists the user's message before starting the
1689
+ * run, so a transcript ending on that user message means nothing came back.
1690
+ * The transcript's shape alone detects it, needing no store method and no
1691
+ * `pagehide` listener neither of which fires on a crash or force-quit.
1770
1692
  *
1771
- * The *agent*-initiated reload is not this case: a navigating tool leaves a
1772
- * checkpoint and resumes, which is why the caller returns early on one before
1773
- * reaching here.
1693
+ * An agent-initiated reload is not this case: a navigating tool leaves a
1694
+ * checkpoint and resumes, so the caller returns early on one.
1774
1695
  *
1775
- * Deliberately a notice and never a resume. AG-UI has no
1776
- * resume-an-aborted-run primitive; re-sending the accumulated messages is
1777
- * semantically a **new** run, so any server-side tool the agent had already
1778
- * executed before the interruption would run a second time. Saying plainly
1779
- * that the answer was lost is the honest option, and the user can re-ask.
1696
+ * Deliberately a notice, never a resume. AG-UI has no resume-an-aborted-run
1697
+ * primitive, and re-sending the accumulated messages is semantically a new
1698
+ * run, so any server-side tool already executed would run a second time.
1780
1699
  */
1781
1700
  /**
1782
1701
  * Build a round's context, recording which page it describes.
@@ -1837,22 +1756,23 @@ export class AgUiChat extends HTMLElement {
1837
1756
  // wrap words.
1838
1757
  this.appendMessage(MESSAGE_ROLE.ASSISTANT, text).classList.add("message--restored");
1839
1758
  }
1840
- const toolCalls = message.toolCalls;
1841
- if (toolCalls !== undefined) {
1842
- for (const call of toolCalls) {
1843
- const restored = {
1844
- id: call.id,
1845
- name: call.function.name,
1846
- args: this.#parseArgs(call.function.arguments),
1847
- };
1848
- // Restored history goes through the same interception as the live
1849
- // stream — otherwise a reload resurrects the raw `load_capability`
1850
- // card the live path deliberately replaced.
1851
- if (this.#noticeIfSkillLoad(restored)) {
1852
- continue;
1853
- }
1854
- this.#cardFor(restored);
1759
+ // Narrowed rather than trusted, for the same reason `messageAttachments`
1760
+ // narrows the neighbouring field: anything that throws in this loop aborts
1761
+ // the replay at this message, and every later turn silently disappears from
1762
+ // the transcript. See `restoredToolCalls`.
1763
+ for (const call of restoredToolCalls(message.toolCalls)) {
1764
+ const restored = {
1765
+ id: call.id,
1766
+ name: call.function.name,
1767
+ args: this.#parseArgs(call.function.arguments),
1768
+ };
1769
+ // Restored history goes through the same interception as the live
1770
+ // stream — otherwise a reload resurrects the raw `load_capability`
1771
+ // card the live path deliberately replaced.
1772
+ if (this.#noticeIfSkillLoad(restored)) {
1773
+ continue;
1855
1774
  }
1775
+ this.#cardFor(restored);
1856
1776
  }
1857
1777
  return;
1858
1778
  }
@@ -1864,8 +1784,14 @@ export class AgUiChat extends HTMLElement {
1864
1784
  }
1865
1785
  }
1866
1786
 
1867
- /** Parse a tool call's JSON `arguments` string from history into an object. */
1868
- #parseArgs(raw: string): Record<string, unknown> {
1787
+ /** Parse a tool call's JSON `arguments` from history into an object. */
1788
+ #parseArgs(raw: unknown): Record<string, unknown> {
1789
+ if (typeof raw !== "string") {
1790
+ // A restored call whose `arguments` are missing or not a string still has
1791
+ // a name worth showing, so this renders an empty-args card rather than
1792
+ // dropping the card.
1793
+ return {};
1794
+ }
1869
1795
  try {
1870
1796
  const parsed: unknown = JSON.parse(raw);
1871
1797
  if (typeof parsed === "object" && parsed !== null) {
@@ -1898,13 +1824,12 @@ export class AgUiChat extends HTMLElement {
1898
1824
  /**
1899
1825
  * Append a message bubble and return it.
1900
1826
  *
1901
- * Assistant content is rendered as sanitised markdown/HTML; user content
1902
- * stays literal text (no need to parse what the user typed, and it avoids
1903
- * rendering user-authored markup).
1827
+ * Assistant content renders as sanitised markdown/HTML; user content stays
1828
+ * literal text, which also avoids rendering user-authored markup.
1904
1829
  *
1905
- * Assistant bubbles land in the current answer group, opening one if
1906
- * needed; a user bubble closes the prior group and sits directly in the list
1907
- * (the well wraps the *assistant* turn, the user message precedes it).
1830
+ * Assistant bubbles land in the current answer group, opening one if needed;
1831
+ * a user bubble closes the prior group and sits directly in the list, the
1832
+ * well wrapping only the assistant turn.
1908
1833
  */
1909
1834
  appendMessage(role: MessageRole, content: string): HTMLDivElement {
1910
1835
  const bubble = document.createElement("div");
@@ -2170,12 +2095,9 @@ export class AgUiChat extends HTMLElement {
2170
2095
  * Build a header control button: a named slot a host can project markup into,
2171
2096
  * with the built-in glyph as the slot's fallback.
2172
2097
  *
2173
- * The glyph used to be the button's own `textContent`, which left a host able
2174
- * to restyle the control through its `part` but unable to replace it — a CSS
2175
- * `content` override could swap one character for another, and nothing could
2176
- * supply a brand `<img>` or `<svg>`. This is the same slot-with-fallback
2177
- * idiom the header icon already uses, so existing embeds render exactly as
2178
- * before.
2098
+ * The slot is what lets a host replace the mark with its own `<img>` or
2099
+ * `<svg>` rather than only restyle it through the `part`; the same
2100
+ * slot-with-fallback idiom the header icon uses.
2179
2101
  */
2180
2102
  #headerButton(modifier: string, label: string, glyph: string): HTMLButtonElement {
2181
2103
  const button = document.createElement("button");
@@ -2417,19 +2339,16 @@ export class AgUiChat extends HTMLElement {
2417
2339
  * Send a message as if the user had typed it — renders the user bubble,
2418
2340
  * dispatches {@link SUBMIT_EVENT}, and starts the run.
2419
2341
  *
2420
- * The programmatic half of the composer, for a host driving its own input
2421
- * (a "Ask about this order" button, a command palette, a custom composer
2422
- * replacing the built-in one). Everything the built-in Send does happens
2423
- * here; Send itself now reads the composer, clears it, and calls this.
2342
+ * The programmatic half of the composer, for a host driving its own input.
2343
+ * Everything the built-in Send does happens here; Send reads the composer,
2344
+ * clears it, and calls this.
2424
2345
  *
2425
- * `attachments` are durable {@link AttachmentRef}s — the shape
2426
- * {@link attachFile}'s upload resolves to, and the shape
2427
- * {@link ATTACHMENT_EVENT} reports. Pass them to attach files to the message.
2346
+ * `attachments` are durable {@link AttachmentRef}s — what {@link attachFile}
2347
+ * resolves to and what {@link ATTACHMENT_EVENT} reports.
2428
2348
  *
2429
- * No-ops while a run is in flight (a second concurrent run would orphan the
2430
- * first) and for an entirely empty message. Unlike the built-in Send, this
2431
- * does **not** consult the tray: what you pass is what is sent, so a host
2432
- * composer stays in charge of its own state.
2349
+ * No-ops on an empty message, and while a run is in flight, since a second
2350
+ * concurrent run would orphan the first. Unlike the built-in Send it does not
2351
+ * consult the tray: what you pass is what is sent.
2433
2352
  */
2434
2353
  async sendMessage(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
2435
2354
  if (this.#running || (content === "" && attachments.length === 0)) {
@@ -2439,8 +2358,6 @@ export class AgUiChat extends HTMLElement {
2439
2358
  if (attachments.length > 0) {
2440
2359
  bubble.appendChild(renderAttachmentChips(attachments));
2441
2360
  }
2442
- // Surfaced to the run via the context manifest until the run settles.
2443
- this.#runAttachments = attachments;
2444
2361
  this.dispatchEvent(
2445
2362
  new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
2446
2363
  detail: { content, attachments },
@@ -2456,11 +2373,11 @@ export class AgUiChat extends HTMLElement {
2456
2373
  * picker and drag-and-drop do — validation, progress chip, and all.
2457
2374
  *
2458
2375
  * Returns `false` when uploads are not configured (no `data-attachments-url`
2459
- * and no {@link uploadHandler}), which is the only way for a host to tell;
2460
- * the tray does not exist to report anything in that case.
2376
+ * and no {@link uploadHandler}) the only signal a host gets, since the tray
2377
+ * does not exist to report anything then.
2461
2378
  *
2462
2379
  * Uploading is asynchronous: watch {@link ATTACHMENT_EVENT} for the resulting
2463
- * {@link AttachmentRef}, and pass it to {@link sendMessage} once `pending`
2380
+ * {@link AttachmentRef} and pass it to {@link sendMessage} once `pending`
2464
2381
  * reaches zero.
2465
2382
  */
2466
2383
  attachFile(file: File): boolean {
@@ -2565,17 +2482,15 @@ export class AgUiChat extends HTMLElement {
2565
2482
  return null;
2566
2483
  }
2567
2484
  // The page moved under this round. Acting now would target whatever
2568
- // happens to match on the *new* page usually nothing, which the handler
2569
- // would report as a miss anyway, but occasionally a same-named control that
2570
- // matches silently. That last case is the one worth preventing: it is the
2571
- // only way the agent acts on the wrong page without either side noticing.
2485
+ // matches on the new page, and the case worth preventing is a same-named
2486
+ // control matching silently the only way the agent acts on the wrong page
2487
+ // without either side noticing.
2572
2488
  //
2573
- // Placed before the confirmation prompt so the user is never asked to
2574
- // approve an action that is about to be refused. Navigating tools are
2575
- // exempt (moving the page is their job including re-navigating after a
2576
- // move), as is `read_page`, which is the documented recovery. Gated on a
2577
- // page-map provider: without one there is no `read_page` to recommend and
2578
- // the host's tools are not page-scoped in the first place.
2489
+ // Must precede the confirmation prompt, so the user is never asked to
2490
+ // approve an action about to be refused. Navigating tools are exempt, since
2491
+ // moving the page is their job, as is read_page, the documented recovery.
2492
+ // Gated on a page-map provider: without one there is no read_page to
2493
+ // recommend and the host's tools are not page-scoped anyway.
2579
2494
  if (
2580
2495
  this.getPageMap !== null &&
2581
2496
  call.name !== READ_PAGE_TOOL &&
@@ -2651,12 +2566,11 @@ export class AgUiChat extends HTMLElement {
2651
2566
  * Render an approval card per server-side-tool interrupt and collect the
2652
2567
  * user's decisions (approve → run it, deny → decline it).
2653
2568
  *
2654
- * The run is suspended on these cards; a Stop while any is open aborts the
2569
+ * The run is suspended on these cards. A Stop while any is open aborts the
2655
2570
  * shared {@link #confirmAbort} controller, resolving every still-open card as
2656
- * denied (and the client loop then sees the cancellation and stops). An
2657
- * approved tool runs on the follow-up (resume) run and streams its result
2658
- * back into the same pending card; a denied one is settled here, since no
2659
- * result will ever arrive for it.
2571
+ * denied. An approved tool runs on the follow-up resume run and streams its
2572
+ * result into the same pending card; a denied one settles here, since no
2573
+ * result will ever arrive.
2660
2574
  */
2661
2575
  async #resolveInterrupts(
2662
2576
  interrupts: readonly Interrupt[],
@@ -2804,9 +2718,6 @@ export class AgUiChat extends HTMLElement {
2804
2718
  this.#hidePending();
2805
2719
  this.#setRunning(false);
2806
2720
  this.#streamingBubble = null;
2807
- // The attachment manifest was for this run only; the model has read what
2808
- // it needed (results now live in history).
2809
- this.#runAttachments = [];
2810
2721
  // Belt-and-suspenders: a tool card still pending at settle (e.g. a
2811
2722
  // server tool whose result never streamed because the connection
2812
2723
  // dropped) would hang forever — settle it to the no-result fallback.
@@ -2955,15 +2866,54 @@ export class AgUiChat extends HTMLElement {
2955
2866
  }
2956
2867
  }
2957
2868
 
2869
+ /** One tool call as a restored assistant message carries it. */
2870
+ interface RestoredToolCall {
2871
+ readonly id: string;
2872
+ readonly function: { readonly name: string; readonly arguments?: unknown };
2873
+ }
2874
+
2875
+ /**
2876
+ * The tool calls a restored assistant turn carries, with anything shapeless dropped.
2877
+ *
2878
+ * Narrowing here rather than trusting the declared type, for three reasons that
2879
+ * point the same way.
2880
+ *
2881
+ * **`null` is a value this field really takes.** `@ag-ui/core` types `toolCalls`
2882
+ * as optional (`z.ZodOptional`), so TypeScript offers only `undefined` — but the
2883
+ * protocol's Python models declare `tool_calls: list[ToolCall] | None`, and a
2884
+ * server dumping them without `exclude_none` sends `null`. The two SDKs disagree
2885
+ * about the wire, and a client cannot afford to take either one's word for it.
2886
+ *
2887
+ * **A throw here costs the rest of the transcript.** This runs inside the replay
2888
+ * of stored history, one message at a time; an exception aborts the whole replay,
2889
+ * so a single bad entry silently truncates the conversation from that point on —
2890
+ * with no error state and nothing on screen to explain the gap.
2891
+ *
2892
+ * **Storage is untrusted anyway** — hand-edited, truncated, written by an older
2893
+ * version, or supplied by a host's own store. `messageAttachments` already takes
2894
+ * exactly this stance for the neighbouring field on the same message.
2895
+ */
2896
+ function restoredToolCalls(value: unknown): readonly RestoredToolCall[] {
2897
+ return Array.isArray(value) ? value.filter(isRestoredToolCall) : [];
2898
+ }
2899
+
2900
+ /** Whether an unknown history entry has enough shape to render a tool card. */
2901
+ function isRestoredToolCall(value: unknown): value is RestoredToolCall {
2902
+ if (typeof value !== "object" || value === null) {
2903
+ return false;
2904
+ }
2905
+ const call = value as { id?: unknown; function?: { name?: unknown } };
2906
+ return typeof call.id === "string" && typeof call.function?.name === "string";
2907
+ }
2908
+
2958
2909
  /**
2959
2910
  * The skill name a `load_capability` call activated, or `null` when the call is
2960
2911
  * something else.
2961
2912
  *
2962
- * Every deferred capability loads through this one tool, so the id is only a
2963
- * *skill* name when the project wired agent skills a project deferring some
2964
- * other capability would surface its id here too. That is acceptable for a
2965
- * muted notice and preferable to inventing a parallel signal: the id is exactly
2966
- * what the model selected, which is what the notice reports.
2913
+ * Every deferred capability loads through this one tool, so the id is a skill
2914
+ * name only when the project wired agent skills; another project's capability
2915
+ * id surfaces here too. Acceptable for a muted notice, and better than a
2916
+ * parallel signal the id is exactly what the model selected.
2967
2917
  */
2968
2918
  function skillNameFrom(call: AgUiToolCall): string | null {
2969
2919
  if (call.name !== LOAD_CAPABILITY_TOOL) {