@artooi/ag-ui-web-component 0.39.0 → 0.41.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 (58) hide show
  1. package/CHANGELOG.md +208 -1
  2. package/README.md +116 -36
  3. package/dist/ag-ui-web-component.bundle.js +255 -101
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +3 -2
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +7 -3
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +29 -8
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/core/attachment.d.ts +11 -4
  12. package/dist/core/attachment.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +14 -2
  14. package/dist/core/client_seed.d.ts.map +1 -1
  15. package/dist/core/tool_outcome.d.ts +4 -3
  16. package/dist/core/tool_outcome.d.ts.map +1 -1
  17. package/dist/core/utils.d.ts +19 -0
  18. package/dist/core/utils.d.ts.map +1 -1
  19. package/dist/index.js +1078 -664
  20. package/dist/index.js.map +4 -4
  21. package/dist/tools/tool_catalog.d.ts.map +1 -1
  22. package/dist/ui/composer/composer_attachments.d.ts +6 -1
  23. package/dist/ui/composer/composer_attachments.d.ts.map +1 -1
  24. package/dist/ui/composer/composer_voice.d.ts +3 -0
  25. package/dist/ui/composer/composer_voice.d.ts.map +1 -1
  26. package/dist/ui/excerpts/transcript_quote_offer.d.ts +7 -2
  27. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -1
  28. package/dist/ui/history/conversation_history.d.ts +30 -4
  29. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  30. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  31. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  32. package/dist/ui/placement/panel_placement.d.ts +1 -1
  33. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  34. package/dist/ui/styles.d.ts +1 -1
  35. package/dist/ui/styles.d.ts.map +1 -1
  36. package/dist/ui/transcript/transcript.d.ts +3 -2
  37. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  38. package/dist/ui/ui_strings.d.ts +2 -0
  39. package/dist/ui/ui_strings.d.ts.map +1 -1
  40. package/package.json +3 -3
  41. package/src/constants.ts +3 -2
  42. package/src/core/ag_ui_chat.ts +217 -64
  43. package/src/core/agui_client.ts +155 -125
  44. package/src/core/attachment.ts +13 -5
  45. package/src/core/client_seed.ts +14 -2
  46. package/src/core/tool_outcome.ts +4 -3
  47. package/src/core/utils.ts +23 -0
  48. package/src/tools/tool_catalog.ts +63 -12
  49. package/src/ui/composer/composer_attachments.ts +64 -41
  50. package/src/ui/composer/composer_voice.ts +5 -0
  51. package/src/ui/excerpts/transcript_quote_offer.ts +26 -13
  52. package/src/ui/history/conversation_history.ts +149 -38
  53. package/src/ui/placement/launcher_drag.ts +104 -89
  54. package/src/ui/placement/panel_placement.ts +27 -3
  55. package/src/ui/styles.ts +137 -14
  56. package/src/ui/transcript/transcript.ts +10 -5
  57. package/src/ui/ui_strings.ts +3 -0
  58. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,211 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.41.0] — 2026-09-22
