@artooi/ag-ui-web-component 0.27.0 → 0.29.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 (85) hide show
  1. package/CHANGELOG.md +663 -1
  2. package/README.md +557 -11
  3. package/dist/ag-ui-web-component.bundle.js +294 -36
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +69 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +262 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +46 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/core/conversation_store.d.ts +43 -1
  12. package/dist/core/conversation_store.d.ts.map +1 -1
  13. package/dist/core/create_http_agent.d.ts +13 -0
  14. package/dist/core/create_http_agent.d.ts.map +1 -1
  15. package/dist/core/remote_conversation_store.d.ts +23 -1
  16. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  17. package/dist/core/utils.d.ts +28 -0
  18. package/dist/core/utils.d.ts.map +1 -1
  19. package/dist/index.d.ts +7 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2104 -194
  22. package/dist/index.js.map +4 -4
  23. package/dist/tools/is_destructive.d.ts +8 -2
  24. package/dist/tools/is_destructive.d.ts.map +1 -1
  25. package/dist/tools/parse_tool_catalog.d.ts +11 -4
  26. package/dist/tools/parse_tool_catalog.d.ts.map +1 -1
  27. package/dist/ui/approval_card.d.ts +18 -0
  28. package/dist/ui/approval_card.d.ts.map +1 -1
  29. package/dist/ui/checkpoint_menu.d.ts +10 -0
  30. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  31. package/dist/ui/confirmation_card.d.ts +16 -0
  32. package/dist/ui/confirmation_card.d.ts.map +1 -1
  33. package/dist/ui/message_actions.d.ts +46 -0
  34. package/dist/ui/message_actions.d.ts.map +1 -0
  35. package/dist/ui/page_quote_offer.d.ts +33 -0
  36. package/dist/ui/page_quote_offer.d.ts.map +1 -0
  37. package/dist/ui/quote_selection.d.ts +66 -0
  38. package/dist/ui/quote_selection.d.ts.map +1 -0
  39. package/dist/ui/relative_time.d.ts +10 -0
  40. package/dist/ui/relative_time.d.ts.map +1 -1
  41. package/dist/ui/render_markdown.d.ts +23 -5
  42. package/dist/ui/render_markdown.d.ts.map +1 -1
  43. package/dist/ui/resize_handle.d.ts +5 -1
  44. package/dist/ui/resize_handle.d.ts.map +1 -1
  45. package/dist/ui/stick_to_bottom.d.ts +55 -0
  46. package/dist/ui/stick_to_bottom.d.ts.map +1 -0
  47. package/dist/ui/styles.d.ts +1 -1
  48. package/dist/ui/styles.d.ts.map +1 -1
  49. package/dist/ui/suggestion_chips.d.ts +29 -0
  50. package/dist/ui/suggestion_chips.d.ts.map +1 -0
  51. package/dist/ui/thread_drawer.d.ts +10 -0
  52. package/dist/ui/thread_drawer.d.ts.map +1 -1
  53. package/dist/ui/tool_call_card.d.ts +8 -0
  54. package/dist/ui/tool_call_card.d.ts.map +1 -1
  55. package/dist/ui/ui_strings.d.ts +53 -7
  56. package/dist/ui/ui_strings.d.ts.map +1 -1
  57. package/dist/ui/voice_input.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/constants.ts +75 -0
  60. package/src/core/ag_ui_chat.ts +1357 -113
  61. package/src/core/agui_client.ts +81 -1
  62. package/src/core/conversation_store.ts +128 -42
  63. package/src/core/create_http_agent.ts +24 -2
  64. package/src/core/remote_conversation_store.ts +35 -2
  65. package/src/core/utils.ts +58 -0
  66. package/src/index.ts +39 -0
  67. package/src/tools/is_destructive.ts +8 -2
  68. package/src/tools/parse_tool_catalog.ts +18 -6
  69. package/src/ui/approval_card.ts +90 -2
  70. package/src/ui/checkpoint_menu.ts +22 -5
  71. package/src/ui/confirmation_card.ts +29 -1
  72. package/src/ui/message_actions.ts +158 -0
  73. package/src/ui/page_quote_offer.ts +215 -0
  74. package/src/ui/quote_selection.ts +345 -0
  75. package/src/ui/relative_time.ts +11 -0
  76. package/src/ui/render_markdown.ts +111 -21
  77. package/src/ui/resize_handle.ts +32 -2
  78. package/src/ui/stick_to_bottom.ts +126 -0
  79. package/src/ui/styles.ts +227 -0
  80. package/src/ui/suggestion_chips.ts +73 -0
  81. package/src/ui/thread_drawer.ts +22 -2
  82. package/src/ui/tool_call_card.ts +9 -0
  83. package/src/ui/ui_strings.ts +79 -8
  84. package/src/ui/voice_input.ts +43 -0
  85. package/src/version.ts +1 -1
