@artooi/ag-ui-web-component 0.28.0 → 0.30.0

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 (65) hide show
  1. package/CHANGELOG.md +615 -1
  2. package/README.md +564 -35
  3. package/dist/ag-ui-web-component.bundle.js +491 -50
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +129 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +232 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +56 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +8 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2081 -98
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/approval_card.d.ts +18 -0
  16. package/dist/ui/approval_card.d.ts.map +1 -1
  17. package/dist/ui/checkpoint_menu.d.ts +10 -0
  18. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  19. package/dist/ui/confirmation_card.d.ts +16 -0
  20. package/dist/ui/confirmation_card.d.ts.map +1 -1
  21. package/dist/ui/message_actions.d.ts +56 -0
  22. package/dist/ui/message_actions.d.ts.map +1 -0
  23. package/dist/ui/page_quote_offer.d.ts +33 -0
  24. package/dist/ui/page_quote_offer.d.ts.map +1 -0
  25. package/dist/ui/quote_selection.d.ts +66 -0
  26. package/dist/ui/quote_selection.d.ts.map +1 -0
  27. package/dist/ui/relative_time.d.ts +10 -0
  28. package/dist/ui/relative_time.d.ts.map +1 -1
  29. package/dist/ui/stick_to_bottom.d.ts +55 -0
  30. package/dist/ui/stick_to_bottom.d.ts.map +1 -0
  31. package/dist/ui/styles.d.ts +1 -1
  32. package/dist/ui/styles.d.ts.map +1 -1
  33. package/dist/ui/subagent_panel.d.ts +92 -0
  34. package/dist/ui/subagent_panel.d.ts.map +1 -0
  35. package/dist/ui/subagent_update.d.ts +19 -0
  36. package/dist/ui/subagent_update.d.ts.map +1 -0
  37. package/dist/ui/suggestion_chips.d.ts +29 -0
  38. package/dist/ui/suggestion_chips.d.ts.map +1 -0
  39. package/dist/ui/thread_drawer.d.ts +10 -0
  40. package/dist/ui/thread_drawer.d.ts.map +1 -1
  41. package/dist/ui/tool_call_card.d.ts +81 -1
  42. package/dist/ui/tool_call_card.d.ts.map +1 -1
  43. package/dist/ui/ui_strings.d.ts +50 -0
  44. package/dist/ui/ui_strings.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/constants.ts +138 -1
  47. package/src/core/ag_ui_chat.ts +1081 -73
  48. package/src/core/agui_client.ts +89 -2
  49. package/src/index.ts +43 -0
  50. package/src/ui/approval_card.ts +90 -2
  51. package/src/ui/checkpoint_menu.ts +22 -5
  52. package/src/ui/confirmation_card.ts +29 -1
  53. package/src/ui/message_actions.ts +170 -0
  54. package/src/ui/page_quote_offer.ts +215 -0
  55. package/src/ui/quote_selection.ts +345 -0
  56. package/src/ui/relative_time.ts +11 -0
  57. package/src/ui/stick_to_bottom.ts +126 -0
  58. package/src/ui/styles.ts +410 -0
  59. package/src/ui/subagent_panel.ts +213 -0
  60. package/src/ui/subagent_update.ts +80 -0
  61. package/src/ui/suggestion_chips.ts +73 -0
  62. package/src/ui/thread_drawer.ts +22 -2
  63. package/src/ui/tool_call_card.ts +138 -3
  64. package/src/ui/ui_strings.ts +75 -0
  65. package/src/version.ts +1 -1
