@artooi/ag-ui-web-component 0.27.0 → 0.28.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 +247 -1
- package/README.md +186 -6
- package/dist/ag-ui-web-component.bundle.js +50 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +55 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +8 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +43 -1
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +23 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +28 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +564 -94
- package/dist/index.js.map +4 -4
- package/dist/tools/is_destructive.d.ts +8 -2
- package/dist/tools/is_destructive.d.ts.map +1 -1
- package/dist/tools/parse_tool_catalog.d.ts +11 -4
- package/dist/tools/parse_tool_catalog.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +23 -5
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +5 -1
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +13 -7
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +431 -41
- package/src/core/agui_client.ts +18 -1
- package/src/core/conversation_store.ts +128 -42
- package/src/core/create_http_agent.ts +24 -2
- package/src/core/remote_conversation_store.ts +35 -2
- package/src/core/utils.ts +58 -0
- package/src/tools/is_destructive.ts +8 -2
- package/src/tools/parse_tool_catalog.ts +18 -6
- package/src/ui/render_markdown.ts +111 -21
- package/src/ui/resize_handle.ts +32 -2
- package/src/ui/ui_strings.ts +19 -8
- package/src/ui/voice_input.ts +43 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,251 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.28.0] — 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A conversation could carry from one signed-in user to the next in the same
|
|
15
|
+
tab.** New `user-key` attribute (and matching `userKey` property): set it to
|
|
16
|
+
whatever identifies the signed-in principal, and the stored conversation is
|
|
17
|
+
scoped to them, so two principals in one tab cannot reach each other's
|
|
18
|
+
transcript. Changing it — or clearing it — purges everything the previous
|
|
19
|
+
principal left behind under this element: the transcript, the history drawer
|
|
20
|
+
index and any navigation checkpoint. That purge is why it is a live attribute
|
|
21
|
+
rather than a connect-time one: `sessionStorage` is scoped to the tab and not
|
|
22
|
+
to the session, so it survives the navigation a logout is, and a single-page
|
|
23
|
+
app signs out through its own router without remounting anything — the host
|
|
24
|
+
naming the new principal is the only signal there is. The first value to
|
|
25
|
+
arrive is treated as a host naming the user who was already there rather than
|
|
26
|
+
as a handover, so an element configured by an async auth handshake keeps the
|
|
27
|
+
conversation on screen. Absent, behaviour is exactly what it was, including
|
|
28
|
+
that carry-over; the README says so where the attribute is documented.
|
|
29
|
+
`SessionStorageStore.purge(namespace)` exposes the same primitive for a host
|
|
30
|
+
driving its own sign-out.
|
|
31
|
+
|
|
32
|
+
- **Pointing history at a server still left a full copy of every transcript in
|
|
33
|
+
the browser.** `RemoteConversationStore` mirrored each message body into
|
|
34
|
+
`sessionStorage` whatever the deployment had chosen, so an operator who
|
|
35
|
+
configured `data-threads-url` precisely to keep conversations in the database
|
|
36
|
+
got them in both places. The mirror is now a `cacheMessages` constructor
|
|
37
|
+
option, exposed on the element as `data-threads-cache="false"`, and it still
|
|
38
|
+
defaults to the caching behaviour so nobody's setup changes silently. Turning
|
|
39
|
+
it off keeps only the client-only state — the active thread id and the
|
|
40
|
+
navigation checkpoint — so reloads and navigating tools work as before; what
|
|
41
|
+
it costs is the offline fallback, since there is no longer a local copy to
|
|
42
|
+
fall back to.
|
|
43
|
+
|
|
44
|
+
- **`UiStrings.recordingLimit`** — what the mic button says after a recording
|
|
45
|
+
stopped itself at the length cap. Token: `{n}`, the cap in minutes. Like every
|
|
46
|
+
other key it has an English default, so an existing `strings` override keeps
|
|
47
|
+
working untouched.
|
|
48
|
+
|
|
49
|
+
- **`trustedOrigins` on the agent factory options** — the origins, besides the
|
|
50
|
+
document's own, the agent may carry host credentials to. Naming one confirms
|
|
51
|
+
the destination was chosen deliberately and silences the notice above for it.
|
|
52
|
+
Compared as serialized origins (`https://agent.example.com`), scheme and port
|
|
53
|
+
included. Reachable through a custom `agentFactory`, which is how a host wraps
|
|
54
|
+
`createHttpAgent` today.
|
|
55
|
+
|
|
56
|
+
- **`trustedOrigins` on the element** — the origins, besides the page's own, that
|
|
57
|
+
every one of its seven configurable URLs may carry host credentials to without
|
|
58
|
+
a console notice. Forwarded to the agent factory as well, so a host that does
|
|
59
|
+
not override `agentFactory` configures all seven in one place.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **`parseToolCatalog` returns whole catalog entries, not bare summaries.** Its
|
|
64
|
+
return type was `Record<string, string>`, so the `description` field that
|
|
65
|
+
`ToolCatalogEntry` declares and documents was dropped at parse time for every
|
|
66
|
+
entry — a documented wire field no consumer could reach, and none could be
|
|
67
|
+
added without changing this signature first. It now returns
|
|
68
|
+
`Record<string, ToolCatalogEntry>`. Tool-call cards still label themselves from
|
|
69
|
+
`summary`; callers of the exported parser get the entry the server actually
|
|
70
|
+
sent. A malformed `description` costs that field, not the entry, matching the
|
|
71
|
+
tolerance the rest of the parse already had.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- **A reasoning block lost its last sentence, and a short one never appeared at
|
|
76
|
+
all.** The protocol client hands a delta subscriber the text accumulated
|
|
77
|
+
*before* the delta it is announcing, so following that callback alone trails
|
|
78
|
+
the stream by one and renders nothing whatsoever for reasoning that arrives as
|
|
79
|
+
a single delta. The answer text was spared because its own end event carries
|
|
80
|
+
the whole message; reasoning subscribed to an end event that carries no buffer.
|
|
81
|
+
It now also listens to the one that does. The test helper was the reason this
|
|
82
|
+
went unseen: it handed the subscriber the full buffer on every delta, a wire no
|
|
83
|
+
server writes, so every existing test agreed with the bug.
|
|
84
|
+
|
|
85
|
+
- **Two chats on one page shared one conversation.** The storage namespace falls
|
|
86
|
+
back from the element's `id` to its `endpoint`, so two `<ag-ui-chat>` elements
|
|
87
|
+
with no `id` against the same agent mount — a docked support panel and an
|
|
88
|
+
inline page assistant, say, and nothing requires an `id` — resolved to the same
|
|
89
|
+
namespace and shared a thread pointer, a history drawer and every message key.
|
|
90
|
+
Whichever mounted second adopted the first's active thread and rehydrated its
|
|
91
|
+
transcript into its own panel. The first element to mount now keeps the
|
|
92
|
+
namespace, so the ordinary single-element case is untouched, and a second is
|
|
93
|
+
given a throwaway namespace of its own plus a console warning naming the fix.
|
|
94
|
+
The throwaway namespace is minted per mount, so give each element an `id` for
|
|
95
|
+
its conversation to survive a reload.
|
|
96
|
+
|
|
97
|
+
- **A full storage quota was reported as an agent failure.** `sessionStorage`
|
|
98
|
+
writes throw once the quota is exhausted — a long conversation, or one turn
|
|
99
|
+
carrying a large tool result — and in privacy modes that deny storage
|
|
100
|
+
outright. The transcript is persisted from inside the run loop, so that throw
|
|
101
|
+
surfaced as a run error and told the user the agent had failed when nothing
|
|
102
|
+
but the browser's storage had; on the cancel path it escaped as an unhandled
|
|
103
|
+
rejection instead. A write that cannot be made now costs the reload and not
|
|
104
|
+
the conversation, and says so in the console once rather than once per turn.
|
|
105
|
+
|
|
106
|
+
- **A tool a page deliberately withheld from a run still ran when the agent
|
|
107
|
+
called it.** `getTools` is a per-run catalog provider, so a host is invited to
|
|
108
|
+
scope what a given page offers while registering everything once at mount.
|
|
109
|
+
Dispatch never consulted it: it resolved the call name straight against the
|
|
110
|
+
mount-wide registry, so a call naming a scoped-out tool found its handler and
|
|
111
|
+
ran it — with the confirmation card the only remaining gate, and that card is
|
|
112
|
+
waived by `autoConfirm`, by a `confirmPredicate`, or by a schema without
|
|
113
|
+
`x-destructive`. The page's decision not to offer the tool carried no weight
|
|
114
|
+
where it mattered. The names a run advertises are now captured as the catalog
|
|
115
|
+
goes out, and a call outside that set is treated exactly as a call naming a
|
|
116
|
+
tool that was never registered. The set is the snapshot, not a fresh
|
|
117
|
+
`getTools()` at dispatch time — asking the provider again would re-open the
|
|
118
|
+
window it exists to close. Hosts that never override `getTools` advertise the
|
|
119
|
+
built-ins plus everything registered, which is exactly what dispatch could
|
|
120
|
+
reach before, so nothing changes for them.
|
|
121
|
+
|
|
122
|
+
- **A long streamed answer slowed the tab down as it arrived, and destroyed any
|
|
123
|
+
text selected inside it on every token.** Each `TEXT_MESSAGE_CONTENT` event
|
|
124
|
+
carries the whole answer so far, and every one of them re-parsed the markdown,
|
|
125
|
+
re-sanitised it and replaced the bubble's entire subtree. Cost grew with the
|
|
126
|
+
square of the answer's length — a 40 KB answer streamed token by token meant
|
|
127
|
+
thousands of full parses over a document that kept getting longer — and
|
|
128
|
+
because the subtree was rebuilt each time, a selection or a focus inside the
|
|
129
|
+
bubble could not survive a single token. Since a long answer is entirely
|
|
130
|
+
agent-controlled, an agent induced to produce one turned an ordinary run into
|
|
131
|
+
a stalling tab. Deltas now coalesce into one render per animation frame: a
|
|
132
|
+
burst of tokens costs one parse, and a frame is the fastest anything on screen
|
|
133
|
+
can change anyway, so the text still flows rather than arriving in chunks. The
|
|
134
|
+
bubble opens on the first token as before, keeps its identity throughout, and
|
|
135
|
+
a run that ends without closing its text message — a cancel, an error, a round
|
|
136
|
+
boundary — draws the queued delta before letting the bubble go, so a stopped
|
|
137
|
+
answer keeps its last words.
|
|
138
|
+
|
|
139
|
+
- **Model output could draw a pixel-accurate copy of the approval card.** The
|
|
140
|
+
markdown sanitiser kept `class` on every element it allowed, and the shadow
|
|
141
|
+
stylesheet's component classes are unscoped selectors, so a
|
|
142
|
+
`<span class="approval-btn approval-btn--approve">` in an assistant message
|
|
143
|
+
resolved to the same background, border and radius as the genuine
|
|
144
|
+
human-in-the-loop approve button — rendered as ordinary prose, inside the one
|
|
145
|
+
surface where the user decides whether to approve something. The same trick
|
|
146
|
+
reproduced the question card, the tool-call card and a turn the user never
|
|
147
|
+
took. `class` was on the allowlist for exactly one thing, `marked`'s
|
|
148
|
+
`language-*` code-fence hint, and is now narrowed to it: a `language-*` token
|
|
149
|
+
on a `code` or `pre` element survives, and every other class is dropped.
|
|
150
|
+
Highlighting a fenced code block is unaffected.
|
|
151
|
+
|
|
152
|
+
- **The sanitiser allowed far more attributes than the three it declared.**
|
|
153
|
+
DOMPurify's `ALLOW_DATA_ATTR` and `ALLOW_ARIA_ATTR` default to `true`, so every
|
|
154
|
+
`data-*` and `aria-*` attribute passed through alongside `href`/`title`/`class`
|
|
155
|
+
while the configuration read as though only those three could survive. That
|
|
156
|
+
handed model output the attributes the cards drive their resolved, status and
|
|
157
|
+
expanded appearance from, and let an `aria-label` make a screen reader announce
|
|
158
|
+
something other than what a sighted user reads. Both are now off, so the
|
|
159
|
+
declared allowlist is the effective one.
|
|
160
|
+
|
|
161
|
+
- **Rendered markdown was edited after the sanitiser had finished with it.** The
|
|
162
|
+
sanitised string was parsed into a `<template>`, given its `target`/`rel` link
|
|
163
|
+
hardening there, and re-serialised — so the markup actually inserted into a
|
|
164
|
+
bubble was never markup DOMPurify inspected, and it carried two attributes the
|
|
165
|
+
allowlist did not name. Nothing exploitable came of it, because no allowed
|
|
166
|
+
element serialises asymmetrically, but that held by accident rather than by
|
|
167
|
+
design: adding `svg`, `style` or `noscript` to the allowlist would have turned
|
|
168
|
+
the round trip into a bypass, and the suite asserted on the sanitiser's output
|
|
169
|
+
instead of on what was inserted. Link hardening now runs inside the sanitiser,
|
|
170
|
+
so what a caller inserts is exactly what DOMPurify approved, and a test holds
|
|
171
|
+
it there.
|
|
172
|
+
|
|
173
|
+
- **Resizing the panel from the keyboard called `commit` on every key repeat.**
|
|
174
|
+
It is documented as one call per completed resize and the pointer path honours
|
|
175
|
+
that, but the keyboard path called it straight from each `keydown`. Holding an
|
|
176
|
+
arrow key means OS key repeat at twenty to thirty events a second, so a host
|
|
177
|
+
that put a `sessionStorage` write or a `PATCH` behind `commit` got that many
|
|
178
|
+
for a single press — landing hardest on the keyboard users the path exists for.
|
|
179
|
+
Live feedback still happens per key event; the commit now waits for the key to
|
|
180
|
+
come up, or for focus to leave the grip mid-press.
|
|
181
|
+
|
|
182
|
+
- **A voice recording ran until somebody stopped it.** Tapping the mic and then
|
|
183
|
+
being interrupted left `MediaRecorder` running with no upper bound: audio
|
|
184
|
+
accumulated in memory, the browser's recording indicator stayed lit in a tab
|
|
185
|
+
nobody was looking at, and whenever the user came back the whole accumulated
|
|
186
|
+
clip was posted to the transcription endpoint in one body no client-side check
|
|
187
|
+
sized. A recording now stops itself after two minutes — far longer than a
|
|
188
|
+
dictated chat message, short enough to bound a forgotten one. The audio is kept
|
|
189
|
+
and transcribed rather than discarded, and the mic button says why it stopped.
|
|
190
|
+
|
|
191
|
+
- **The README's "Public API surface" tables were missing a third of the surface
|
|
192
|
+
they enumerate.** The section presents itself as complete, so a consumer who
|
|
193
|
+
went looking for the chart-drawing seam, the approval and question card
|
|
194
|
+
helpers, the transcription defaults, `prettifyToolName`, `parseToolCatalog` or
|
|
195
|
+
two of the event-name constants found no mention and concluded the package had
|
|
196
|
+
none. Thirty-two absent exports are now listed, and a test compares the tables
|
|
197
|
+
against the package root's export list so the next new export cannot go missing
|
|
198
|
+
quietly.
|
|
199
|
+
|
|
200
|
+
- **`UiStrings.checkpoints` had no documentation to hover.** An insertion of the
|
|
201
|
+
copy-button strings landed between the checkpoint panel's doc comment and the
|
|
202
|
+
field it described, stranding the comment above `copyCode`. It is back on
|
|
203
|
+
`checkpoints`, and the copy-button and checkpoint fields now have sections of
|
|
204
|
+
their own rather than splitting the relative-time group in half.
|
|
205
|
+
|
|
206
|
+
### Security
|
|
207
|
+
|
|
208
|
+
- **Host credentials no longer leave the page's origin silently.** `endpoint` and
|
|
209
|
+
its sibling URL attributes are plain HTML, so a page that builds one from a
|
|
210
|
+
query parameter or from tenant-authored configuration has handed whoever wrote
|
|
211
|
+
that value the destination of the element's requests. The browser preflights
|
|
212
|
+
the custom header, any server willing to answer receives it, and the CSRF token
|
|
213
|
+
or bearer the host supplies through `headers` / `getHeaders()` leaves on the
|
|
214
|
+
very first request — before the user has done anything. Nothing in the package
|
|
215
|
+
compared a configured URL against an expected origin, so the delivery was
|
|
216
|
+
invisible. The agent now reports it on the console, naming the destination and
|
|
217
|
+
the header names, once per origin.
|
|
218
|
+
|
|
219
|
+
It reports rather than refuses: an agent on another subdomain is a documented
|
|
220
|
+
deployment and keeps working unchanged. What is removed is the silence.
|
|
221
|
+
|
|
222
|
+
All seven configurable URLs are covered, not the agent endpoint alone. The tool
|
|
223
|
+
catalog, the skills list, the thread index, the attachment upload and the
|
|
224
|
+
transcription endpoint are named by the same kind of host attribute and carry
|
|
225
|
+
the same headers, so reporting only the agent would have reported the least
|
|
226
|
+
interesting of the seven.
|
|
227
|
+
|
|
228
|
+
### Documentation
|
|
229
|
+
|
|
230
|
+
- **Registering a duplicate tool name does not throw.** The README said it did.
|
|
231
|
+
`ClientToolRegistry.register` is a plain map write, deliberately, so a re-fired
|
|
232
|
+
host ref or React StrictMode's double-invoke replaces rather than raises. The
|
|
233
|
+
harm in the claim is not the defensive guard nobody needs; it is a host
|
|
234
|
+
believing two tools cannot quietly share a name, where the second wins.
|
|
235
|
+
|
|
236
|
+
- **`x-destructive` gates frontend tools only, and the docs claimed otherwise.**
|
|
237
|
+
`isDestructive` described the flag as one the server stamps, which cannot
|
|
238
|
+
reach it: tool schemas travel client-to-server on `RunAgentInput.tools`, and
|
|
239
|
+
the only channel coming back is the label catalog, which carries names and
|
|
240
|
+
summaries and no flags. Marking a server-side tool destructive therefore
|
|
241
|
+
produces no confirmation card in the browser — it has to be gated server-side,
|
|
242
|
+
which surfaces as an approval card instead. The helper's doc comment and the
|
|
243
|
+
README's confirmation section now say so.
|
|
244
|
+
|
|
245
|
+
- **A tool handler's thrown message reaches the model.** When a handler rejects,
|
|
246
|
+
its `Error.message` is posted back as that call's tool result: into the
|
|
247
|
+
conversation, on to the AG-UI endpoint, persisted there, and replayed to the
|
|
248
|
+
model provider on every later round. That is deliberate — a real reason is
|
|
249
|
+
what lets the agent recover — but the same string is only ever shown to the
|
|
250
|
+
user as a short card label, so a host rethrowing an internal error had no way
|
|
251
|
+
to see that an internal hostname, a signed URL or a stack-derived path had
|
|
252
|
+
left the browser. `registerTool` and the README now say it plainly, so hosts
|
|
253
|
+
can throw the message they would be content for the model to read.
|
|
254
|
+
|
|
10
255
|
## [0.27.0] — 2026-08-26
|
|
11
256
|
|
|
12
257
|
### Fixed
|
|
@@ -1705,7 +1950,8 @@ hosts that both arrange the page the way it expects.
|
|
|
1705
1950
|
### Notes
|
|
1706
1951
|
- First release — exercising the automated npm OIDC publish pipeline end-to-end.
|
|
1707
1952
|
|
|
1708
|
-
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.
|
|
1953
|
+
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.28.0...HEAD
|
|
1954
|
+
[0.28.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.27.0...v0.28.0
|
|
1709
1955
|
[0.27.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.26.1...v0.27.0
|
|
1710
1956
|
[0.26.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.26.0...v0.26.1
|
|
1711
1957
|
[0.26.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.25.2...v0.26.0
|
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
|
|
|
53
53
|
- [Run notices: compaction and agent skills](#run-notices-compaction-and-agent-skills)
|
|
54
54
|
- [Skills: prompt chips and slash palette](#skills-prompt-chips-and-slash-palette)
|
|
55
55
|
- [MPA durability: surviving full page reloads](#mpa-durability-surviving-full-page-reloads)
|
|
56
|
+
- [Who the stored conversation belongs to (`user-key`)](#who-the-stored-conversation-belongs-to-user-key)
|
|
57
|
+
- [Mounting more than one chat on a page](#mounting-more-than-one-chat-on-a-page)
|
|
56
58
|
- [Host seams: the SPA story](#host-seams-the-spa-story)
|
|
57
59
|
- [Public API surface](#public-api-surface)
|
|
58
60
|
- [Theming, density, and placement](#theming-density-and-placement)
|
|
@@ -162,7 +164,9 @@ another origin, add `credentials="include"` too; see
|
|
|
162
164
|
| `data-skills` | — | Inline JSON skill catalog. |
|
|
163
165
|
| `data-skills-url` | — | URL of a JSON skill catalog (fetched with the element's headers and cookie policy). |
|
|
164
166
|
| `data-tools-url` | — | URL of a server tool-label catalog (`[{ name, summary, description? }]`), fetched with the element's headers and cookie policy; labels tool-call cards for server-side tools. |
|
|
167
|
+
| `user-key` | `userKey` | Who the stored conversation belongs to — any string identifying the signed-in principal. Joins the storage namespace, and **changing it purges what the previous principal left behind**. Live (not connect-time): a logout is the host's to announce. See [Who the stored conversation belongs to](#who-the-stored-conversation-belongs-to-user-key). |
|
|
165
168
|
| `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
|
|
169
|
+
| `data-threads-cache` | — | **On by default.** `="false"` stops mirroring message bodies into `sessionStorage` when `data-threads-url` is set, for a deployment that put history on the server so transcripts stay off the client. Only meaningful alongside `data-threads-url`. |
|
|
166
170
|
| `data-runs-url` | — | URL of a server run index (django-ag-ui's `RunsView`); reveals the header's ⭯ *Continue a run* panel. See [Resuming a run](#resuming-a-run). |
|
|
167
171
|
| `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker + drag-and-drop. |
|
|
168
172
|
| `data-attachment-accept` | — | `<input accept>` list for client-side type filtering (e.g. `image/*,.pdf`). The server stays authoritative. |
|
|
@@ -196,7 +200,7 @@ same way: `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`.
|
|
|
196
200
|
`confirmPredicate`, `askUser`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`,
|
|
197
201
|
`getPageMap`, `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`,
|
|
198
202
|
`navigationResult`, `skillContext`, `toolSummaries`, `strings`, `resolvePageTarget`, plus the
|
|
199
|
-
mirrors `endpoint` / `toolDisplay` / `collapsed` / `credentials`.
|
|
203
|
+
mirrors `endpoint` / `userKey` / `toolDisplay` / `collapsed` / `credentials`.
|
|
200
204
|
|
|
201
205
|
`headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
|
|
202
206
|
run; `getHeaders` is the one to use for a credential that rotates. See
|
|
@@ -332,6 +336,41 @@ One asymmetry: uploads use `XMLHttpRequest` for real progress events, and its co
|
|
|
332
336
|
two-state. `include` turns it on; every other value leaves it off. `omit` therefore cannot suppress
|
|
333
337
|
cookies on a *same-origin* upload — supply your own `uploadHandler` if that matters.
|
|
334
338
|
|
|
339
|
+
### Where those credentials are allowed to go
|
|
340
|
+
|
|
341
|
+
Every URL in the table above is a plain HTML attribute, and `headers` / `getHeaders` are attached to
|
|
342
|
+
whatever they name. That is what makes a cross-origin agent work — and it is also why a page must
|
|
343
|
+
never build one of those attributes out of a URL parameter, a CMS field, or anything else it did not
|
|
344
|
+
choose itself. Whoever supplies the value chooses where the token goes: the browser preflights the
|
|
345
|
+
custom header, any server willing to answer receives it, and it leaves on the element's first
|
|
346
|
+
request, before the user has typed anything.
|
|
347
|
+
|
|
348
|
+
Treat all seven as trusted configuration. When any of them resolves to another origin, the element
|
|
349
|
+
says so on the console once per origin, naming the destination and the header names it is about to
|
|
350
|
+
send. That covers all seven, not the agent endpoint alone: the tool catalog, the skills list, the
|
|
351
|
+
thread index, the attachment upload and the transcription endpoint carry the same headers, and
|
|
352
|
+
reporting only the agent would report the least interesting of them.
|
|
353
|
+
|
|
354
|
+
To confirm destinations you chose on purpose and silence the notice, name their origins:
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
chat.trustedOrigins = ["https://api.example.com"];
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
That covers every endpoint the element requests itself, and is forwarded to `createHttpAgent`, so a
|
|
361
|
+
host that does not override `agentFactory` needs nothing else. A custom factory can also be given
|
|
362
|
+
the option directly:
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
chat.agentFactory = (options) =>
|
|
366
|
+
createHttpAgent({ ...options, trustedOrigins: ["https://api.example.com"] });
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Origins are compared as `URL.origin` produces them — scheme, host and port. A notice is a notice,
|
|
370
|
+
not a refusal: nothing is blocked, because a cross-origin agent is a supported deployment and
|
|
371
|
+
refusing would break working installations to defend against a page that is already interpolating
|
|
372
|
+
untrusted data into its own markup.
|
|
373
|
+
|
|
335
374
|
### Framework hosts: configure before you insert
|
|
336
375
|
|
|
337
376
|
`headers`, `getHeaders` and `credentials` are read when a request is made, so they can be set at any
|
|
@@ -449,6 +488,13 @@ card (honouring `data-tool-display`), so server-side output is visible too. The
|
|
|
449
488
|
and context are read **fresh on every run** (`getTools()` / `getContext()`), so they always reflect
|
|
450
489
|
the current page state.
|
|
451
490
|
|
|
491
|
+
The catalog a run advertises is also the set that run can execute. Override `getTools` to scope
|
|
492
|
+
what a page offers — say, exposing `delete_record` only where deleting makes sense — and a call
|
|
493
|
+
naming a tool you withheld is treated exactly as a call naming a tool you never registered: no
|
|
494
|
+
handler runs, and the card settles with the no-result label. Withholding is per run, so the
|
|
495
|
+
mount-wide registry can stay complete. Hosts that leave `getTools` alone advertise the built-ins
|
|
496
|
+
plus everything registered, which is precisely what dispatch could reach anyway.
|
|
497
|
+
|
|
452
498
|
### Stopping a run
|
|
453
499
|
|
|
454
500
|
While a run is in flight the **Send button becomes Stop** (same button, label/`aria-label` swap,
|
|
@@ -491,10 +537,19 @@ chat.registerTool({
|
|
|
491
537
|
});
|
|
492
538
|
```
|
|
493
539
|
|
|
494
|
-
|
|
540
|
+
Registering a name twice replaces the earlier handler rather than throwing, so a re-fired
|
|
541
|
+
host ref or React StrictMode's double-invoke is harmless -- but two different tools sharing
|
|
542
|
+
a name means the second silently wins. Each `<ag-ui-chat>` element owns its own
|
|
495
543
|
registry, AG-UI client, and Shadow DOM, so **multiple instances on one page never interfere** —
|
|
496
544
|
there is no module-level shared state anywhere in the package.
|
|
497
545
|
|
|
546
|
+
**A handler's thrown message leaves the browser.** If a handler rejects, its `Error.message` is
|
|
547
|
+
posted back as that call's tool result: into the conversation, on to the AG-UI endpoint, persisted
|
|
548
|
+
there, and replayed to the model provider on every later round. That is deliberate — a real reason
|
|
549
|
+
is what lets the agent recover — but it means an internal hostname, a signed URL or a
|
|
550
|
+
stack-derived path in a rethrown error is disclosed to parties you never chose. Throw the message
|
|
551
|
+
you would be content for the model to read, and log the detail instead.
|
|
552
|
+
|
|
498
553
|
### Inline confirmation (`x-destructive` / `x-confirm` / `confirmPredicate`)
|
|
499
554
|
|
|
500
555
|
When a tool call needs confirmation, the element appends an **inline confirmation card** (a
|
|
@@ -525,6 +580,13 @@ the `x-destructive` flag (or `confirmPredicate`). The registry forwards the flag
|
|
|
525
580
|
If the schema carries an `x-confirm` string (use `X_CONFIRM_KEY`), the card shows it as the prompt;
|
|
526
581
|
otherwise it falls back to a generic `Run "<tool>"?`.
|
|
527
582
|
|
|
583
|
+
**This gate covers frontend tools only.** A server-side tool's schema never reaches the browser —
|
|
584
|
+
tool definitions travel client-to-server on `RunAgentInput.tools`, and the only channel coming back
|
|
585
|
+
is the label catalog (`data-tools-url`), which carries `{ name, summary, description? }` and no
|
|
586
|
+
flags. So marking a server tool destructive does not produce a card here; gate it server-side
|
|
587
|
+
instead (see [Server-side tool approval](#server-side-tool-approval-interrupts)), which surfaces as
|
|
588
|
+
an approval card in the same transcript.
|
|
589
|
+
|
|
528
590
|
```js
|
|
529
591
|
// Per-call: confirm a delete only when it would remove more than one row.
|
|
530
592
|
chat.confirmPredicate = (name, args) =>
|
|
@@ -1042,9 +1104,12 @@ via the `resize-handle` part.
|
|
|
1042
1104
|
Assistant bubbles render sanitized markdown/HTML via [`marked`](https://www.npmjs.com/package/marked)
|
|
1043
1105
|
(GitHub-flavoured, single-newline line breaks) piped through
|
|
1044
1106
|
[DOMPurify](https://www.npmjs.com/package/dompurify). User messages stay literal text. The
|
|
1045
|
-
allowlist permits emphasis, code, lists, quotes, headings, links, tables, and
|
|
1046
|
-
are hardened with `target="_blank" rel="noopener noreferrer"`;
|
|
1047
|
-
excluded
|
|
1107
|
+
allowlist permits emphasis, code, lists, quotes, headings, links, tables, and — when `allowImages`
|
|
1108
|
+
is set — images; links are hardened with `target="_blank" rel="noopener noreferrer"`;
|
|
1109
|
+
`iframe`/`style`/scripting are excluded, as are every `data-*` and `aria-*` attribute and every
|
|
1110
|
+
`class` but a code fence's `language-*` hint, so model output cannot dress itself up as the
|
|
1111
|
+
component's own approval or tool-call chrome. The exported helper `renderMarkdown(text)` does this
|
|
1112
|
+
standalone. `marked` and `dompurify`
|
|
1048
1113
|
are runtime dependencies.
|
|
1049
1114
|
|
|
1050
1115
|
An animated 3-dot "thinking" indicator (`role="status"`, with an aria-label) appears before the
|
|
@@ -1177,6 +1242,87 @@ triggers a full reload. Before the handler navigates, the element writes a check
|
|
|
1177
1242
|
|
|
1178
1243
|
The MPA round-trip becomes a clean observation point instead of a dropped conversation.
|
|
1179
1244
|
|
|
1245
|
+
### Who the stored conversation belongs to (`user-key`)
|
|
1246
|
+
|
|
1247
|
+
`sessionStorage` is scoped to a tab, not to a session. It survives every same-tab navigation,
|
|
1248
|
+
and a logout is a navigation — so on a shared workstation, one user's transcript is still sitting
|
|
1249
|
+
there when the next user signs in and the chat mounts again. Transcripts routinely quote record
|
|
1250
|
+
data, so treat that as the default and turn it off:
|
|
1251
|
+
|
|
1252
|
+
```html
|
|
1253
|
+
<ag-ui-chat endpoint="/agent/" user-key="{{ request.user.pk }}"></ag-ui-chat>
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
The value is any string that identifies the principal — a user id, an account id, a hash of one.
|
|
1257
|
+
It joins the storage namespace, so two principals in the same tab cannot reach each other's
|
|
1258
|
+
conversation, and **changing it purges everything the previous principal stored**: transcript,
|
|
1259
|
+
history drawer index and navigation checkpoints, for this element's namespace only.
|
|
1260
|
+
|
|
1261
|
+
Set it live, from script, as part of signing out or in:
|
|
1262
|
+
|
|
1263
|
+
```js
|
|
1264
|
+
chat.userKey = String(session.userId); // or "" on sign-out
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
That is why it is a live attribute rather than a connect-time one. A single-page app signs a user
|
|
1268
|
+
out through its own router without remounting anything, so the host naming the new principal — or
|
|
1269
|
+
dropping the attribute — is the only signal the element will ever get. Removing the attribute
|
|
1270
|
+
purges too, so a sign-out that simply clears it is safe.
|
|
1271
|
+
|
|
1272
|
+
The **first** value to arrive is treated as a host naming the user who was already there, not as a
|
|
1273
|
+
handover: the conversation in progress moves into the principal's namespace instead of being
|
|
1274
|
+
destroyed. So an element configured by an async auth handshake — the shape described in
|
|
1275
|
+
[Framework hosts](#framework-hosts-configure-before-you-insert) — keeps what is on screen.
|
|
1276
|
+
|
|
1277
|
+
Two things it deliberately does not do. It does not scope the panel's own collapsed / dragged-size
|
|
1278
|
+
/ theme preferences, which are this element's UI state and carry no conversation content. And it
|
|
1279
|
+
does not encrypt or hide anything from the page: any script on the origin can still read
|
|
1280
|
+
`sessionStorage`. It scopes and it purges.
|
|
1281
|
+
|
|
1282
|
+
**Without it, nothing changes** — including the carry-over above. A conversation is scoped to the
|
|
1283
|
+
element and to nobody in particular, and on a shared workstation it will be there for whoever signs
|
|
1284
|
+
in next in the same tab.
|
|
1285
|
+
|
|
1286
|
+
For a deployment that keeps history server-side, `data-threads-cache="false"` stops the local
|
|
1287
|
+
mirror of the message bodies as well, so choosing `data-threads-url` actually keeps transcripts off
|
|
1288
|
+
the client:
|
|
1289
|
+
|
|
1290
|
+
```html
|
|
1291
|
+
<ag-ui-chat endpoint="/agent/" data-threads-url="/agent/threads/" data-threads-cache="false">
|
|
1292
|
+
</ag-ui-chat>
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
The client-only concerns (the active thread id, the navigation checkpoint) keep their local store
|
|
1296
|
+
either way, so reloads and navigating tools still work. What is lost is the offline fallback: when
|
|
1297
|
+
the thread endpoint is unreachable the transcript comes back empty rather than stale, and the
|
|
1298
|
+
drawer's offline list loses its previews — a preview being an excerpt of a message, which is the
|
|
1299
|
+
thing being kept off the client. Constructing the store yourself takes the same option:
|
|
1300
|
+
|
|
1301
|
+
```js
|
|
1302
|
+
chat.conversationStore = new RemoteConversationStore(
|
|
1303
|
+
"/agent/threads/",
|
|
1304
|
+
() => ({ "X-CSRFToken": token }),
|
|
1305
|
+
new SessionStorageStore(),
|
|
1306
|
+
() => "same-origin",
|
|
1307
|
+
false, // cacheMessages
|
|
1308
|
+
);
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
`SessionStorageStore.purge(namespace)` is the same primitive the element uses, for a host driving
|
|
1312
|
+
its own store from its own sign-out path.
|
|
1313
|
+
|
|
1314
|
+
### Mounting more than one chat on a page
|
|
1315
|
+
|
|
1316
|
+
Give each `<ag-ui-chat>` its own `id`. The storage namespace is the element's `id`, falling back to
|
|
1317
|
+
its `endpoint` — so two elements with no `id` against the same agent mount (a docked support panel
|
|
1318
|
+
and an inline page assistant, say) would resolve to the same namespace and share a thread pointer,
|
|
1319
|
+
a history drawer and every message key.
|
|
1320
|
+
|
|
1321
|
+
They no longer do: the first element to mount keeps the namespace, and a second is given a
|
|
1322
|
+
throwaway one of its own plus a console warning. That keeps the two conversations apart, but the
|
|
1323
|
+
throwaway namespace is minted per mount, so the second element will not restore its conversation
|
|
1324
|
+
across a reload until it has an `id`.
|
|
1325
|
+
|
|
1180
1326
|
---
|
|
1181
1327
|
|
|
1182
1328
|
## Host seams: the SPA story
|
|
@@ -1452,6 +1598,8 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1452
1598
|
| `SubmitDetail` | type | `detail` shape of the submit event. |
|
|
1453
1599
|
| `ToggleDetail` | type | `detail` shape of the `ag-ui-toggle` event (`{ collapsed }`). |
|
|
1454
1600
|
| `UnreadDetail` | type | `detail` shape of the `ag-ui-unread` event (`{ unread }`). |
|
|
1601
|
+
| `AttachmentsDetail` | type | `detail` shape of the `ag-ui-attachments` event (`{ attachments, pending }`). |
|
|
1602
|
+
| `StateDetail` | type | `detail` shape of the `ag-ui-state` event (`{ state }`). |
|
|
1455
1603
|
|
|
1456
1604
|
### AG-UI client & agent
|
|
1457
1605
|
|
|
@@ -1463,6 +1611,8 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1463
1611
|
| `ConnectionLostError` | class | Raised (→ `onError`) when a run's stream closes with no terminal AG-UI event. |
|
|
1464
1612
|
| `createHttpAgent(options)` | function | Default agent factory (wraps `HttpAgent`). |
|
|
1465
1613
|
| `AgentFactory` / `HttpAgentOptions` | type | Factory signature and its options. |
|
|
1614
|
+
| `ResolveInterrupts` | type | Resolver for server-side-tool approval interrupts (one decision per interrupt). |
|
|
1615
|
+
| `InterruptResponse` | type | One interrupt's answer: `resolved` (with an optional payload) or `cancelled`. |
|
|
1466
1616
|
|
|
1467
1617
|
### Tools & flags
|
|
1468
1618
|
|
|
@@ -1476,6 +1626,9 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1476
1626
|
| `PAGE_ACTIONS` | const | The page-action opt-in tokens (`scroll` / `drag`). |
|
|
1477
1627
|
| `ResolvePageTarget` | type | `(target) => HTMLElement | null` — the page-target resolver. |
|
|
1478
1628
|
| `X_DESTRUCTIVE_KEY` / `X_NAVIGATES_KEY` | const | The JSON-Schema extension keys. |
|
|
1629
|
+
| `parseToolCatalog(data)` | function | Parse a fetched `data-tools-url` catalog into a `name` → `summary` map. |
|
|
1630
|
+
| `ToolCatalogEntry` | type | One row of that catalog. |
|
|
1631
|
+
| `prettifyToolName(name)` | function | Last fallback of the tool-card label chain (`delete_record` reads as *Delete record*). |
|
|
1479
1632
|
|
|
1480
1633
|
### Host seams
|
|
1481
1634
|
|
|
@@ -1490,6 +1643,7 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1490
1643
|
| `PageState` | type | A page-state binding declaration. |
|
|
1491
1644
|
| `Skill` | type | A launchable prompt (chip / `/`-command). |
|
|
1492
1645
|
| `RunFinishedDetail` / `ToolRun` | type | `ag-ui-run-finished` detail: the tools an interaction ran, and which side ran them. |
|
|
1646
|
+
| `createStateHookTools(binding)` / `StateHook` | deprecated | The former names for `createPageStateTools` / `PageState`. |
|
|
1493
1647
|
|
|
1494
1648
|
### Durability
|
|
1495
1649
|
|
|
@@ -1515,6 +1669,14 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1515
1669
|
| `AttachmentRef` | type | The durable upload ref (`{ id, name, mime, size, url? }`). |
|
|
1516
1670
|
| `messageAttachments(message)` | function | Read the refs a restored user message carries. |
|
|
1517
1671
|
|
|
1672
|
+
### Voice input
|
|
1673
|
+
|
|
1674
|
+
| Export | Kind | Summary |
|
|
1675
|
+
| --- | --- | --- |
|
|
1676
|
+
| `transcribeAudio(audio, options)` | function | The built-in transcription POST (multipart) → the transcript text. |
|
|
1677
|
+
| `TranscribeOptions` | type | `{ url, headers? }`. |
|
|
1678
|
+
| `TranscribeHandler` | type | `(audio) => Promise<string>` — the `transcribeHandler` swap seam (Web Speech, direct-to-provider). |
|
|
1679
|
+
|
|
1518
1680
|
### UI & DOM primitives
|
|
1519
1681
|
|
|
1520
1682
|
| Export | Kind | Summary |
|
|
@@ -1527,7 +1689,19 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1527
1689
|
| `UiStrings` | type | The flat table of every user-facing string. |
|
|
1528
1690
|
| `DEFAULT_UI_STRINGS` | const | The English defaults (the override floor). |
|
|
1529
1691
|
| `mergeUiStrings(overrides)` | function | Merge a partial override over the defaults. |
|
|
1530
|
-
| `renderMarkdown(text)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
|
|
1692
|
+
| `renderMarkdown(text, options?)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
|
|
1693
|
+
| `RenderMarkdownOptions` | type | `{ allowImages? }` — opt `<img>` back into the sanitized output. |
|
|
1694
|
+
| `requestApproval(host, request, options?)` | function | Append the inline approval card that gates a server-side tool. |
|
|
1695
|
+
| `ApprovalRequest` | type | What that card displays (`{ message?, toolName? }`). |
|
|
1696
|
+
| `ApprovalOptions` | type | `{ signal?, strings? }` — abort resolves the card as denied; `strings` localizes it. |
|
|
1697
|
+
| `ApprovalRenderer` | type | Replace the built-in approval card outright (`AgUiChat.approvalRenderer`). |
|
|
1698
|
+
| `requestQuestion(host, request, options?)` | function | Append the inline `ask_user` card (radios and/or free text). |
|
|
1699
|
+
| `QuestionRequest` | type | What that card asks. |
|
|
1700
|
+
| `QuestionOptions` | type | `{ signal?, strings? }` — abort resolves it with an empty answer. |
|
|
1701
|
+
| `QuestionRenderer` | type | Replace the built-in question card outright (`AgUiChat.questionRenderer`). |
|
|
1702
|
+
| `renderChart(spec)` | function | Draw one spec as a self-contained block, or `null` when it says nothing. |
|
|
1703
|
+
| `chartSpecFrom(value)` | function | Read an arbitrary payload into a `ChartSpec`, or `null` if it cannot be drawn honestly. |
|
|
1704
|
+
| `ChartSpec` / `ChartSeries` / `ChartKind` | type | A chart as data, one named series, and how it is drawn. |
|
|
1531
1705
|
| `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `flash` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
|
|
1532
1706
|
| `fillField` / `clickElement` / `pressButton` / `selectControl` / `setControlValue` / `toggleCheckbox` | function | DOM-driver primitives. |
|
|
1533
1707
|
| `setNativeValue` / `setNativeChecked` | function | Set a control via its native prototype setter (React-controlled inputs). |
|
|
@@ -1542,6 +1716,12 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1542
1716
|
| `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
|
|
1543
1717
|
| `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
|
|
1544
1718
|
| `RUN_FINISHED_EVENT` | The interaction-finished CustomEvent name (`ag-ui-run-finished`). |
|
|
1719
|
+
| `ATTACHMENT_EVENT` | The attachments-changed CustomEvent name (`ag-ui-attachments`). |
|
|
1720
|
+
| `STATE_EVENT` | The shared-state CustomEvent name (`ag-ui-state`). |
|
|
1721
|
+
| `CHART_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type a server sets to push a chart. |
|
|
1722
|
+
| `CHART_TOOL_NAME` | The name the built-in chart tool registers under (`render_chart`). |
|
|
1723
|
+
| `COMPACTION_ACTIVITY_TYPE` | The `ACTIVITY_SNAPSHOT` type reporting a trimmed history. |
|
|
1724
|
+
| `LOAD_CAPABILITY_TOOL` | The agent-side capability-loading tool's name. |
|
|
1545
1725
|
| `MESSAGE_ROLE` | Message role constants. |
|
|
1546
1726
|
| `TOOL_CALL_STATUS` | Tool-call card status constants. |
|
|
1547
1727
|
| `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
|