@artooi/ag-ui-web-component 0.38.0 → 0.40.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 (142) hide show
  1. package/CHANGELOG.md +515 -1
  2. package/README.md +232 -56
  3. package/dist/ag-ui-web-component.bundle.js +370 -139
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +17 -14
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/activity_registry.d.ts +46 -0
  8. package/dist/core/activity_registry.d.ts.map +1 -0
  9. package/dist/core/ag_ui_chat.d.ts +23 -10
  10. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  11. package/dist/core/agui_client.d.ts +28 -0
  12. package/dist/core/agui_client.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +27 -0
  14. package/dist/core/client_seed.d.ts.map +1 -0
  15. package/dist/core/read_max_tool_rounds.d.ts +9 -0
  16. package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
  17. package/dist/core/run_handlers.d.ts +80 -0
  18. package/dist/core/run_handlers.d.ts.map +1 -0
  19. package/dist/core/storage_scope.d.ts +110 -0
  20. package/dist/core/storage_scope.d.ts.map +1 -0
  21. package/dist/core/tool_outcome.d.ts +2 -3
  22. package/dist/core/tool_outcome.d.ts.map +1 -1
  23. package/dist/core/utils.d.ts +46 -0
  24. package/dist/core/utils.d.ts.map +1 -1
  25. package/dist/index.js +16268 -14747
  26. package/dist/index.js.map +4 -4
  27. package/dist/skills/skill_catalog.d.ts +67 -0
  28. package/dist/skills/skill_catalog.d.ts.map +1 -0
  29. package/dist/skills/skill_name_from.d.ts +12 -0
  30. package/dist/skills/skill_name_from.d.ts.map +1 -0
  31. package/dist/tools/tool_catalog.d.ts +79 -0
  32. package/dist/tools/tool_catalog.d.ts.map +1 -0
  33. package/dist/tools/tool_dispatch.d.ts +104 -0
  34. package/dist/tools/tool_dispatch.d.ts.map +1 -0
  35. package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
  36. package/dist/ui/composer/auto_grow.d.ts +10 -0
  37. package/dist/ui/composer/auto_grow.d.ts.map +1 -0
  38. package/dist/ui/composer/composer_attachments.d.ts +57 -0
  39. package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
  40. package/dist/ui/composer/composer_voice.d.ts +48 -0
  41. package/dist/ui/composer/composer_voice.d.ts.map +1 -0
  42. package/dist/ui/composer/voice_input.d.ts.map +1 -1
  43. package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
  44. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
  45. package/dist/ui/fill_ui_string.d.ts +32 -0
  46. package/dist/ui/fill_ui_string.d.ts.map +1 -0
  47. package/dist/ui/history/conversation_history.d.ts +203 -0
  48. package/dist/ui/history/conversation_history.d.ts.map +1 -0
  49. package/dist/ui/history/relative_time.d.ts.map +1 -1
  50. package/dist/ui/history/thread_drawer.d.ts +0 -14
  51. package/dist/ui/history/thread_drawer.d.ts.map +1 -1
  52. package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
  53. package/dist/ui/interrupts/pending_decision.d.ts +23 -0
  54. package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
  55. package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
  56. package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
  57. package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
  58. package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
  59. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  60. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  61. package/dist/ui/placement/panel_placement.d.ts +119 -0
  62. package/dist/ui/placement/panel_placement.d.ts.map +1 -0
  63. package/dist/ui/progress/run_announcer.d.ts +41 -0
  64. package/dist/ui/progress/run_announcer.d.ts.map +1 -0
  65. package/dist/ui/progress/subagent_progress.d.ts +58 -0
  66. package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
  67. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  68. package/dist/ui/shell/adopt_styles.d.ts +23 -0
  69. package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
  70. package/dist/ui/shell/glyph_slot.d.ts +7 -0
  71. package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
  72. package/dist/ui/shell/header_button.d.ts +10 -0
  73. package/dist/ui/shell/header_button.d.ts.map +1 -0
  74. package/dist/ui/shell/icon_element.d.ts +12 -0
  75. package/dist/ui/shell/icon_element.d.ts.map +1 -0
  76. package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
  77. package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
  78. package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
  79. package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
  80. package/dist/ui/styles.d.ts +1 -1
  81. package/dist/ui/styles.d.ts.map +1 -1
  82. package/dist/ui/transcript/answer_actions.d.ts +39 -0
  83. package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
  84. package/dist/ui/transcript/answer_stream.d.ts +56 -0
  85. package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
  86. package/dist/ui/transcript/render_or_warn.d.ts +16 -0
  87. package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
  88. package/dist/ui/transcript/starter_chips.d.ts +21 -0
  89. package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
  90. package/dist/ui/transcript/transcript.d.ts +171 -0
  91. package/dist/ui/transcript/transcript.d.ts.map +1 -0
  92. package/dist/ui/ui_strings.d.ts +29 -1
  93. package/dist/ui/ui_strings.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/constants.ts +17 -15
  96. package/src/core/activity_registry.ts +133 -0
  97. package/src/core/ag_ui_chat.ts +945 -4419
  98. package/src/core/agui_client.ts +172 -16
  99. package/src/core/client_seed.ts +27 -0
  100. package/src/core/read_max_tool_rounds.ts +13 -0
  101. package/src/core/remote_conversation_store.ts +1 -1
  102. package/src/core/run_handlers.ts +501 -0
  103. package/src/core/storage_scope.ts +279 -0
  104. package/src/core/tool_outcome.ts +5 -3
  105. package/src/core/utils.ts +109 -0
  106. package/src/skills/skill_catalog.ts +166 -0
  107. package/src/skills/skill_name_from.ts +19 -0
  108. package/src/tools/tool_catalog.ts +354 -0
  109. package/src/tools/tool_dispatch.ts +521 -0
  110. package/src/ui/composer/attachment_tray.ts +6 -5
  111. package/src/ui/composer/auto_grow.ts +12 -0
  112. package/src/ui/composer/composer_attachments.ts +324 -0
  113. package/src/ui/composer/composer_voice.ts +93 -0
  114. package/src/ui/composer/voice_input.ts +4 -4
  115. package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
  116. package/src/ui/fill_ui_string.ts +41 -0
  117. package/src/ui/history/conversation_history.ts +699 -0
  118. package/src/ui/history/relative_time.ts +5 -4
  119. package/src/ui/history/thread_drawer.ts +3 -50
  120. package/src/ui/interrupts/confirmation_card.ts +4 -2
  121. package/src/ui/interrupts/pending_decision.ts +34 -0
  122. package/src/ui/placement/is_collapsible_placement.ts +19 -0
  123. package/src/ui/placement/is_draggable_placement.ts +23 -0
  124. package/src/ui/placement/launcher_drag.ts +104 -89
  125. package/src/ui/placement/panel_placement.ts +1130 -0
  126. package/src/ui/progress/run_announcer.ts +67 -0
  127. package/src/ui/progress/subagent_progress.ts +201 -0
  128. package/src/ui/progress/tool_call_card.ts +2 -0
  129. package/src/ui/shell/adopt_styles.ts +28 -0
  130. package/src/ui/shell/glyph_slot.ts +12 -0
  131. package/src/ui/shell/header_button.ts +21 -0
  132. package/src/ui/shell/icon_element.ts +33 -0
  133. package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
  134. package/src/ui/shell/read_launcher_icon_url.ts +9 -0
  135. package/src/ui/styles.ts +301 -70
  136. package/src/ui/transcript/answer_actions.ts +139 -0
  137. package/src/ui/transcript/answer_stream.ts +123 -0
  138. package/src/ui/transcript/render_or_warn.ts +22 -0
  139. package/src/ui/transcript/starter_chips.ts +42 -0
  140. package/src/ui/transcript/transcript.ts +429 -0
  141. package/src/ui/ui_strings.ts +38 -1
  142. package/src/version.ts +1 -1