11
+
12
+ ### Changed
13
+
14
+ - **Requires `@ag-ui/client` and `@ag-ui/core` 1.0.** Both ranges are now
15
+ `>=1.0.0 <2`. A host that bundles the package itself resolves 1.0 beside it;
16
+ the vendored bundle carries its own copy.
17
+
18
+ - **A tool result whose content arrives as parts shows its text.** Since 1.0 a
19
+ tool may return an ordered list of parts rather than a string, so a result can
20
+ carry an image beside its text. The card shows the text parts joined, both
21
+ while the run streams and when a reload replays it, and the saved conversation
22
+ keeps the message whole, parts included.
23
+
24
+ - **A tool call's outcome and a message's attachment refs travel in
25
+ `metadata`.** `@ag-ui/client` 1.0 strips every key its schemas do not declare,
26
+ from an inbound event before any subscriber sees it and from the outgoing run
27
+ input before it is sent, and `metadata` is the one field on events and
28
+ messages it declares open by key. The element now reads a `TOOL_CALL_RESULT`'s
29
+ outcome from `metadata.outcome` only, which is also what
30
+ `AgUiClientHandlers.onToolResult` receives, and `sendMessage` puts a user
31
+ message's refs at `metadata.attachments`.
32
+
33
+ The client folds a result's metadata onto the tool message it keeps, so a
34
+ server's outcome is now saved on the message itself, and the element writes
35
+ the outcomes it decides on its own -- a declined confirmation, a frontend tool
36
+ that threw, a call the run left unfinished -- in the same place. The outcome
37
+ therefore goes back to the server with the rest of the history on every run,
38
+ which it never did before. `metadata` is a declared field, and a server that
39
+ does not read the key ignores it.
40
+
41
+ - **Needs django-ag-ui 0.63.0 or later for outcomes and attachments.** An older
42
+ server writes the outcome, and reads the refs, at the top level, where 1.0
43
+ strips them. The chat still runs against one, but the agent is never told a
44
+ file was attached, and a failed or declined server-side tool call settles as
45
+ done.
46
+
47
+ - **Conversations stored by an earlier release keep both.** Their tool messages
48
+ carry `outcome`, and their user messages `attachments`, at the top level. The
49
+ replay reads `metadata` first and falls back to the top-level key, so a
50
+ declined card stays declined after the upgrade and a bubble keeps its chips.
51
+ A client seeded from such a conversation moves both keys into `metadata`
52
+ before its first request, so the server is still told about every file
53
+ attached before the upgrade, and the next save writes the new shape.
54
+
55
+ ### Deprecated
56
+
57
+ - **`AgUiClient.annotatedMessages` is the same list as `messages`.** It returned
58
+ a copy with each tool call's outcome added for the store, which the outcome
59
+ being on the message has made unnecessary. It is kept so code written against
60
+ it compiles and gets the same history.
61
+
62
+ ## [0.40.0] — 2026-09-17
63
+
64
+ ### Changed
65
+
66
+ - **An `askUserRenderer` that throws now hands the question to the built-in
67
+ question card.** A throw, or a rejected promise, used to escape the `ask_user`
68
+ tool: its card settled as an error quoting the host's message, that message
69
+ went on to the agent as the tool result although it was never written for the
70
+ model, and the pending wait was never closed, so the next Stop aborted the
71
+ signal of a question that had already ended.
72
+
73
+ The renderer decides how the question looks, not whether it is asked -- the
74
+ same rule `approvalRenderer` follows -- so it now falls back to the built-in
75
+ card, with a `console.warn` naming the tool call, and the run carries on as if
76
+ no renderer had been set. The exception is a rejection after the signal has
77
+ fired, which is how a renderer is expected to honour Stop: that wait resolves
78
+ with an empty answer, as the built-in card does on the same signal, with no
79
+ card drawn and no warning.
80
+
81
+ - **On a small viewport the composer stays at the foot of the panel, with the
82
+ greeting over the space above it.** Until a conversation had something in it,
83
+ `page` -- and an `embedded` panel that opts into a greeting -- centred the
84
+ composer between the greeting and the foot at every width. Centring is a shape
85
+ for a screen with room to spare: on a phone the on-screen keyboard takes the
86
+ room, and the composer was left halfway up what remained with an empty band
87
+ under it, which is the part of the screen a thumb is already on. At 600px wide
88
+ and below it now stays at the foot, directly over the keyboard, and the
89
+ greeting takes the space above it. The prompts in `data-starters`, or
90
+ whatever a host slots as `empty`, go to the foot with it and sit against the
91
+ composer: a prompt chip is a way into the conversation, and next to the field
92
+ it starts it reads as one. The greeting takes the middle of what they leave,
93
+ and the middle of the transcript where there are none.
94
+ `data-small-viewport="off"` keeps the centred shape at every width, as it
95
+ keeps the rest of the desktop layout.
96
+
97
+ ### Fixed
98
+
99
+ - **A tool card called with no arguments stops drawing an empty ARGUMENTS
100
+ heading.** The card hides the region rather than framing an empty object, but
101
+ the region declares its own `display`, which beats the user-agent rule for
102
+ the hidden attribute, so 42px of card was drawn holding a heading over
103
+ nothing -- on every call an agent made with no arguments, in the display mode
104
+ that shows arguments by default, and on a pending card's result region too.
105
+
106
+ - **A checkpoint continuation is let go of when its first save fails.** The
107
+ save runs through `conversationStore` synchronously inside the send, and a
108
+ store is the host's to replace: the built-in one swallows a write the browser
109
+ refused, a server-backed one need not. A throw there left the element holding
110
+ a client that would never run, so every later Resume or Fork was refused for
111
+ it with no way to clear it -- the composer's button is Send until a run
112
+ reports a start, so Stop was never offered -- and `sharedState` went on
113
+ writing into the dead client, which the next run then sent stale. The failure
114
+ is now reported with a `console.warn` and the panel takes another pick.
115
+
116
+ - **A turn typed between picking a checkpoint and its run starting is parked
117
+ rather than raced.** The composer learns a run is going from its first event,
118
+ a request round trip behind the pick, so its button was still Send: a turn
119
+ sent in that window started a second run against the conversation the
120
+ continuation had already frozen to save against, and whichever saved last
121
+ dropped the other's turn from the store while both answers streamed into one
122
+ transcript. The composer now queues it, as it does during a run, and sends it
123
+ when the continuation settles. `sendMessage` no-ops in the same window, as it
124
+ already did during a run.
125
+
126
+ - **An empty row of skill chips stops taking space above the composer.** The
127
+ row is hidden whenever the host offers no skills, which is most elements most
128
+ of the time, but it declares its own `display`, and an author display beats
129
+ the user-agent rule for the hidden attribute. So it kept its 20px of padding
130
+ under every placement: a band of panel between the transcript and the
131
+ composer, reading as the gap under whatever the transcript ended with. The
132
+ palette and the queued row beside it were fixed for this reason already, and
133
+ a tool card's payload regions turned out to be a fourth case, below.
134
+
135
+ - **A checkpoint continuation waits for the run in flight.** Picking Resume or
136
+ Fork while an answer was streaming started a second run beside it, and an
137
+ earlier continuation kept streaming where Stop no longer reached it. The pick
138
+ is now refused with a composer hint, as Retry is; the run in flight is left
139
+ alone and the typed turn stays in the composer. `continueWhileRunning` is new
140
+ in the string table.
141
+
142
+ - **A continued exchange is kept with its conversation.** It was never saved,
143
+ so a reload lost it, and the next message went out without it and with the
144
+ shared state from before the continuation. Each save now writes the
145
+ conversation with the exchange after it, through the existing `saveMessages`
146
+ -- `ClientConversationStore` is unchanged -- and the next message carries
147
+ both. After continuing from an earlier run, what is saved is what the
148
+ transcript shows, not only the turns up to that run. `sharedState` reads a
149
+ running continuation, and `AgUiClient.annotatedMessages` is new: the history
150
+ in the form a save writes it, with how each tool call ended.
151
+
152
+ - **A full-screen panel stays on the visible screen when a keyboard opens under
153
+ the field.** To show the composer, iOS Safari pans the visible area down the
154
+ page, and a panel anchored at the top of the screen stayed where it was: sized
155
+ to the visible height, it showed only its lower part, with the header and the
156
+ greeting off the top and an empty band above the keyboard. That covered
157
+ `page`, `full`, `side` and `sidebar`, and at phone width the corner placements
158
+ too, because they become the whole screen there, and their keyboard lift never
159
+ moved them: a box with its top, height and bottom all set ignores the bottom.
160
+ The element now also publishes how far the visible area is panned, as
161
+ `--ag-ui-visual-viewport-inset-top`, and every panel anchored at the top moves
162
+ down by as much of it as goes past the host's `--ag-ui-viewport-inset-top`.
163
+ `--ag-ui-keyboard-inset-top` outranks that, as `--ag-ui-keyboard-inset` does
164
+ at the bottom; `0px` opts out.
165
+
166
+ The same panel with a bar reserved at the top also ran past the bottom of the
167
+ visible area by the bar's height whenever a keyboard was up, pan or no pan:
168
+ the measured height replaced the host's box rather than being cut from it, so
169
+ the bar stayed in the position and left the height. The height is now the
170
+ host's box cut to the visible area.
171
+
172
+ At the deepest pan, the one that shows a composer docked at the foot of the
173
+ screen, the panel still went back to the top of the page at full height. The
174
+ element read the page's height from `innerHeight`, which iOS Safari reports
175
+ less the pan for as long as the pan lasts; at that depth it equals the visible
176
+ height, so the element took the two viewports to agree and withdrew its
177
+ measurements. The band below the visible area came out as nothing at any pan
178
+ too, leaving a bottom-anchored panel behind the keyboard. The page's height
179
+ is now read from the root element's `clientHeight`, which an iPhone reported
180
+ unchanged through the pan.
181
+
182
+ - **A full-screen panel no longer runs under Safari's bars with no keyboard
183
+ up.** Where nothing has been measured, the panel's height fell back to
184
+ `100vh`, which iOS Safari resolves to the screen with its bars collapsed, so
185
+ on a page that never scrolls them away the foot of the panel sat under the
186
+ address bar: 40px on the iPhone measured, with a docked composer in it. The
187
+ fallback is now `100dvh`, the screen with the bars as they are.
188
+
189
+ - **An element removed from the document and inserted again is one element.**
190
+ Every insertion built the chrome again into the same shadow root, so a
191
+ re-inserted `<ag-ui-chat>` had two headers, two composers, a second set of
192
+ resize grips, and the conversation drawn a second time beneath the first. The
193
+ listeners on its long-lived parts were added again too:
194
+ - one click on Stop cancelled the run three times;
195
+ - one press of the built-in theme toggle flipped it twice, so it appeared to
196
+ do nothing;
197
+ - a dropped or pasted file was added twice;
198
+ - one arrow press moved the launcher two steps.
199
+
200
+ With `data-threads-url`, a second remote store wrapped the first, so every
201
+ rename and delete reached the server twice. Frameworks do this without being
202
+ asked (a portal, a keyed list, a move between containers), and removing and
203
+ re-inserting is the documented way to apply a connect-time attribute written
204
+ late.
205
+
206
+ Connecting now builds the chrome in place of the last connection's, and ties
207
+ every listener on a long-lived part to the connection, so leaving the document
208
+ removes them. The upload tray and the mic are mounted afresh. The thread store
209
+ is wrapped once, and a `data-threads-url` removed while detached takes its
210
+ remote store away. The transcript is rebuilt from history, as `reload()`
211
+ rebuilds it. The composer's recall history is kept. Re-inserting still applies
212
+ the attributes as they stand when the element comes back. A host that called
213
+ `offerQuoteInPage()` calls it again after re-inserting, as before.
214
+
10
215
  ## [0.39.0] — 2026-09-17