package/README.md CHANGED
@@ -50,9 +50,16 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
50
50
  - [Collapsing to the launcher](#collapsing-to-the-launcher)
51
51
  - [Tool-call display modes](#tool-call-display-modes)
52
52
  - [Markdown rendering](#markdown-rendering)
53
+ - [Follow-up suggestions](#follow-up-suggestions)
54
+ - [Editing a gated call before approving it](#editing-a-gated-call-before-approving-it)
55
+ - [Localizing the timestamps](#localizing-the-timestamps)
56
+ - [Message actions: copy, retry, feedback](#message-actions-copy-retry-feedback)
57
+ - [Quoting a selection](#quoting-a-selection)
53
58
  - [Run notices: compaction and agent skills](#run-notices-compaction-and-agent-skills)
54
59
  - [Skills: prompt chips and slash palette](#skills-prompt-chips-and-slash-palette)
55
60
  - [MPA durability: surviving full page reloads](#mpa-durability-surviving-full-page-reloads)
61
+ - [Who the stored conversation belongs to (`user-key`)](#who-the-stored-conversation-belongs-to-user-key)
62
+ - [Mounting more than one chat on a page](#mounting-more-than-one-chat-on-a-page)
56
63
  - [Host seams: the SPA story](#host-seams-the-spa-story)
57
64
  - [Public API surface](#public-api-surface)
58
65
  - [Theming, density, and placement](#theming-density-and-placement)
@@ -162,7 +169,9 @@ another origin, add `credentials="include"` too; see
162
169
  | `data-skills` | — | Inline JSON skill catalog. |
163
170
  | `data-skills-url` | — | URL of a JSON skill catalog (fetched with the element's headers and cookie policy). |
164
171
  | `data-tools-url` | — | URL of a server tool-label catalog (`[{ name, summary, description? }]`), fetched with the element's headers and cookie policy; labels tool-call cards for server-side tools. |
172
+ | `user-key` | `userKey` | Who the stored conversation belongs to — any string identifying the signed-in principal. Joins the storage namespace, and **changing it purges what the previous principal left behind**. Live (not connect-time): a logout is the host's to announce. See [Who the stored conversation belongs to](#who-the-stored-conversation-belongs-to-user-key). |
165
173
  | `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
174
+ | `data-threads-cache` | — | **On by default.** `="false"` stops mirroring message bodies into `sessionStorage` when `data-threads-url` is set, for a deployment that put history on the server so transcripts stay off the client. Only meaningful alongside `data-threads-url`. |
166
175
  | `data-runs-url` | — | URL of a server run index (django-ag-ui's `RunsView`); reveals the header's ⭯ *Continue a run* panel. See [Resuming a run](#resuming-a-run). |
167
176
  | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker + drag-and-drop. |
168
177
  | `data-attachment-accept` | — | `<input accept>` list for client-side type filtering (e.g. `image/*,.pdf`). The server stays authoritative. |
@@ -173,6 +182,7 @@ another origin, add `credentials="include"` too; see
173
182
  | `data-icon-url` | — | Header (and launcher) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
174
183
  | `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. |
175
184
  | `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). |
185
+ | `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). |
176
186
 
177
187
  Each header control also takes its own icon slot — `icon-history`, `icon-checkpoints`,
178
188
  `icon-new`, `icon-collapse` — with the built-in glyph as the fallback, so a host can project a
@@ -196,7 +206,7 @@ same way: `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`.
196
206
  `confirmPredicate`, `askUser`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`,
197
207
  `getPageMap`, `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`,
198
208
  `navigationResult`, `skillContext`, `toolSummaries`, `strings`, `resolvePageTarget`, plus the
199
- mirrors `endpoint` / `toolDisplay` / `collapsed` / `credentials`.
209
+ mirrors `endpoint` / `userKey` / `toolDisplay` / `collapsed` / `credentials`.
200
210
 
201
211
  `headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
202
212
  run; `getHeaders` is the one to use for a credential that rotates. See
@@ -332,6 +342,41 @@ One asymmetry: uploads use `XMLHttpRequest` for real progress events, and its co
332
342
  two-state. `include` turns it on; every other value leaves it off. `omit` therefore cannot suppress
333
343
  cookies on a *same-origin* upload — supply your own `uploadHandler` if that matters.
334
344
 
345
+ ### Where those credentials are allowed to go
346
+
347
+ Every URL in the table above is a plain HTML attribute, and `headers` / `getHeaders` are attached to
348
+ whatever they name. That is what makes a cross-origin agent work — and it is also why a page must
349
+ never build one of those attributes out of a URL parameter, a CMS field, or anything else it did not
350
+ choose itself. Whoever supplies the value chooses where the token goes: the browser preflights the
351
+ custom header, any server willing to answer receives it, and it leaves on the element's first
352
+ request, before the user has typed anything.
353
+
354
+ Treat all seven as trusted configuration. When any of them resolves to another origin, the element
355
+ says so on the console once per origin, naming the destination and the header names it is about to
356
+ send. That covers all seven, not the agent endpoint alone: the tool catalog, the skills list, the
357
+ thread index, the attachment upload and the transcription endpoint carry the same headers, and
358
+ reporting only the agent would report the least interesting of them.
359
+
360
+ To confirm destinations you chose on purpose and silence the notice, name their origins:
361
+
362
+ ```js
363
+ chat.trustedOrigins = ["https://api.example.com"];
364
+ ```
365
+
366
+ That covers every endpoint the element requests itself, and is forwarded to `createHttpAgent`, so a
367
+ host that does not override `agentFactory` needs nothing else. A custom factory can also be given
368
+ the option directly:
369
+
370
+ ```js
371
+ chat.agentFactory = (options) =>
372
+ createHttpAgent({ ...options, trustedOrigins: ["https://api.example.com"] });
373
+ ```
374
+
375
+ Origins are compared as `URL.origin` produces them — scheme, host and port. A notice is a notice,
376
+ not a refusal: nothing is blocked, because a cross-origin agent is a supported deployment and
377
+ refusing would break working installations to defend against a page that is already interpolating
378
+ untrusted data into its own markup.
379
+
335
380
  ### Framework hosts: configure before you insert
336
381
 
337
382
  `headers`, `getHeaders` and `credentials` are read when a request is made, so they can be set at any
@@ -449,6 +494,13 @@ card (honouring `data-tool-display`), so server-side output is visible too. The
449
494
  and context are read **fresh on every run** (`getTools()` / `getContext()`), so they always reflect
450
495
  the current page state.
451
496
 
497
+ The catalog a run advertises is also the set that run can execute. Override `getTools` to scope
498
+ what a page offers — say, exposing `delete_record` only where deleting makes sense — and a call
499
+ naming a tool you withheld is treated exactly as a call naming a tool you never registered: no
500
+ handler runs, and the card settles with the no-result label. Withholding is per run, so the
501
+ mount-wide registry can stay complete. Hosts that leave `getTools` alone advertise the built-ins
502
+ plus everything registered, which is precisely what dispatch could reach anyway.
503
+
452
504
  ### Stopping a run
453
505
 
454
506
  While a run is in flight the **Send button becomes Stop** (same button, label/`aria-label` swap,
@@ -491,10 +543,19 @@ chat.registerTool({
491
543
  });
492
544
  ```
493
545
 
494
- Names must be unique (registering a duplicate throws). Each `<ag-ui-chat>` element owns its own
546
+ Registering a name twice replaces the earlier handler rather than throwing, so a re-fired
547
+ host ref or React StrictMode's double-invoke is harmless -- but two different tools sharing
548
+ a name means the second silently wins. Each `<ag-ui-chat>` element owns its own
495
549
  registry, AG-UI client, and Shadow DOM, so **multiple instances on one page never interfere** —
496
550
  there is no module-level shared state anywhere in the package.
497
551
 
552
+ **A handler's thrown message leaves the browser.** If a handler rejects, its `Error.message` is
553
+ posted back as that call's tool result: into the conversation, on to the AG-UI endpoint, persisted
554
+ there, and replayed to the model provider on every later round. That is deliberate — a real reason
555
+ is what lets the agent recover — but it means an internal hostname, a signed URL or a
556
+ stack-derived path in a rethrown error is disclosed to parties you never chose. Throw the message
557
+ you would be content for the model to read, and log the detail instead.
558
+
498
559
  ### Inline confirmation (`x-destructive` / `x-confirm` / `confirmPredicate`)
499
560
 
500
561
  When a tool call needs confirmation, the element appends an **inline confirmation card** (a
@@ -506,6 +567,8 @@ transcript as a resolved record after the decision:
506
567
  - **Confirm** → the handler runs and the result is posted back.
507
568
  - **Cancel** → a `"User declined the action."` result is posted; the agent acknowledges on its
508
569
  next turn.
570
+ - **Always allow** → the handler runs *and* this tool stops prompting for the rest of the
571
+ session. See below for when this button appears.
509
572
 
510
573
  Whether a call is gated is decided in this order:
511
574
 
@@ -513,9 +576,27 @@ Whether a call is gated is decided in this order:
513
576
  2. Else if `chat.confirmPredicate` is set, its boolean return is authoritative — given the tool
514
577
  name + parsed args it decides per-call (so one tool can be instant for some args and confirmed
515
578
  for others, which a static flag can't express).
516
- 3. Else the element falls back to [`isDestructive(parameters)`](src/tools/is_destructive.ts),
579
+ 3. Else if the user has waived this tool name for the session, the call runs.
580
+ 4. Else the element falls back to [`isDestructive(parameters)`](src/tools/is_destructive.ts),
517
581
  which reads the `x-destructive` JSON-Schema flag.
518
582
 
583
+ #### "Always allow", and why only sometimes
584
+
585
+ A prompt that is approved nearly every time is not a decision, it is a speed bump — and the
586
+ reflex it trains is what makes the rare refusal easy to miss. Anthropic published that users
587
+ approve **~93%** of Claude Code permission prompts manually and called interactive confirmation
588
+ *"behaviorally unreliable as a sole safety mechanism"* on that basis. The waiver exists so the
589
+ prompts that remain still mean something.
590
+
591
+ **The button is offered only on cards raised by step 4** — the `x-destructive` default. Where
592
+ `confirmPredicate` is what gated the call, there is no button, because that predicate is
593
+ documented as authoritative and letting one click retire it would silently defeat a host policy.
594
+ The offer and the allowlist sit on the same path, so there is no dead button either.
595
+
596
+ The waiver is **per tool name and per element**, held in memory and never persisted. A session
597
+ decision that outlived the tab would be a permanent grant made by one click — which is what
598
+ `autoConfirm` already exists to say deliberately. It is cleared when the element goes away.
599
+
519
600
  AG-UI has no built-in risk flag, so destructiveness is carried as a JSON-Schema extension at the
520
601
  **schema root**: `parameters["x-destructive"] = true` (use the exported `X_DESTRUCTIVE_KEY`
521
602
  constant). There is no parallel metadata channel and no name heuristic — destructiveness is exactly
@@ -525,6 +606,13 @@ the `x-destructive` flag (or `confirmPredicate`). The registry forwards the flag
525
606
  If the schema carries an `x-confirm` string (use `X_CONFIRM_KEY`), the card shows it as the prompt;
526
607
  otherwise it falls back to a generic `Run "<tool>"?`.
527
608
 
609
+ **This gate covers frontend tools only.** A server-side tool's schema never reaches the browser —
610
+ tool definitions travel client-to-server on `RunAgentInput.tools`, and the only channel coming back
611
+ is the label catalog (`data-tools-url`), which carries `{ name, summary, description? }` and no
612
+ flags. So marking a server tool destructive does not produce a card here; gate it server-side
613
+ instead (see [Server-side tool approval](#server-side-tool-approval-interrupts)), which surfaces as
614
+ an approval card in the same transcript.
615
+
528
616
  ```js
529
617
  // Per-call: confirm a delete only when it would remove more than one row.
530
618
  chat.confirmPredicate = (name, args) =>
@@ -947,6 +1035,67 @@ it runs again every time the conversation is restored.
947
1035
 
948
1036
  ---
949
1037
 
1038
+ ## Drawing other things the server pushes
1039
+
1040
+ `activityType` is an open string the protocol does not enumerate — `"chart"` is
1041
+ just the one the component ships a renderer for. Register your own and the
1042
+ server can push anything it likes into the transcript:
1043
+
1044
+ ```js
1045
+ chat.registerActivityRenderer({
1046
+ type: "build_status",
1047
+ render: (content) => {
1048
+ const el = document.createElement("div");
1049
+ el.className = "build";
1050
+ el.textContent = `Build ${content.status}`;
1051
+ return el; // return null for content not worth drawing
1052
+ },
1053
+ });
1054
+ ```
1055
+
1056
+ **`render` runs again on every thread load**, so it carries the same contract as
1057
+ a client tool's `render`: a pure function of `content`, deterministic, and free
1058
+ of effects outside the node it returns. Activities are materialised into
1059
+ `role: "activity"` messages and persisted with the transcript, so a renderer that
1060
+ writes to the page instead of returning DOM fires again on every restore.
1061
+
1062
+ The component places what you return, keyed by the activity's `messageId`, so a
1063
+ server repeating an id **replaces** your node rather than adding a second one —
1064
+ the same in-place update charts get. Returning `null` removes whatever was there:
1065
+ live and reload should agree, and the stored content is the version that could
1066
+ not be drawn.
1067
+
1068
+ `chart` and `compaction` are registrations exactly like yours, not privileged
1069
+ branches, so registering either name **replaces the built-in**.
1070
+
1071
+ ### Which carrier should the server use?
1072
+
1073
+ AG-UI leaves exactly two payload names open, and they are not
1074
+ interchangeable:
1075
+
1076
+ | | Carrier | Reaches | Persisted | Replayed |
1077
+ | --- | --- | --- | --- | --- |
1078
+ | **Content** | `ACTIVITY_SNAPSHOT` | the transcript | yes | yes |
1079
+ | **Imperative** | `CUSTOM` | your page, as [`ag-ui-custom`](#events) | no | no |
1080
+
1081
+ ⇒ **Content has a place in the conversation and should come back. An imperative
1082
+ has no place and no meaning once acted on** — replaying "refetch the board" on
1083
+ every thread load is a bug, not a feature. If it has to survive a reload, it is
1084
+ content.
1085
+
1086
+ ### Finding out what arrived
1087
+
1088
+ An activity nobody registered for draws nothing and logs nothing — that is the
1089
+ protocol's own answer, and warning would fire on every forward-compatible
1090
+ server. But silence is hard to debug, so the names are readable:
1091
+
1092
+ ```js
1093
+ chat.unhandledActivityTypes; // ["pydantic_ai_thinking", …]
1094
+ ```
1095
+
1096
+ Note `"chart"` appears there until you call `enableCharts(["activity"])`, which
1097
+ is the honest answer to "I pushed a chart and nothing happened".
1098
+
950
1099
  ## Tool-call display modes
951
1100
 
952
1101
  How much a tool-call card shows is set via the `data-tool-display` attribute (or `toolDisplay`
@@ -1042,9 +1191,12 @@ via the `resize-handle` part.
1042
1191
  Assistant bubbles render sanitized markdown/HTML via [`marked`](https://www.npmjs.com/package/marked)
1043
1192
  (GitHub-flavoured, single-newline line breaks) piped through
1044
1193
  [DOMPurify](https://www.npmjs.com/package/dompurify). User messages stay literal text. The
1045
- allowlist permits emphasis, code, lists, quotes, headings, links, tables, and images (`img`); links
1046
- are hardened with `target="_blank" rel="noopener noreferrer"`; `iframe`/`style`/scripting are
1047
- excluded. The exported helper `renderMarkdown(text)` does this standalone. `marked` and `dompurify`
1194
+ allowlist permits emphasis, code, lists, quotes, headings, links, tables, and when `allowImages`
1195
+ is set — images; links are hardened with `target="_blank" rel="noopener noreferrer"`;
1196
+ `iframe`/`style`/scripting are excluded, as are every `data-*` and `aria-*` attribute and every
1197
+ `class` but a code fence's `language-*` hint, so model output cannot dress itself up as the
1198
+ component's own approval or tool-call chrome. The exported helper `renderMarkdown(text)` does this
1199
+ standalone. `marked` and `dompurify`
1048
1200
  are runtime dependencies.
1049
1201
 
1050
1202
  An animated 3-dot "thinking" indicator (`role="status"`, with an aria-label) appears before the
@@ -1062,6 +1214,181 @@ reveal). It honours `prefers-reduced-motion` (collapsing to instant).
1062
1214
 
1063
1215
  ---
1064
1216
 
1217
+ ## Follow-up suggestions
1218
+
1219
+ The agent can offer the *next* question, not just answer this one. Registered
1220
+ skill chips are static and host-configured, so they can say "summarize this" but
1221
+ never "want me to update the shipping address too?" after a tool has run.
1222
+
1223
+ The server pushes an `ACTIVITY_SNAPSHOT` under the `suggestions` type — with
1224
+ django-ag-ui, `suggestions_activity([...])`. The component draws each prompt as
1225
+ a chip; clicking one sends it as the user's message, exactly as if they had
1226
+ typed it.
1227
+
1228
+ ```json
1229
+ { "activityType": "suggestions",
1230
+ "content": { "prompts": ["Update the shipping address too"] } }
1231
+ ```
1232
+
1233
+ **Chips are content.** They ride the same activity envelope as charts, so they
1234
+ persist and a reload puts them back, and a set pushed under an id already on
1235
+ screen replaces that row rather than adding a second one.
1236
+
1237
+ At most **4** prompts of **120** characters each — `MAX_SUGGESTIONS` and
1238
+ `MAX_SUGGESTION_CHARS`, both exported. The server-side helper *raises* past
1239
+ those bounds while this side silently drops: that asymmetry is deliberate, since
1240
+ the producer can report the problem and the client cannot.
1241
+
1242
+ Nothing to enable. A `suggestions` activity from a server that pushes one is
1243
+ drawn; an `activity_type` this component does not know is ignored, which is what
1244
+ the open field is for.
1245
+
1246
+ ## Editing a gated call before approving it
1247
+
1248
+ AG-UI's resume payload carries `editedArgs`, and the protocol gates it on the
1249
+ agent's own `approveWithEdits` capability. The approval card can offer it:
1250
+
1251
+ ```js
1252
+ chat.approveWithEdits = true; // your server accepts editedArgs
1253
+ ```
1254
+
1255
+ **Off by default, and an assertion about your server rather than a
1256
+ negotiation** — capabilities are not on the wire this component reads, so it
1257
+ cannot check. Turned on against a server that ignores `editedArgs`, a user would
1258
+ edit arguments it silently discards, which is worse than not offering.
1259
+
1260
+ The card then shows the call's arguments as editable JSON. `editedArgs` rides
1261
+ the resume payload **only when something actually changed**, so a server can tell
1262
+ "approved as proposed" from "approved, but like this" without diffing what it
1263
+ already sent. Unparseable JSON, or JSON that is not an object, keeps the card
1264
+ open with the reason on it rather than approving the original behind the user's
1265
+ back.
1266
+
1267
+ Only offered for an interrupt naming a tool call this component holds a card
1268
+ for — the card is where the arguments still are.
1269
+
1270
+ ## Localizing the timestamps
1271
+
1272
+ There is **no `Intl` anywhere in this component**. The relative timestamps in the
1273
+ thread drawer and checkpoint panel (`"5m ago"`, `"2d ago"`) are deliberately
1274
+ locale-neutral: a component that guessed a locale would disagree with the page
1275
+ it is embedded in, and being wrong in a second language is worse than being
1276
+ neutral in one.
1277
+
1278
+ That is a good default and a bad requirement, so it is replaceable:
1279
+
1280
+ ```js
1281
+ const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
1282
+ chat.formatRelativeTime = (ts) =>
1283
+ rtf.format(Math.round((ts - Date.now()) / 60000), "minute");
1284
+ ```
1285
+
1286
+ `relativeTime` is exported too, for a host that wants to build on the built-in
1287
+ rather than replace it.
1288
+
1289
+ ## Message actions: copy, retry, feedback
1290
+
1291
+ Every finished assistant message carries a small row of actions beneath it —
1292
+ a **sibling** of the bubble, never a child, so the buttons never join the
1293
+ message's own text.
1294
+
1295
+ - **Retry** re-asks the question. History is truncated to the most recent user
1296
+ message inclusive and the run repeats, so the agent answers what it was asked
1297
+ rather than being told its last answer was wrong.
1298
+ - **Copy** puts the message's text on the clipboard, and says so on the button.
1299
+ A refused clipboard permission is reported there too, rather than thrown.
1300
+ - **Thumbs up / down** fire [`ag-ui-feedback`](#events) and **store nothing**.
1301
+
1302
+ Retry sits on the **last** answer only. Re-running an older turn is branching,
1303
+ and for a page-driving agent editing a past turn is not neutral — those turns
1304
+ clicked buttons, and re-running turn 3 does not un-save what turn 5 saved.
1305
+
1306
+ !!! note
1307
+ A retried turn **re-runs its tools.** The previous attempt already did what
1308
+ it did, and this does not undo it. Confirmation still applies, so a
1309
+ destructive tool asks again — unless the user waived it for this session
1310
+ with *Always allow*.
1311
+
1312
+ A failed run gets the same row, with Retry and Copy and no rating: error text is
1313
+ what people paste into a bug report, but "the connection dropped" is not a
1314
+ statement about answer quality and mixing it into feedback makes that signal say
1315
+ less. This is why a dropped connection is still rendered as an **error** rather
1316
+ than demoted to a run notice — a notice "never settles, takes no action, and
1317
+ carries no controls", and a failure with a way back needs one.
1318
+
1319
+ `retryLastTurn()` is public, for a host driving its own message UI.
1320
+
1321
+ ```js
1322
+ chat.addEventListener("ag-ui-feedback", (e) => {
1323
+ analytics.track("assistant_rating", e.detail); // { content, rating }
1324
+ });
1325
+
1326
+ await chat.retryLastTurn(); // false when there is nothing to ask again
1327
+ ```
1328
+
1329
+ ## Quoting a selection
1330
+
1331
+ Select any text in the transcript and a small **Quote** offer floats beside it.
1332
+ Taking it drops the selection into the composer as a markdown blockquote and
1333
+ leaves the caret on a fresh line under it — a quotation is how a question
1334
+ narrows to one part of an answer, so nothing is sent until you say what you are
1335
+ asking.
1336
+
1337
+ Quoting **appends**, after whatever is already typed, so a second quotation is a
1338
+ second thing being asked about rather than a replacement for the first. Long
1339
+ selections are capped at 500 characters: select-all-then-quote is a gesture the
1340
+ transcript already answers, and pasting the whole conversation back costs tokens
1341
+ to say nothing.
1342
+
1343
+ Set `data-quote-selection="false"` to turn the offer off. The `quote-selection`
1344
+ `part` styles it.
1345
+
1346
+ ### The half that matters: selection in **your** page
1347
+
1348
+ The transcript is the easy half. A chat mounted beside a table, a diff or a
1349
+ report is sitting in the surface the user actually works in — and *that*
1350
+ selection is one no hosted chat can reach.
1351
+
1352
+ `offerQuoteInPage()` extends the same select-then-offer gesture to the whole
1353
+ page. It is opt-in, because it listens on your document:
1354
+
1355
+ ```js
1356
+ const stop = chat.offerQuoteInPage(); // the whole page
1357
+ chat.offerQuoteInPage(document.querySelector("#report")); // or one region
1358
+ ```
1359
+
1360
+ For a deliberate trigger instead of a selection, `quote(text)` is the seam
1361
+ underneath:
1362
+
1363
+ ```js
1364
+ // "Ask about this row" — a button on each row of your own table.
1365
+ row.querySelector(".ask").addEventListener("click", () => {
1366
+ chat.quote(row.innerText);
1367
+ });
1368
+ ```
1369
+
1370
+ `quote()` never sends — pair it with [`sendMessage()`](#sending-from-your-own-ui)
1371
+ if you want a one-click "explain this" that skips the composer entirely.
1372
+
1373
+ > **Do not write the four-line version of `offerQuoteInPage()`.**
1374
+ > A `mouseup` listener that quotes every settled selection appends to the
1375
+ > composer on every drag the user made to *read*, to copy, or to fix a typo —
1376
+ > and it cannot tell a selection in your prose from one inside the user's own
1377
+ > half-typed `<input>`, because Chrome reports a field's internal selection
1378
+ > through `document.getSelection()` as an ordinary range over the field's
1379
+ > **wrapper**. The text reads back perfectly and nothing about the range says
1380
+ > where it came from; the only signal is `document.activeElement`. That guard,
1381
+ > plus skipping the widget's own transcript, plus retiring a fixed-position
1382
+ > affordance on scroll, is what the method is for.
1383
+
1384
+ > **Reading a selection out of a shadow tree takes care too.**
1385
+ > Engines disagree about what `document.getSelection()` reports for a selection
1386
+ > made *inside* a shadow root: WebKit rescopes the endpoints to the host element,
1387
+ > so you get the whole widget and none of the words, while Chromium hands back
1388
+ > the shadow nodes directly. `getComposedRanges` settles it, and this component
1389
+ > uses it where it exists. `quotableSelection(container, roots)` is exported if
1390
+ > you have the same problem in your own component.
1391
+
1065
1392
  ## Run notices: compaction and agent skills
1066
1393
 
1067
1394
  Some things a run does are neither text nor a tool the user asked for — the server condensed
@@ -1177,6 +1504,87 @@ triggers a full reload. Before the handler navigates, the element writes a check
1177
1504
 
1178
1505
  The MPA round-trip becomes a clean observation point instead of a dropped conversation.
1179
1506
 
1507
+ ### Who the stored conversation belongs to (`user-key`)
1508
+
1509
+ `sessionStorage` is scoped to a tab, not to a session. It survives every same-tab navigation,
1510
+ and a logout is a navigation — so on a shared workstation, one user's transcript is still sitting
1511
+ there when the next user signs in and the chat mounts again. Transcripts routinely quote record
1512
+ data, so treat that as the default and turn it off:
1513
+
1514
+ ```html
1515
+ <ag-ui-chat endpoint="/agent/" user-key="{{ request.user.pk }}"></ag-ui-chat>
1516
+ ```
1517
+
1518
+ The value is any string that identifies the principal — a user id, an account id, a hash of one.
1519
+ It joins the storage namespace, so two principals in the same tab cannot reach each other's
1520
+ conversation, and **changing it purges everything the previous principal stored**: transcript,
1521
+ history drawer index and navigation checkpoints, for this element's namespace only.
1522
+
1523
+ Set it live, from script, as part of signing out or in:
1524
+
1525
+ ```js
1526
+ chat.userKey = String(session.userId); // or "" on sign-out
1527
+ ```
1528
+
1529
+ That is why it is a live attribute rather than a connect-time one. A single-page app signs a user
1530
+ out through its own router without remounting anything, so the host naming the new principal — or
1531
+ dropping the attribute — is the only signal the element will ever get. Removing the attribute
1532
+ purges too, so a sign-out that simply clears it is safe.
1533
+
1534
+ The **first** value to arrive is treated as a host naming the user who was already there, not as a
1535
+ handover: the conversation in progress moves into the principal's namespace instead of being
1536
+ destroyed. So an element configured by an async auth handshake — the shape described in
1537
+ [Framework hosts](#framework-hosts-configure-before-you-insert) — keeps what is on screen.
1538
+
1539
+ Two things it deliberately does not do. It does not scope the panel's own collapsed / dragged-size
1540
+ / theme preferences, which are this element's UI state and carry no conversation content. And it
1541
+ does not encrypt or hide anything from the page: any script on the origin can still read
1542
+ `sessionStorage`. It scopes and it purges.
1543
+
1544
+ **Without it, nothing changes** — including the carry-over above. A conversation is scoped to the
1545
+ element and to nobody in particular, and on a shared workstation it will be there for whoever signs
1546
+ in next in the same tab.
1547
+
1548
+ For a deployment that keeps history server-side, `data-threads-cache="false"` stops the local
1549
+ mirror of the message bodies as well, so choosing `data-threads-url` actually keeps transcripts off
1550
+ the client:
1551
+
1552
+ ```html
1553
+ <ag-ui-chat endpoint="/agent/" data-threads-url="/agent/threads/" data-threads-cache="false">
1554
+ </ag-ui-chat>
1555
+ ```
1556
+
1557
+ The client-only concerns (the active thread id, the navigation checkpoint) keep their local store
1558
+ either way, so reloads and navigating tools still work. What is lost is the offline fallback: when
1559
+ the thread endpoint is unreachable the transcript comes back empty rather than stale, and the
1560
+ drawer's offline list loses its previews — a preview being an excerpt of a message, which is the
1561
+ thing being kept off the client. Constructing the store yourself takes the same option:
1562
+
1563
+ ```js
1564
+ chat.conversationStore = new RemoteConversationStore(
1565
+ "/agent/threads/",
1566
+ () => ({ "X-CSRFToken": token }),
1567
+ new SessionStorageStore(),
1568
+ () => "same-origin",
1569
+ false, // cacheMessages
1570
+ );
1571
+ ```
1572
+
1573
+ `SessionStorageStore.purge(namespace)` is the same primitive the element uses, for a host driving
1574
+ its own store from its own sign-out path.
1575
+
1576
+ ### Mounting more than one chat on a page
1577
+
1578
+ Give each `<ag-ui-chat>` its own `id`. The storage namespace is the element's `id`, falling back to
1579
+ its `endpoint` — so two elements with no `id` against the same agent mount (a docked support panel
1580
+ and an inline page assistant, say) would resolve to the same namespace and share a thread pointer,
1581
+ a history drawer and every message key.
1582
+
1583
+ They no longer do: the first element to mount keeps the namespace, and a second is given a
1584
+ throwaway one of its own plus a console warning. That keeps the two conversations apart, but the
1585
+ throwaway namespace is minted per mount, so the second element will not restore its conversation
1586
+ across a reload until it has an `id`.
1587
+
1180
1588
  ---
1181
1589
 
1182
1590
  ## Host seams: the SPA story
@@ -1321,6 +1729,83 @@ substitute the other way round either, because shared state requires the *agent*
1321
1729
  `STATE_SNAPSHOT`, which is not the host's decision to make. Use state when the two ends edit one
1322
1730
  object; use this when your page owns the data and just needs to know it moved.
1323
1731
 
1732
+ **`ag-ui-custom`** *(event)* — the agent sent an AG-UI `CUSTOM` event.
1733
+ `detail: { name, value }` (typed `CustomAgentDetail`), both verbatim and uninterpreted.
1734
+
1735
+ ```js
1736
+ chat.addEventListener("ag-ui-custom", (e) => {
1737
+ if (e.detail.name === "invalidate") {
1738
+ void refetch(e.detail.value);
1739
+ }
1740
+ // Any other name: no listener, nothing happens. That is the intended outcome.
1741
+ });
1742
+ ```
1743
+
1744
+ `CUSTOM` is one of exactly two AG-UI carriers whose payload name is an open
1745
+ string the protocol does not enumerate, and it is the **imperative** one:
1746
+ something for your page to *do*. Its sibling `ACTIVITY_SNAPSHOT` carries
1747
+ transcript **content**, which is why an activity is materialised into a message,
1748
+ persisted with the thread and replayed on restore, and this is not.
1749
+
1750
+ **That asymmetry is the rule for choosing between them.** Content has a place in
1751
+ the conversation and should replay. An imperative has no place and no meaning
1752
+ once acted on — replaying "refetch the board" on every thread load is a bug, not
1753
+ a feature. If it must survive a reload, it belongs on the other carrier.
1754
+
1755
+ The element takes no view of what a name means and forwards every one, so a name
1756
+ it has never heard of reaches you unchanged. A host with no listener for a name
1757
+ simply ignores it, which is the graceful outcome an open field exists for.
1758
+
1759
+ **`ag-ui-invalidate`** *(event)* — the agent named resources its write moved.
1760
+ `detail: { keys, reason }` (typed `InvalidateDetail`).
1761
+
1762
+ One `CUSTOM` name routed to its own event so you do not have to string-match;
1763
+ every other name still arrives as `ag-ui-custom`. It fires **as each
1764
+ announcement arrives**, during the run, and the same keys ride
1765
+ `ag-ui-run-finished` again at the end as `invalidated`, de-duplicated.
1766
+
1767
+ > **Do not reload the page on this.** The user was probably typing. An
1768
+ > agent-triggered reload or a blind refetch into a live form destroys unsaved
1769
+ > input, and from their side the page threw their work away on its own. Check
1770
+ > first, and offer rather than act:
1771
+
1772
+ ```js
1773
+ chat.addEventListener("ag-ui-invalidate", (e) => {
1774
+ if (formIsDirty()) {
1775
+ showBanner("This data changed. Refresh when you're ready.", e.detail.keys);
1776
+ return;
1777
+ }
1778
+ refetch(e.detail.keys); // e.detail.keys → ["orders", "orders/42"]
1779
+ });
1780
+ ```
1781
+
1782
+ **Keys are opaque and matching is exact.** `orders/42` does not imply `orders` —
1783
+ a prefix rule would be the component guessing at a scheme it does not own, and
1784
+ `orders/1` would match `orders/11`. A server that wants the collection refreshed
1785
+ names it. Your own matching may be hierarchical, because in your vocabulary the
1786
+ scheme is known; that is what TanStack query keys are built for.
1787
+
1788
+ Already listening on `ag-ui-run-finished`? Upgrading is one line, and the `else`
1789
+ is the whole compatibility story:
1790
+
1791
+ ```js
1792
+ if (detail.invalidated.length > 0) refetchOnly(detail.invalidated);
1793
+ else if (detail.tools.some((t) => t.side === "server")) refetchEverything();
1794
+ ```
1795
+
1796
+ | Server | Client | Result |
1797
+ | --- | --- | --- |
1798
+ | old | old | coarse refetch, as today |
1799
+ | new | old | the `CUSTOM` event is ignored; coarse refetch still fires |
1800
+ | old | new | `invalidated` is empty; the `else` branch runs |
1801
+ | new | new | precise, and live during the run |
1802
+
1803
+ Nothing negotiates and nothing handshakes, which is what makes this shippable
1804
+ across repos with independent release cadences.
1805
+
1806
+ **Note:** it reaches **the page that started the run**, during the run. There is one
1807
+ response stream per run and no channel to anybody else's browser.
1808
+
1324
1809
  ## Resuming a run
1325
1810
 
1326
1811
  When the server persists run checkpoints (django-ag-ui's `step_store`), a run
@@ -1452,6 +1937,8 @@ re-export point. Internal modules import from leaf paths.
1452
1937
  | `SubmitDetail` | type | `detail` shape of the submit event. |
1453
1938
  | `ToggleDetail` | type | `detail` shape of the `ag-ui-toggle` event (`{ collapsed }`). |
1454
1939
  | `UnreadDetail` | type | `detail` shape of the `ag-ui-unread` event (`{ unread }`). |
1940
+ | `AttachmentsDetail` | type | `detail` shape of the `ag-ui-attachments` event (`{ attachments, pending }`). |
1941
+ | `StateDetail` | type | `detail` shape of the `ag-ui-state` event (`{ state }`). |
1455
1942
 
1456
1943
  ### AG-UI client & agent
1457
1944
 
@@ -1463,6 +1950,8 @@ re-export point. Internal modules import from leaf paths.
1463
1950
  | `ConnectionLostError` | class | Raised (→ `onError`) when a run's stream closes with no terminal AG-UI event. |
1464
1951
  | `createHttpAgent(options)` | function | Default agent factory (wraps `HttpAgent`). |
1465
1952
  | `AgentFactory` / `HttpAgentOptions` | type | Factory signature and its options. |
1953
+ | `ResolveInterrupts` | type | Resolver for server-side-tool approval interrupts (one decision per interrupt). |
1954
+ | `InterruptResponse` | type | One interrupt's answer: `resolved` (with an optional payload) or `cancelled`. |
1466
1955
 
1467
1956
  ### Tools & flags
1468
1957
 
@@ -1476,6 +1965,9 @@ re-export point. Internal modules import from leaf paths.
1476
1965
  | `PAGE_ACTIONS` | const | The page-action opt-in tokens (`scroll` / `drag`). |
1477
1966
  | `ResolvePageTarget` | type | `(target) => HTMLElement | null` — the page-target resolver. |
1478
1967
  | `X_DESTRUCTIVE_KEY` / `X_NAVIGATES_KEY` | const | The JSON-Schema extension keys. |
1968
+ | `parseToolCatalog(data)` | function | Parse a fetched `data-tools-url` catalog into a `name` → `summary` map. |
1969
+ | `ToolCatalogEntry` | type | One row of that catalog. |
1970
+ | `prettifyToolName(name)` | function | Last fallback of the tool-card label chain (`delete_record` reads as *Delete record*). |
1479
1971
 
1480
1972
  ### Host seams
1481
1973
 
@@ -1490,6 +1982,19 @@ re-export point. Internal modules import from leaf paths.
1490
1982
  | `PageState` | type | A page-state binding declaration. |
1491
1983
  | `Skill` | type | A launchable prompt (chip / `/`-command). |
1492
1984
  | `RunFinishedDetail` / `ToolRun` | type | `ag-ui-run-finished` detail: the tools an interaction ran, and which side ran them. |
1985
+ | `CustomAgentDetail` | type | `ag-ui-custom` detail: an AG-UI `CUSTOM` event's `name` and `value`, verbatim. |
1986
+ | `InvalidateDetail` | type | `ag-ui-invalidate` detail: the resource `keys` that moved, and the `reason`. |
1987
+ | `FeedbackDetail` | type | `ag-ui-feedback` detail: the rated message's `content` and the `rating`. |
1988
+ | `relativeTime` | function | The built-in locale-neutral timestamp formatter (`"5m ago"`), for a host building on it. |
1989
+ | `RelativeTimeFormatter` | type | What `formatRelativeTime` takes: an epoch-ms timestamp in, row text out. |
1990
+ | `renderSuggestionChips` | function | Draw a `suggestions` activity as chips that send themselves; `null` when nothing survives. |
1991
+ | `suggestionPrompts` | function | The usable prompts in a `suggestions` activity's content, bounded and trimmed. |
1992
+ | `attachMessageActions` | function | Give a finished bubble its action row (copy, and feedback when a handler is passed). |
1993
+ | `messageActionBar` | function | The empty action row for a bubble, created if it has none — the shared shell both callers use. |
1994
+ | `MessageActionsOptions` | type | What `attachMessageActions` takes: `strings`, a `text` source, an optional `onFeedback`. |
1995
+ | `ActivityRenderer` | type | Draws one activity from its `content`. Pure: it runs again on every restore. |
1996
+ | `ActivityRegistration` | type | What `registerActivityRenderer` takes: `type`, `render`, and an optional `removedNotice`. |
1997
+ | `createStateHookTools(binding)` / `StateHook` | deprecated | The former names for `createPageStateTools` / `PageState`. |
1493
1998
 
1494
1999
  ### Durability
1495
2000
 
@@ -1515,6 +2020,14 @@ re-export point. Internal modules import from leaf paths.
1515
2020
  | `AttachmentRef` | type | The durable upload ref (`{ id, name, mime, size, url? }`). |
1516
2021
  | `messageAttachments(message)` | function | Read the refs a restored user message carries. |
1517
2022
 
2023
+ ### Voice input
2024
+
2025
+ | Export | Kind | Summary |
2026
+ | --- | --- | --- |
2027
+ | `transcribeAudio(audio, options)` | function | The built-in transcription POST (multipart) → the transcript text. |
2028
+ | `TranscribeOptions` | type | `{ url, headers? }`. |
2029
+ | `TranscribeHandler` | type | `(audio) => Promise<string>` — the `transcribeHandler` swap seam (Web Speech, direct-to-provider). |
2030
+
1518
2031
  ### UI & DOM primitives
1519
2032
 
1520
2033
  | Export | Kind | Summary |
@@ -1527,7 +2040,25 @@ re-export point. Internal modules import from leaf paths.
1527
2040
  | `UiStrings` | type | The flat table of every user-facing string. |
1528
2041
  | `DEFAULT_UI_STRINGS` | const | The English defaults (the override floor). |
1529
2042
  | `mergeUiStrings(overrides)` | function | Merge a partial override over the defaults. |
1530
- | `renderMarkdown(text)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
2043
+ | `renderMarkdown(text, options?)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
2044
+ | `RenderMarkdownOptions` | type | `{ allowImages? }` — opt `<img>` back into the sanitized output. |
2045
+ | `requestApproval(host, request, options?)` | function | Append the inline approval card that gates a server-side tool. |
2046
+ | `ApprovalRequest` | type | What that card displays (`{ message?, toolName? }`). |
2047
+ | `ApprovalOptions` | type | `{ signal?, strings? }` — abort resolves the card as denied; `strings` localizes it. |
2048
+ | `ApprovalRenderer` | type | Replace the built-in approval card outright (`AgUiChat.approvalRenderer`). |
2049
+ | `requestQuestion(host, request, options?)` | function | Append the inline `ask_user` card (radios and/or free text). |
2050
+ | `QuestionRequest` | type | What that card asks. |
2051
+ | `QuestionOptions` | type | `{ signal?, strings? }` — abort resolves it with an empty answer. |
2052
+ | `QuestionRenderer` | type | Replace the built-in question card outright (`AgUiChat.questionRenderer`). |
2053
+ | `renderChart(spec)` | function | Draw one spec as a self-contained block, or `null` when it says nothing. |
2054
+ | `chartSpecFrom(value)` | function | Read an arbitrary payload into a `ChartSpec`, or `null` if it cannot be drawn honestly. |
2055
+ | `ChartSpec` / `ChartSeries` / `ChartKind` | type | A chart as data, one named series, and how it is drawn. |
2056
+ | `attachQuoteOffer(options)` | function | The page-side select-then-quote offer, with its guards. `AgUiChat.offerQuoteInPage()` is the one-line form. |
2057
+ | `PageQuoteOffer` / `PageQuoteOfferOptions` | type | The live offer (`{ element, detach }`) and what it takes. |
2058
+ | `quotableSelection(container, roots)` | function | The current selection when it lies inside `container`, read through the shadow-aware API where the engine has one. |
2059
+ | `QuotableSelection` | type | `{ text, rect }` — what was selected, and where it sits. |
2060
+ | `asQuote(text)` | function | Shape text as a markdown blockquote with a blank line after it. |
2061
+ | `MAX_QUOTE_CHARS` | const | The cap a quotation is truncated to (500). |
1531
2062
  | `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `flash` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
1532
2063
  | `fillField` / `clickElement` / `pressButton` / `selectControl` / `setControlValue` / `toggleCheckbox` | function | DOM-driver primitives. |
1533
2064
  | `setNativeValue` / `setNativeChecked` | function | Set a control via its native prototype setter (React-controlled inputs). |
@@ -1542,6 +2073,19 @@ re-export point. Internal modules import from leaf paths.
1542
2073
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1543
2074
  | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1544
2075
  | `RUN_FINISHED_EVENT` | The interaction-finished CustomEvent name (`ag-ui-run-finished`). |
2076
+ | `CUSTOM_AGENT_EVENT` | The agent-`CUSTOM` CustomEvent name (`ag-ui-custom`). |
2077
+ | `INVALIDATE_EVENT` | The resource-invalidation CustomEvent name (`ag-ui-invalidate`). |
2078
+ | `FEEDBACK_EVENT` | The message-rating CustomEvent name (`ag-ui-feedback`). |
2079
+ | `SUGGESTIONS_ACTIVITY_TYPE` | The `activity_type` carrying follow-up prompts (`suggestions`). |
2080
+ | `MAX_SUGGESTIONS` | Most prompts one push draws (4). Mirrors the server's cap. |
2081
+ | `MAX_SUGGESTION_CHARS` | Longest one prompt may be (120). Mirrors the server's cap. |
2082
+ | `INVALIDATE_CUSTOM_NAME` | The AG-UI `CUSTOM` `name` that carries one (`ag_ui.invalidate`). |
2083
+ | `ATTACHMENT_EVENT` | The attachments-changed CustomEvent name (`ag-ui-attachments`). |
2084
+ | `STATE_EVENT` | The shared-state CustomEvent name (`ag-ui-state`). |
2085
+ | `CHART_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type a server sets to push a chart. |
2086
+ | `CHART_TOOL_NAME` | The name the built-in chart tool registers under (`render_chart`). |
2087
+ | `COMPACTION_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type reporting a trimmed history. |
2088
+ | `LOAD_CAPABILITY_TOOL` | The agent-side capability-loading tool's name. |
1545
2089
  | `MESSAGE_ROLE` | Message role constants. |
1546
2090
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1547
2091
  | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
@@ -1675,15 +2219,17 @@ component sets, so a new one cannot ship undocumented.
1675
2219
 
1676
2220
  | Feature | Parts |
1677
2221
  | --- | --- |
1678
- | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `resize-handle` |
2222
+ | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `jump-latest`, `resize-handle` |
1679
2223
  | Header buttons | `header-button` on each, plus `history-button`, `checkpoints-button`, `new-button`, `collapse-button`, `theme-toggle` |
1680
2224
  | Collapsed widget | `launcher`, `launcher-icon`, `launcher-badge` |
1681
2225
  | Answers | `answer` (the per-turn group), `message` (plus `message-user`, `message-assistant`), `code-copy` |
1682
2226
  | Reasoning | `thoughts`, `thoughts-toggle`, `thoughts-body`, `thoughts-label` |
1683
- | Run notices | `run-notice` (plus `run-notice-interrupted`, `run-notice-attachment-pending`, `run-notice-compaction`, `run-notice-skill`), `run-notice-icon`, `run-notice-text` |
2227
+ | Follow-up suggestions | `suggestions`, `suggestion-chip` |
2228
+ | Message actions | `message-actions`, `message-action` (plus `message-action-retry`, `message-action-copy`, `message-action-up`, `message-action-down`) |
2229
+ | 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` |
1684
2230
  | 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` |
1685
- | Client-side confirmation | `confirm`, `confirm-body`, `confirm-args`, `confirm-actions`, `confirm-button` (plus `confirm-confirm`, `confirm-cancel`) |
1686
- | Server-side approval | `approval`, `approval-body`, `approval-actions`, `approval-button` (plus `approval-approve`, `approval-deny`) |
2231
+ | Client-side confirmation | `confirm`, `confirm-body`, `confirm-args`, `confirm-actions`, `confirm-button` (plus `confirm-confirm`, `confirm-cancel`, `confirm-always`) |
2232
+ | Server-side approval | `approval`, `approval-body`, `approval-actions`, `approval-button` (plus `approval-approve`, `approval-deny`), `approval-edit`, `approval-args`, `approval-error` |
1687
2233
  | Typed question | `question`, `question-body`, `question-options`, `question-choice`, `question-choice-text`, `question-radio`, `question-input`, `question-actions`, `question-button` |
1688
2234
  | Composer | `composer`, `composer-surface`, `composer-tools`, `input`, `send`, `attach-button`, `voice-button` |
1689
2235
  | 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` |