@artooi/ag-ui-web-component 0.23.1 → 0.25.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 (37) hide show
  1. package/CHANGELOG.md +194 -1
  2. package/README.md +188 -38
  3. package/dist/ag-ui-web-component.bundle.js +189 -49
  4. package/dist/ag-ui-web-component.bundle.js.map +3 -3
  5. package/dist/constants.d.ts +25 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +24 -0
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/conversation_store.d.ts +18 -0
  10. package/dist/core/conversation_store.d.ts.map +1 -1
  11. package/dist/core/remote_conversation_store.d.ts +2 -0
  12. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  13. package/dist/core/run_index.d.ts +10 -0
  14. package/dist/core/run_index.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +335 -44
  18. package/dist/index.js.map +2 -2
  19. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  20. package/dist/ui/styles.d.ts +1 -1
  21. package/dist/ui/styles.d.ts.map +1 -1
  22. package/dist/ui/tool_call_card.d.ts +26 -2
  23. package/dist/ui/tool_call_card.d.ts.map +1 -1
  24. package/dist/ui/ui_strings.d.ts +2 -0
  25. package/dist/ui/ui_strings.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/constants.ts +26 -0
  28. package/src/core/ag_ui_chat.ts +186 -40
  29. package/src/core/conversation_store.ts +30 -0
  30. package/src/core/remote_conversation_store.ts +14 -0
  31. package/src/core/run_index.ts +10 -0
  32. package/src/index.ts +3 -0
  33. package/src/ui/checkpoint_menu.ts +32 -7
  34. package/src/ui/styles.ts +153 -13
  35. package/src/ui/tool_call_card.ts +41 -3
  36. package/src/ui/ui_strings.ts +3 -0
  37. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,197 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.25.0] — 2026-08-14