package/README.md CHANGED
@@ -49,7 +49,13 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
49
49
  - [New chat and collapse](#new-chat-and-collapse)
50
50
  - [Collapsing to the launcher](#collapsing-to-the-launcher)
51
51
  - [Tool-call display modes](#tool-call-display-modes)
52
+ - [Delegated sub-agents](#delegated-sub-agents)
52
53
  - [Markdown rendering](#markdown-rendering)
54
+ - [Follow-up suggestions](#follow-up-suggestions)
55
+ - [Editing a gated call before approving it](#editing-a-gated-call-before-approving-it)
56
+ - [Localizing the timestamps](#localizing-the-timestamps)
57
+ - [Message actions: copy, retry, feedback](#message-actions-copy-retry-feedback)
58
+ - [Quoting a selection](#quoting-a-selection)
53
59
  - [Run notices: compaction and agent skills](#run-notices-compaction-and-agent-skills)
54
60
  - [Skills: prompt chips and slash palette](#skills-prompt-chips-and-slash-palette)
55
61
  - [MPA durability: surviving full page reloads](#mpa-durability-surviving-full-page-reloads)
@@ -156,7 +162,7 @@ another origin, add `credentials="include"` too; see
156
162
  | --- | --- | --- |
157
163
  | `endpoint` | `endpoint` | The AG-UI endpoint URL. Required to send. Reflecting getter + setter. |
158
164
  | `credentials` | `credentials` | Cookie policy for every request the element makes: `omit` / `same-origin` / `include`. Unset means the browser default (`same-origin`), which sends no cookies cross-origin. See [Authenticating requests](#authenticating-requests). |
159
- | `title-text` | — | Header label; defaults to `"Assistant"`. The only **observed** attribute (live-updates the header). |
165
+ | `title-text` | — | Header label; defaults to `"Assistant"`. Live: writing it after the element connects re-labels the header. See [When each attribute is read](#when-each-attribute-is-read). |
160
166
  | `data-tool-display` | `toolDisplay` | Tool-call card detail: `inline` / `minimal` / `compact` / `full` (default `full`). |
161
167
  | `data-text-animation` | — | Incoming-text reveal: `none` (default) / `fade` / `word`. |
162
168
  | `data-prompt-chips` | — | Present (bare, or any value but `"false"`) to surface skills as chips. |
@@ -177,6 +183,16 @@ another origin, add `credentials="include"` too; see
177
183
  | `data-icon-url` | — | Header (and launcher) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
178
184
  | `data-launcher-icon-url` | — | Icon image URL for the collapsed launcher only, when it should differ from the header's. Falls back to `data-icon-url`; a slotted `slot="launcher"` wins over both. |
179
185
  | `data-unread-badge` | — | **On by default.** `="false"` hides the launcher's unread badge; the count and the `ag-ui-unread` event keep running. See [Collapsing to the launcher](#collapsing-to-the-launcher). |
186
+ | `data-quote-selection` | — | **On by default.** `="false"` stops the transcript offering to quote a selection. `quote()` keeps working either way. See [Quoting a selection](#quoting-a-selection). |
187
+ | `data-message-actions` | — | **All on by default.** A comma list of the actions a finished answer keeps: `copy` / `retry` / `feedback` (e.g. `"copy,retry"`). `="false"` removes the row entirely. See [Message actions](#message-actions-copy-retry-feedback). |
188
+ | `data-max-tool-rounds` | — | Upper bound on frontend tool-call → re-run rounds within one send (default 10; a value below 1 is ignored). Raise it for a page-driving agent whose turn takes many small steps. See [The run loop](#the-run-loop-and-the-ag-ui-client). |
189
+ | `data-page-actions` | — | Opt-in built-in page-action tools: a comma list of `scroll` / `drag` (e.g. `"scroll,drag"`). See [Page-action tools](#page-action-tools). |
190
+ | `data-side` | — | CSS-only, for `placement="sidebar"`: which edge it docks to — `right` (default) / `left`. |
191
+ | `data-answer-well` | — | CSS-only boolean: box each assistant turn (its text, tool cards, and thinking) in one bordered "well". Off by default. See [The answer well](#the-answer-well). |
192
+ | `collapsed` | `collapsed` | Reflected boolean; collapses the widget to its [launcher](#collapsing-to-the-launcher) (a rail under `placement="sidebar"`, the header bar under `embedded` / `page`). Persisted per-tab in `sessionStorage`. |
193
+ | `theme` | — | CSS-only: `light` (default) / `dark` / `auto` / `code`. |
194
+ | `density` | — | CSS-only: `comfortable` (default) / `compact`. |
195
+ | `placement` | — | CSS-only: `floating` (default) / `bottom-left` / `side` / `sidebar` / `full` / `page` / `embedded`. |
180
196
 
181
197
  Each header control also takes its own icon slot — `icon-history`, `icon-checkpoints`,
182
198
  `icon-new`, `icon-collapse` — with the built-in glyph as the fallback, so a host can project a
@@ -188,19 +204,39 @@ same way: `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`.
188
204
  <svg slot="icon-new" width="16" height="16"><!-- ... --></svg>
189
205
  </ag-ui-chat>
190
206
  ```
191
- | `data-page-actions` | — | Opt-in built-in page-action tools: a comma list of `scroll` / `drag` (e.g. `"scroll,drag"`). See [Page-action tools](#page-action-tools). |
192
- | `data-side` | — | CSS-only, for `placement="sidebar"`: which edge it docks to — `right` (default) / `left`. |
193
- | `data-answer-well` | — | CSS-only boolean: box each assistant turn (its text, tool cards, and thinking) in one bordered "well". Off by default. See [The answer well](#the-answer-well). |
194
- | `collapsed` | `collapsed` | Reflected boolean; collapses the widget to its [launcher](#collapsing-to-the-launcher) (a rail under `placement="sidebar"`, the header bar under `embedded` / `page`). Persisted per-tab in `sessionStorage`. |
195
- | `theme` | — | CSS-only: `light` (default) / `dark` / `auto` / `code`. |
196
- | `density` | — | CSS-only: `comfortable` (default) / `compact`. |
197
- | `placement` | — | CSS-only: `floating` (default) / `bottom-left` / `side` / `sidebar` / `full` / `page` / `embedded`. |
198
207
 
199
- **Properties** (JS only, not attributes): `headers`, `getHeaders`, `allowImages`, `autoConfirm`,
200
- `confirmPredicate`, `askUser`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`,
201
- `getPageMap`, `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`,
202
- `navigationResult`, `skillContext`, `toolSummaries`, `strings`, `resolvePageTarget`, plus the
203
- mirrors `endpoint` / `userKey` / `toolDisplay` / `collapsed` / `credentials`.
208
+ #### When each attribute is read
209
+
210
+ The element observes two groups of attributes, and they behave differently once it is in the DOM.
211
+ Nothing outside those groups is observed: a CSS-only attribute (`theme`, `density`, `data-side`,
212
+ `data-answer-well`) is read by the stylesheet rather than by script, and `endpoint`,
213
+ `data-tool-display`, `data-text-animation`, `data-runs-url`, `data-page-actions`,
214
+ `data-message-actions`, `data-max-tool-rounds`, `data-unread-badge` and `data-quote-selection` are
215
+ re-read at each use, so a late write to any of those simply takes effect. The one attribute in
216
+ neither camp is `data-launcher-icon-url`: it is read while the element connects, like the group
217
+ below, but is not observed, so a late write is inert and says nothing.
218
+
219
+ **Live attributes.** Written at any time, before or after the element connects, and acted on
220
+ either way: `title-text`, `placement`, `credentials`, `user-key`.
221
+
222
+ **Connect-time attributes.** Read once, while the element connects, to decide what chrome exists at
223
+ all — the tray, the mic, the skills menu, the header mark. Writing one afterwards has **no effect**;
224
+ the element logs a console warning naming the attribute rather than failing silently, because the
225
+ symptom is an affordance that never appears and that reads as a broken component. Set them before
226
+ the element enters the DOM, or remove and re-insert it. See
227
+ [Framework hosts](#framework-hosts-configure-before-you-insert), where the boundary bites hardest.
228
+ The list: `data-attachments-url`, `data-attachment-accept`, `data-attachment-max-bytes`,
229
+ `data-transcribe-url`, `data-threads-url`, `data-threads-cache`, `data-tools-url`,
230
+ `data-skills-url`, `data-skills`, `data-prompt-chips`, `data-slash-commands`, `data-theme-toggle`,
231
+ `data-strings`, `data-icon-url`.
232
+
233
+ **Properties** (JS only, not attributes): `headers`, `getHeaders`, `trustedOrigins`, `allowImages`,
234
+ `autoConfirm`, `confirmPredicate`, `askUser`, `askUserRenderer`, `approvalRenderer`,
235
+ `approveWithEdits`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`, `getPageMap`,
236
+ `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`, `navigationResult`,
237
+ `skillContext`, `toolSummaries`, `formatToolPayload`, `formatRelativeTime`, `strings`,
238
+ `resolvePageTarget`, `sharedState`, plus the read-only `unread` and `unhandledActivityTypes`, and
239
+ the attribute mirrors `endpoint` / `userKey` / `toolDisplay` / `collapsed` / `credentials`.
204
240
 
205
241
  `headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
206
242
  run; `getHeaders` is the one to use for a credential that rotates. See
@@ -220,15 +256,20 @@ the django-ag-ui `@tool` registry), whose schema never reaches the browser — e
220
256
  labels are fetched automatically — per card, `x-summary` → an explicit
221
257
  `toolSummaries` entry → the fetched catalog → the raw name.
222
258
 
223
- **Properties** (selected): `sharedState` AG-UI shared state (documented under Tools & state).
259
+ `sharedState` is AG-UI shared state, documented under
260
+ [the run loop](#the-run-loop-and-the-ag-ui-client); `unread` and `unhandledActivityTypes` are
261
+ read-only counters, covered under [the unread badge](#the-unread-badge) and
262
+ [finding out what arrived](#finding-out-what-arrived).
224
263
 
225
264
  Code blocks in an agent's answer carry a **copy button**, revealed on hover or
226
265
  keyboard focus and styleable via the `code-copy` part. Override its labels with
227
266
  the `copyCode` / `copied` / `copyFailed` strings.
228
267
 
229
- **Methods**: `registerTool`, `registerPageState`, `setSkills`, `sendMessage`, `attachFile`,
230
- `appendMessage`, `newChat`, `setCollapsed`, `toggleCollapsed`, `toggleTheme`, `openThreads`,
231
- `openCheckpoints`, `reload`.
268
+ **Methods**: `registerTool`, `registerPageState`, `registerActivityRenderer`, `setSkills`,
269
+ `sendMessage`, `attachFile`, `appendMessage`, `retryLastTurn`, `quote`, `offerQuoteInPage`,
270
+ `enableCharts`, `newChat`, `setCollapsed`, `toggleCollapsed`, `toggleTheme`, `openThreads`,
271
+ `openCheckpoints`, `closeCheckpoints`, `toggleCheckpoints`, `reload`, and the deprecated
272
+ `registerStateHook` (renamed to `registerPageState`).
232
273
 
233
274
  ### Sending from your own UI
234
275
 
@@ -480,7 +521,12 @@ an AG-UI `AbstractAgent`. On the first send the element builds a client (via the
480
521
  render into a bubble; each `TOOL_CALL_END` becomes a tool-call card.
481
522
  3. Any **frontend** tool calls collected during the run are executed locally, their results are
482
523
  appended as `tool` messages, and the agent is re-run with the results.
483
- 4. This repeats until the agent stops calling frontend tools, bounded by `MAX_TOOL_ROUNDS`.
524
+ 4. This repeats until the agent stops calling frontend tools, bounded by `MAX_TOOL_ROUNDS`
525
+ (10) — raise it with `data-max-tool-rounds`, or `AgUiClientConfig.maxToolRounds` when you
526
+ drive the client yourself. The default suits a chat whose tools answer questions; a
527
+ page-driving deployment reaches it legitimately, one round per field filled, and the symptom
528
+ is not an error but an answer that stops mid-task. A value below 1 is ignored rather than
529
+ honoured — it would be a send that never runs the agent at all.
484
530
 
485
531
  Tool calls the client doesn't own (server-side tools the server already executed) are left alone —
486
532
  the loop doesn't re-run them, but their streamed `TOOL_CALL_RESULT` is rendered into the tool-call
@@ -561,6 +607,8 @@ transcript as a resolved record after the decision:
561
607
  - **Confirm** → the handler runs and the result is posted back.
562
608
  - **Cancel** → a `"User declined the action."` result is posted; the agent acknowledges on its
563
609
  next turn.
610
+ - **Always allow** → the handler runs *and* this tool stops prompting for the rest of the
611
+ session. See below for when this button appears.
564
612
 
565
613
  Whether a call is gated is decided in this order:
566
614
 
@@ -568,9 +616,27 @@ Whether a call is gated is decided in this order:
568
616
  2. Else if `chat.confirmPredicate` is set, its boolean return is authoritative — given the tool
569
617
  name + parsed args it decides per-call (so one tool can be instant for some args and confirmed
570
618
  for others, which a static flag can't express).
571
- 3. Else the element falls back to [`isDestructive(parameters)`](src/tools/is_destructive.ts),
619
+ 3. Else if the user has waived this tool name for the session, the call runs.
620
+ 4. Else the element falls back to [`isDestructive(parameters)`](src/tools/is_destructive.ts),
572
621
  which reads the `x-destructive` JSON-Schema flag.
573
622
 
623
+ #### "Always allow", and why only sometimes
624
+
625
+ A prompt that is approved nearly every time is not a decision, it is a speed bump — and the
626
+ reflex it trains is what makes the rare refusal easy to miss. Anthropic published that users
627
+ approve **~93%** of Claude Code permission prompts manually and called interactive confirmation
628
+ *"behaviorally unreliable as a sole safety mechanism"* on that basis. The waiver exists so the
629
+ prompts that remain still mean something.
630
+
631
+ **The button is offered only on cards raised by step 4** — the `x-destructive` default. Where
632
+ `confirmPredicate` is what gated the call, there is no button, because that predicate is
633
+ documented as authoritative and letting one click retire it would silently defeat a host policy.
634
+ The offer and the allowlist sit on the same path, so there is no dead button either.
635
+
636
+ The waiver is **per tool name and per element**, held in memory and never persisted. A session
637
+ decision that outlived the tab would be a permanent grant made by one click — which is what
638
+ `autoConfirm` already exists to say deliberately. It is cleared when the element goes away.
639
+
574
640
  AG-UI has no built-in risk flag, so destructiveness is carried as a JSON-Schema extension at the
575
641
  **schema root**: `parameters["x-destructive"] = true` (use the exported `X_DESTRUCTIVE_KEY`
576
642
  constant). There is no parallel metadata channel and no name heuristic — destructiveness is exactly
@@ -1009,6 +1075,67 @@ it runs again every time the conversation is restored.
1009
1075
 
1010
1076
  ---
1011
1077
 
1078
+ ## Drawing other things the server pushes
1079
+
1080
+ `activityType` is an open string the protocol does not enumerate — `"chart"` is
1081
+ just the one the component ships a renderer for. Register your own and the
1082
+ server can push anything it likes into the transcript:
1083
+
1084
+ ```js
1085
+ chat.registerActivityRenderer({
1086
+ type: "build_status",
1087
+ render: (content) => {
1088
+ const el = document.createElement("div");
1089
+ el.className = "build";
1090
+ el.textContent = `Build ${content.status}`;
1091
+ return el; // return null for content not worth drawing
1092
+ },
1093
+ });
1094
+ ```
1095
+
1096
+ **`render` runs again on every thread load**, so it carries the same contract as
1097
+ a client tool's `render`: a pure function of `content`, deterministic, and free
1098
+ of effects outside the node it returns. Activities are materialised into
1099
+ `role: "activity"` messages and persisted with the transcript, so a renderer that
1100
+ writes to the page instead of returning DOM fires again on every restore.
1101
+
1102
+ The component places what you return, keyed by the activity's `messageId`, so a
1103
+ server repeating an id **replaces** your node rather than adding a second one —
1104
+ the same in-place update charts get. Returning `null` removes whatever was there:
1105
+ live and reload should agree, and the stored content is the version that could
1106
+ not be drawn.
1107
+
1108
+ `chart` and `compaction` are registrations exactly like yours, not privileged
1109
+ branches, so registering either name **replaces the built-in**.
1110
+
1111
+ ### Which carrier should the server use?
1112
+
1113
+ AG-UI leaves exactly two payload names open, and they are not
1114
+ interchangeable:
1115
+
1116
+ | | Carrier | Reaches | Persisted | Replayed |
1117
+ | --- | --- | --- | --- | --- |
1118
+ | **Content** | `ACTIVITY_SNAPSHOT` | the transcript | yes | yes |
1119
+ | **Imperative** | `CUSTOM` | your page, as [`ag-ui-custom`](#host-seams-the-spa-story) | no | no |
1120
+
1121
+ ⇒ **Content has a place in the conversation and should come back. An imperative
1122
+ has no place and no meaning once acted on** — replaying "refetch the board" on
1123
+ every thread load is a bug, not a feature. If it has to survive a reload, it is
1124
+ content.
1125
+
1126
+ ### Finding out what arrived
1127
+
1128
+ An activity nobody registered for draws nothing and logs nothing — that is the
1129
+ protocol's own answer, and warning would fire on every forward-compatible
1130
+ server. But silence is hard to debug, so the names are readable:
1131
+
1132
+ ```js
1133
+ chat.unhandledActivityTypes; // ["pydantic_ai_thinking", …]
1134
+ ```
1135
+
1136
+ Note `"chart"` appears there until you call `enableCharts(["activity"])`, which
1137
+ is the honest answer to "I pushed a chart and nothing happened".
1138
+
1012
1139
  ## Tool-call display modes
1013
1140
 
1014
1141
  How much a tool-call card shows is set via the `data-tool-display` attribute (or `toolDisplay`
@@ -1031,6 +1158,42 @@ visible. Style them via the `tool-card-args` / `tool-card-result` parts, their h
1031
1158
  transcript, the way `data-answer-well` does — the modes are pure visibility over one DOM shape,
1032
1159
  selected by the shadow CSS from the host attribute.
1033
1160
 
1161
+ ### Drawing a card's body yourself
1162
+
1163
+ A thirty-field result is a wall of JSON where a host wanted a table, or a
1164
+ sentence. `formatToolPayload` is the seam: it is asked about each region of each
1165
+ card and may return a `Node` to take it over, a `string` to replace its text, or
1166
+ `null` to leave the built-in pretty-print alone.
1167
+
1168
+ ```js
1169
+ chat.formatToolPayload = (payload) => {
1170
+ if (payload.kind !== "result" || payload.toolName !== "list_orders") {
1171
+ return null; // everything else keeps the default rendering
1172
+ }
1173
+ const table = document.createElement("table");
1174
+ // ... build it from JSON.parse(payload.text)
1175
+ return table;
1176
+ };
1177
+ ```
1178
+
1179
+ Both halves come through the same hook, told apart by `kind`: `arguments`
1180
+ carries the parsed record the call was made with, `result` the raw string the
1181
+ tool returned plus the outcome it settled on. A region a formatter took over is
1182
+ marked `data-formatted`, which relaxes the preformatted whitespace the default
1183
+ JSON block relies on — a table would otherwise inherit it as mangled cell
1184
+ spacing. Whitespace only: the card's face, frame and scroll cap stay, so one long
1185
+ payload still cannot stretch the transcript, and a host wanting different
1186
+ typography restyles the `tool-card-result` part.
1187
+
1188
+ This is **presentation, not translation.** The card and the model already read
1189
+ separate copies of a tool result — the model's is maintained by `@ag-ui/client`
1190
+ from the same event — so a formatter changes what the person reads and nothing
1191
+ the agent reads. That is what makes restyling safe here, and it is also why
1192
+ *rewording* belongs on the server: renamed there, the new wording reaches the
1193
+ model's prose too, instead of leaving the card disagreeing with the answer beside
1194
+ it. A returned string is set as text, never parsed as markup — this is not a
1195
+ second HTML channel into the transcript.
1196
+
1034
1197
  A gated call carries the decision (`approved by you` / `declined by you`, part
1035
1198
  `tool-card-decision`, attribute `data-decision`) — from the client-side confirmation card and
1036
1199
  from the server-side approval interrupt alike. The prompt itself disappears once answered: a
@@ -1057,6 +1220,72 @@ speed; the spin respects `prefers-reduced-motion`).
1057
1220
 
1058
1221
  ---
1059
1222
 
1223
+ ## Delegated sub-agents
1224
+
1225
+ A run that hands work to a sub-agent reads as a stall. The parent's
1226
+ `delegate_task` card sits at "running…" for the child's entire duration —
1227
+ however many tools the child calls, however long it takes — with nothing on
1228
+ screen to say anything is happening.
1229
+
1230
+ If your server narrates that, the component draws it. The wire is an ordinary
1231
+ AG-UI `CUSTOM` event named `ag_ui.subagent`, carrying:
1232
+
1233
+ | Key | Meaning |
1234
+ | --- | --- |
1235
+ | `delegationId` | the **parent's own `delegate_task` tool-call id** — not the child's run id |
1236
+ | `agent` | the child agent's name |
1237
+ | `phase` | one of `started`, `tool_call`, `tool_result`, `finished`, `failed` |
1238
+ | `status` | a pre-rendered line, ready to show |
1239
+ | `tool` | `toolCallId`, `name` and `ok`, on the two tool phases only |
1240
+
1241
+ Exactly one `started` opens a delegation and exactly one `finished` or `failed`
1242
+ closes it. `ok` is a tri-state: `null` while the child's call runs, `true` on a
1243
+ result it accepted, `false` on one that came back to it.
1244
+
1245
+ Because the key is the *parent's* call id, the surface attaches to a card that
1246
+ already exists rather than floating a second element with the same identity: one
1247
+ **collapsed row per delegation**, live, carrying the server's `status` line and
1248
+ nothing else, expanding onto the child's own tool calls. A ten-step child costs
1249
+ one row until somebody opens it, and there is no second visual language — it
1250
+ reads the way tool cards already read.
1251
+
1252
+ `status` is why the collapsed row needs no wording of its own. The structured
1253
+ keys are there for a host that would rather write its own.
1254
+
1255
+ The row shows in **every** [display mode](#tool-call-display-modes), including
1256
+ `minimal`. It sits outside the card body rather than in it, because the body is
1257
+ what the density modes hide — and a live progress line that only appeared in
1258
+ `full` would leave exactly the stall it exists to end. Same reasoning that shows
1259
+ a deferred card's arguments whatever the mode.
1260
+
1261
+ **A failure carries no exception text on this channel, deliberately** — the same
1262
+ reasoning that redacts a `RUN_ERROR`, since an exception's words are written for
1263
+ an operator. The detail rides the ordinary tool result for that delegation, which
1264
+ lands in the same card's `Result` region a few pixels below. Nothing here invents
1265
+ words the server declined to send.
1266
+
1267
+ **None of it is persisted.** A `CUSTOM` event never enters the message list, so
1268
+ nothing replays on a thread restore — which is the right half of the
1269
+ [carrier split](#which-carrier-should-the-server-use): a delegation that was live
1270
+ an hour ago is not live now, and replaying its progress would be a lie about a
1271
+ run that is over. Reload mid-run and the tool card is still there; the nested
1272
+ detail is not. That is the intended behaviour.
1273
+
1274
+ Like `ag_ui.invalidate`, this name is **routed** rather than forwarded: it draws
1275
+ itself and does not also arrive as an `ag-ui-custom` event. Every other name
1276
+ still reaches your page untouched.
1277
+
1278
+ Style it through `tool-card-subagent` (the region inside the card), `subagent`,
1279
+ `subagent-row`, `subagent-icon`, `subagent-status`, `subagent-steps`,
1280
+ `subagent-step`, `subagent-step-icon` and `subagent-step-name`. The two glyph
1281
+ states reuse the card's own `--ag-ui-tool-icon-done` / `--ag-ui-tool-icon-error`
1282
+ properties and its spinner speed, so re-theming the cards re-themes these. The
1283
+ row's own chrome comes from `subAgentWorking` and `subAgentSteps` in
1284
+ [`UiStrings`](#internationalization-i18n); everything else on the row is the
1285
+ server's text.
1286
+
1287
+ ---
1288
+
1060
1289
  ## Resizing the panel
1061
1290
 
1062
1291
  The panel carries a drag handle on its leading corner (or leading edge, docked),
@@ -1127,6 +1356,197 @@ reveal). It honours `prefers-reduced-motion` (collapsing to instant).
1127
1356
 
1128
1357
  ---
1129
1358
 
1359
+ ## Follow-up suggestions
1360
+
1361
+ The agent can offer the *next* question, not just answer this one. Registered
1362
+ skill chips are static and host-configured, so they can say "summarize this" but
1363
+ never "want me to update the shipping address too?" after a tool has run.
1364
+
1365
+ The server pushes an `ACTIVITY_SNAPSHOT` under the `suggestions` type — with
1366
+ django-ag-ui, `suggestions_activity([...])`. The component draws each prompt as
1367
+ a chip; clicking one sends it as the user's message, exactly as if they had
1368
+ typed it.
1369
+
1370
+ ```json
1371
+ { "activityType": "suggestions",
1372
+ "content": { "prompts": ["Update the shipping address too"] } }
1373
+ ```
1374
+
1375
+ **Chips are content.** They ride the same activity envelope as charts, so they
1376
+ persist and a reload puts them back, and a set pushed under an id already on
1377
+ screen replaces that row rather than adding a second one.
1378
+
1379
+ At most **4** prompts of **120** characters each — `MAX_SUGGESTIONS` and
1380
+ `MAX_SUGGESTION_CHARS`, both exported. The server-side helper *raises* past
1381
+ those bounds while this side silently drops: that asymmetry is deliberate, since
1382
+ the producer can report the problem and the client cannot.
1383
+
1384
+ Nothing to enable. A `suggestions` activity from a server that pushes one is
1385
+ drawn; an `activity_type` this component does not know is ignored, which is what
1386
+ the open field is for.
1387
+
1388
+ ## Editing a gated call before approving it
1389
+
1390
+ AG-UI's resume payload carries `editedArgs`, and the protocol gates it on the
1391
+ agent's own `approveWithEdits` capability. The approval card can offer it:
1392
+
1393
+ ```js
1394
+ chat.approveWithEdits = true; // your server accepts editedArgs
1395
+ ```
1396
+
1397
+ **Off by default, and an assertion about your server rather than a
1398
+ negotiation** — capabilities are not on the wire this component reads, so it
1399
+ cannot check. Turned on against a server that ignores `editedArgs`, a user would
1400
+ edit arguments it silently discards, which is worse than not offering.
1401
+
1402
+ The card then shows the call's arguments as editable JSON. `editedArgs` rides
1403
+ the resume payload **only when something actually changed**, so a server can tell
1404
+ "approved as proposed" from "approved, but like this" without diffing what it
1405
+ already sent. Unparseable JSON, or JSON that is not an object, keeps the card
1406
+ open with the reason on it rather than approving the original behind the user's
1407
+ back.
1408
+
1409
+ Only offered for an interrupt naming a tool call this component holds a card
1410
+ for — the card is where the arguments still are.
1411
+
1412
+ ## Localizing the timestamps
1413
+
1414
+ There is **no `Intl` anywhere in this component**. The relative timestamps in the
1415
+ thread drawer and checkpoint panel (`"5m ago"`, `"2d ago"`) are deliberately
1416
+ locale-neutral: a component that guessed a locale would disagree with the page
1417
+ it is embedded in, and being wrong in a second language is worse than being
1418
+ neutral in one.
1419
+
1420
+ That is a good default and a bad requirement, so it is replaceable:
1421
+
1422
+ ```js
1423
+ const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
1424
+ chat.formatRelativeTime = (ts) =>
1425
+ rtf.format(Math.round((ts - Date.now()) / 60000), "minute");
1426
+ ```
1427
+
1428
+ `relativeTime` is exported too, for a host that wants to build on the built-in
1429
+ rather than replace it.
1430
+
1431
+ ## Message actions: copy, retry, feedback
1432
+
1433
+ Every finished assistant message carries a small row of actions beneath it —
1434
+ a **sibling** of the bubble, never a child, so the buttons never join the
1435
+ message's own text.
1436
+
1437
+ - **Retry** re-asks the question. History is truncated to the most recent user
1438
+ message inclusive and the run repeats, so the agent answers what it was asked
1439
+ rather than being told its last answer was wrong.
1440
+ - **Copy** puts the message's text on the clipboard, and says so on the button.
1441
+ A refused clipboard permission is reported there too, rather than thrown.
1442
+ - **Thumbs up / down** fire `ag-ui-feedback` (wired below) and **store nothing**.
1443
+
1444
+ Retry sits on the **last** answer only. Re-running an older turn is branching,
1445
+ and for a page-driving agent editing a past turn is not neutral — those turns
1446
+ clicked buttons, and re-running turn 3 does not un-save what turn 5 saved.
1447
+
1448
+ !!! note
1449
+ A retried turn **re-runs its tools.** The previous attempt already did what
1450
+ it did, and this does not undo it. Confirmation still applies, so a
1451
+ destructive tool asks again — unless the user waived it for this session
1452
+ with *Always allow*.
1453
+
1454
+ A failed run gets the same row, with Retry and Copy and no rating: error text is
1455
+ what people paste into a bug report, but "the connection dropped" is not a
1456
+ statement about answer quality and mixing it into feedback makes that signal say
1457
+ less. This is why a dropped connection is still rendered as an **error** rather
1458
+ than demoted to a run notice — a notice "never settles, takes no action, and
1459
+ carries no controls", and a failure with a way back needs one.
1460
+
1461
+ The row can be trimmed, or removed, with `data-message-actions` — a comma list of
1462
+ the actions to keep, or `="false"` for none at all:
1463
+
1464
+ ```html
1465
+ <!-- copy only: nothing here listens for a rating, and the surface forbids re-runs -->
1466
+ <ag-ui-chat endpoint="/agent/" data-message-actions="copy"></ag-ui-chat>
1467
+ ```
1468
+
1469
+ It is per-action rather than one switch because the three disappear for
1470
+ different reasons. Thumbs are only useful to a host listening for
1471
+ `ag-ui-feedback`, and two buttons that lead nowhere are worse than none. Retry
1472
+ re-runs the agent, which a constrained surface may not permit. Copy is the one
1473
+ nobody objects to — and with a single switch, dropping either of the others would
1474
+ have cost it too. Nothing survives, and no row is built at all: an empty row
1475
+ still takes its margin and still announces itself as a group of actions.
1476
+
1477
+ `retryLastTurn()` is public, for a host driving its own message UI.
1478
+
1479
+ ```js
1480
+ chat.addEventListener("ag-ui-feedback", (e) => {
1481
+ analytics.track("assistant_rating", e.detail); // { content, rating }
1482
+ });
1483
+
1484
+ await chat.retryLastTurn(); // false when there is nothing to ask again
1485
+ ```
1486
+
1487
+ ## Quoting a selection
1488
+
1489
+ Select any text in the transcript and a small **Quote** offer floats beside it.
1490
+ Taking it drops the selection into the composer as a markdown blockquote and
1491
+ leaves the caret on a fresh line under it — a quotation is how a question
1492
+ narrows to one part of an answer, so nothing is sent until you say what you are
1493
+ asking.
1494
+
1495
+ Quoting **appends**, after whatever is already typed, so a second quotation is a
1496
+ second thing being asked about rather than a replacement for the first. Long
1497
+ selections are capped at 500 characters: select-all-then-quote is a gesture the
1498
+ transcript already answers, and pasting the whole conversation back costs tokens
1499
+ to say nothing.
1500
+
1501
+ Set `data-quote-selection="false"` to turn the offer off. The `quote-selection`
1502
+ `part` styles it.
1503
+
1504
+ ### The half that matters: selection in **your** page
1505
+
1506
+ The transcript is the easy half. A chat mounted beside a table, a diff or a
1507
+ report is sitting in the surface the user actually works in — and *that*
1508
+ selection is one no hosted chat can reach.
1509
+
1510
+ `offerQuoteInPage()` extends the same select-then-offer gesture to the whole
1511
+ page. It is opt-in, because it listens on your document:
1512
+
1513
+ ```js
1514
+ const stop = chat.offerQuoteInPage(); // the whole page
1515
+ chat.offerQuoteInPage(document.querySelector("#report")); // or one region
1516
+ ```
1517
+
1518
+ For a deliberate trigger instead of a selection, `quote(text)` is the seam
1519
+ underneath:
1520
+
1521
+ ```js
1522
+ // "Ask about this row" — a button on each row of your own table.
1523
+ row.querySelector(".ask").addEventListener("click", () => {
1524
+ chat.quote(row.innerText);
1525
+ });
1526
+ ```
1527
+
1528
+ `quote()` never sends — pair it with [`sendMessage()`](#sending-from-your-own-ui)
1529
+ if you want a one-click "explain this" that skips the composer entirely.
1530
+
1531
+ > **Do not write the four-line version of `offerQuoteInPage()`.**
1532
+ > A `mouseup` listener that quotes every settled selection appends to the
1533
+ > composer on every drag the user made to *read*, to copy, or to fix a typo —
1534
+ > and it cannot tell a selection in your prose from one inside the user's own
1535
+ > half-typed `<input>`, because Chrome reports a field's internal selection
1536
+ > through `document.getSelection()` as an ordinary range over the field's
1537
+ > **wrapper**. The text reads back perfectly and nothing about the range says
1538
+ > where it came from; the only signal is `document.activeElement`. That guard,
1539
+ > plus skipping the widget's own transcript, plus retiring a fixed-position
1540
+ > affordance on scroll, is what the method is for.
1541
+
1542
+ > **Reading a selection out of a shadow tree takes care too.**
1543
+ > Engines disagree about what `document.getSelection()` reports for a selection
1544
+ > made *inside* a shadow root: WebKit rescopes the endpoints to the host element,
1545
+ > so you get the whole widget and none of the words, while Chromium hands back
1546
+ > the shadow nodes directly. `getComposedRanges` settles it, and this component
1547
+ > uses it where it exists. `quotableSelection(container, roots)` is exported if
1548
+ > you have the same problem in your own component.
1549
+
1130
1550
  ## Run notices: compaction and agent skills
1131
1551
 
1132
1552
  Some things a run does are neither text nor a tool the user asked for — the server condensed
@@ -1467,6 +1887,83 @@ substitute the other way round either, because shared state requires the *agent*
1467
1887
  `STATE_SNAPSHOT`, which is not the host's decision to make. Use state when the two ends edit one
1468
1888
  object; use this when your page owns the data and just needs to know it moved.
1469
1889
 
1890
+ **`ag-ui-custom`** *(event)* — the agent sent an AG-UI `CUSTOM` event.
1891
+ `detail: { name, value }` (typed `CustomAgentDetail`), both verbatim and uninterpreted.
1892
+
1893
+ ```js
1894
+ chat.addEventListener("ag-ui-custom", (e) => {
1895
+ if (e.detail.name === "invalidate") {
1896
+ void refetch(e.detail.value);
1897
+ }
1898
+ // Any other name: no listener, nothing happens. That is the intended outcome.
1899
+ });
1900
+ ```
1901
+
1902
+ `CUSTOM` is one of exactly two AG-UI carriers whose payload name is an open
1903
+ string the protocol does not enumerate, and it is the **imperative** one:
1904
+ something for your page to *do*. Its sibling `ACTIVITY_SNAPSHOT` carries
1905
+ transcript **content**, which is why an activity is materialised into a message,
1906
+ persisted with the thread and replayed on restore, and this is not.
1907
+
1908
+ **That asymmetry is the rule for choosing between them.** Content has a place in
1909
+ the conversation and should replay. An imperative has no place and no meaning
1910
+ once acted on — replaying "refetch the board" on every thread load is a bug, not
1911
+ a feature. If it must survive a reload, it belongs on the other carrier.
1912
+
1913
+ The element takes no view of what a name means and forwards every one, so a name
1914
+ it has never heard of reaches you unchanged. A host with no listener for a name
1915
+ simply ignores it, which is the graceful outcome an open field exists for.
1916
+
1917
+ **`ag-ui-invalidate`** *(event)* — the agent named resources its write moved.
1918
+ `detail: { keys, reason }` (typed `InvalidateDetail`).
1919
+
1920
+ One `CUSTOM` name routed to its own event so you do not have to string-match;
1921
+ every other name still arrives as `ag-ui-custom`. It fires **as each
1922
+ announcement arrives**, during the run, and the same keys ride
1923
+ `ag-ui-run-finished` again at the end as `invalidated`, de-duplicated.
1924
+
1925
+ > **Do not reload the page on this.** The user was probably typing. An
1926
+ > agent-triggered reload or a blind refetch into a live form destroys unsaved
1927
+ > input, and from their side the page threw their work away on its own. Check
1928
+ > first, and offer rather than act:
1929
+
1930
+ ```js
1931
+ chat.addEventListener("ag-ui-invalidate", (e) => {
1932
+ if (formIsDirty()) {
1933
+ showBanner("This data changed. Refresh when you're ready.", e.detail.keys);
1934
+ return;
1935
+ }
1936
+ refetch(e.detail.keys); // e.detail.keys → ["orders", "orders/42"]
1937
+ });
1938
+ ```
1939
+
1940
+ **Keys are opaque and matching is exact.** `orders/42` does not imply `orders` —
1941
+ a prefix rule would be the component guessing at a scheme it does not own, and
1942
+ `orders/1` would match `orders/11`. A server that wants the collection refreshed
1943
+ names it. Your own matching may be hierarchical, because in your vocabulary the
1944
+ scheme is known; that is what TanStack query keys are built for.
1945
+
1946
+ Already listening on `ag-ui-run-finished`? Upgrading is one line, and the `else`
1947
+ is the whole compatibility story:
1948
+
1949
+ ```js
1950
+ if (detail.invalidated.length > 0) refetchOnly(detail.invalidated);
1951
+ else if (detail.tools.some((t) => t.side === "server")) refetchEverything();
1952
+ ```
1953
+
1954
+ | Server | Client | Result |
1955
+ | --- | --- | --- |
1956
+ | old | old | coarse refetch, as today |
1957
+ | new | old | the `CUSTOM` event is ignored; coarse refetch still fires |
1958
+ | old | new | `invalidated` is empty; the `else` branch runs |
1959
+ | new | new | precise, and live during the run |
1960
+
1961
+ Nothing negotiates and nothing handshakes, which is what makes this shippable
1962
+ across repos with independent release cadences.
1963
+
1964
+ **Note:** it reaches **the page that started the run**, during the run. There is one
1965
+ response stream per run and no channel to anybody else's browser.
1966
+
1470
1967
  ## Resuming a run
1471
1968
 
1472
1969
  When the server persists run checkpoints (django-ag-ui's `step_store`), a run
@@ -1624,9 +2121,9 @@ re-export point. Internal modules import from leaf paths.
1624
2121
  | `isNavigates(parameters)` | function | Read the `x-navigates` flag. |
1625
2122
  | `createPageActionTools(enabled, resolveTarget)` | function | Build the opt-in `scroll_to` / `drag_and_drop` tools. |
1626
2123
  | `PAGE_ACTIONS` | const | The page-action opt-in tokens (`scroll` / `drag`). |
1627
- | `ResolvePageTarget` | type | `(target) => HTMLElement | null` — the page-target resolver. |
2124
+ | `ResolvePageTarget` | type | `(target) => HTMLElement \| null` — the page-target resolver. |
1628
2125
  | `X_DESTRUCTIVE_KEY` / `X_NAVIGATES_KEY` | const | The JSON-Schema extension keys. |
1629
- | `parseToolCatalog(data)` | function | Parse a fetched `data-tools-url` catalog into a `name` `summary` map. |
2126
+ | `parseToolCatalog(data)` | function | Parse a fetched `data-tools-url` catalog into a `Record<string, ToolCatalogEntry>` — whole entries, not bare summaries, so a caller can reach `description` too. Malformed input yields an empty map rather than throwing. |
1630
2127
  | `ToolCatalogEntry` | type | One row of that catalog. |
1631
2128
  | `prettifyToolName(name)` | function | Last fallback of the tool-card label chain (`delete_record` reads as *Delete record*). |
1632
2129
 
@@ -1643,6 +2140,18 @@ re-export point. Internal modules import from leaf paths.
1643
2140
  | `PageState` | type | A page-state binding declaration. |
1644
2141
  | `Skill` | type | A launchable prompt (chip / `/`-command). |
1645
2142
  | `RunFinishedDetail` / `ToolRun` | type | `ag-ui-run-finished` detail: the tools an interaction ran, and which side ran them. |
2143
+ | `CustomAgentDetail` | type | `ag-ui-custom` detail: an AG-UI `CUSTOM` event's `name` and `value`, verbatim. |
2144
+ | `InvalidateDetail` | type | `ag-ui-invalidate` detail: the resource `keys` that moved, and the `reason`. |
2145
+ | `FeedbackDetail` | type | `ag-ui-feedback` detail: the rated message's `content` and the `rating`. |
2146
+ | `relativeTime` | function | The built-in locale-neutral timestamp formatter (`"5m ago"`), for a host building on it. |
2147
+ | `RelativeTimeFormatter` | type | What `formatRelativeTime` takes: an epoch-ms timestamp in, row text out. |
2148
+ | `renderSuggestionChips` | function | Draw a `suggestions` activity as chips that send themselves; `null` when nothing survives. |
2149
+ | `suggestionPrompts` | function | The usable prompts in a `suggestions` activity's content, bounded and trimmed. |
2150
+ | `attachMessageActions` | function | Give a finished bubble its action row (copy, and feedback when a handler is passed). |
2151
+ | `messageActionBar` | function | The empty action row for a bubble, created if it has none — the shared shell both callers use. |
2152
+ | `MessageActionsOptions` | type | What `attachMessageActions` takes: `strings`, a `text` source, an optional `onFeedback`. |
2153
+ | `ActivityRenderer` | type | Draws one activity from its `content`. Pure: it runs again on every restore. |
2154
+ | `ActivityRegistration` | type | What `registerActivityRenderer` takes: `type`, `render`, and an optional `removedNotice`. |
1646
2155
  | `createStateHookTools(binding)` / `StateHook` | deprecated | The former names for `createPageStateTools` / `PageState`. |
1647
2156
 
1648
2157
  ### Durability
@@ -1657,15 +2166,15 @@ re-export point. Internal modules import from leaf paths.
1657
2166
  | `RunIndex` | class | Reads a `data-runs-url` run index and derives its resume / fork endpoints. |
1658
2167
  | `RunRow` | type | One run index row (`{ run_id, thread_id, parent_run_id, started_at, continuable, preview? }`). |
1659
2168
  | `CheckpointMenu` | class | The *Continue a run* panel. |
1660
- | `CheckpointVerb` | type | `"resume" | "fork"`. |
2169
+ | `CheckpointVerb` | type | `"resume" \| "fork"`. |
1661
2170
 
1662
2171
  ### Attachments
1663
2172
 
1664
2173
  | Export | Kind | Summary |
1665
2174
  | --- | --- | --- |
1666
2175
  | `uploadAttachment(file, options)` | function | The built-in upload (multipart, progress) → `AttachmentRef`. |
1667
- | `UploadOptions` | type | `{ url, headers?, onProgress?, signal? }`. |
1668
- | `UploadHandler` | type | `(file, onProgress) => Promise<AttachmentRef>` — the `uploadHandler` swap seam (TUS / S3). |
2176
+ | `UploadOptions` | type | `{ url, headers?, credentials?, onProgress?, signal? }`. `credentials` is spelled as a fetch mode but carried by `XMLHttpRequest.withCredentials`, so only `"include"` is distinguishable. |
2177
+ | `UploadHandler` | type | `(file, onProgress, signal?) => Promise<AttachmentRef>` — the `uploadHandler` swap seam (tus / S3). The signal fires when the tray removes a chip or the element is torn down; a handler that honours it aborts its own transport, so a cancelled upload leaves no orphaned file on the server. |
1669
2178
  | `AttachmentRef` | type | The durable upload ref (`{ id, name, mime, size, url? }`). |
1670
2179
  | `messageAttachments(message)` | function | Read the refs a restored user message carries. |
1671
2180
 
@@ -1674,7 +2183,7 @@ re-export point. Internal modules import from leaf paths.
1674
2183
  | Export | Kind | Summary |
1675
2184
  | --- | --- | --- |
1676
2185
  | `transcribeAudio(audio, options)` | function | The built-in transcription POST (multipart) → the transcript text. |
1677
- | `TranscribeOptions` | type | `{ url, headers? }`. |
2186
+ | `TranscribeOptions` | type | `{ url, headers?, credentials? }` — `credentials` as fetch's own cookie mode. |
1678
2187
  | `TranscribeHandler` | type | `(audio) => Promise<string>` — the `transcribeHandler` swap seam (Web Speech, direct-to-provider). |
1679
2188
 
1680
2189
  ### UI & DOM primitives
@@ -1683,25 +2192,34 @@ re-export point. Internal modules import from leaf paths.
1683
2192
  | --- | --- | --- |
1684
2193
  | `ToolCallCard` | class | A live tool-call card for the transcript. |
1685
2194
  | `ToolCallStatus` / `SettledStatus` / `ToolDisplayMode` | type | Card lifecycle states + display mode. |
2195
+ | `ToolPayloadFormatter` | type | Draws one region of a card's body (`AgUiChat.formatToolPayload`); `null` falls through to the built-in pretty-print. |
2196
+ | `ToolPayload` | type | The region being drawn: `arguments` (the parsed record) or `result` (the raw string and its outcome). |
2197
+ | `ToolCallCardOptions` | type | Per-card wiring beyond name / args / label / strings — currently `formatPayload`. |
1686
2198
  | `requestConfirmation(host, request, options?)` | function | Append the inline confirmation card to the transcript. |
1687
2199
  | `ConfirmationRequest` | type | What the card displays. |
1688
- | `ConfirmationOptions` | type | `{ signal?, strings? }` — abort resolves the card as declined; `strings` localizes it. |
2200
+ | `ConfirmationOptions` | type | `{ signal?, strings?, onAlwaysAllow? }` — abort resolves the card as declined; `strings` localizes it; passing `onAlwaysAllow` is what adds the third button. |
1689
2201
  | `UiStrings` | type | The flat table of every user-facing string. |
1690
2202
  | `DEFAULT_UI_STRINGS` | const | The English defaults (the override floor). |
1691
2203
  | `mergeUiStrings(overrides)` | function | Merge a partial override over the defaults. |
1692
2204
  | `renderMarkdown(text, options?)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
1693
2205
  | `RenderMarkdownOptions` | type | `{ allowImages? }` — opt `<img>` back into the sanitized output. |
1694
2206
  | `requestApproval(host, request, options?)` | function | Append the inline approval card that gates a server-side tool. |
1695
- | `ApprovalRequest` | type | What that card displays (`{ message?, toolName? }`). |
1696
- | `ApprovalOptions` | type | `{ signal?, strings? }` — abort resolves the card as denied; `strings` localizes it. |
2207
+ | `ApprovalRequest` | type | What that card displays (`{ message?, toolName?, args? }`). |
2208
+ | `ApprovalOptions` | type | `{ signal?, strings?, onEdit? }` — abort resolves the card as denied; `strings` localizes it; passing `onEdit` offers the call's arguments for editing and is called only when they actually changed. |
1697
2209
  | `ApprovalRenderer` | type | Replace the built-in approval card outright (`AgUiChat.approvalRenderer`). |
1698
2210
  | `requestQuestion(host, request, options?)` | function | Append the inline `ask_user` card (radios and/or free text). |
1699
2211
  | `QuestionRequest` | type | What that card asks. |
1700
2212
  | `QuestionOptions` | type | `{ signal?, strings? }` — abort resolves it with an empty answer. |
1701
- | `QuestionRenderer` | type | Replace the built-in question card outright (`AgUiChat.questionRenderer`). |
2213
+ | `QuestionRenderer` | type | Replace the built-in question card outright (`AgUiChat.askUserRenderer`). |
1702
2214
  | `renderChart(spec)` | function | Draw one spec as a self-contained block, or `null` when it says nothing. |
1703
2215
  | `chartSpecFrom(value)` | function | Read an arbitrary payload into a `ChartSpec`, or `null` if it cannot be drawn honestly. |
1704
2216
  | `ChartSpec` / `ChartSeries` / `ChartKind` | type | A chart as data, one named series, and how it is drawn. |
2217
+ | `attachQuoteOffer(options)` | function | The page-side select-then-quote offer, with its guards. `AgUiChat.offerQuoteInPage()` is the one-line form. |
2218
+ | `PageQuoteOffer` / `PageQuoteOfferOptions` | type | The live offer (`{ element, detach }`) and what it takes. |
2219
+ | `quotableSelection(container, roots, near?)` | function | The current selection when it lies inside `container`, read through the shadow-aware API where the engine has one. `near` is where the gesture ended, used to pick the line the offer hangs from. |
2220
+ | `QuotableSelection` | type | `{ text, rect }` — what was selected, and where it sits. |
2221
+ | `asQuote(text)` | function | Shape text as a markdown blockquote with a blank line after it. |
2222
+ | `MAX_QUOTE_CHARS` | const | The cap a quotation is truncated to (500). |
1705
2223
  | `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `flash` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
1706
2224
  | `fillField` / `clickElement` / `pressButton` / `selectControl` / `setControlValue` / `toggleCheckbox` | function | DOM-driver primitives. |
1707
2225
  | `setNativeValue` / `setNativeChecked` | function | Set a control via its native prototype setter (React-controlled inputs). |
@@ -1716,6 +2234,13 @@ re-export point. Internal modules import from leaf paths.
1716
2234
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1717
2235
  | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1718
2236
  | `RUN_FINISHED_EVENT` | The interaction-finished CustomEvent name (`ag-ui-run-finished`). |
2237
+ | `CUSTOM_AGENT_EVENT` | The agent-`CUSTOM` CustomEvent name (`ag-ui-custom`). |
2238
+ | `INVALIDATE_EVENT` | The resource-invalidation CustomEvent name (`ag-ui-invalidate`). |
2239
+ | `FEEDBACK_EVENT` | The message-rating CustomEvent name (`ag-ui-feedback`). |
2240
+ | `SUGGESTIONS_ACTIVITY_TYPE` | The `activity_type` carrying follow-up prompts (`suggestions`). |
2241
+ | `MAX_SUGGESTIONS` | Most prompts one push draws (4). Mirrors the server's cap. |
2242
+ | `MAX_SUGGESTION_CHARS` | Longest one prompt may be (120). Mirrors the server's cap. |
2243
+ | `INVALIDATE_CUSTOM_NAME` | The AG-UI `CUSTOM` `name` that carries one (`ag_ui.invalidate`). |
1719
2244
  | `ATTACHMENT_EVENT` | The attachments-changed CustomEvent name (`ag-ui-attachments`). |
1720
2245
  | `STATE_EVENT` | The shared-state CustomEvent name (`ag-ui-state`). |
1721
2246
  | `CHART_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type a server sets to push a chart. |
@@ -1723,8 +2248,9 @@ re-export point. Internal modules import from leaf paths.
1723
2248
  | `COMPACTION_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type reporting a trimmed history. |
1724
2249
  | `LOAD_CAPABILITY_TOOL` | The agent-side capability-loading tool's name. |
1725
2250
  | `MESSAGE_ROLE` | Message role constants. |
2251
+ | `MESSAGE_ACTIONS` | The message-action tokens `data-message-actions` selects by (`copy` / `retry` / `feedback`). |
1726
2252
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1727
- | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
2253
+ | `TOOL_DISPLAY` | Tool-call display-mode constants (`inline` / `minimal` / `compact` / `full`). |
1728
2254
  | `X_CONFIRM_KEY` | Confirmation-prompt key: on a tool's JSON Schema for a client-side confirmation, and in an AG-UI interrupt's `metadata` for a server-side approval. |
1729
2255
  | `X_SUMMARY_KEY` | JSON-Schema key carrying a short tool-card label. |
1730
2256
  | `MAX_TOOL_ROUNDS` | Upper bound on tool-call → re-run rounds per send. |
@@ -1855,15 +2381,18 @@ component sets, so a new one cannot ship undocumented.
1855
2381
 
1856
2382
  | Feature | Parts |
1857
2383
  | --- | --- |
1858
- | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `resize-handle` |
2384
+ | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `jump-latest`, `resize-handle` |
1859
2385
  | Header buttons | `header-button` on each, plus `history-button`, `checkpoints-button`, `new-button`, `collapse-button`, `theme-toggle` |
1860
2386
  | Collapsed widget | `launcher`, `launcher-icon`, `launcher-badge` |
1861
2387
  | Answers | `answer` (the per-turn group), `message` (plus `message-user`, `message-assistant`), `code-copy` |
1862
2388
  | Reasoning | `thoughts`, `thoughts-toggle`, `thoughts-body`, `thoughts-label` |
1863
- | Run notices | `run-notice` (plus `run-notice-interrupted`, `run-notice-attachment-pending`, `run-notice-compaction`, `run-notice-skill`), `run-notice-icon`, `run-notice-text` |
1864
- | Tool cards | `tool-card`, `tool-card-head`, `tool-card-icon`, `tool-card-name`, `tool-card-status`, `tool-card-decision`, `tool-card-toggle`, `tool-card-body`, `tool-card-section` (plus `tool-card-args-section`, `tool-card-result-section`), `tool-card-section-label` (plus `tool-card-args-label`, `tool-card-result-label`), `tool-card-args`, `tool-card-result`, `tool-card-approval` |
1865
- | Client-side confirmation | `confirm`, `confirm-body`, `confirm-args`, `confirm-actions`, `confirm-button` (plus `confirm-confirm`, `confirm-cancel`) |
1866
- | Server-side approval | `approval`, `approval-body`, `approval-actions`, `approval-button` (plus `approval-approve`, `approval-deny`) |
2389
+ | Follow-up suggestions | `suggestions`, `suggestion-chip` |
2390
+ | Message actions | `message-actions`, `message-action` (plus `message-action-retry`, `message-action-copy`, `message-action-up`, `message-action-down`) |
2391
+ | Run notices | `run-notice` (plus `run-notice-interrupted`, `run-notice-attachment-pending`, `run-notice-compaction`, `run-notice-skill`, `run-notice-history-replaced`, `run-notice-chart-undrawable`), `run-notice-icon`, `run-notice-text` |
2392
+ | Tool cards | `tool-card`, `tool-card-head`, `tool-card-icon`, `tool-card-name`, `tool-card-status`, `tool-card-decision`, `tool-card-toggle`, `tool-card-body`, `tool-card-section` (plus `tool-card-args-section`, `tool-card-result-section`), `tool-card-section-label` (plus `tool-card-args-label`, `tool-card-result-label`), `tool-card-args`, `tool-card-result`, `tool-card-approval`, `tool-card-subagent` |
2393
+ | Delegated sub-agents | `subagent`, `subagent-row`, `subagent-icon`, `subagent-status`, `subagent-steps`, `subagent-step`, `subagent-step-icon`, `subagent-step-name` |
2394
+ | Client-side confirmation | `confirm`, `confirm-body`, `confirm-args`, `confirm-actions`, `confirm-button` (plus `confirm-confirm`, `confirm-cancel`, `confirm-always`) |
2395
+ | Server-side approval | `approval`, `approval-body`, `approval-actions`, `approval-button` (plus `approval-approve`, `approval-deny`), `approval-edit`, `approval-args`, `approval-error` |
1867
2396
  | Typed question | `question`, `question-body`, `question-options`, `question-choice`, `question-choice-text`, `question-radio`, `question-input`, `question-actions`, `question-button` |
1868
2397
  | Composer | `composer`, `composer-surface`, `composer-tools`, `input`, `send`, `attach-button`, `voice-button` |
1869
2398
  | Attachments | `attachment-tray`, `attachment-chips` (the read-only chips on sent bubbles), and the shared chip parts `attachment-chip`, `attachment-chip-icon`, `attachment-chip-name`, `attachment-chip-size`, `attachment-chip-bar`, `attachment-chip-bar-fill`, `attachment-chip-retry`, `attachment-chip-remove` |