package/README.md CHANGED
@@ -184,6 +184,7 @@ another origin, add `credentials="include"` too; see
184
184
  | `data-skills-url` | — | URL of a JSON skill catalog (fetched with the element's headers and cookie policy). |
185
185
  | `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. |
186
186
  | `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). |
187
+ | `user-name` | `userName` | The signed-in user's display name, for the [greeting](#the-greeting-on-an-empty-page) an empty conversation shows under `placement="page"`. Presentation only: unlike `user-key` it scopes nothing and is never sent. Absent or blank gives the nameless greeting. Live. |
187
188
  | `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
188
189
  | `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`. |
189
190
  | `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). |
@@ -206,6 +207,8 @@ another origin, add `credentials="include"` too; see
206
207
  | `collapsed` | `collapsed` | Reflected boolean; collapses the widget to its [launcher](#collapsing-to-the-launcher) (a rail under `placement="sidebar"`, the header bar under `embedded`). Persisted per tab. `placement="page"` has no collapsed state and ignores it. |
207
208
  | `data-dragging` | — | **Written by the element, not by you.** Stamped on whichever handle a gesture is currently using, so the styles can react and so the element knows not to re-place the widget under a drag in progress. Cleared on `pointerup` and on `pointercancel`. |
208
209
  | `data-expand-corner` | — | **Written by the element, not by you.** It stamps the corner a dragged or agent-moved panel opens from, so the collapse animation starts where the panel actually is. Listed because the element reads its own stamp back; setting it yourself is overwritten on the next move. |
210
+ | `data-greeting` | — | CSS-only: `off` turns the [greeting layout](#the-greeting-on-an-empty-page) off under `placement="page"`, where it is on by default. Any other value turns it on under `placement="embedded"`. No other placement has it. |
211
+ | `data-empty` | — | **Written by the element, not by you.** Present while the conversation has nothing in it, which is when the greeting shows. Public as a styling hook, so the chrome around a full-page chat can react to the same state, e.g. `ag-ui-chat[data-empty] ~ .page-footer { display: none }`. |
209
212
  | `data-small-viewport` | — | CSS-only: `off` keeps the desktop layout at every width, opting out of the [small-viewport override](#small-viewports). Everything that override sets is a token you can re-state; its trigger is a media query, which is the one thing you cannot. |
210
213
  | `data-paste-attach` | — | When to turn a long text paste into an attachment instead of composer text: absent for the 5000-character default, `off` to never, or a positive number of characters. Only acts where `data-attachments-url` (or a custom `uploadHandler`) gives it somewhere to go. |
211
214
  | `data-starters` | — | JSON array of prompts offered on an empty transcript, e.g. `'["Summarise this page"]'`. Fallback content for `slot="empty"`, so slotting your own replaces them. Shares the four-prompt and 120-character limits with the suggestion chips a run pushes. Read once at connect. |
@@ -237,7 +240,7 @@ neither camp is `data-launcher-icon-url`: it is read while the element connects,
237
240
  below, but is not observed, so a late write is inert and says nothing.
238
241
 
239
242
  **Live attributes.** Written at any time, before or after the element connects, and acted on
240
- either way: `title-text`, `placement`, `credentials`, `user-key`.
243
+ either way: `title-text`, `placement`, `credentials`, `user-key`, `user-name`.
241
244
 
242
245
  **Connect-time attributes.** Read once, while the element connects, to decide what chrome exists at
243
246
  all — the tray, the mic, the skills menu, the header mark. Writing one afterwards has **no effect**;
@@ -250,13 +253,19 @@ The list: `data-attachments-url`, `data-attachment-accept`, `data-attachment-max
250
253
  `data-skills-url`, `data-skills`, `data-prompt-chips`, `data-slash-commands`, `data-theme-toggle`,
251
254
  `data-strings`, `data-icon-url`.
252
255
 
256
+ What re-inserting costs: the chrome is built again from the attributes as they stand, a run in
257
+ progress is cancelled, and the conversation is redrawn from its stored history — the same rebuild
258
+ `reload()` performs, and the reason that method exists for a host that can only configure the
259
+ element after the fact.
260
+
253
261
  **Properties** (JS only, not attributes): `headers`, `getHeaders`, `trustedOrigins`, `allowImages`,
254
262
  `autoConfirm`, `confirmPredicate`, `askUser`, `askUserRenderer`, `approvalRenderer`,
255
263
  `approveWithEdits`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`, `getPageMap`,
256
264
  `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`, `navigationResult`,
257
265
  `skillContext`, `toolSummaries`, `formatToolPayload`, `formatRelativeTime`, `strings`,
258
266
  `resolvePageTarget`, `sharedState`, plus the read-only `unread` and `unhandledActivityTypes`, and
259
- the attribute mirrors `endpoint` / `userKey` / `toolDisplay` / `collapsed` / `credentials`.
267
+ the attribute mirrors `endpoint` / `userKey` / `userName` / `toolDisplay` / `collapsed` /
268
+ `credentials`.
260
269
 
261
270
  `headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
262
271
  run; `getHeaders` is the one to use for a credential that rotates. See
@@ -296,10 +305,13 @@ the `copyCode` / `copied` / `copyFailed` strings.
296
305
 
297
306
  `sendMessage(content, attachments?)` sends as if the user had typed it — user bubble,
298
307
  `ag-ui-submit` event, run started. Use it for an "Ask about this order" button, a command
299
- palette, or a composer of your own replacing the built-in one. It no-ops while a run is in
300
- flight and for an entirely empty message, and unlike the built-in Send it does **not** consult
301
- the attachment tray: what you pass is what is sent, so your composer stays in charge of its
302
- own state.
308
+ palette, or a composer of your own replacing the built-in one. It no-ops for an entirely
309
+ empty message, and while a run or a checkpoint continuation picked from the panel is in
310
+ flight; a continuation counts from the pick, not from its first event, so the gap where two
311
+ runs could start against one conversation is closed. Unlike the built-in Send it does **not**
312
+ queue, so your composer keeps what it tried to send, and it does **not** consult the
313
+ attachment tray: what you pass is what is sent, so your composer stays in charge of its own
314
+ state.
303
315
 
304
316
  `attachFile(file)` queues a file into the tray exactly as the picker and drag-and-drop do, with
305
317
  the same validation and progress chip. It returns `false` when uploads are not configured
@@ -566,9 +578,10 @@ pydantic-ai's own `ToolReturnPart` vocabulary — read the values off `TOOL_OUTC
566
578
  | `success` | done | The same thing, stated. |
567
579
  | `failed` | error | The call ran and failed. |
568
580
  | `denied` | declined | A person or a guard refused it, so it never ran. |
581
+ | `interrupted` | not finished | It produced no result because the run ended first, and nobody refused it. |
569
582
 
570
583
  **Absent is a success, and anything unrecognised is too** — including a value from a later protocol
571
- version, and pydantic-ai's own `interrupted`. A card is a claim about what happened, and refusing to
584
+ version. A card is a claim about what happened, and refusing to
572
585
  recognise a word is not grounds for claiming failure. So no server has to change to keep the
573
586
  rendering it has today, and a server that adds the field gets the truth on screen instead of a
574
587
  green card with a refusal folded inside it.
@@ -587,10 +600,23 @@ Either way the outcome is **persisted beside the tool message**, so a reload rep
587
600
  settled rather than as a plain result. See
588
601
  [MPA durability](#mpa-durability-surviving-full-page-reloads).
589
602
 
603
+ **Every request carries a result for every tool call in it.** Several model providers reject a
604
+ turn holding a tool call with no result, and a run can leave one open in more ways than one: Stop
605
+ while the stream is still arriving, a round that ends on `RUN_ERROR`, a call naming a tool nothing
606
+ here owns, a server that never streamed the result. So before each request the client answers
607
+ every call still open with the `callNotFinished` string (by default `Not finished: the run ended
608
+ or moved on before this tool call returned a result.`) and the outcome `interrupted`, placed at the
609
+ end of the round that made the call, and the card settles to **not finished** in the same words.
610
+ That result blames nobody, because nobody declined: `declinedAction` is kept for a call a person
611
+ was asked about and turned down — a confirmation card or an approval they declined, or one Stop
612
+ closed while it was open. The calls a resumed approval is answering are left alone, because the
613
+ resume is their answer.
614
+
590
615
  The catalog a run advertises is also the set that run can execute. Override `getTools` to scope
591
616
  what a page offers — say, exposing `delete_record` only where deleting makes sense — and a call
592
617
  naming a tool you withheld is treated exactly as a call naming a tool you never registered: no
593
- handler runs, and the card settles with the no-result label. Withholding is per run, so the
618
+ handler runs, the card settles as not finished, and the next request answers the call in those
619
+ words. Withholding is per run, so the
594
620
  mount-wide registry can stay complete. Hosts that leave `getTools` alone advertise the built-ins
595
621
  plus everything registered, which is precisely what dispatch could reach anyway.
596
622
 
@@ -606,16 +632,29 @@ AG-UI has no server-side cancel route: cancelling **aborts the streaming request
606
632
  `onPersist`, so a reload shows the truncated exchange. A muted **"⏹ Stopped"** note is appended
607
633
  (`.stopped-note`) — a deliberate stop is not an error, so no bubble.
608
634
  - The run loop stops: tool calls collected before the abort are **not executed**, and no further
609
- round starts. A frontend tool handler already running completes, but its result doesn't trigger
610
- a re-run.
635
+ round starts. A frontend tool handler already running completes and its result is kept, but it
636
+ doesn't trigger a re-run, and the calls after it in the same round do not start. Each call that
637
+ did not run settles as **not finished**, and the next request answers
638
+ it in those words rather than sending it without a result.
611
639
  - An **open confirmation card is declined** (`data-resolved="declined"`) — cancelling the run
612
- answers the pending question. Likewise an open **approval card** is denied and an open
613
- **question card** (`ask_user`) resolves with an empty answer.
640
+ answers the pending question. Likewise an open **approval card** is denied, and the next request
641
+ carries that decline as the call's result; a call already approved when Stop lands was declined
642
+ by nobody, so it is answered as not finished instead. An open **question card** (`ask_user`)
643
+ resolves with an empty answer. Reloading the page while a card is open does not decline it,
644
+ because nobody answered: the call comes back not finished; see
645
+ [MPA durability](#mpa-durability-surviving-full-page-reloads).
614
646
  - The new `onCancelled()` handler fires instead of `onError()`; `onSettled()` still follows
615
647
  (the terminal-rest guarantee), returning the button to **Send**.
616
648
 
617
649
  `cancel()` with no run in flight is a safe no-op. `newChat()` cancels any in-flight run before
618
- discarding the client.
650
+ discarding the client, and so do switching conversations, `reload()` and removing the element. A
651
+ [checkpoint continuation](#resuming-a-run) is the run in flight while it lasts, so Stop and all of
652
+ these end it too.
653
+
654
+ A cancelled run ends a moment later, once its request has closed or a running tool handler has
655
+ returned. What it does then stays with the conversation it belonged to: its truncated exchange is
656
+ saved to that thread, `ag-ui-run-finished` still reports the tools it ran, and nothing reaches the
657
+ transcript or the Stop button of the conversation that replaced it.
619
658
 
620
659
  ### Registering tools
621
660
 
@@ -639,8 +678,9 @@ chat.registerTool({
639
678
  Registering a name twice replaces the earlier handler rather than throwing, so a re-fired
640
679
  host ref or React StrictMode's double-invoke is harmless -- but two different tools sharing
641
680
  a name means the second silently wins. Each `<ag-ui-chat>` element owns its own
642
- registry, AG-UI client, and Shadow DOM, so **multiple instances on one page never interfere** —
643
- there is no module-level shared state anywhere in the package.
681
+ registry, AG-UI client, and Shadow DOM, so **multiple instances on one page never interfere**.
682
+ The one record they share is which storage namespace each has claimed, and it exists to keep two
683
+ of them out of each other's conversation.
644
684
 
645
685
  **A handler's thrown message leaves the browser.** If a handler rejects, its `Error.message` is
646
686
  posted back as that call's tool result: into the conversation, on to the AG-UI endpoint, persisted
@@ -668,7 +708,10 @@ Whether a call is gated is decided in this order:
668
708
  1. If `chat.autoConfirm === true`, the call **never** prompts (an "autopilot" toggle).
669
709
  2. Else if `chat.confirmPredicate` is set, its boolean return is authoritative — given the tool
670
710
  name + parsed args it decides per-call (so one tool can be instant for some args and confirmed
671
- for others, which a static flag can't express).
711
+ for others, which a static flag can't express). **A predicate that throws or rejects refuses
712
+ the call**: no card, the handler does not run, the tool card settles as declined, and the agent
713
+ gets the `confirmCheckFailed` string as the result and carries on, as after a decline. The
714
+ error goes to the console, never to the endpoint.
672
715
  3. Else if the user has waived this tool name for the session, the call runs.
673
716
  4. Else the element falls back to [`isDestructive(parameters)`](src/tools/is_destructive.ts),
674
717
  which reads the `x-destructive` JSON-Schema flag.
@@ -688,7 +731,10 @@ The offer and the allowlist sit on the same path, so there is no dead button eit
688
731
 
689
732
  The waiver is **per tool name and per element**, held in memory and never persisted. A session
690
733
  decision that outlived the tab would be a permanent grant made by one click — which is what
691
- `autoConfirm` already exists to say deliberately. It is cleared when the element goes away.
734
+ `autoConfirm` already exists to say deliberately. It is cleared when the element goes away, and
735
+ when [`user-key`](#who-the-stored-conversation-belongs-to-user-key) changes hands: one person's
736
+ *Always allow* is not the next person's, and a sign-out in the same tab does not remount anything.
737
+ The first key to arrive keeps it, for the same reason it keeps the conversation.
692
738
 
693
739
  AG-UI has no built-in risk flag, so destructiveness is carried as a JSON-Schema extension at the
694
740
  **schema root**: `parameters["x-destructive"] = true` (use the exported `X_DESTRUCTIVE_KEY`
@@ -789,6 +835,12 @@ chat.approvalRenderer = (request, { signal }) =>
789
835
  myConfirmDialog(request.message ?? `Run ${request.toolName}?`, { signal });
790
836
  ```
791
837
 
838
+ **A renderer that throws or rejects falls back to the built-in card** for that interrupt, with a
839
+ `console.warn` naming it. The renderer decides how the question looks, not whether it is asked, so
840
+ nothing runs without a click and the run carries on as if no renderer were set. The one exception
841
+ is a rejection after the signal fired, which is how a renderer is expected to honour Stop: that
842
+ wait resolves as denied, as the built-in card does on the same signal, with no card and no warning.
843
+
792
844
  ### Asking the user a question (`ask_user`)
793
845
 
794
846
  Set `chat.askUser = true` to offer the agent a built-in `ask_user` frontend tool. When the agent
@@ -825,6 +877,13 @@ chat.askUserRenderer = (request, { signal }) =>
825
877
  myModal.ask(request.question, request.options, { allowCustom: request.allowCustom, signal });
826
878
  ```
827
879
 
880
+ **A renderer that throws or rejects falls back to the built-in card** for that call, with a
881
+ `console.warn` naming the tool call. As with `approvalRenderer`, the renderer decides how the
882
+ question looks, not whether it is asked, so the run carries on as if no renderer were set. The one
883
+ exception is a rejection after the signal fired, which is how a renderer is expected to honour Stop:
884
+ that wait resolves with an empty answer, as the built-in card does on the same signal, with no card
885
+ and no warning.
886
+
828
887
  ### DOM-driver and animation primitives
829
888
 
830
889
  So the agent can visibly drive the host page, the package ships generic, framework-free
@@ -1012,7 +1071,8 @@ matching JS API:
1012
1071
  The conversation it leaves is **kept**: it stays in the history drawer to return to, and on a
1013
1072
  server-backed store it stays on the server. Deleting one is the drawer row's own action. A chat
1014
1073
  nothing was ever sent in is the exception — it was never listed, so it is dropped rather than
1015
- left behind.
1074
+ left behind. Focus moves to the composer, whichever control started the new chat, unless the
1075
+ widget is collapsed, and the page is not scrolled to it.
1016
1076
  - `describeSurface()` — where the panel is and what can be done to it: placement, collapsed,
1017
1077
  whether it can be moved, whether it fills the screen, its box and the viewport. `movable` folds
1018
1078
  the two reasons a move can fail into the one answer a caller needs.
@@ -1037,7 +1097,10 @@ chat.addEventListener("ag-ui-toggle", (e) => console.log(e.detail.collapsed));
1037
1097
  ### The composer's own keys
1038
1098
 
1039
1099
  **Enter during a run queues.** A second run cannot start while one is in flight —
1040
- it would orphan the first — so that key used to do nothing at all, silently. What
1100
+ it would orphan the first — so that key used to do nothing at all, silently. A
1101
+ checkpoint continuation picked from the panel counts as in flight from the pick
1102
+ rather than from its first event, which is a request later, so a turn typed in
1103
+ that gap is parked too rather than racing it. What
1041
1104
  is waiting shows above the composer as chips, each of which takes its message
1042
1105
  back when pressed, and the next one is sent when the run settles. Stopping the
1043
1106
  run discards them: sending into a conversation someone has just stopped is the
@@ -1414,9 +1477,10 @@ If a tool's schema carries an `x-summary` string (use `X_SUMMARY_KEY`), the card
1414
1477
  label instead of the raw tool name.
1415
1478
 
1416
1479
  Every card leads with a **status icon** drawn entirely in CSS — a spinning ring while the call
1417
- runs, then a check / cross / slash on success / error / decline. Re-theme it via custom
1418
- properties (or the `tool-card-icon` part): `--ag-ui-tool-icon-done`, `--ag-ui-tool-icon-error`,
1419
- `--ag-ui-tool-icon-declined` (quoted-string glyphs) and `--ag-ui-tool-spin-duration` (spinner
1480
+ runs, then a check / cross / slash / dotted ring on success / error / decline / not finished.
1481
+ Re-theme it via custom properties (or the `tool-card-icon` part): `--ag-ui-tool-icon-done`,
1482
+ `--ag-ui-tool-icon-error`, `--ag-ui-tool-icon-declined`, `--ag-ui-tool-icon-interrupted`
1483
+ (quoted-string glyphs) and `--ag-ui-tool-spin-duration` (spinner
1420
1484
  speed; the spin respects `prefers-reduced-motion`).
1421
1485
 
1422
1486
  ```html
@@ -1962,7 +2026,9 @@ does not declare the field, exactly as it does not declare the `attachments` an
1962
2026
  user message; the default store round-trips both through `JSON.stringify`. **A store that drops
1963
2027
  unknown fields loses only the distinction** — the card falls back to *done*, which is what it did
1964
2028
  before. It is written onto the copy handed to the store and never onto `agent.messages`, so it is
1965
- not sent back to the server on the next run.
2029
+ not sent back to the server on the next run. That holds after a restore too: the client seeded from
2030
+ a stored conversation takes the field off every message it is seeded with and writes it back on the
2031
+ next save.
1966
2032
 
1967
2033
  **3. Resumable loop (`x-navigates` + `navigationResult`).** A tool whose schema carries
1968
2034
  `x-navigates: true` (use `X_NAVIGATES_KEY`; read back by [`isNavigates`](src/tools/is_navigates.ts))
@@ -1978,6 +2044,20 @@ triggers a full reload. Before the handler navigates, the element writes a check
1978
2044
 
1979
2045
  The MPA round-trip becomes a clean observation point instead of a dropped conversation.
1980
2046
 
2047
+ **A reload the run did not expect settles every open call as not finished.** A round's history is
2048
+ saved when its stream ends, before the element asks about a gated call, runs a frontend tool or
2049
+ collects a server-side approval, so a reload in that window finds the round's calls with no result
2050
+ and no run left to produce one. On restore, every call without a result (other than a checkpointed
2051
+ navigating call) is answered the way the client answers an open call before a request: its card
2052
+ settles as `interrupted` ("not finished"), and the restored conversation gains the `callNotFinished`
2053
+ result with the outcome `interrupted`, so the next request carries a result for every call and a
2054
+ later reload shows the same.
2055
+
2056
+ It is not declined, although Stop declines an open card. Pressing Stop is a person answering the
2057
+ question; a reload answers nothing. The saved history is the same whether the round was waiting on a
2058
+ person or on a frontend tool the reload killed, so a decline would be unproven for one and false for
2059
+ the other, and "not finished" is true of both. The same goes for a server-side approval left open.
2060
+
1981
2061
  ### Who the stored conversation belongs to (`user-key`)
1982
2062
 
1983
2063
  `sessionStorage` is scoped to a tab, not to a session. It survives every same-tab navigation,
@@ -1992,7 +2072,11 @@ data, so treat that as the default and turn it off:
1992
2072
  The value is any string that identifies the principal — a user id, an account id, a hash of one.
1993
2073
  It joins the storage namespace, so two principals in the same tab cannot reach each other's
1994
2074
  conversation, and **changing it purges everything the previous principal stored**: transcript,
1995
- history drawer index and navigation checkpoints, for this element's namespace only.
2075
+ history drawer index and navigation checkpoints, for this element's namespace only. The tools they
2076
+ waived with *Always allow* are forgotten with it, so the next principal is asked again, and so is
2077
+ [`sharedState`](#host-seams-the-spa-story): whatever the agent last wrote into it for them is cleared rather than
2078
+ sent on the next principal's first run, so a host that seeds shared state assigns it again for the
2079
+ principal who arrived.
1996
2080
 
1997
2081
  Set it live, from script, as part of signing out or in:
1998
2082
 
@@ -2158,7 +2242,9 @@ async def write_document(ctx: RunContext[AgentDeps], body: str) -> ToolReturn:
2158
2242
  ```
2159
2243
 
2160
2244
  Use this when the agent and the page are editing **the same object** (a document, a form, a
2161
- canvas). Use `registerPageState` when the agent should *ask* for a value or *request* a change
2245
+ canvas). Because it is the page's object rather than the conversation's, **New chat keeps it**; a
2246
+ change of [`user-key`](#who-the-stored-conversation-belongs-to-user-key) clears it. Use
2247
+ `registerPageState` when the agent should *ask* for a value or *request* a change —
2162
2248
  the tool call is visible in the transcript and can be gated by a confirmation card, which state
2163
2249
  events cannot.
2164
2250
 
@@ -2316,6 +2402,12 @@ Picking a row with an empty composer says so above the input and puts the caret
2316
2402
  there, rather than closing the panel over nothing: a continuation sends **only**
2317
2403
  the new turn, so with nothing typed there is nothing to send.
2318
2404
 
2405
+ Picking a row while an answer is still streaming — the conversation's own, or
2406
+ another continuation's — is refused the same way: the hint asks you to wait or
2407
+ stop it, and what you typed stays in the composer for the pick after. The run in
2408
+ flight is not cancelled for you, since a continuation starting over it would
2409
+ leave two answers streaming into one transcript with Stop reaching only one.
2410
+
2319
2411
  ### One URL, three endpoints
2320
2412
 
2321
2413
  `data-runs-url` is the only thing to configure. `resume/<id>/` and `fork/<id>/`
@@ -2333,11 +2425,24 @@ The component satisfies that structurally rather than by remembering a rule. A
2333
2425
  continuation runs on its own short-lived agent, built pointing at the resume
2334
2426
  endpoint and seeded with **no** history — so "only the new turn" is the only
2335
2427
  thing it *can* send, and the fresh run id comes free because a new agent mints
2336
- one. Your main agent's history is never touched.
2428
+ one. Nothing of the conversation's history goes out on its request.
2337
2429
 
2338
2430
  A resumed run is a normal run in every other respect: frontend tools execute,
2339
- approval interrupts render their card, and `headers` are re-read per request so
2340
- a rotated CSRF token or JWT still reaches the endpoint.
2431
+ approval interrupts render their card, `headers` are re-read per request so
2432
+ a rotated CSRF token or JWT still reaches the endpoint, it carries and updates
2433
+ [shared state](#host-seams-the-spa-story) like any other run, it is bounded by
2434
+ `data-max-tool-rounds`, and Stop ends it. Its client is built by the same
2435
+ construction as the conversation's own, differing only in the endpoint and the
2436
+ empty seed.
2437
+
2438
+ The continued exchange joins the conversation. Its agent holds only the new turn
2439
+ and the answer, and a store keeps one message list per thread, so each save
2440
+ writes the conversation as it stood when you picked the row with the exchange
2441
+ after it — through the same `saveMessages` as any other run, and as far as it
2442
+ got if it was stopped. A reload then replays the exchange, and the next message
2443
+ you send, now to `endpoint` again, carries it together with the shared state the
2444
+ continuation left. What is saved is what the transcript shows: a fork's exchange
2445
+ follows the conversation it was picked from, in that same thread.
2341
2446
 
2342
2447
  If the index can't be reached, the panel shows its empty state rather than an
2343
2448
  error — a history affordance that fails is empty, not broken.
@@ -2650,6 +2755,13 @@ ag-ui-chat {
2650
2755
  }
2651
2756
  ```
2652
2757
 
2758
+ `--ag-ui-radius` is the theme's corner radius. It rounds the panel's frame and the corners inside
2759
+ the panel that have no radius of their own: the answer well, the conversation list's *New chat*
2760
+ button and filter field, and the *Undo* on a run notice. The frame is the exception where it meets
2761
+ the edge of the window. `page` and `sidebar` draw it square, and so does every placement but
2762
+ `embedded` on a small viewport, while the corners inside keep the theme's radius, since none of them
2763
+ meets that edge. `--ag-ui-panel-radius` sets the frame on its own, in any placement.
2764
+
2653
2765
  ### Small viewports
2654
2766
 
2655
2767
  At **600px wide and below** every placement but `embedded` becomes one full-bleed
@@ -2662,25 +2774,34 @@ of the screen with a frame drawn round it.
2662
2774
  `embedded` is left alone deliberately: it sits in a box you sized and placed, and
2663
2775
  only you know whether that column should become the whole screen.
2664
2776
 
2777
+ The [greeting layout](#the-greeting-on-an-empty-page) changes shape here too, and
2778
+ it is the one part of this that `embedded` gets as well, because it moves nothing
2779
+ about the box you placed: an empty conversation keeps its composer at the foot of
2780
+ the panel, with the starters against it and the greeting over the space above
2781
+ them, instead of centring all three together.
2782
+
2665
2783
  The corner placements still rest at their launcher, so a full-bleed panel is
2666
2784
  something the user opens rather than something they are given.
2667
2785
 
2668
- ### The conversation list on a full page
2786
+ **To keep your desktop layout at every width**, set `data-small-viewport="off"`.
2787
+ That exists because the *trigger* is the one part of this you cannot reach: every
2788
+ value the override sets is a `--ag-ui-*` token you can re-state, but a media
2789
+ query cannot read a custom property, so the breakpoint itself is a literal.
2669
2790
 
2670
- On `placement="page"`, once the panel is at least **900px** wide, the chat-history
2671
- list **docks beside the transcript** instead of covering it no backdrop, no
2672
- focus trap, and `role="region"` rather than a modal dialog. Covering the
2673
- conversation to show the list of conversations hides the thing you are trying to
2674
- get back to, and a dedicated route is the one surface with width to spare.
2791
+ The breakpoint is a width rather than a pointer test, and that is on purpose: a
2792
+ touch laptop is coarse-pointered and wide, a narrow desktop window is
2793
+ fine-pointered and small. Width decides the layout; the pointer decides which
2794
+ controls make sense.
2675
2795
 
2676
- Narrower than that, or under any other placement, it stays the slide-over it was:
2677
- a few hundred pixels of panel with a list docked into it leaves a column of
2678
- transcript narrower than the messages in it. Width alone is not the test — an app
2679
- shell can hand `embedded` a page-sized box, and that box is still a column of
2680
- somebody's layout.
2796
+ ### The conversation list
2681
2797
 
2682
- `--ag-ui-threads-rail-width` sets the docked width (default 280px). While it is
2683
- docked the host carries `data-threads-docked`, so your own CSS can react.
2798
+ The chat-history list slides over the transcript at every width and under every
2799
+ placement, a full page included. Opening it moves nothing underneath: the
2800
+ conversation, the centred greeting and the composer are exactly where they were.
2801
+ While it is open it is a modal dialog: focus moves into it, Tab stays inside it,
2802
+ and Escape, its close control or a click on the backdrop beside it closes it and
2803
+ returns focus to where it was. `openThreads()` and `closeThreads()` do the same
2804
+ from your own chrome.
2684
2805
 
2685
2806
  The list also grows a filter once there are eight or more conversations in it
2686
2807
  — above that a search box is worth having, below it it is a control asking to be
@@ -2690,16 +2811,6 @@ phrase you remember is as likely to be inside the conversation as on it, and it
2690
2811
  filters what the drawer already holds rather than going back to the server for a
2691
2812
  list that is already in memory.
2692
2813
 
2693
- **To keep your desktop layout at every width**, set `data-small-viewport="off"`.
2694
- That exists because the *trigger* is the one part of this you cannot reach: every
2695
- value the override sets is a `--ag-ui-*` token you can re-state, but a media
2696
- query cannot read a custom property, so the breakpoint itself is a literal.
2697
-
2698
- The breakpoint is a width rather than a pointer test, and that is on purpose: a
2699
- touch laptop is coarse-pointered and wide, a narrow desktop window is
2700
- fine-pointered and small. Width decides the layout; the pointer decides which
2701
- controls make sense.
2702
-
2703
2814
  ### Reserving the space your own chrome occupies
2704
2815
 
2705
2816
  A fixed placement covers the viewport it is given, and it does not know about
@@ -2755,11 +2866,25 @@ widget measures the hidden band and publishes it as
2755
2866
  `--ag-ui-visual-viewport-inset-bottom`; state this one instead to outrank that
2756
2867
  measurement, or set it to `0px` to opt out of the lift entirely.
2757
2868
 
2869
+ `--ag-ui-keyboard-inset-top` does the same at the top. To show a field a
2870
+ keyboard would cover, a mobile browser pans the visible area down the page, and
2871
+ a panel anchored to the screen (`page`, `full`, `side`, `sidebar`, and at phone
2872
+ width the corner placements too) moves down with it. An `embedded` panel renders
2873
+ in your own box rather than against the screen, so it does not move at any width.
2874
+ The widget publishes the pan as `--ag-ui-visual-viewport-inset-top`, and the
2875
+ panel moves by as much of it as goes past your `--ag-ui-viewport-inset-top`,
2876
+ since the pan scrolls a reserved bar away with the page. State
2877
+ `--ag-ui-keyboard-inset-top` to outrank that distance, or `0px` to keep the
2878
+ panel below your reserved top.
2879
+
2758
2880
  `--ag-ui-viewport-height` and `--ag-ui-viewport-width` state the usable box
2759
2881
  outright, for the case where no viewport-percentage length describes it. An
2760
2882
  on-screen keyboard is the one that matters: it changes neither `vh` nor `dvh` nor
2761
2883
  `svh` on any current mobile browser, so a full-bleed panel has to be told the
2762
- visual viewport's height rather than deriving it.
2884
+ visual viewport's height rather than deriving it. The widget does that itself;
2885
+ where it has nothing to report, the panel falls back to `100dvh` rather than
2886
+ `100vh`, because iOS Safari resolves `vh` to the screen with its bars collapsed
2887
+ and a panel sized from it runs under them.
2763
2888
 
2764
2889
  Marks are variables too, so one vocabulary covers a re-theme rather than
2765
2890
  leaving half the transcript in the built-in set: `--ag-ui-tool-icon-done` /
@@ -2869,7 +2994,7 @@ component sets, so a new one cannot ship undocumented.
2869
2994
 
2870
2995
  | Feature | Parts |
2871
2996
  | --- | --- |
2872
- | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `jump-latest`, and one per resize grip: `resize-handle` plus `resize-handle-top`, `resize-handle-bottom`, `resize-handle-left`, `resize-handle-right`, `resize-handle-top-left`, `resize-handle-top-right`, `resize-handle-bottom-left`, `resize-handle-bottom-right` |
2997
+ | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `greeting`, `pending`, `stopped`, `jump-latest`, and one per resize grip: `resize-handle` plus `resize-handle-top`, `resize-handle-bottom`, `resize-handle-left`, `resize-handle-right`, `resize-handle-top-left`, `resize-handle-top-right`, `resize-handle-bottom-left`, `resize-handle-bottom-right` |
2873
2998
  | Header buttons | `header-button` on each, plus `history-button`, `checkpoints-button`, `new-button`, `collapse-button`, `theme-toggle` |
2874
2999
  | Collapsed widget | `launcher`, `launcher-icon`, `launcher-badge`, `rail-label` |
2875
3000
  | Answers | `answer` (the per-turn group), `message` (plus `message-user`, `message-assistant`), `code-copy` |
@@ -2885,7 +3010,7 @@ component sets, so a new one cannot ship undocumented.
2885
3010
  | Typed question | `question`, `question-body`, `question-options`, `question-choice`, `question-choice-text`, `question-radio`, `question-input`, `question-actions`, `question-button` |
2886
3011
  | Composer | `composer`, `composer-surface`, `composer-tools`, `input`, `send`, `attach-button`, `voice-button` |
2887
3012
  | 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` |
2888
- | Skills | `skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`, `skill-item-desc`, `skill-item-token`, `skill-hint` (the composer hint: a skill’s missing placeholders, and a run continuation with nothing typed) |
3013
+ | Skills | `skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`, `skill-item-desc`, `skill-item-token`, `skill-hint` (the composer hint: a skill’s missing placeholders, and a run continuation picked with nothing typed or while a run is in flight) |
2889
3014
  | Thread drawer | `drawer`, `drawer-backdrop`, `drawer-panel`, `drawer-header`, `drawer-title`, `drawer-new`, `drawer-close`, `drawer-filter`, `drawer-list`, `drawer-empty`, `drawer-row`, `drawer-row-select`, `drawer-row-title`, `drawer-row-time`, `drawer-row-preview`, `drawer-row-actions`, `drawer-row-rename`, `drawer-row-delete`, `drawer-rename-input`, `drawer-confirm`, `drawer-confirm-label`, `drawer-confirm-yes`, `drawer-confirm-no` |
2890
3015
  | Charts | `chart-block`, `chart-title`, `chart-legend` |
2891
3016
  | Checkpoints panel | `checkpoints`, `checkpoints-header`, `checkpoints-title`, `checkpoints-list`, `checkpoints-empty`, `checkpoint-row`, `checkpoint-label`, `checkpoint-time`, `checkpoint-id`, `checkpoint-branch`, `checkpoint-action` (plus `checkpoint-resume`, `checkpoint-fork`) |
@@ -2917,6 +3042,7 @@ with a matching `slot=`):
2917
3042
  | --- | --- |
2918
3043
  | `icon` | A header brand icon, before the title. |
2919
3044
  | `header-actions` | Extra controls between the title and the built-in buttons. |
3045
+ | `greeting` | The greeting's text, above the empty state, where the [greeting layout](#the-greeting-on-an-empty-page) is on. Replaces the text only; `empty` and the starters still render beneath it. |
2920
3046
  | `empty` | The empty-state shown before any message. |
2921
3047
  | `footer` | Below the composer. |
2922
3048
  | `launcher` | The collapsed widget's mark — the floating launcher, or the sidebar rail. |
@@ -2970,14 +3096,64 @@ naturally with the [answer well](#the-answer-well).
2970
3096
  <ag-ui-chat endpoint="/agent/" placement="page" data-answer-well></ag-ui-chat>
2971
3097
  ```
2972
3098
 
3099
+ #### The greeting on an empty page
3100
+
3101
+ Until the first message is sent, a page shows a greeting with the composer centred beneath it,
3102
+ and the composer moves to the foot of the page once the conversation has something in it. On a
3103
+ phone it starts at the foot, for the reason below.
3104
+
3105
+ ```html
3106
+ <ag-ui-chat endpoint="/agent/" placement="page" user-name="Ada"></ag-ui-chat>
3107
+ ```
3108
+
3109
+ - **The text** is `Hello, {name}`, filled from `user-name`, or `Hello there` when there is no
3110
+ name. Both are string-table keys, `greeting` and `greetingNoName`, so `strings` and
3111
+ `data-strings` translate them like everything else. `user-name` is live: a name that arrives
3112
+ after an auth handshake replaces the greeting on screen.
3113
+ - **Your own greeting** goes in `slot="greeting"`, which replaces the text and nothing else. A
3114
+ brand mark belongs there too. The `empty` slot and `data-starters` still render beneath it.
3115
+ - **Styling**: the `greeting` part, plus `--ag-ui-greeting-font` (default: the widget's font) and
3116
+ `--ag-ui-greeting-size` (default `clamp(1.5rem, 4vw, 2.25rem)`, scaling with the viewport rather
3117
+ than stepping at a breakpoint).
3118
+ - **`data-empty`** is present on the element while the conversation is empty, for chrome of your
3119
+ own that should react to the same state.
3120
+ - **Which placements**: on by default for `page`, which is a route of its own. `data-greeting="off"`
3121
+ restores the plain layout there. `embedded` opts in with `data-greeting` (any value but `off`),
3122
+ for an app shell that gives the panel a page-sized box. The corner placements and the sidebar
3123
+ never show it: a panel opened from a launcher is already mid-task.
3124
+ - **On a phone** the composer stays at the foot, the starters (or your own `empty` content) sit
3125
+ against it, and the greeting takes the middle of what they leave — the middle of the
3126
+ transcript when there are none. Centring is a shape for a screen with room to spare; at
3127
+ [600px wide and below](#small-viewports) the room is what the on-screen keyboard takes, and a
3128
+ composer centred over an empty band sits halfway up what is left of the screen, with the
3129
+ prompts that start a conversation nowhere near the field that sends one.
3130
+ `data-small-viewport="off"` keeps the centred shape at every width, as it keeps the rest of
3131
+ the desktop layout.
3132
+
3133
+ The composer's rows stay centred while you type, while a draft grows and while attachments are
3134
+ added, because the centring is two equal flexible halves either side of them and growth splits
3135
+ into both. Nothing in the DOM moves, so focus, the caret and IME composition are unaffected.
3136
+
3137
+ **Sending is what moves it**, not typing, and the composer slides to the foot of the page over
3138
+ `--ag-ui-motion` (a single frame under reduced motion). Everything else that changes the
3139
+ conversation snaps: a restored conversation mounts docked, picking a thread from the list or
3140
+ continuing a run docks at once, and a new chat returns to the centre at once. While a slow
3141
+ conversation store is still fetching, the page stays docked with the greeting hidden, so a
3142
+ restored conversation never paints the empty layout first.
3143
+
2973
3144
  ### The answer well
2974
3145
 
2975
3146
  Each assistant turn renders inside one `.answer` group (part `answer`) that holds its streamed
2976
3147
  text, tool cards, and pending indicator — so a turn that calls tools reads as a single answer
2977
3148
  rather than a string of loose siblings. Add the boolean `data-answer-well` attribute to box that
2978
3149
  group in a bordered, padded "well"; without it the layout is the flat stack as before. The well is
2979
- pure CSS and turn-scoped — no JS API — and themeable via `--ag-ui-well-bg` / `--ag-ui-well-border`
2980
- (and `::part(answer)`).
3150
+ pure CSS and turn-scoped — no JS API — and themeable via `--ag-ui-well-bg`, `--ag-ui-well-border`
3151
+ and `--ag-ui-well-radius` (and `::part(answer)`).
3152
+
3153
+ The well's corners default to `--ag-ui-radius`, 12px unless you set it, in every placement. The
3154
+ panel's own frame is different: `page` and `sidebar` draw it square because it meets the edge of
3155
+ the window, but nothing inside the panel does, so the well stays round there too. Set
3156
+ `--ag-ui-well-radius` to round the well differently from the rest of the panel.
2981
3157
 
2982
3158
  ```html
2983
3159
  <ag-ui-chat endpoint="/agent/" data-answer-well></ag-ui-chat>