11
+
12
+ ### Added
13
+
14
+ - **Every deferred call is asked about in its own card, and all of them at once.**
15
+ A run can defer more than one call, and the wire takes a different answer for
16
+ each — so the UI has to let a person give one, which means saying which card is
17
+ which. Importing three rows gated three `create_event` calls and produced three
18
+ identical prompts: the text comes from the tool, so all three read "Add this
19
+ event to the board?", and nothing else on the card named the row. They were also
20
+ serial, each appearing only once the previous was answered, appended below the
21
+ three calls they gated — so a person answering the first could not compare them
22
+ or tell that two more were coming. **A batch gate that can only be answered
23
+ uniformly is one you have to answer blind.**
24
+
25
+ Each prompt now renders into the tool card of the call it gates, under that
26
+ call's own arguments, and every card opens at once. No new wire field was needed:
27
+ the arguments were already there, and the call site already held the card. A
28
+ card being asked about shows its arguments in every `data-tool-display` mode,
29
+ since a density setting must not hide the answer to "which one is this". New
30
+ part: `tool-card-approval`.
31
+
32
+ - **A status for "deferred, awaiting a decision".** `TOOL_CALL_STATUS` was
33
+ `pending | done | error | declined`, so a gated call sat at `pending` and read
34
+ **running…** while the stream was over and the server idle. A status enum missing
35
+ a state does not omit that state, it renders it as whichever neighbour is
36
+ closest, and this one claimed the opposite of the truth. Gated cards now read
37
+ `waiting for you` (`data-status="deferred"`, `strings.toolDeferred`), with a
38
+ steady dot instead of a spinner, and go back to `pending` on approval — because
39
+ then the tool really is running. A **frontend** tool such as `ask_user` keeps
40
+ saying "running…" while its card is open: the browser is executing it.
41
+
42
+ - **The checkpoint rows lead with what the run was about.** A run index that
43
+ answers `GET runs/` with a `preview` — the run's first user message — and the
44
+ panel now shows it, with the time demoted to a chip beside it (new part:
45
+ `checkpoint-time`). A row with no preview keeps the old time-plus-short-id
46
+ layout, so an older server is unaffected.
47
+
48
+ - **`toggleCheckpoints()` and `closeCheckpoints()`.** The built-in ⭯ control now
49
+ calls the toggle, so pressing it a second time dismisses the panel;
50
+ `openCheckpoints()` keeps its open-only meaning for a host that wants exactly
51
+ that.
52
+ - **The checkpoint rows show a short run id.** A time is not an identity: two runs
53
+ of the same minute both read "just now", and choosing between them was choosing
54
+ blind. Eight characters of the id, muted, beside the time — with the whole id on
55
+ its own `title` rather than on the row's label, so hovering a row no longer
56
+ raises a full UUID over it. Now the fallback rather than the rule: a row that
57
+ arrives with a `preview` leads with the words instead (see above).
58
+
59
+ ### Changed
60
+
61
+ - **`approvalRenderer` is called once per interrupt, concurrently.** It used to be
62
+ awaited one at a time. A host that can only ask one question at a time should
63
+ queue inside its renderer.
64
+
65
+ ### Fixed
66
+
67
+ - **The README's parts list was missing an entire feature, and now a test reads
68
+ it.** Part names are declared public API there, and an undocumented part is not a
69
+ part: a host cannot style what it cannot know exists, and a name guessed wrong
70
+ fails silently. Absent were all twelve checkpoint-panel parts plus
71
+ `checkpoints-button`, and five strays from other features (`code-copy`,
72
+ `resize-handle`, `run-notice-icon`, `run-notice-text`, `skill-item-token`). The
73
+ list drifted because nothing read it, so the fix is not only the missing names:
74
+ it is now a table, spelled out in full rather than as `tool-card` *plus* `-icon`
75
+ shorthand, and a test collects every part the source sets and fails on one the
76
+ table does not name. The handful assembled at runtime are enumerated in that
77
+ test, and adding another such call site fails it too.
78
+
79
+ - **The checkpoint panel could not be closed by the control that opened it.**
80
+ The button called `open()`, which returns early when the panel is already open,
81
+ so the first gesture anyone tries did nothing. Escape worked, and answering a row
82
+ worked, and that was all. It now toggles, and a pointer landing anywhere else in
83
+ the widget dismisses it — the thread drawer has a backdrop that swallows such a
84
+ click, and this popover has none.
85
+
86
+ - **The thread drawer and the checkpoint popover could be open at once.** Opening
87
+ either now closes the other. The pointer case was already covered by the
88
+ click-away above, but a host driving its own chrome through `openThreads()` /
89
+ `openCheckpoints()` raises no pointer event, and the drawer would slide open
90
+ underneath a popover still floating over it.
91
+
92
+ - **The panel read as a list of clickable rows.** Each row painted itself on hover
93
+ while nothing about the row was pressable: the two buttons that *were* pressable
94
+ sat on that highlight as transparent outlines. The row now carries a resting
95
+ surface and no hover at all, and the actions carry the filled-primary /
96
+ outlined-secondary pair the confirmation and approval cards already use, plus
97
+ `:active` and — new — a visible `:focus-visible` ring, in a panel that traps
98
+ focus and is reached by Tab.
99
+
100
+ - **A narrow panel crushed the timestamp to nothing.** The row is a flex line whose
101
+ only flexible child is the label, so it absorbs every fixed-width element the row
102
+ gains; at 320px the label kept its text, reported it correctly, and measured 0px.
103
+ This is the tool-call head's defect (0.24.0) one panel along, and it is now
104
+ measured in a real browser at both widths — happy-dom lays out no boxes and
105
+ called the broken row a pass.
106
+
107
+ - **The ⭯ glyph is now ↺.** The old one has no font behind it in most browsers and
108
+ rendered as an unreadable mark at 14px. A header control nobody can name is one
109
+ nobody presses.
110
+
111
+ - **The composer painted a paperclip that could not upload anything.** The element
112
+ sets `hidden` on the attach button until a host gives it somewhere to upload —
113
+ an `uploadHandler` or `data-attachments-url` — but the button's own
114
+ `display: inline-flex` beat the UA stylesheet's rule for the hidden property, so
115
+ it stayed visible and clickable, opening a file picker whose file had nowhere to
116
+ go. This is the third instance of one trap: the attachment tray was fixed in
117
+ 0.23.0 and carries a comment about it two rules away in the same file. Found in
118
+ the framework gallery, where three of the four host apps were in exactly that
119
+ state and nobody had noticed the clip was inert.
120
+
121
+ **The mic does not share it**, and the asymmetry is pinned by a test rather than
122
+ assumed: the clip is created and hidden, while the voice wiring returns before
123
+ constructing anything, leaving an empty slot that measures nothing. A
124
+ hidden-state rule for the mic would match no element, so there is one rule.
125
+
126
+ ## [0.24.0] — 2026-08-13
127
+
128
+ ### Added
129
+
130
+ - **`ag-ui-run-finished`** — an event fired once per interaction, carrying the
131
+ tools that ran and which side ran each (`{ tools: [{ name, side }] }`, typed
132
+ `RunFinishedDetail` / `ToolRun`). **For hosts that render data the agent can
133
+ change.** A server-side tool writes without the page's knowledge, and nothing
134
+ the element dispatched implied "something may have moved underneath you": a
135
+ page that fetched its data on mount had no reason to refetch, so approving a
136
+ server-side write left it showing stale data with no way to notice. Shared
137
+ state was the only channel back, and it is not one a host can rely on, because
138
+ it needs the *agent* to emit `STATE_SNAPSHOT`. Fires on completion, error and
139
+ cancellation alike, since a partial write is still a write; a capability load
140
+ is not counted, since it moves nothing a host renders.
141
+
142
+ ### Changed
143
+
144
+ - **A server-side approval can now ask a readable question.** An AG-UI
145
+ interrupt's question defaults to the call spelled out —
146
+ `Approve create_event({"title": "Design sync", …})?` — which is accurate and
147
+ not something to put in front of a person, while the *client-side*
148
+ confirmation card has had `x-confirm` for exactly this. The approval card now
149
+ prefers `x-confirm` from the interrupt's `metadata`, so one key covers both
150
+ gates, and a server that supplies nothing keeps the generated text. Anything
151
+ non-string or blank under that key is ignored rather than rendered, since a
152
+ wire field typed `Record<string, any>` can carry an object into the one place
153
+ a person is being asked to allow a write.
154
+
155
+ ### Fixed
156
+
157
+ - **Every first visit spent a request to be told `404`.** With
158
+ `data-threads-url` set, the element minted a thread id on mount and
159
+ immediately asked the server for its history — history that cannot exist,
160
+ because the id was three lines old. The response was correct and harmless, and
161
+ it put a red `404` in the console of a page where nothing had gone wrong, on
162
+ every first visit to every host. `ClientConversationStore` gained an optional
163
+ `isUnsent(threadId)`, which the session store answers from a marker it sets when
164
+ it mints and drops on the first save, and the remote store skips the fetch when
165
+ it is `true`. Deliberately narrow: *"I hold no messages for this id"* is not the
166
+ same claim as *"this id is new"*, and only the store that minted it can make the
167
+ second one — so a thread picked from the drawer, or created on another device,
168
+ is still fetched.
169
+ - **An approved call's tool card broke its own name into pieces.** The card's
170
+ head is a flex row in which the name is the only flexible child, so every
171
+ fixed badge the row gains is taken out of it. The decision badge ("approved by
172
+ you") appears only on the server-approval path, and in a sidebar-width panel it
173
+ left the name **37px** wide: `word-break: break-word` then split *Create event*
174
+ into "Creat / e / event" across three lines. The head wraps now and the name
175
+ keeps a floor instead of a zero min-width, so a badge drops to its own row
176
+ rather than shredding a word, while a genuinely unbreakable name still breaks
177
+ instead of overflowing the card. Measured in a real browser at 470px: the name
178
+ went 37px to 144px and three lines to one.
179
+
180
+ ### Documentation
181
+
182
+ - **The four framework recipes, not just React's.** The connect-time
183
+ configuration boundary is reached differently by each host, and only **Vue**
184
+ has a hook that runs before insertion (a directive's `beforeMount`); React,
185
+ Svelte 5 and Angular all create the element by hand. Angular additionally needs
186
+ `:host { display: contents }` or its own host element breaks the page's layout.
187
+ - **Page actions**: a page action reports that it *fired*, not that it worked;
188
+ a page that saves asynchronously should say so (a `saving` flag in the page
189
+ map) or a verification read will outrun the save; `drag_and_drop` dispatches
190
+ the **native HTML5 drag sequence**, which a pointer-event drag library
191
+ (dnd-kit, the Angular CDK) never sees; and `scroll_to` centres vertically but
192
+ brings into view horizontally.
193
+ - **`placement="embedded"` fills the box the host gives it — so give it one.**
194
+ `min-height: 0` plus `overflow: hidden` on the containing element, or a growing
195
+ transcript pushes the composer off the bottom of the window.
196
+ - **The auto-injected page map may go nowhere.** It rides in
197
+ `RunAgentInput.context`, and pydantic-ai's AG-UI adapter does not read that
198
+ field, so on such a backend the injected copy is silently dropped and
199
+ `read_page` is the channel that works.
200
+
10
201
  ## [0.23.1] — 2026-08-13
11
202
 
12
203
  ### Fixed
@@ -1346,7 +1537,9 @@ hosts that both arrange the page the way it expects.
1346
1537
  ### Notes
1347
1538
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
1348
1539
 
1349
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.1...HEAD
1540
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.25.0...HEAD
1541
+ [0.25.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.24.0...v0.25.0
1542
+ [0.24.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.1...v0.24.0
1350
1543
  [0.23.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.0...v0.23.1
1351
1544
  [0.23.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.22.0...v0.23.0
1352
1545
  [0.22.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.21.0...v0.22.0
package/README.md CHANGED
@@ -387,6 +387,45 @@ with the configuration as it then stands. It is a reload, not a merge: the in-fl
387
387
  cancelled and the transcript is rebuilt from the persisted history, so call it when configuration
388
388
  lands rather than between turns.
389
389
 
390
+ #### The same boundary in four frameworks
391
+
392
+ Each framework reaches that pre-insertion window differently, and only one of them reaches it
393
+ declaratively. Built and driven in all four:
394
+
395
+ | Host | Pre-insertion window | What to do |
396
+ | --- | --- | --- |
397
+ | React | None — refs attach after insertion | `createElement`, configure, `appendChild` (above) |
398
+ | **Vue 3** | **Yes** — a directive's `beforeMount` | Attributes in the template, properties in the directive |
399
+ | Svelte 5 | None — `use:` actions and `$effect` run after insertion | Same as React |
400
+ | Angular | None — bindings apply during change detection | Same as React, in `ngOnInit` with `@ViewChild({ static: true })` |
401
+
402
+ **Vue** is the one host that can configure declaratively, because a custom directive's `beforeMount`
403
+ runs while the element is still detached:
404
+
405
+ ```vue
406
+ <script setup>
407
+ const vConfigure = {
408
+ beforeMount(element) {
409
+ element.getHeaders = () => ({ Authorization: `Bearer ${token()}` });
410
+ element.registerTool(myTool);
411
+ },
412
+ };
413
+ </script>
414
+
415
+ <template>
416
+ <ag-ui-chat v-configure endpoint="/agent/" data-threads-url="/agent/threads/" />
417
+ </template>
418
+ ```
419
+
420
+ Tell Vue's compiler the tag is a custom element, or it will warn and try to resolve a component:
421
+ `vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === "ag-ui-chat" } } })`.
422
+
423
+ **Svelte 5**'s `use:` action and `$effect` both run after the node is in the DOM, so build the
424
+ element by hand in an `$effect` and append it — the React shape, in runes. **Angular** needs
425
+ `CUSTOM_ELEMENTS_SCHEMA` on the component and, if it wraps the panel in its own component, one line
426
+ of CSS: `:host { display: contents }`. Angular's host element otherwise lands between your grid and
427
+ the children it sizes, and the panel renders a few hundred pixels tall in the middle of the page.
428
+
390
429
  ---
391
430
 
392
431
  ## Core concepts
@@ -507,24 +546,62 @@ approval card below is for.
507
546
  ### Server-side tool approval (interrupts)
508
547
 
509
548
  When the server gates a destructive tool (e.g. django-ag-ui's `ToolGuard`), the tool **defers**
510
- instead of executing and the run finishes on an AG-UI *interrupt*. The element then appends an
549
+ instead of executing and the run finishes on an AG-UI *interrupt*. The element then renders an
511
550
  **inline approval card** (a `<div class="approval">`) via
512
- [`requestApproval`](src/ui/approval_card.ts), next to the pending tool-call card:
551
+ [`requestApproval`](src/ui/approval_card.ts) **inside the tool card of the call it gates**:
513
552
 
514
553
  - **Approve** → the run resumes and the server runs the tool; its result streams back into the
515
554
  same card.
516
555
  - **Deny** → the run resumes carrying a `cancelled` answer, so the model learns the tool was
517
- declined; the pending card settles as declined.
556
+ declined; the card settles as declined.
518
557
 
519
558
  This uses the AG-UI protocol's own interrupt/resume mechanism (`RunAgentInput.resume[]`) — the
520
559
  wire stays vanilla AG-UI. A **Stop** while an approval card is open denies every open card and
521
560
  cancels the run. No configuration is needed on the client; the gate is enabled server-side.
522
561
 
562
+ **A run can defer several calls, and each is answered on its own.** Importing three rows defers
563
+ three `create_event` calls, and the wire takes a different answer for each. So every question is
564
+ asked **at once**, each in its own tool card, above that call's own arguments — which is what says
565
+ *which* call it is about. The prompt cannot: it comes from the tool, so all three read "Add this
566
+ event to the board?". While the run waits, those cards read **`waiting for you`**
567
+ (`data-status="deferred"`), not "running…" — nothing is running, the stream is over and the server
568
+ is idle. A card asking a question shows its arguments in every `data-tool-display` mode, since
569
+ hiding them would hide the answer to "which one is this".
570
+
571
+ A **frontend** tool such as `ask_user` stays at "running…" while its card is open, because the
572
+ browser really is running it. Only a deferred call was claiming something untrue.
573
+
574
+ **What the card asks.** An AG-UI interrupt carries the question as `message`, and the default is the
575
+ call spelled out — `Approve create_event({"title": "Design sync", …})?` — which is accurate and not
576
+ something to put in front of a person. A server can supply its own wording as **`x-confirm` in the
577
+ interrupt's `metadata`**, the same key a client-side confirmation reads off the tool's schema, and
578
+ the card prefers it:
579
+
580
+ ```json
581
+ { "id": "int-1", "reason": "tool_call", "toolCallId": "call-1",
582
+ "message": "Approve create_event({\"title\": \"Design sync\"})?",
583
+ "metadata": { "x-confirm": "Book Design sync on Friday at 14:00?" } }
584
+ ```
585
+
586
+ Anything non-string or blank under that key is ignored in favour of `message`, and with neither the
587
+ card falls back to `strings.approvalPrompt`.
588
+
589
+ **The card approves or denies, and nothing else.** The interrupt's `responseSchema` also advertises
590
+ `editedArgs` and `reason` — the protocol allows a client to rewrite a gated call's arguments before
591
+ letting it run. The built-in card does not offer that; a host that wants it can implement
592
+ `approvalRenderer` and resolve the interrupt itself.
593
+
594
+ **A gated write is still a write the page cannot see.** Approving one runs a *server-side* tool, so
595
+ if your page renders the data it touched, listen for
596
+ [`ag-ui-run-finished`](#host-seams-the-spa-story) and refetch.
597
+
523
598
  Like the question card, the approval card is customizable at three levels: **text** (`strings`:
524
- `approveAction` / `approvalPrompt` / `approve` / `deny`), **CSS** (`::part()`: `approval`,
525
- `approval-body`, `approval-actions`, `approval-button`, `approval-approve`, `approval-deny`), and
526
- **full replacement** via `chat.approvalRenderer` given the request (`message` + `toolName`) and
527
- a Stop `AbortSignal`, render your own UI and resolve `true`/`false`:
599
+ `approveAction` / `approvalPrompt` / `approve` / `deny` / `toolDeferred`), **CSS** (`::part()`:
600
+ `approval`, `approval-body`, `approval-actions`, `approval-button`, `approval-approve`,
601
+ `approval-deny`, and `tool-card-approval` for the region inside the card), and **full replacement**
602
+ via `chat.approvalRenderer` given the request (`message` + `toolName`) and a Stop `AbortSignal`,
603
+ render your own UI and resolve `true`/`false`. A renderer is called **once per interrupt,
604
+ concurrently**, so a host that can only ask one thing at a time should queue inside it:
528
605
 
529
606
  ```js
530
607
  chat.approvalRenderer = (request, { signal }) =>
@@ -648,11 +725,33 @@ want — so you control the agent's interaction surface:
648
725
  ```
649
726
 
650
727
  - **`scroll_to`** — scroll a target into view. `target` is `"top"`, `"bottom"`, or a CSS selector
651
- / page-map element id. Read-only (no confirmation).
728
+ / page-map element id. Read-only (no confirmation). It centres the target **vertically** and
729
+ brings it into view **horizontally** (`inline: "nearest"`), so on a two-axis surface a
730
+ horizontal target lands at the near edge rather than in the middle. In view is the contract;
731
+ "centred" is not, in that axis.
652
732
  - **`drag_and_drop`** — drag the `from` element onto the `to` element (selectors / page-map ids),
653
733
  firing the standard HTML5 drag sequence (`dragstart` → `dragenter`/`dragover`/`drop` → `dragend`)
654
734
  so the page's own drop handler reacts. Useful for reordering sortable lists.
655
735
 
736
+ **Your drag surface must listen to drag events, and many "modern" ones do not.** `drag_and_drop`
737
+ dispatches the native HTML5 sequence with one shared `DataTransfer`. A surface built on a
738
+ pointer-event drag library — dnd-kit, most React DnD packages, the Angular CDK — listens to
739
+ `pointerdown`/`pointermove` and **never sees any of it**: the agent's drag is a silent no-op that
740
+ still reports success. Either use the native API or pick a library that listens to drag events.
741
+ React's synthetic `onDrop` does receive the dispatched sequence, `DataTransfer` included.
742
+
743
+ **A page action reports that it fired, not that it worked.** `drag_and_drop` returns as soon as the
744
+ sequence is dispatched; whether your drop handler's save succeeded is invisible to it, so a refused
745
+ change still looks like a successful tool call. Two things follow. Have the page report its own
746
+ refusals somewhere the agent can read them, and have the agent re-read the page before claiming
747
+ anything. Where the outcome matters more than the gesture, call the operation as a **server tool**
748
+ instead — it can return the real error.
749
+
750
+ **A page that saves asynchronously should say so.** A verification read straight after a drag can
751
+ outrun the page's own save and conclude that nothing happened. Report a busy flag in your
752
+ `getPageMap` (`{ saving: true }` while a write is in flight) and the agent can wait for a page that
753
+ says it is busy. It cannot wait for one that does not.
754
+
656
755
  Targets resolve through the overridable `resolvePageTarget` property — `(target) => HTMLElement |
657
756
  null`, defaulting to `document.querySelector`. A host with a page map overrides it to map its own
658
757
  element ids (the same way the DOM-driver primitives are wrapped with environment-aware lookups):
@@ -1027,6 +1126,13 @@ acts, the next round already sees the resulting page. Within a round the agent c
1027
1126
  view at any time with the built-in `read_page` tool, which is registered whenever this provider is
1028
1127
  set.
1029
1128
 
1129
+ **Check that your server reads `context` at all — pydantic-ai's AG-UI adapter does not.** The
1130
+ auto-injected `page_map` rides in `RunAgentInput.context`, and an adapter that ignores that field
1131
+ drops it silently: nothing errors, and the model simply never sees the page. On such a backend
1132
+ `read_page` is the channel that works, and it is the one to rely on. Nothing to configure — just do
1133
+ not assume the injected copy arrived, and if the page map matters to your prompt, put it there
1134
+ server-side or let the agent call `read_page`.
1135
+
1030
1136
  That leaves one window: the page can move *after* a round's context was built but *before* the
1031
1137
  agent's tool call arrives — the user clicks a link, or presses back. Calls landing in that window
1032
1138
  are **refused** with a result telling the agent to call `read_page` and retry. Most would have
@@ -1096,6 +1202,34 @@ chat.navigate = (path) => router.push(path); // SPA: in-page, no reload
1096
1202
 
1097
1203
  Route map + `navigate()` and the reload model are the same feature seen from two ends.
1098
1204
 
1205
+ **`ag-ui-run-finished`** *(event)* — an interaction has ended, and here is what ran in it.
1206
+ `detail: { tools: readonly ToolRun[] }` (typed `RunFinishedDetail`), where each `ToolRun` is
1207
+ `{ name, side: "server" | "client" }` in settle order.
1208
+
1209
+ **This is the seam for a host that renders data the agent can change.** A server-side tool writes
1210
+ without your page's knowledge: nothing else the element dispatches implies "something may have moved
1211
+ underneath you", so a page that fetched its data on mount has no reason to refetch and quietly goes
1212
+ stale. Approve a server-side booking on a calendar and the row exists while the calendar keeps
1213
+ showing the week it loaded.
1214
+
1215
+ ```js
1216
+ chat.addEventListener("ag-ui-run-finished", (e) => {
1217
+ // A "client" tool ran in your own handler, so you already know what it did.
1218
+ if (e.detail.tools.some((tool) => tool.side === "server")) {
1219
+ void refetchBoard();
1220
+ }
1221
+ });
1222
+ ```
1223
+
1224
+ It fires **once per interaction**, not once per tool round, and it fires on completion, error and
1225
+ cancellation alike — a partial write is still a write. A capability load (an agent skill activating)
1226
+ is not counted: it moves nothing a host renders.
1227
+
1228
+ `sharedState` above is the richer channel and this is not a replacement for it — but it is not a
1229
+ substitute the other way round either, because shared state requires the *agent* to emit
1230
+ `STATE_SNAPSHOT`, which is not the host's decision to make. Use state when the two ends edit one
1231
+ object; use this when your page owns the data and just needs to know it moved.
1232
+
1099
1233
  ## Resuming a run
1100
1234
 
1101
1235
  When the server persists run checkpoints (django-ag-ui's `step_store`), a run
@@ -1108,10 +1242,17 @@ component at the run index and a ⭯ button appears in the header:
1108
1242
 
1109
1243
  The panel lists runs the server marked **continuable** — those with a saved
1110
1244
  snapshot to seed from. A run that never reached a provider-valid boundary has
1111
- none, so it isn't offered: resuming it would start from nothing. Each row shows
1112
- when the run started (the id is on hover, for correlating with server logs) and
1113
- marks a run that branched from another, so a fork doesn't read as a duplicate
1114
- of its parent.
1245
+ none, so it isn't offered: resuming it would start from nothing.
1246
+
1247
+ A row leads with the run's **first user message**, from the index's `preview`
1248
+ field, and shows when it started beside it. That is what makes the list a list of
1249
+ conversations: a time is not an identity, since two runs a minute apart both read
1250
+ "just now", and a run id is not something a person recognises. Where the server
1251
+ sends no preview — an older index, or a run that opened on an image with no
1252
+ caption — the row falls back to the time plus the first eight characters of the
1253
+ id (full id on hover, for correlating with server logs). Either way a run that
1254
+ branched from another is marked, so a fork doesn't read as a duplicate of its
1255
+ parent.
1115
1256
 
1116
1257
  Type the next turn in the composer, then pick a row:
1117
1258
 
@@ -1256,6 +1397,7 @@ re-export point. Internal modules import from leaf paths.
1256
1397
  | `createPageStateTools(binding)` | function | Build `read_<name>` / `set_<name>` tools. |
1257
1398
  | `PageState` | type | A page-state binding declaration. |
1258
1399
  | `Skill` | type | A launchable prompt (chip / `/`-command). |
1400
+ | `RunFinishedDetail` / `ToolRun` | type | `ag-ui-run-finished` detail: the tools an interaction ran, and which side ran them. |
1259
1401
 
1260
1402
  ### Durability
1261
1403
 
@@ -1267,7 +1409,7 @@ re-export point. Internal modules import from leaf paths.
1267
1409
  | `ThreadMeta` | type | A thread-drawer row (`{ threadId, title, updatedAt, preview }`). |
1268
1410
  | `NavigationCheckpoint` | type | The pre-reload checkpoint marker. |
1269
1411
  | `RunIndex` | class | Reads a `data-runs-url` run index and derives its resume / fork endpoints. |
1270
- | `RunRow` | type | One run index row (`{ run_id, thread_id, parent_run_id, started_at, continuable }`). |
1412
+ | `RunRow` | type | One run index row (`{ run_id, thread_id, parent_run_id, started_at, continuable, preview? }`). |
1271
1413
  | `CheckpointMenu` | class | The *Continue a run* panel. |
1272
1414
  | `CheckpointVerb` | type | `"resume" | "fork"`. |
1273
1415
 
@@ -1307,10 +1449,11 @@ re-export point. Internal modules import from leaf paths.
1307
1449
  | `SUBMIT_EVENT` | The submit CustomEvent name. |
1308
1450
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1309
1451
  | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1452
+ | `RUN_FINISHED_EVENT` | The interaction-finished CustomEvent name (`ag-ui-run-finished`). |
1310
1453
  | `MESSAGE_ROLE` | Message role constants. |
1311
1454
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1312
1455
  | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
1313
- | `X_CONFIRM_KEY` | JSON-Schema key carrying a confirmation prompt. |
1456
+ | `X_CONFIRM_KEY` | Confirmation-prompt key: on a tool's JSON Schema for a client-side confirmation, and in an AG-UI interrupt's `metadata` for a server-side approval. |
1314
1457
  | `X_SUMMARY_KEY` | JSON-Schema key carrying a short tool-card label. |
1315
1458
  | `MAX_TOOL_ROUNDS` | Upper bound on tool-call → re-run rounds per send. |
1316
1459
  | `VERSION` | The package version string. |
@@ -1402,6 +1545,15 @@ have to hand-tune the variables:
1402
1545
  `embedded`. `embedded` drops the fixed positioning and z-index so the widget sits in normal
1403
1546
  document flow; `page` is a full-screen [centred reading column](#page-placement).
1404
1547
 
1548
+ **`embedded` fills the box your page gives it, so give it one.** It is the placement app-shell
1549
+ layouts reach for, and a grid or flex item defaults to `min-height: auto` — which lets a growing
1550
+ transcript push the composer off the bottom of the window instead of scrolling inside the panel. The
1551
+ fix belongs to the containing element, not to the widget:
1552
+
1553
+ ```css
1554
+ .assistant-pane { min-height: 0; overflow: hidden; } /* the box the element is given */
1555
+ ```
1556
+
1405
1557
  ```html
1406
1558
  <ag-ui-chat endpoint="/agent/" theme="dark" density="compact" placement="side"></ag-ui-chat>
1407
1559
  ```
@@ -1424,30 +1576,28 @@ ag-ui-chat::part(send) { text-transform: uppercase; }
1424
1576
  ag-ui-chat::part(tool-card) { font-family: var(--my-mono); }
1425
1577
  ```
1426
1578
 
1427
- Available parts: `panel`, `header`, `title`, `icon`, `header-controls`, `header-button`
1428
- (plus `history-button` / `new-button` / `collapse-button` / `theme-toggle`), `messages`,
1429
- `answer` (the per-turn group), `thoughts` (plus `thoughts-toggle` / `thoughts-body` /
1430
- `thoughts-label`), `message`
1431
- (plus `message-user` / `message-assistant`), `empty`, `pending`, `stopped` (the "⏹ Stopped" note),
1432
- `tool-card`
1433
- (plus `tool-card-head` / `-icon` / `-name` / `-status` / `-args` / `-toggle` / `-result`),
1434
- `confirm` (plus `confirm-body` /
1435
- `-args` / `-actions` / `-button` / `-cancel` / `-confirm`),
1436
- `approval` (plus `approval-body` / `-actions` / `-button` / `-approve` / `-deny`),
1437
- `question` (plus `question-body` / `-options` / `-choice` / `-choice-text` / `-radio` / `-input` /
1438
- `-actions` / `-button`), `composer` (plus `composer-surface` / `composer-tools`), `input`, `send`,
1439
- `attach-button`, `voice-button`,
1440
- the attachment chips `attachment-tray` and `attachment-chips` (the read-only chips on sent
1441
- bubbles) with the shared chip parts `attachment-chip` (plus `-icon` / `-name` / `-size` / `-bar` /
1442
- `-bar-fill` / `-retry` / `-remove`),
1443
- the skills UI (`skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`,
1444
- `skill-item-desc`, and the missing-placeholder `skill-hint`),
1445
- `launcher`, `launcher-icon`, `launcher-badge`, and the drawer parts
1446
- (`drawer`, `drawer-backdrop`, `drawer-panel`, `drawer-header`, `drawer-title`, `drawer-new`,
1447
- `drawer-list`, `drawer-empty`, `drawer-row`, `drawer-row-select`, `drawer-row-title`,
1448
- `drawer-row-time`, `drawer-row-preview`, `drawer-row-actions`, `drawer-row-rename`,
1449
- `drawer-row-delete`, `drawer-rename-input`, `drawer-confirm`, `drawer-confirm-label`,
1450
- `drawer-confirm-yes`, `drawer-confirm-no`).
1579
+ Every part, by the feature it belongs to. Spelled out rather than abbreviated: the list used to
1580
+ read `tool-card` *plus* `-icon` / `-name`, which is compact and is also how an entire feature went
1581
+ missing from it for two releases. A test reads this table and compares it with the parts the
1582
+ component sets, so a new one cannot ship undocumented.
1583
+
1584
+ | Feature | Parts |
1585
+ | --- | --- |
1586
+ | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `resize-handle` |
1587
+ | Header buttons | `header-button` on each, plus `history-button`, `checkpoints-button`, `new-button`, `collapse-button`, `theme-toggle` |
1588
+ | Collapsed widget | `launcher`, `launcher-icon`, `launcher-badge` |
1589
+ | Answers | `answer` (the per-turn group), `message` (plus `message-user`, `message-assistant`), `code-copy` |
1590
+ | Reasoning | `thoughts`, `thoughts-toggle`, `thoughts-body`, `thoughts-label` |
1591
+ | Run notices | `run-notice` (plus `run-notice-interrupted`, `run-notice-attachment-pending`, `run-notice-compaction`, `run-notice-skill`), `run-notice-icon`, `run-notice-text` |
1592
+ | Tool cards | `tool-card`, `tool-card-head`, `tool-card-icon`, `tool-card-name`, `tool-card-status`, `tool-card-decision`, `tool-card-toggle`, `tool-card-body`, `tool-card-section` (plus `tool-card-args-section`, `tool-card-result-section`), `tool-card-section-label` (plus `tool-card-args-label`, `tool-card-result-label`), `tool-card-args`, `tool-card-result`, `tool-card-approval` |
1593
+ | Client-side confirmation | `confirm`, `confirm-body`, `confirm-args`, `confirm-actions`, `confirm-button` (plus `confirm-confirm`, `confirm-cancel`) |
1594
+ | Server-side approval | `approval`, `approval-body`, `approval-actions`, `approval-button` (plus `approval-approve`, `approval-deny`) |
1595
+ | Typed question | `question`, `question-body`, `question-options`, `question-choice`, `question-choice-text`, `question-radio`, `question-input`, `question-actions`, `question-button` |
1596
+ | Composer | `composer`, `composer-surface`, `composer-tools`, `input`, `send`, `attach-button`, `voice-button` |
1597
+ | 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` |
1598
+ | Skills | `skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`, `skill-item-desc`, `skill-item-token`, `skill-hint` (the missing-placeholder hint) |
1599
+ | Thread drawer | `drawer`, `drawer-backdrop`, `drawer-panel`, `drawer-header`, `drawer-title`, `drawer-new`, `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` |
1600
+ | 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`) |
1451
1601
 
1452
1602
  > **Hiding `::part(header)` hides the controls inside it.** The history, checkpoints, new-chat,
1453
1603
  > theme and collapse buttons are all children of the header, so a host that renders its own title