11
216
 
12
217
  ### Changed
@@ -3973,7 +4178,9 @@ hosts that both arrange the page the way it expects.
3973
4178
  ### Notes
3974
4179
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
3975
4180
 
3976
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.39.0...HEAD
4181
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.41.0...HEAD
4182
+ [0.41.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.40.0...v0.41.0
4183
+ [0.40.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.39.0...v0.40.0
3977
4184
  [0.39.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.38.0...v0.39.0
3978
4185
  [0.38.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.37.0...v0.38.0
3979
4186
  [0.37.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.36.0...v0.37.0
package/README.md CHANGED
@@ -253,6 +253,11 @@ The list: `data-attachments-url`, `data-attachment-accept`, `data-attachment-max
253
253
  `data-skills-url`, `data-skills`, `data-prompt-chips`, `data-slash-commands`, `data-theme-toggle`,
254
254
  `data-strings`, `data-icon-url`.
255
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
+
256
261
  **Properties** (JS only, not attributes): `headers`, `getHeaders`, `trustedOrigins`, `allowImages`,
257
262
  `autoConfirm`, `confirmPredicate`, `askUser`, `askUserRenderer`, `approvalRenderer`,
258
263
  `approveWithEdits`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`, `getPageMap`,
@@ -300,10 +305,13 @@ the `copyCode` / `copied` / `copyFailed` strings.
300
305
 
301
306
  `sendMessage(content, attachments?)` sends as if the user had typed it — user bubble,
302
307
  `ag-ui-submit` event, run started. Use it for an "Ask about this order" button, a command
303
- palette, or a composer of your own replacing the built-in one. It no-ops while a run is in
304
- flight and for an entirely empty message, and unlike the built-in Send it does **not** consult
305
- the attachment tray: what you pass is what is sent, so your composer stays in charge of its
306
- 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.
307
315
 
308
316
  `attachFile(file)` queues a file into the tray exactly as the picker and drag-and-drop do, with
309
317
  the same validation and progress chip. It returns `false` when uploads are not configured
@@ -561,8 +569,9 @@ the current page state.
561
569
 
562
570
  #### A tool call that failed
563
571
 
564
- A `TOOL_CALL_RESULT` may carry an optional **`outcome`** field saying how the call ended. It takes
565
- pydantic-ai's own `ToolReturnPart` vocabulary — read the values off `TOOL_OUTCOME`:
572
+ A `TOOL_CALL_RESULT` may say how the call ended, as an optional **`outcome`** key in the event's
573
+ `metadata` (`"metadata": { "outcome": "failed" }`). It takes pydantic-ai's own `ToolReturnPart`
574
+ vocabulary — read the values off `TOOL_OUTCOME`:
566
575
 
567
576
  | `outcome` | Card | Means |
568
577
  | --- | --- | --- |
@@ -578,18 +587,23 @@ recognise a word is not grounds for claiming failure. So no server has to change
578
587
  rendering it has today, and a server that adds the field gets the truth on screen instead of a
579
588
  green card with a refusal folded inside it.
580
589
 
581
- Nothing in `@ag-ui/core` needs to declare the field: AG-UI's event schemas are zod `passthrough`, so
582
- an unknown key survives parsing and reaches the subscriber. It arrives on
583
- `AgUiClientHandlers.onToolResult` as an optional third argument, typed `unknown` because the
584
- protocol does not validate it; `toolStatusFromOutcome` does the narrowing.
590
+ It has to be in `metadata`. `@ag-ui/client` 1.0 strips every key its schemas do not declare from an
591
+ event before any subscriber sees it, with a `[ag-ui][enforce]` console warning, and `metadata` is
592
+ the one place on an event it declares open by key. An `outcome` at the top level of the event —
593
+ where a server wrote it for the 0.x client, and where a django-ag-ui older than 0.63.0 still does —
594
+ never arrives, so the card settles as done. The key arrives on `AgUiClientHandlers.onToolResult` as
595
+ an optional third argument, typed `unknown` because the protocol validates nothing inside
596
+ `metadata`; `toolStatusFromOutcome` does the narrowing.
585
597
 
586
598
  A **frontend** tool never streams this event — the client posts its own result — so its outcome is
587
599
  recorded on the `ToolExecution` the executor returns (`{ content, outcome: "denied" }`). That covers
588
600
  the two refusals the component makes itself: a confirmation card the user declined, and a call
589
601
  blocked because the page moved under the round.
590
602
 
591
- Either way the outcome is **persisted beside the tool message**, so a reload replays the card as it
592
- settled rather than as a plain result. See
603
+ Either way the outcome is **persisted on the tool message, in its `metadata`**, so a reload replays
604
+ the card as it settled rather than as a plain result. For a server's result `@ag-ui/client` puts it
605
+ there itself, folding the event's metadata onto the tool message it appends; for its own results the
606
+ client writes it in the same place. See
593
607
  [MPA durability](#mpa-durability-surviving-full-page-reloads).
594
608
 
595
609
  **Every request carries a result for every tool call in it.** Several model providers reject a
@@ -869,6 +883,13 @@ chat.askUserRenderer = (request, { signal }) =>
869
883
  myModal.ask(request.question, request.options, { allowCustom: request.allowCustom, signal });
870
884
  ```
871
885
 
886
+ **A renderer that throws or rejects falls back to the built-in card** for that call, with a
887
+ `console.warn` naming the tool call. As with `approvalRenderer`, the renderer decides how the
888
+ question looks, not whether it is asked, so the run carries on as if no renderer were set. The one
889
+ exception is a rejection after the signal fired, which is how a renderer is expected to honour Stop:
890
+ that wait resolves with an empty answer, as the built-in card does on the same signal, with no card
891
+ and no warning.
892
+
872
893
  ### DOM-driver and animation primitives
873
894
 
874
895
  So the agent can visibly drive the host page, the package ships generic, framework-free
@@ -1082,7 +1103,10 @@ chat.addEventListener("ag-ui-toggle", (e) => console.log(e.detail.collapsed));
1082
1103
  ### The composer's own keys
1083
1104
 
1084
1105
  **Enter during a run queues.** A second run cannot start while one is in flight —
1085
- it would orphan the first — so that key used to do nothing at all, silently. What
1106
+ it would orphan the first — so that key used to do nothing at all, silently. A
1107
+ checkpoint continuation picked from the panel counts as in flight from the pick
1108
+ rather than from its first event, which is a request later, so a turn typed in
1109
+ that gap is parked too rather than racing it. What
1086
1110
  is waiting shows above the composer as chips, each of which takes its message
1087
1111
  back when pressed, and the next one is sent when the run settles. Stopping the
1088
1112
  run discards them: sending into a conversation someone has just stopped is the
@@ -2001,16 +2025,23 @@ On mount the element rehydrates the transcript from the store, so the chat looks
2001
2025
  including tool-call cards and their results (reconstructed from the persisted `toolCalls` and `tool`
2002
2026
  messages), not just the text turns.
2003
2027
 
2004
- A tool message is saved with an extra **`outcome`** field when the call did not simply succeed, in
2005
- the same vocabulary the wire uses (see [A tool call that failed](#a-tool-call-that-failed)), so a
2006
- failed or declined card replays as failed or declined instead of turning green on reload. `Message`
2007
- does not declare the field, exactly as it does not declare the `attachments` an upload rides on a
2008
- user message; the default store round-trips both through `JSON.stringify`. **A store that drops
2009
- unknown fields loses only the distinction** — the card falls back to *done*, which is what it did
2010
- before. It is written onto the copy handed to the store and never onto `agent.messages`, so it is
2011
- not sent back to the server on the next run. That holds after a restore too: the client seeded from
2012
- a stored conversation takes the field off every message it is seeded with and writes it back on the
2013
- next save.
2028
+ A tool message is saved with an **`outcome`** in its `metadata` when the call did not simply
2029
+ succeed, in the same vocabulary the wire uses (see [A tool call that failed](#a-tool-call-that-failed)),
2030
+ so a failed or declined card replays as failed or declined instead of turning green on reload. A user
2031
+ message keeps the refs of its uploads the same way, under `attachments` in its `metadata`. Both
2032
+ round-trip through `JSON.stringify` in the default store. **A store that drops `metadata` loses only
2033
+ the distinction** — the card falls back to *done*, which is what it did before, and the bubble loses
2034
+ its chips.
2035
+
2036
+ What is saved is exactly what the next request sends, so both go back to the server on every run.
2037
+ That is the protocol's own behaviour for a declared field rather than a client-side addition: a server
2038
+ validates `metadata` as an open object, and one that does not read a key ignores it — django-ag-ui
2039
+ reads `attachments` and ignores `outcome`.
2040
+
2041
+ A conversation stored by an earlier release has both keys at the top level of the message instead.
2042
+ The history replay reads them from there when the metadata has none, and a client seeded from such a
2043
+ conversation moves them into `metadata` before its first request, because `@ag-ui/client` 1.0 would
2044
+ strip them from the request as it stands. The next save writes them in the new shape.
2014
2045
 
2015
2046
  **3. Resumable loop (`x-navigates` + `navigationResult`).** A tool whose schema carries
2016
2047
  `x-navigates: true` (use `X_NAVIGATES_KEY`; read back by [`isNavigates`](src/tools/is_navigates.ts))
@@ -2384,6 +2415,12 @@ Picking a row with an empty composer says so above the input and puts the caret
2384
2415
  there, rather than closing the panel over nothing: a continuation sends **only**
2385
2416
  the new turn, so with nothing typed there is nothing to send.
2386
2417
 
2418
+ Picking a row while an answer is still streaming — the conversation's own, or
2419
+ another continuation's — is refused the same way: the hint asks you to wait or
2420
+ stop it, and what you typed stays in the composer for the pick after. The run in
2421
+ flight is not cancelled for you, since a continuation starting over it would
2422
+ leave two answers streaming into one transcript with Stop reaching only one.
2423
+
2387
2424
  ### One URL, three endpoints
2388
2425
 
2389
2426
  `data-runs-url` is the only thing to configure. `resume/<id>/` and `fork/<id>/`
@@ -2401,7 +2438,7 @@ The component satisfies that structurally rather than by remembering a rule. A
2401
2438
  continuation runs on its own short-lived agent, built pointing at the resume
2402
2439
  endpoint and seeded with **no** history — so "only the new turn" is the only
2403
2440
  thing it *can* send, and the fresh run id comes free because a new agent mints
2404
- one. Your main agent's history is never touched.
2441
+ one. Nothing of the conversation's history goes out on its request.
2405
2442
 
2406
2443
  A resumed run is a normal run in every other respect: frontend tools execute,
2407
2444
  approval interrupts render their card, `headers` are re-read per request so
@@ -2411,9 +2448,14 @@ a rotated CSRF token or JWT still reaches the endpoint, it carries and updates
2411
2448
  construction as the conversation's own, differing only in the endpoint and the
2412
2449
  empty seed.
2413
2450
 
2414
- It does not write the conversation store. Its agent holds only the new turn and
2415
- the answer, and a store keeps one message list per thread, so saving that would
2416
- replace the stored conversation with its last exchange.
2451
+ The continued exchange joins the conversation. Its agent holds only the new turn
2452
+ and the answer, and a store keeps one message list per thread, so each save
2453
+ writes the conversation as it stood when you picked the row with the exchange
2454
+ after it — through the same `saveMessages` as any other run, and as far as it
2455
+ got if it was stopped. A reload then replays the exchange, and the next message
2456
+ you send, now to `endpoint` again, carries it together with the shared state the
2457
+ continuation left. What is saved is what the transcript shows: a fork's exchange
2458
+ follows the conversation it was picked from, in that same thread.
2417
2459
 
2418
2460
  If the index can't be reached, the panel shows its empty state rather than an
2419
2461
  error — a history affordance that fails is empty, not broken.
@@ -2426,7 +2468,10 @@ out-of-band (multipart, with the element's `headers`) and shows a chip in a pend
2426
2468
  `uploading` (with a progress bar) → `ready`, or `error` with a retry. On send, the ready files'
2427
2469
  **refs** ride on the user bubble as read-only chips and the agent reads their contents
2428
2470
  server-side via the `read_attachment` tool. The wire stays vanilla AG-UI: only lightweight refs
2429
- (`{ id, name, mime, size }`) travel, never the bytes.
2471
+ (`{ id, name, mime, size }`) travel, never the bytes, in the user message's `metadata` under
2472
+ `attachments` — the one place on a message `@ag-ui/client` 1.0 does not strip an unknown key from.
2473
+ django-ag-ui reads them from there as of 0.63.0; an older one reads only the top-level key 1.0
2474
+ removes, so against it the agent is never told a file was attached.
2430
2475
 
2431
2476
  ### Pasting
2432
2477
 
@@ -2597,7 +2642,7 @@ re-export point. Internal modules import from leaf paths.
2597
2642
  | `UploadOptions` | type | `{ url, headers?, credentials?, onProgress?, signal? }`. `credentials` is spelled as a fetch mode but carried by `XMLHttpRequest.withCredentials`, so only `"include"` is distinguishable. |
2598
2643
  | `UploadHandler` | type | `(file, onProgress, signal?) => Promise<AttachmentRef>` — the `uploadHandler` swap seam (tus / S3). The signal fires when the tray removes a chip or the element is torn down; a handler that honours it aborts its own transport, so a cancelled upload leaves no orphaned file on the server. |
2599
2644
  | `AttachmentRef` | type | The durable upload ref (`{ id, name, mime, size, url? }`). |
2600
- | `messageAttachments(message)` | function | Read the refs a restored user message carries. |
2645
+ | `messageAttachments(message)` | function | Read the refs a restored user message carries: from its `metadata`, or from the top level where an earlier release stored them. |
2601
2646
 
2602
2647
  ### Voice input
2603
2648
 
@@ -2745,6 +2790,12 @@ of the screen with a frame drawn round it.
2745
2790
  `embedded` is left alone deliberately: it sits in a box you sized and placed, and
2746
2791
  only you know whether that column should become the whole screen.
2747
2792
 
2793
+ The [greeting layout](#the-greeting-on-an-empty-page) changes shape here too, and
2794
+ it is the one part of this that `embedded` gets as well, because it moves nothing
2795
+ about the box you placed: an empty conversation keeps its composer at the foot of
2796
+ the panel, with the starters against it and the greeting over the space above
2797
+ them, instead of centring all three together.
2798
+
2748
2799
  The corner placements still rest at their launcher, so a full-bleed panel is
2749
2800
  something the user opens rather than something they are given.
2750
2801
 
@@ -2831,11 +2882,25 @@ widget measures the hidden band and publishes it as
2831
2882
  `--ag-ui-visual-viewport-inset-bottom`; state this one instead to outrank that
2832
2883
  measurement, or set it to `0px` to opt out of the lift entirely.
2833
2884
 
2885
+ `--ag-ui-keyboard-inset-top` does the same at the top. To show a field a
2886
+ keyboard would cover, a mobile browser pans the visible area down the page, and
2887
+ a panel anchored to the screen (`page`, `full`, `side`, `sidebar`, and at phone
2888
+ width the corner placements too) moves down with it. An `embedded` panel renders
2889
+ in your own box rather than against the screen, so it does not move at any width.
2890
+ The widget publishes the pan as `--ag-ui-visual-viewport-inset-top`, and the
2891
+ panel moves by as much of it as goes past your `--ag-ui-viewport-inset-top`,
2892
+ since the pan scrolls a reserved bar away with the page. State
2893
+ `--ag-ui-keyboard-inset-top` to outrank that distance, or `0px` to keep the
2894
+ panel below your reserved top.
2895
+
2834
2896
  `--ag-ui-viewport-height` and `--ag-ui-viewport-width` state the usable box
2835
2897
  outright, for the case where no viewport-percentage length describes it. An
2836
2898
  on-screen keyboard is the one that matters: it changes neither `vh` nor `dvh` nor
2837
2899
  `svh` on any current mobile browser, so a full-bleed panel has to be told the
2838
- visual viewport's height rather than deriving it.
2900
+ visual viewport's height rather than deriving it. The widget does that itself;
2901
+ where it has nothing to report, the panel falls back to `100dvh` rather than
2902
+ `100vh`, because iOS Safari resolves `vh` to the screen with its bars collapsed
2903
+ and a panel sized from it runs under them.
2839
2904
 
2840
2905
  Marks are variables too, so one vocabulary covers a re-theme rather than
2841
2906
  leaving half the transcript in the built-in set: `--ag-ui-tool-icon-done` /
@@ -2961,7 +3026,7 @@ component sets, so a new one cannot ship undocumented.
2961
3026
  | Typed question | `question`, `question-body`, `question-options`, `question-choice`, `question-choice-text`, `question-radio`, `question-input`, `question-actions`, `question-button` |
2962
3027
  | Composer | `composer`, `composer-surface`, `composer-tools`, `input`, `send`, `attach-button`, `voice-button` |
2963
3028
  | 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` |
2964
- | 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) |
3029
+ | 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) |
2965
3030
  | 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` |
2966
3031
  | Charts | `chart-block`, `chart-title`, `chart-legend` |
2967
3032
  | 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`) |
@@ -3050,7 +3115,8 @@ naturally with the [answer well](#the-answer-well).
3050
3115
  #### The greeting on an empty page
3051
3116
 
3052
3117
  Until the first message is sent, a page shows a greeting with the composer centred beneath it,
3053
- and the composer moves to the foot of the page once the conversation has something in it.
3118
+ and the composer moves to the foot of the page once the conversation has something in it. On a
3119
+ phone it starts at the foot, for the reason below.
3054
3120
 
3055
3121
  ```html
3056
3122
  <ag-ui-chat endpoint="/agent/" placement="page" user-name="Ada"></ag-ui-chat>
@@ -3071,6 +3137,14 @@ and the composer moves to the foot of the page once the conversation has somethi
3071
3137
  restores the plain layout there. `embedded` opts in with `data-greeting` (any value but `off`),
3072
3138
  for an app shell that gives the panel a page-sized box. The corner placements and the sidebar
3073
3139
  never show it: a panel opened from a launcher is already mid-task.
3140
+ - **On a phone** the composer stays at the foot, the starters (or your own `empty` content) sit
3141
+ against it, and the greeting takes the middle of what they leave — the middle of the
3142
+ transcript when there are none. Centring is a shape for a screen with room to spare; at
3143
+ [600px wide and below](#small-viewports) the room is what the on-screen keyboard takes, and a
3144
+ composer centred over an empty band sits halfway up what is left of the screen, with the
3145
+ prompts that start a conversation nowhere near the field that sends one.
3146
+ `data-small-viewport="off"` keeps the centred shape at every width, as it keeps the rest of
3147
+ the desktop layout.
3074
3148
 
3075
3149
  The composer's rows stay centred while you type, while a draft grows and while attachments are
3076
3150
  added, because the centring is two equal flexible halves either side of them and growth splits
@@ -3107,9 +3181,9 @@ When a reasoning model streams its chain-of-thought (django-ag-ui forwards it as
3107
3181
  events — enable a thinking budget via `MODEL_SETTINGS`, see its docs), the element renders a muted,
3108
3182
  collapsible **thoughts region** (part `thoughts`) at the top of the current answer group. It opens
3109
3183
  while the model reasons and folds away on the answer's first token; the reader can reopen it. The
3110
- web component handles the `REASONING_*` event family (and the deprecated `THINKING_*`, which
3111
- `@ag-ui/client` maps onto it), so no client config is needed — the thoughts appear whenever the
3112
- server forwards reasoning.
3184
+ web component handles the `REASONING_*` event family (and the retired `THINKING_*`, which
3185
+ `@ag-ui/client` still converts onto it, with a console warning, during its deprecation window), so
3186
+ no client config is needed — the thoughts appear whenever the server forwards reasoning.
3113
3187
 
3114
3188
  ### The composer
3115
3189
 
@@ -3240,11 +3314,17 @@ Other workflow targets (all identical in name to the sibling Python packages):
3240
3314
  | --- | --- | --- |
3241
3315
  | Node (tooling/tests only) | 22 | 22, 24 |
3242
3316
  | Browsers (runtime target) | ES2022 / evergreen | Chrome / Firefox / Safari 17+ |
3243
- | `@ag-ui/client` | latest 0.x | — |
3317
+ | `@ag-ui/client`, `@ag-ui/core` | 1.0 | 1.0 |
3318
+ | django-ag-ui (server) | 0.63.0 | — |
3244
3319
 
3245
3320
  The shipped artefact targets evergreen browsers (Shadow DOM, Custom Elements v1, ES2022). Node is
3246
3321
  only the build/test runtime, not a runtime target.
3247
3322
 
3323
+ The server floor is a wire contract rather than a dependency. A tool call's outcome and a user
3324
+ message's attachment refs travel in `metadata`, since `@ag-ui/client` 1.0 strips every key its
3325
+ schemas do not declare. Against an older django-ag-ui the chat still runs, but the agent is never
3326
+ told about an attached file and a failed or declined server-side tool call settles as done.
3327
+
3248
3328
  ---
3249
3329
 
3250
3330
  ## License