@artooi/ag-ui-web-component 0.23.1 → 0.24.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] — 2026-08-13
11
+
12
+ ### Added
13
+
14
+ - **`ag-ui-run-finished`** — an event fired once per interaction, carrying the
15
+ tools that ran and which side ran each (`{ tools: [{ name, side }] }`, typed
16
+ `RunFinishedDetail` / `ToolRun`). **For hosts that render data the agent can
17
+ change.** A server-side tool writes without the page's knowledge, and nothing
18
+ the element dispatched implied "something may have moved underneath you": a
19
+ page that fetched its data on mount had no reason to refetch, so approving a
20
+ server-side write left it showing stale data with no way to notice. Shared
21
+ state was the only channel back, and it is not one a host can rely on, because
22
+ it needs the *agent* to emit `STATE_SNAPSHOT`. Fires on completion, error and
23
+ cancellation alike, since a partial write is still a write; a capability load
24
+ is not counted, since it moves nothing a host renders.
25
+
26
+ ### Changed
27
+
28
+ - **A server-side approval can now ask a readable question.** An AG-UI
29
+ interrupt's question defaults to the call spelled out —
30
+ `Approve create_event({"title": "Design sync", …})?` — which is accurate and
31
+ not something to put in front of a person, while the *client-side*
32
+ confirmation card has had `x-confirm` for exactly this. The approval card now
33
+ prefers `x-confirm` from the interrupt's `metadata`, so one key covers both
34
+ gates, and a server that supplies nothing keeps the generated text. Anything
35
+ non-string or blank under that key is ignored rather than rendered, since a
36
+ wire field typed `Record<string, any>` can carry an object into the one place
37
+ a person is being asked to allow a write.
38
+
39
+ ### Fixed
40
+
41
+ - **Every first visit spent a request to be told `404`.** With
42
+ `data-threads-url` set, the element minted a thread id on mount and
43
+ immediately asked the server for its history — history that cannot exist,
44
+ because the id was three lines old. The response was correct and harmless, and
45
+ it put a red `404` in the console of a page where nothing had gone wrong, on
46
+ every first visit to every host. `ClientConversationStore` gained an optional
47
+ `isUnsent(threadId)`, which the session store answers from a marker it sets when
48
+ it mints and drops on the first save, and the remote store skips the fetch when
49
+ it is `true`. Deliberately narrow: *"I hold no messages for this id"* is not the
50
+ same claim as *"this id is new"*, and only the store that minted it can make the
51
+ second one — so a thread picked from the drawer, or created on another device,
52
+ is still fetched.
53
+ - **An approved call's tool card broke its own name into pieces.** The card's
54
+ head is a flex row in which the name is the only flexible child, so every
55
+ fixed badge the row gains is taken out of it. The decision badge ("approved by
56
+ you") appears only on the server-approval path, and in a sidebar-width panel it
57
+ left the name **37px** wide: `word-break: break-word` then split *Create event*
58
+ into "Creat / e / event" across three lines. The head wraps now and the name
59
+ keeps a floor instead of a zero min-width, so a badge drops to its own row
60
+ rather than shredding a word, while a genuinely unbreakable name still breaks
61
+ instead of overflowing the card. Measured in a real browser at 470px: the name
62
+ went 37px to 144px and three lines to one.
63
+
64
+ ### Documentation
65
+
66
+ - **The four framework recipes, not just React's.** The connect-time
67
+ configuration boundary is reached differently by each host, and only **Vue**
68
+ has a hook that runs before insertion (a directive's `beforeMount`); React,
69
+ Svelte 5 and Angular all create the element by hand. Angular additionally needs
70
+ `:host { display: contents }` or its own host element breaks the page's layout.
71
+ - **Page actions**: a page action reports that it *fired*, not that it worked;
72
+ a page that saves asynchronously should say so (a `saving` flag in the page
73
+ map) or a verification read will outrun the save; `drag_and_drop` dispatches
74
+ the **native HTML5 drag sequence**, which a pointer-event drag library
75
+ (dnd-kit, the Angular CDK) never sees; and `scroll_to` centres vertically but
76
+ brings into view horizontally.
77
+ - **`placement="embedded"` fills the box the host gives it — so give it one.**
78
+ `min-height: 0` plus `overflow: hidden` on the containing element, or a growing
79
+ transcript pushes the composer off the bottom of the window.
80
+ - **The auto-injected page map may go nowhere.** It rides in
81
+ `RunAgentInput.context`, and pydantic-ai's AG-UI adapter does not read that
82
+ field, so on such a backend the injected copy is silently dropped and
83
+ `read_page` is the channel that works.
84
+
10
85
  ## [0.23.1] — 2026-08-13
11
86
 
12
87
  ### Fixed
@@ -1346,7 +1421,8 @@ hosts that both arrange the page the way it expects.
1346
1421
  ### Notes
1347
1422
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
1348
1423
 
1349
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.1...HEAD
1424
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.24.0...HEAD
1425
+ [0.24.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.1...v0.24.0
1350
1426
  [0.23.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.0...v0.23.1
1351
1427
  [0.23.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.22.0...v0.23.0
1352
1428
  [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
@@ -520,6 +559,30 @@ This uses the AG-UI protocol's own interrupt/resume mechanism (`RunAgentInput.re
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
+ **What the card asks.** An AG-UI interrupt carries the question as `message`, and the default is the
563
+ call spelled out — `Approve create_event({"title": "Design sync", …})?` — which is accurate and not
564
+ something to put in front of a person. A server can supply its own wording as **`x-confirm` in the
565
+ interrupt's `metadata`**, the same key a client-side confirmation reads off the tool's schema, and
566
+ the card prefers it:
567
+
568
+ ```json
569
+ { "id": "int-1", "reason": "tool_call", "toolCallId": "call-1",
570
+ "message": "Approve create_event({\"title\": \"Design sync\"})?",
571
+ "metadata": { "x-confirm": "Book Design sync on Friday at 14:00?" } }
572
+ ```
573
+
574
+ Anything non-string or blank under that key is ignored in favour of `message`, and with neither the
575
+ card falls back to `strings.approvalPrompt`.
576
+
577
+ **The card approves or denies, and nothing else.** The interrupt's `responseSchema` also advertises
578
+ `editedArgs` and `reason` — the protocol allows a client to rewrite a gated call's arguments before
579
+ letting it run. The built-in card does not offer that; a host that wants it can implement
580
+ `approvalRenderer` and resolve the interrupt itself.
581
+
582
+ **A gated write is still a write the page cannot see.** Approving one runs a *server-side* tool, so
583
+ if your page renders the data it touched, listen for
584
+ [`ag-ui-run-finished`](#host-seams-the-spa-story) and refetch.
585
+
523
586
  Like the question card, the approval card is customizable at three levels: **text** (`strings`:
524
587
  `approveAction` / `approvalPrompt` / `approve` / `deny`), **CSS** (`::part()`: `approval`,
525
588
  `approval-body`, `approval-actions`, `approval-button`, `approval-approve`, `approval-deny`), and
@@ -648,11 +711,33 @@ want — so you control the agent's interaction surface:
648
711
  ```
649
712
 
650
713
  - **`scroll_to`** — scroll a target into view. `target` is `"top"`, `"bottom"`, or a CSS selector
651
- / page-map element id. Read-only (no confirmation).
714
+ / page-map element id. Read-only (no confirmation). It centres the target **vertically** and
715
+ brings it into view **horizontally** (`inline: "nearest"`), so on a two-axis surface a
716
+ horizontal target lands at the near edge rather than in the middle. In view is the contract;
717
+ "centred" is not, in that axis.
652
718
  - **`drag_and_drop`** — drag the `from` element onto the `to` element (selectors / page-map ids),
653
719
  firing the standard HTML5 drag sequence (`dragstart` → `dragenter`/`dragover`/`drop` → `dragend`)
654
720
  so the page's own drop handler reacts. Useful for reordering sortable lists.
655
721
 
722
+ **Your drag surface must listen to drag events, and many "modern" ones do not.** `drag_and_drop`
723
+ dispatches the native HTML5 sequence with one shared `DataTransfer`. A surface built on a
724
+ pointer-event drag library — dnd-kit, most React DnD packages, the Angular CDK — listens to
725
+ `pointerdown`/`pointermove` and **never sees any of it**: the agent's drag is a silent no-op that
726
+ still reports success. Either use the native API or pick a library that listens to drag events.
727
+ React's synthetic `onDrop` does receive the dispatched sequence, `DataTransfer` included.
728
+
729
+ **A page action reports that it fired, not that it worked.** `drag_and_drop` returns as soon as the
730
+ sequence is dispatched; whether your drop handler's save succeeded is invisible to it, so a refused
731
+ change still looks like a successful tool call. Two things follow. Have the page report its own
732
+ refusals somewhere the agent can read them, and have the agent re-read the page before claiming
733
+ anything. Where the outcome matters more than the gesture, call the operation as a **server tool**
734
+ instead — it can return the real error.
735
+
736
+ **A page that saves asynchronously should say so.** A verification read straight after a drag can
737
+ outrun the page's own save and conclude that nothing happened. Report a busy flag in your
738
+ `getPageMap` (`{ saving: true }` while a write is in flight) and the agent can wait for a page that
739
+ says it is busy. It cannot wait for one that does not.
740
+
656
741
  Targets resolve through the overridable `resolvePageTarget` property — `(target) => HTMLElement |
657
742
  null`, defaulting to `document.querySelector`. A host with a page map overrides it to map its own
658
743
  element ids (the same way the DOM-driver primitives are wrapped with environment-aware lookups):
@@ -1027,6 +1112,13 @@ acts, the next round already sees the resulting page. Within a round the agent c
1027
1112
  view at any time with the built-in `read_page` tool, which is registered whenever this provider is
1028
1113
  set.
1029
1114
 
1115
+ **Check that your server reads `context` at all — pydantic-ai's AG-UI adapter does not.** The
1116
+ auto-injected `page_map` rides in `RunAgentInput.context`, and an adapter that ignores that field
1117
+ drops it silently: nothing errors, and the model simply never sees the page. On such a backend
1118
+ `read_page` is the channel that works, and it is the one to rely on. Nothing to configure — just do
1119
+ not assume the injected copy arrived, and if the page map matters to your prompt, put it there
1120
+ server-side or let the agent call `read_page`.
1121
+
1030
1122
  That leaves one window: the page can move *after* a round's context was built but *before* the
1031
1123
  agent's tool call arrives — the user clicks a link, or presses back. Calls landing in that window
1032
1124
  are **refused** with a result telling the agent to call `read_page` and retry. Most would have
@@ -1096,6 +1188,34 @@ chat.navigate = (path) => router.push(path); // SPA: in-page, no reload
1096
1188
 
1097
1189
  Route map + `navigate()` and the reload model are the same feature seen from two ends.
1098
1190
 
1191
+ **`ag-ui-run-finished`** *(event)* — an interaction has ended, and here is what ran in it.
1192
+ `detail: { tools: readonly ToolRun[] }` (typed `RunFinishedDetail`), where each `ToolRun` is
1193
+ `{ name, side: "server" | "client" }` in settle order.
1194
+
1195
+ **This is the seam for a host that renders data the agent can change.** A server-side tool writes
1196
+ without your page's knowledge: nothing else the element dispatches implies "something may have moved
1197
+ underneath you", so a page that fetched its data on mount has no reason to refetch and quietly goes
1198
+ stale. Approve a server-side booking on a calendar and the row exists while the calendar keeps
1199
+ showing the week it loaded.
1200
+
1201
+ ```js
1202
+ chat.addEventListener("ag-ui-run-finished", (e) => {
1203
+ // A "client" tool ran in your own handler, so you already know what it did.
1204
+ if (e.detail.tools.some((tool) => tool.side === "server")) {
1205
+ void refetchBoard();
1206
+ }
1207
+ });
1208
+ ```
1209
+
1210
+ It fires **once per interaction**, not once per tool round, and it fires on completion, error and
1211
+ cancellation alike — a partial write is still a write. A capability load (an agent skill activating)
1212
+ is not counted: it moves nothing a host renders.
1213
+
1214
+ `sharedState` above is the richer channel and this is not a replacement for it — but it is not a
1215
+ substitute the other way round either, because shared state requires the *agent* to emit
1216
+ `STATE_SNAPSHOT`, which is not the host's decision to make. Use state when the two ends edit one
1217
+ object; use this when your page owns the data and just needs to know it moved.
1218
+
1099
1219
  ## Resuming a run
1100
1220
 
1101
1221
  When the server persists run checkpoints (django-ag-ui's `step_store`), a run
@@ -1256,6 +1376,7 @@ re-export point. Internal modules import from leaf paths.
1256
1376
  | `createPageStateTools(binding)` | function | Build `read_<name>` / `set_<name>` tools. |
1257
1377
  | `PageState` | type | A page-state binding declaration. |
1258
1378
  | `Skill` | type | A launchable prompt (chip / `/`-command). |
1379
+ | `RunFinishedDetail` / `ToolRun` | type | `ag-ui-run-finished` detail: the tools an interaction ran, and which side ran them. |
1259
1380
 
1260
1381
  ### Durability
1261
1382
 
@@ -1307,10 +1428,11 @@ re-export point. Internal modules import from leaf paths.
1307
1428
  | `SUBMIT_EVENT` | The submit CustomEvent name. |
1308
1429
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1309
1430
  | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1431
+ | `RUN_FINISHED_EVENT` | The interaction-finished CustomEvent name (`ag-ui-run-finished`). |
1310
1432
  | `MESSAGE_ROLE` | Message role constants. |
1311
1433
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1312
1434
  | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
1313
- | `X_CONFIRM_KEY` | JSON-Schema key carrying a confirmation prompt. |
1435
+ | `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
1436
  | `X_SUMMARY_KEY` | JSON-Schema key carrying a short tool-card label. |
1315
1437
  | `MAX_TOOL_ROUNDS` | Upper bound on tool-call → re-run rounds per send. |
1316
1438
  | `VERSION` | The package version string. |
@@ -1402,6 +1524,15 @@ have to hand-tune the variables:
1402
1524
  `embedded`. `embedded` drops the fixed positioning and z-index so the widget sits in normal
1403
1525
  document flow; `page` is a full-screen [centred reading column](#page-placement).
1404
1526
 
1527
+ **`embedded` fills the box your page gives it, so give it one.** It is the placement app-shell
1528
+ layouts reach for, and a grid or flex item defaults to `min-height: auto` — which lets a growing
1529
+ transcript push the composer off the bottom of the window instead of scrolling inside the panel. The
1530
+ fix belongs to the containing element, not to the widget:
1531
+
1532
+ ```css
1533
+ .assistant-pane { min-height: 0; overflow: hidden; } /* the box the element is given */
1534
+ ```
1535
+
1405
1536
  ```html
1406
1537
  <ag-ui-chat endpoint="/agent/" theme="dark" density="compact" placement="side"></ag-ui-chat>
1407
1538
  ```