@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/CHANGELOG.md CHANGED
@@ -7,6 +7,518 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.40.0] — 2026-09-17
11
+
12
+ ### Changed
13
+
14
+ - **An `askUserRenderer` that throws now hands the question to the built-in
15
+ question card.** A throw, or a rejected promise, used to escape the `ask_user`
16
+ tool: its card settled as an error quoting the host's message, that message
17
+ went on to the agent as the tool result although it was never written for the
18
+ model, and the pending wait was never closed, so the next Stop aborted the
19
+ signal of a question that had already ended.
20
+
21
+ The renderer decides how the question looks, not whether it is asked -- the
22
+ same rule `approvalRenderer` follows -- so it now falls back to the built-in
23
+ card, with a `console.warn` naming the tool call, and the run carries on as if
24
+ no renderer had been set. The exception is a rejection after the signal has
25
+ fired, which is how a renderer is expected to honour Stop: that wait resolves
26
+ with an empty answer, as the built-in card does on the same signal, with no
27
+ card drawn and no warning.
28
+
29
+ - **On a small viewport the composer stays at the foot of the panel, with the
30
+ greeting over the space above it.** Until a conversation had something in it,
31
+ `page` -- and an `embedded` panel that opts into a greeting -- centred the
32
+ composer between the greeting and the foot at every width. Centring is a shape
33
+ for a screen with room to spare: on a phone the on-screen keyboard takes the
34
+ room, and the composer was left halfway up what remained with an empty band
35
+ under it, which is the part of the screen a thumb is already on. At 600px wide
36
+ and below it now stays at the foot, directly over the keyboard, and the
37
+ greeting takes the space above it. The prompts in `data-starters`, or
38
+ whatever a host slots as `empty`, go to the foot with it and sit against the
39
+ composer: a prompt chip is a way into the conversation, and next to the field
40
+ it starts it reads as one. The greeting takes the middle of what they leave,
41
+ and the middle of the transcript where there are none.
42
+ `data-small-viewport="off"` keeps the centred shape at every width, as it
43
+ keeps the rest of the desktop layout.
44
+
45
+ ### Fixed
46
+
47
+ - **A tool card called with no arguments stops drawing an empty ARGUMENTS
48
+ heading.** The card hides the region rather than framing an empty object, but
49
+ the region declares its own `display`, which beats the user-agent rule for
50
+ the hidden attribute, so 42px of card was drawn holding a heading over
51
+ nothing -- on every call an agent made with no arguments, in the display mode
52
+ that shows arguments by default, and on a pending card's result region too.
53
+
54
+ - **A checkpoint continuation is let go of when its first save fails.** The
55
+ save runs through `conversationStore` synchronously inside the send, and a
56
+ store is the host's to replace: the built-in one swallows a write the browser
57
+ refused, a server-backed one need not. A throw there left the element holding
58
+ a client that would never run, so every later Resume or Fork was refused for
59
+ it with no way to clear it -- the composer's button is Send until a run
60
+ reports a start, so Stop was never offered -- and `sharedState` went on
61
+ writing into the dead client, which the next run then sent stale. The failure
62
+ is now reported with a `console.warn` and the panel takes another pick.
63
+
64
+ - **A turn typed between picking a checkpoint and its run starting is parked
65
+ rather than raced.** The composer learns a run is going from its first event,
66
+ a request round trip behind the pick, so its button was still Send: a turn
67
+ sent in that window started a second run against the conversation the
68
+ continuation had already frozen to save against, and whichever saved last
69
+ dropped the other's turn from the store while both answers streamed into one
70
+ transcript. The composer now queues it, as it does during a run, and sends it
71
+ when the continuation settles. `sendMessage` no-ops in the same window, as it
72
+ already did during a run.
73
+
74
+ - **An empty row of skill chips stops taking space above the composer.** The
75
+ row is hidden whenever the host offers no skills, which is most elements most
76
+ of the time, but it declares its own `display`, and an author display beats
77
+ the user-agent rule for the hidden attribute. So it kept its 20px of padding
78
+ under every placement: a band of panel between the transcript and the
79
+ composer, reading as the gap under whatever the transcript ended with. The
80
+ palette and the queued row beside it were fixed for this reason already, and
81
+ a tool card's payload regions turned out to be a fourth case, below.
82
+
83
+ - **A checkpoint continuation waits for the run in flight.** Picking Resume or
84
+ Fork while an answer was streaming started a second run beside it, and an
85
+ earlier continuation kept streaming where Stop no longer reached it. The pick
86
+ is now refused with a composer hint, as Retry is; the run in flight is left
87
+ alone and the typed turn stays in the composer. `continueWhileRunning` is new
88
+ in the string table.
89
+
90
+ - **A continued exchange is kept with its conversation.** It was never saved,
91
+ so a reload lost it, and the next message went out without it and with the
92
+ shared state from before the continuation. Each save now writes the
93
+ conversation with the exchange after it, through the existing `saveMessages`
94
+ -- `ClientConversationStore` is unchanged -- and the next message carries
95
+ both. After continuing from an earlier run, what is saved is what the
96
+ transcript shows, not only the turns up to that run. `sharedState` reads a
97
+ running continuation, and `AgUiClient.annotatedMessages` is new: the history
98
+ in the form a save writes it, with how each tool call ended.
99
+
100
+ - **A full-screen panel stays on the visible screen when a keyboard opens under
101
+ the field.** To show the composer, iOS Safari pans the visible area down the
102
+ page, and a panel anchored at the top of the screen stayed where it was: sized
103
+ to the visible height, it showed only its lower part, with the header and the
104
+ greeting off the top and an empty band above the keyboard. That covered
105
+ `page`, `full`, `side` and `sidebar`, and at phone width the corner placements
106
+ too, because they become the whole screen there, and their keyboard lift never
107
+ moved them: a box with its top, height and bottom all set ignores the bottom.
108
+ The element now also publishes how far the visible area is panned, as
109
+ `--ag-ui-visual-viewport-inset-top`, and every panel anchored at the top moves
110
+ down by as much of it as goes past the host's `--ag-ui-viewport-inset-top`.
111
+ `--ag-ui-keyboard-inset-top` outranks that, as `--ag-ui-keyboard-inset` does
112
+ at the bottom; `0px` opts out.
113
+
114
+ The same panel with a bar reserved at the top also ran past the bottom of the
115
+ visible area by the bar's height whenever a keyboard was up, pan or no pan:
116
+ the measured height replaced the host's box rather than being cut from it, so
117
+ the bar stayed in the position and left the height. The height is now the
118
+ host's box cut to the visible area.
119
+
120
+ At the deepest pan, the one that shows a composer docked at the foot of the
121
+ screen, the panel still went back to the top of the page at full height. The
122
+ element read the page's height from `innerHeight`, which iOS Safari reports
123
+ less the pan for as long as the pan lasts; at that depth it equals the visible
124
+ height, so the element took the two viewports to agree and withdrew its
125
+ measurements. The band below the visible area came out as nothing at any pan
126
+ too, leaving a bottom-anchored panel behind the keyboard. The page's height
127
+ is now read from the root element's `clientHeight`, which an iPhone reported
128
+ unchanged through the pan.
129
+
130
+ - **A full-screen panel no longer runs under Safari's bars with no keyboard
131
+ up.** Where nothing has been measured, the panel's height fell back to
132
+ `100vh`, which iOS Safari resolves to the screen with its bars collapsed, so
133
+ on a page that never scrolls them away the foot of the panel sat under the
134
+ address bar: 40px on the iPhone measured, with a docked composer in it. The
135
+ fallback is now `100dvh`, the screen with the bars as they are.
136
+
137
+ - **An element removed from the document and inserted again is one element.**
138
+ Every insertion built the chrome again into the same shadow root, so a
139
+ re-inserted `<ag-ui-chat>` had two headers, two composers, a second set of
140
+ resize grips, and the conversation drawn a second time beneath the first. The
141
+ listeners on its long-lived parts were added again too:
142
+ - one click on Stop cancelled the run three times;
143
+ - one press of the built-in theme toggle flipped it twice, so it appeared to
144
+ do nothing;
145
+ - a dropped or pasted file was added twice;
146
+ - one arrow press moved the launcher two steps.
147
+
148
+ With `data-threads-url`, a second remote store wrapped the first, so every
149
+ rename and delete reached the server twice. Frameworks do this without being
150
+ asked (a portal, a keyed list, a move between containers), and removing and
151
+ re-inserting is the documented way to apply a connect-time attribute written
152
+ late.
153
+
154
+ Connecting now builds the chrome in place of the last connection's, and ties
155
+ every listener on a long-lived part to the connection, so leaving the document
156
+ removes them. The upload tray and the mic are mounted afresh. The thread store
157
+ is wrapped once, and a `data-threads-url` removed while detached takes its
158
+ remote store away. The transcript is rebuilt from history, as `reload()`
159
+ rebuilds it. The composer's recall history is kept. Re-inserting still applies
160
+ the attributes as they stand when the element comes back. A host that called
161
+ `offerQuoteInPage()` calls it again after re-inserting, as before.
162
+
163
+ ## [0.39.0] — 2026-09-17
164
+
165
+ ### Changed
166
+
167
+ - **An empty conversation under `placement="page"` now greets the user, with the
168
+ composer centred beneath the greeting.** A full-page chat used to open on a
169
+ blank transcript with the composer at the foot of the screen, which is the
170
+ layout of a conversation in progress rather than one waiting to start. The
171
+ page now shows *Hello, {name}* over a centred composer until the conversation
172
+ has something in it, then lays out as before.
173
+
174
+ This changes what any existing `page` host shows on upgrade, which is why it
175
+ is listed here. **`data-greeting="off"` restores the previous layout.** No
176
+ other placement changes by default; `embedded` opts in with `data-greeting`,
177
+ and the corner placements and the sidebar never show it, because a panel
178
+ opened from a launcher is already mid-task.
179
+
180
+ The surface it adds:
181
+
182
+ - `user-name` / `userName`, a live attribute for the name. Presentation only:
183
+ unlike `user-key` it scopes nothing and is never sent. Absent or blank gives
184
+ *Hello there*.
185
+ - `greeting` and `greetingNoName` in the string table.
186
+ - A `greeting` slot and part, so a host can replace or restyle the text; the
187
+ `empty` slot and `data-starters` still render beneath it.
188
+ - `--ag-ui-greeting-font` and `--ag-ui-greeting-size`.
189
+ - `data-empty`, stamped on the element while the conversation is empty, and
190
+ documented as a styling hook for the chrome around a full-page chat.
191
+
192
+ The centring is two equal flexible halves either side of the composer's rows,
193
+ so the rows stay centred as a draft or an attachment tray grows them, and
194
+ nothing in the DOM moves: focus, the caret and IME composition are untouched.
195
+ Every condition in its selectors is held by a Chromium test that fails with
196
+ that condition removed, since a stylesheet has no coverage to fall back on.
197
+
198
+ **Only a send moves the composer, and it slides.** It moves on send rather
199
+ than on the first keystroke, because a field that moves under the caret moves
200
+ during IME composition too, and because taking the greeting away before
201
+ anything is committed would mean bringing it back when the draft is deleted.
202
+ The slide uses `--ag-ui-motion`, so reduced motion jumps straight to the
203
+ dock, and the rule over the composer fades in with it rather than being drawn
204
+ at its destination while the composer is still on the way. Every other way
205
+ out of the empty state -- a restored conversation, a thread picked from the
206
+ list, a continued run -- and every way back into it snap, because each is a
207
+ change of context rather than a continuation of what the user was doing.
208
+
209
+ A conversation a slow store is still fetching is held in the docked layout
210
+ with the greeting hidden, so a page restoring an existing conversation never
211
+ paints a centred composer that then drops when the messages land. A new chat
212
+ started during that fetch greets at once rather than waiting on it.
213
+
214
+ - **Internal restructuring: `ag_ui_chat.ts` is split into modules that each own
215
+ one concern.** The element was one class of more than six thousand lines in
216
+ which a single concern -- the transcript, the run handlers -- was spread over
217
+ a dozen separate stretches of the file. Each concern moves into a controller
218
+ in the directory it already belonged to, holding its own private state, and
219
+ the element keeps its public members as delegations with their documentation
220
+ unchanged.
221
+
222
+ What has moved, each into the directory its concern already had:
223
+
224
+ - The panel's placement -- the dragged launcher, the dragged header, the
225
+ eight resize grips, the corner the panel opens from and the persisted
226
+ record of all of it -- is `PanelPlacement` in `src/ui/placement/`.
227
+ - Quoting a selection, in the transcript and over the host page, is
228
+ `TranscriptQuoteOffer` in `src/ui/excerpts/`.
229
+ - The skill catalog -- its three sources, their merge into the chips and the
230
+ palette, and what a pick does to the composer -- is `SkillCatalog` in
231
+ `src/skills/`.
232
+ - The composer's attachments (the tray and the picker, drop and paste routes
233
+ into it) and its mic are `ComposerAttachments` and `ComposerVoice` in
234
+ `src/ui/composer/`.
235
+ - Which storage keys are the element's -- the namespace it claims in the
236
+ document, the keys its layout preferences live under, and the built-in
237
+ conversation store scoped to them -- is `StorageScope` in `src/core/`.
238
+ - The answer streaming into its bubble is `AnswerStream` in
239
+ `src/ui/transcript/`, a delegated sub-agent's progress is
240
+ `SubAgentProgress` in `src/ui/progress/`, and the registry of activity
241
+ renderers and the blocks they drew is `ActivityRegistry` in `src/core/`.
242
+ - The decision a run is suspended on, which Stop abandons, is
243
+ `PendingDecision` in `src/ui/interrupts/`: one owner where four places
244
+ used to write one field.
245
+ - The frontend tools the agent is offered -- the host's, the built-ins, the
246
+ `ask_user` question, what the current round advertised, and the server's
247
+ labels for its own tools -- are `ToolCatalog` in `src/tools/`.
248
+ - The transcript -- its bubbles, the open answer group, the pending dots, the
249
+ reasoning region, the tool cards and the scroller that follows the foot --
250
+ is `Transcript` in `src/ui/transcript/`, and the action row under a
251
+ finished answer is `AnswerActions` beside it.
252
+ - Running the frontend tool calls a round produced and answering the
253
+ server's interrupts -- the page each round's context described, the
254
+ confirmation gate and the tools the user waived it for, and the approval
255
+ cards -- is `ToolDispatch` in `src/tools/`.
256
+ - The AG-UI event handlers a run is drawn by, and what one interaction
257
+ accumulates for the host until it settles -- the tool calls it made, the
258
+ keys it invalidated, and whether it already said how it ended -- are
259
+ `RunHandlers` in `src/core/`. The screen-reader status region the run and
260
+ its tool calls report into is `RunAnnouncer` in `src/ui/progress/`.
261
+ - Which conversation is on screen and how one gets there -- the active
262
+ thread, restoring it from the store, switching, renaming and deleting from
263
+ the conversation list, and continuing a run from the checkpoint panel -- is
264
+ `ConversationHistory` in `src/ui/history/`, beside the list and the panel
265
+ it drives.
266
+ - What held no state of its own became plain functions. The builders of the
267
+ element's shell -- a header control, an icon holder, a glyph slot, the
268
+ adopted stylesheet -- and the readers of the launcher's icon URL and its
269
+ unread badge switch are in `src/ui/shell/`. The starter prompts are beside
270
+ the suggestion chips, the composer's sizing is in `src/ui/composer/`,
271
+ whether a placement can collapse is beside whether it can be dragged, and
272
+ the tool-round limit is read in `src/core/`. The attachment tray's `accept`
273
+ list is split by the same comma tokeniser the other opt-in lists use.
274
+
275
+ Nothing a consumer can see changes. Every declaration file the package
276
+ already emitted was compared byte for byte before and after each step, and
277
+ the moves altered one: the internal `core/utils.d.ts` gained the shared comma
278
+ tokeniser. No declaration reachable from the package entry point imports
279
+ that file or any of the new modules' declarations, so a consumer's types
280
+ cannot reach them. No test was edited to make a move pass.
281
+
282
+ The public declarations do differ from the previous release, by the other
283
+ entries in this release and not by this one. The bundle differs by this one
284
+ as well: on its own it asks a re-vendor for no host wiring, and it is still a
285
+ different build.
286
+
287
+ - **A new chat puts focus in the composer.** Starting a conversation is a
288
+ request to type one, but pressing *New chat* left focus on the button, so a
289
+ keyboard user emptied the composer and then had to find their way back to it.
290
+ `newChat()` now focuses the composer whichever control called it -- the
291
+ header's button, the history list's own, or a host's -- unless the widget is
292
+ collapsed and the composer is not on screen. It does so without scrolling, so
293
+ a host resetting a chat further down the page from its own code does not move
294
+ the page to it.
295
+
296
+ - **The conversation list now slides over the transcript at every width,
297
+ including a wide full page, where it used to dock beside it.** Under
298
+ `placement="page"` with at least 900px of panel, opening the list padded the
299
+ rows over to make room for it, so the conversation, the centred greeting and
300
+ the composer all moved sideways the moment it opened. It is now the same
301
+ slide-over there as under every other placement and width: a modal dialog
302
+ over a backdrop, with nothing behind it moving. `data-threads-docked` and
303
+ `--ag-ui-threads-rail-width` no longer exist, so host CSS written against
304
+ either can be deleted.
305
+
306
+ - **The answer well and the controls inside the panel keep round corners when
307
+ the panel's frame is square.** Under `placement="page"` and `"sidebar"`, and
308
+ on a small viewport, the frame is square because it meets the edge of the
309
+ window. Those placements squared it by setting the theme's radius to 0, so
310
+ everything else that reads that radius went square with it: the answer well,
311
+ the conversation list's *New chat* button and filter field, and a run
312
+ notice's *Undo*, beside message bubbles and a composer that stayed round. They
313
+ now square only the frame, through a new `--ag-ui-panel-radius`, and the
314
+ corners inside keep `--ag-ui-radius` (12px unless set). The floating and
315
+ embedded panels look as they did.
316
+
317
+ **A host that sets `--ag-ui-radius` under one of those placements now gets a
318
+ square frame**, where the token used to round the frame too. Set
319
+ `--ag-ui-panel-radius` to round it again. The well also gains
320
+ `--ag-ui-well-radius`, beside `--ag-ui-well-bg` and `--ag-ui-well-border`, to
321
+ round it apart from the rest of the panel.
322
+
323
+ ### Fixed
324
+
325
+ - **Stop ends a checkpoint continuation.** While a resumed or forked run was
326
+ streaming, the composer offered Stop and pressing it did nothing; New chat,
327
+ switching conversations and removing the element left it streaming too, so the
328
+ rest of the resumed answer drew itself into whatever came next. The element
329
+ cancelled only the conversation's own client, and a continuation runs on a
330
+ client of its own. Every path that stops the conversation's run now stops a
331
+ continuation as well.
332
+
333
+ - **A run cut off by New chat or a thread switch stays with the conversation it
334
+ belonged to.** Cancelling closes the request, but the run finishes a moment
335
+ later, and by then the element had moved on. So its "Stopped" note appeared at
336
+ the top of the new conversation; its truncated exchange was saved under the new
337
+ thread, which the drawer then listed as a second copy of the old one; and its
338
+ `ag-ui-run-finished` reported a server-side tool as the host's own, because the
339
+ record of what streamed back had been cleared with the transcript. Where a host
340
+ tool's handler was still running -- which cancelling cannot interrupt -- and the
341
+ user sent the next message before it returned, the old run's settle also put the
342
+ new run's Stop button back to Send, and one event carried both runs' tools while
343
+ the new run's own reported none.
344
+
345
+ Each conversation's runs now keep their bookkeeping apart. A run left behind by
346
+ a reset still reports what it ran, since a stopped run may already have written
347
+ something, and saves to the thread it was started in; nothing else it does
348
+ reaches the conversation that replaced it.
349
+
350
+ - **A resumed or forked run now carries the page's shared state, reports the
351
+ state it changes, and stops at `data-max-tool-rounds`.** A checkpoint
352
+ continuation sent an empty state object, so an agent whose tools read the
353
+ page's state resumed without it; a `STATE_SNAPSHOT` it streamed never reached
354
+ `ag-ui-state`; and on a page that had raised the tool-round bound, the resumed
355
+ half of a form-filling task still stopped at ten.
356
+
357
+ The continuation's client was built separately from the conversation's own,
358
+ and both options arrived after continuations did, so each was wired into one
359
+ construction only. There is now one construction, and a continuation differs
360
+ from the conversation's client only in its endpoint, its empty seed, and in
361
+ not writing the conversation store -- which it never did, deliberately: its
362
+ agent holds only the new turn and its answer, so saving them would replace the
363
+ stored conversation with its last exchange.
364
+
365
+ - **Text a user or a server wrote could rewrite the label it was put into.**
366
+ Every string-table template was filled with `template.replace("{token}",
367
+ value)`, and a string replacement interprets dollar patterns in the value: `$&`
368
+ inserts the matched token and `` $` `` and `$'` the text either side of it. So a
369
+ queued message reading `costs $& more` was labelled
370
+ `Do not send "costs {text} more"`, and the same went for a tool name in the
371
+ confirmation prompt, a skill id in the "Using skill" notice and an agent name
372
+ on a delegation row. The skill hint was two replacements chained, so a skill
373
+ titled with `{fields}` in it was filled twice.
374
+
375
+ A string pattern also fills only its first occurrence, so a translation that
376
+ used a token twice was left half filled. That one reached every fill, the
377
+ numeric ones included.
378
+
379
+ Every fill -- seventeen, across the element, the confirmation card, the
380
+ attachment tray, the voice button and the relative timestamps -- now goes
381
+ through one helper that fills every occurrence in a single pass with a replacer
382
+ function, whose return value is never interpreted. Each file that filled has a
383
+ test that failed against the old fill.
384
+
385
+ - **A closed slash-command palette and an empty queued-messages row still took
386
+ up space.** Both set the hidden attribute and both declare their own
387
+ `display`, which beats the browser's rule for hidden elements -- the defect the
388
+ attachment tray's own hidden rule was written for. The palette is mounted on
389
+ every element and closed nearly all of the time, so it painted 2px of border
390
+ and its shadow as a line above the composer; the queued row kept 10px of
391
+ padding. Measured in Chromium, which is where the new tests live, because
392
+ happy-dom lays out no boxes and reports both as zero either way.
393
+
394
+ - **Opening the conversation list or the checkpoints panel did not move focus
395
+ into it.** Each asks for focus as it opens -- the list on *New chat*, the
396
+ checkpoints panel on its first row or itself -- but both eased their
397
+ visibility in so the exit could play, and a transition starts at its first
398
+ value: at the instant of the call the panel was still hidden, so the browser
399
+ dropped it and focus stayed wherever it had been, at every placement and under
400
+ reduced motion. A keyboard user opened a modal dialog they were not in.
401
+ Visibility now changes at once on the way in and is delayed on the way out, so
402
+ the list's slide and the panel's fade are unchanged. happy-dom computes no
403
+ transitions, which is why the unit tests of the same calls passed; the new
404
+ tests are in Chromium.
405
+
406
+ - **A new `user-key` now gets its own confirmation cards.** A tool one principal
407
+ waived with *Always allow* kept running without a card for whoever signed in
408
+ next in the same tab, because the waiver lives in the element's memory rather
409
+ than in the stored conversation that changing `user-key` purges. `user-key`
410
+ exists because a sign-out is a navigation inside one tab, not a remount, so the
411
+ element outlives the person who clicked -- and the next person's destructive
412
+ calls ran on that click, with nothing on screen to say anyone had been asked.
413
+
414
+ Changing the key, or removing it, now forgets every waiver along with the
415
+ transcript. The first key to arrive keeps them, for the same reason it keeps
416
+ the conversation on screen: it names the user who was already there.
417
+
418
+ - **A `confirmPredicate` that throws now refuses the call, and the run carries
419
+ on.** A throw, or a rejected promise, used to end the whole run on an error
420
+ bubble quoting the host's own message, and left that call's tool card reading
421
+ "running..." for good, because dispatch had already taken the card out of the
422
+ sweep that settles leftovers when a run ends. The handler did not run, but
423
+ nothing on screen said so and the agent was never told.
424
+
425
+ The call now fails closed: no confirmation card, no handler, the tool card
426
+ settles as declined, and the agent receives the new `confirmCheckFailed`
427
+ string as the result and goes on to its next round, as it does after a
428
+ decline. It is refused rather than put to a card because for a tool with no
429
+ `x-destructive` flag the predicate is the only guard, and one click would run
430
+ what the host's policy could not vouch for. The error is reported with
431
+ `console.warn`, as a failed `render` already is, and is not sent to the
432
+ endpoint: unlike a handler's message, it was never written for the model.
433
+
434
+ - **An `approvalRenderer` that throws now hands the decision to the built-in
435
+ approval card.** A throw, or a rejected promise, used to end the run on an
436
+ error bubble quoting the host's message without ever answering the server's
437
+ interrupt, and the sweep that settles leftover cards at the end of a run then
438
+ marked the gated call a green "done" -- for a call that never ran and that
439
+ nobody had been asked about. One failing interrupt took every other interrupt
440
+ in its batch down with it.
441
+
442
+ The renderer decides how the question looks, not whether it is asked, so it
443
+ now falls back to the built-in card for that interrupt, with a `console.warn`
444
+ naming the interrupt, the way a failed `render` falls back to the default.
445
+ Nothing runs without a click, and the run carries on as if no renderer had
446
+ been set. The exception is a rejection after the signal has fired, which is
447
+ how a renderer is expected to honour Stop: that wait resolves as denied, as
448
+ the built-in card does on the same signal, with no card drawn and no warning.
449
+
450
+ - **Reloading the page while a tool call waits for approval now shows that call
451
+ as not finished, where it showed a spinner that never stopped.** The run loop
452
+ saves a round's history when its stream ends, which is before it asks about a
453
+ gated call, runs a frontend tool or collects a server-side approval. A reload in
454
+ that window left the call stored with no result, and the request that would
455
+ have produced one had died with the page, so the restored card had no Approve
456
+ or Decline and nothing left to settle it. The next message then went out
457
+ carrying a tool call with no result, which several model providers reject.
458
+
459
+ The card now settles as not finished, and the conversation carries the same
460
+ not-finished result the client gives any call a run left open, so the next
461
+ request is a valid turn and a later reload shows the same. It is not declined,
462
+ although Stop declines an open card, because Stop is a person answering the
463
+ question and a reload is not: the saved history looks the same whether the
464
+ round was waiting on a person or on a frontend tool the reload killed, so a
465
+ decline would be unproven for the first and false for the second, and "not
466
+ finished" is true of both. A server-side approval left open by a
467
+ reload comes back the same way. A navigating tool's call is unchanged: it
468
+ resumes from the page it landed on. A restored card for a call that an earlier
469
+ round went past without a result settles as not finished too, and the restore
470
+ now gives that call its result as well, at the end of its own round.
471
+
472
+ - **A request sent after a reload no longer carries the element's `outcome`
473
+ labels to the server.** The label that lets a reload replay a declined or
474
+ failed card is written onto the stored copy of a tool message only, and the
475
+ README promises it is never sent. A restore seeds the next agent from that
476
+ stored copy, though, so every tool message restored with a label went out
477
+ with it on the next request. The client now takes the labels off the history
478
+ it is seeded with and keeps them beside it, so the request carries none, the
479
+ restored cards still settle as they did, and the next save writes each label
480
+ back.
481
+
482
+ - **A request no longer carries a tool call without a result, however the run
483
+ that made the call ended.** Several model providers reject such a turn, so the
484
+ conversation could not continue past it, and a live run left one in four ways
485
+ with no reload involved: Stop while the stream was still arriving, Stop on an
486
+ open server-side approval, a round that ended on `RUN_ERROR` after emitting
487
+ calls, and a call naming a tool nothing on the page owns. Each was guarded,
488
+ if at all, where it happened, and the guards between them left gaps.
489
+
490
+ The client now answers every call still open before each request it makes,
491
+ in one place, so a path nobody listed cannot send one either. The answer goes
492
+ at the end of the round that made the call, where a provider looks for it.
493
+ It says the call did not finish, in the new `callNotFinished` string, with
494
+ the outcome `interrupted`, pydantic-ai's word for the same thing, because
495
+ nobody refused it; the card settles to a new **not finished** status in the
496
+ same words, with its own `--ag-ui-tool-icon-interrupted` glyph, where it used
497
+ to claim "done" over "No result returned.". The decline stays where a person
498
+ declined: an approval Stop closed while it was open is answered with
499
+ `declinedAction`, and one already approved when Stop landed is answered as not
500
+ finished. The calls a resumed approval is answering are left for the server.
501
+ `UiStrings.noResult` is no longer drawn and is deprecated, and a server's own
502
+ `interrupted` outcome now renders as not finished rather than as done.
503
+
504
+ - **Stop during a frontend tool no longer lets the next call in the same round
505
+ run.** The loop checked for Stop between rounds but not between the calls of
506
+ one round, so when the agent asked for two page actions at once and Stop was
507
+ pressed while the first ran, the second ran anyway as soon as the first
508
+ returned, after the person had asked for the run to end. A handler already
509
+ running still completes and keeps its result, since it cannot be aborted, but
510
+ the calls after it no longer start, and each is answered as not finished.
511
+
512
+ - **Changing `user-key` no longer sends the previous principal's shared state
513
+ on the next principal's first run.** A handover purges the stored
514
+ conversation, the transcript and the tools waived with Always allow, but
515
+ `sharedState` survived it, and it holds whatever the agent last wrote into it
516
+ for the principal who left. The next client is seeded from it, so it went out
517
+ as `RunAgentInput.state` on the new principal's first request. It is now
518
+ cleared on the handover. New chat still keeps it, because shared state is the
519
+ page's object and the same person is still editing it, and the first key to
520
+ arrive keeps it for the same reason it keeps the conversation.
521
+
10
522
  ## [0.38.0] — 2026-09-14
11
523
 
12
524
  ### Changed
@@ -3614,7 +4126,9 @@ hosts that both arrange the page the way it expects.
3614
4126
  ### Notes
3615
4127
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
3616
4128
 
3617
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.38.0...HEAD
4129
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.40.0...HEAD
4130
+ [0.40.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.39.0...v0.40.0
4131
+ [0.39.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.38.0...v0.39.0
3618
4132
  [0.38.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.37.0...v0.38.0
3619
4133
  [0.37.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.36.0...v0.37.0
3620
4134
  [0.36.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.35.2...v0.36.0