@artooi/ag-ui-web-component 0.20.0 → 0.21.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 +135 -1
- package/README.md +286 -20
- package/dist/ag-ui-web-component.bundle.js +407 -382
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +82 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +9 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +8 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +8 -1
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +7 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +9 -0
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/core/utils.d.ts +12 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/dom/animations.d.ts +51 -11
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/dom_driver.d.ts +12 -7
- package/dist/dom/dom_driver.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +766 -430
- package/dist/index.js.map +3 -3
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +292 -31
- package/src/core/create_http_agent.ts +14 -3
- package/src/core/remote_conversation_store.ts +25 -6
- package/src/core/run_index.ts +27 -5
- package/src/core/transcribe_audio.ts +20 -5
- package/src/core/upload_attachment.ts +13 -0
- package/src/core/utils.ts +18 -0
- package/src/dom/animations.ts +175 -31
- package/src/dom/dom_driver.ts +18 -12
- package/src/index.ts +2 -0
- package/src/ui/styles.ts +377 -352
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.21.0] — 2026-08-11
|
|
11
|
+
|
|
12
|
+
Ten findings from a real embed — a cross-origin, cookie-authenticated React host.
|
|
13
|
+
Six of them are one defect: **the component assumed it owned the page.** It had
|
|
14
|
+
only ever been embedded in its own playground and in `django-admin-agent`, two
|
|
15
|
+
hosts that both arrange the page the way it expects.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`getHeaders`** — a function consulted immediately before **every** request, so
|
|
20
|
+
a rotating credential (a short-lived JWT, a re-issued CSRF token) reaches the
|
|
21
|
+
request that needs it. `headers` was a plain field the element never wrote to,
|
|
22
|
+
read at ten sites; configuring auth through `agentFactory` therefore
|
|
23
|
+
authenticated the run and nothing else, and thread history, attachments and the
|
|
24
|
+
catalogs went out anonymous and 401'd — which reads as a backend fault. The two
|
|
25
|
+
compose, merged per key with `getHeaders` winning, so adding a rotating header
|
|
26
|
+
cannot silently drop a static one.
|
|
27
|
+
|
|
28
|
+
- **`credentials`** (attribute and property) — `omit` / `same-origin` / `include`,
|
|
29
|
+
applied to every request. There was previously no occurrence of `credentials`
|
|
30
|
+
anywhere in the source, so every request used the browser default and a host
|
|
31
|
+
serving its SPA and API from different subdomains sent no cookies, with **no way
|
|
32
|
+
to express the fix** short of replacing the transport. An unknown value is
|
|
33
|
+
rejected where it was written (the property throws; the attribute logs and stays
|
|
34
|
+
inert) rather than becoming a 401 later.
|
|
35
|
+
|
|
36
|
+
- **`flash(el, options)`** — ring an element without moving focus.
|
|
37
|
+
`focusWithFlash` calls `el.focus()`, which for a helper named "flash" is
|
|
38
|
+
surprising: it takes focus off the composer, can fire blur validation on
|
|
39
|
+
whatever the user was mid-edit in, and can close a menu. Both now accept an
|
|
40
|
+
explicit `focus` option, and `focusWithFlash` focuses with `preventScroll: true`
|
|
41
|
+
so it no longer fights the scroll it just started.
|
|
42
|
+
|
|
43
|
+
- **`openThreads()` / `openCheckpoints()` / `reload()`.** The header's controls all
|
|
44
|
+
live inside `::part(header)`, so a host rendering its own title bar and hiding it
|
|
45
|
+
lost thread switching entirely. The built-in buttons call exactly these methods,
|
|
46
|
+
so the two routes cannot drift. `reload()` re-runs the startup fetches once
|
|
47
|
+
credentials that arrive late have landed.
|
|
48
|
+
|
|
49
|
+
- **`ScrollOptions`**, and `scrollIntoCenterView` now returns a promise that
|
|
50
|
+
resolves when the scroll has settled (`scrollend` where available, a short probe
|
|
51
|
+
when nothing moved, a 600 ms cap otherwise). Every DOM-driver primitive awaits it.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **CSS custom properties now work from an ancestor**, which is what the README
|
|
56
|
+
always described and the one thing that could not work: every `--ag-ui-*` default
|
|
57
|
+
was declared on `:host`, which sets the property *on the element*, and an
|
|
58
|
+
element's own value beats anything inherited. A consumer ran the full token map
|
|
59
|
+
on a wrapper for an entire build and concluded they had the names wrong. The
|
|
60
|
+
defaults now sit behind private aliases, so `:root`, a wrapper, the element and
|
|
61
|
+
an inline style all work and resolve in the usual order. **One vocabulary — the
|
|
62
|
+
public `--ag-ui-*` names are unchanged**, and a built-in `theme` / `density` /
|
|
63
|
+
`placement` preset still loses to an explicit page rule.
|
|
64
|
+
|
|
65
|
+
- **The flash is an `outline`, not a `box-shadow`**, and holds for **1200 ms** with
|
|
66
|
+
a fade rather than 200 ms. A shadow paints outside the border box, so any
|
|
67
|
+
`overflow: hidden` ancestor sharing the element's box — a card, a table cell —
|
|
68
|
+
clipped it entirely while the tool reported success. And 200 ms is below the
|
|
69
|
+
threshold at which someone who does not know where to look notices anything.
|
|
70
|
+
⭐ Neither our tests nor the consumer's could have found the second one:
|
|
71
|
+
headless Chromium hides the scroll race, and no automated check has an opinion
|
|
72
|
+
about whether a human sees a 200 ms ring.
|
|
73
|
+
|
|
74
|
+
- **The flash colour comes from the target's `--ag-ui-accent`** rather than a
|
|
75
|
+
hardcoded indigo, so a themed page is flashed in its own colour.
|
|
76
|
+
|
|
77
|
+
- **`prefersReducedMotion()` is now honoured where the README already claimed it
|
|
78
|
+
was.** The flash ignored it entirely while the docs said the preference was
|
|
79
|
+
"honoured throughout"; a consumer investigated reduced motion as the cause of an
|
|
80
|
+
invisible highlight on the strength of that sentence. Under reduced motion the
|
|
81
|
+
ring drops its fade but keeps its full hold — reduced motion asks for no
|
|
82
|
+
animation, not for no feedback. `typeInto` and `highlightThenClick` keep their
|
|
83
|
+
explicit-duration contract, and the docs now say so instead of overclaiming.
|
|
84
|
+
|
|
85
|
+
- **The tool and skill catalog fetches are deferred by one microtask**, so a React
|
|
86
|
+
`ref` assigned in the same commit as insertion is honoured. ⚠ The thread-history
|
|
87
|
+
request is deliberately **not** deferred: a deferred replay can land after a
|
|
88
|
+
`sendMessage()` and duplicate the transcript. Configure before you insert, or
|
|
89
|
+
call `reload()`; the new React recipe in the README shows both.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- **The resize grip now sits on the corner that actually moves.** The element
|
|
94
|
+
measures which edges its host's layout holds still and stamps them as
|
|
95
|
+
`data-resize-anchor="<y>-<x>"`, but the rules meant to read it were written as
|
|
96
|
+
`[data-resize-anchor~="left"]` — and `~=` matches whitespace-separated words
|
|
97
|
+
while the stamped value is a single hyphenated token, so they could never
|
|
98
|
+
match. ⚠ **The cursor rules used `=` and did match**, so the pointer followed
|
|
99
|
+
the measurement while the grip stayed where `placement` had guessed: for any
|
|
100
|
+
host that aligns the panel the other way, the cursor promised a diagonal the
|
|
101
|
+
grip was not on, and the grip sat on the corner that stays put.
|
|
102
|
+
|
|
103
|
+
Each anchor rule now sets **both** sides of its axis. One that flipped a
|
|
104
|
+
single way could not undo a placement guess that had flipped the other, which
|
|
105
|
+
put the grip back on the anchored corner for an embedded panel its host
|
|
106
|
+
right-aligns.
|
|
107
|
+
|
|
108
|
+
- **`placement="page"` gutters its composer to the same reading column as its
|
|
109
|
+
messages.** The rule was unscoped, which tied it on specificity with the base
|
|
110
|
+
`.input-row` rule that sets the `padding` shorthand later in the stylesheet;
|
|
111
|
+
source order decided and the shorthand won. ⇒ *No placement ever got the
|
|
112
|
+
gutter* — least of all the one it was written for, where the messages sat in a
|
|
113
|
+
centred column and the composer spanned the full width.
|
|
114
|
+
|
|
115
|
+
A duplicated copy of the whole resize block also sat earlier in the file,
|
|
116
|
+
welded to the preceding rule by a comment left between a selector and its
|
|
117
|
+
subject; it parsed as `:host([placement="page"]) .resize-handle`, scoping the
|
|
118
|
+
grip's base positioning to the one placement that hides it.
|
|
119
|
+
|
|
120
|
+
## [0.20.1] — 2026-08-11
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
|
|
124
|
+
- ⛔ **`placement="side"` (and `sidebar`) stopped being full height once the
|
|
125
|
+
panel had been resized.** A dragged size is written as a custom property on
|
|
126
|
+
the host, and an inline custom property **outranks a `:host([placement=…])`
|
|
127
|
+
rule setting the same property** — so a height dragged while floating capped a
|
|
128
|
+
docked sidebar that had asked for `100vh`. Since the size persists per tab,
|
|
129
|
+
one drag broke every later visit.
|
|
130
|
+
|
|
131
|
+
⚠ **The previous release claimed this was already handled, and the reasoning
|
|
132
|
+
was wrong.** Writing `--ag-ui-height` rather than inline `height` was supposed
|
|
133
|
+
to leave placement with the final say; it does not, because the indirection
|
|
134
|
+
changes nothing about the cascade. The fix is explicit rather than
|
|
135
|
+
cascade-dependent: **a placement owns the axes it fixes**, a persisted size is
|
|
136
|
+
applied only to the axes it leaves free, and switching placement hands the
|
|
137
|
+
owned axes back.
|
|
138
|
+
|
|
139
|
+
⇒ *"I used the more specific-looking mechanism" is not a substitute for
|
|
140
|
+
checking which declaration actually wins.*
|
|
141
|
+
|
|
10
142
|
## [0.20.0] — 2026-08-11
|
|
11
143
|
|
|
12
144
|
### Added
|
|
@@ -1039,7 +1171,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1039
1171
|
### Notes
|
|
1040
1172
|
- First release — exercising the automated npm OIDC publish pipeline end-to-end.
|
|
1041
1173
|
|
|
1042
|
-
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.
|
|
1174
|
+
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.21.0...HEAD
|
|
1175
|
+
[0.21.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.20.1...v0.21.0
|
|
1176
|
+
[0.20.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.20.0...v0.20.1
|
|
1043
1177
|
[0.20.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.19.0...v0.20.0
|
|
1044
1178
|
[0.19.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.18.0...v0.19.0
|
|
1045
1179
|
[0.18.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.17.0...v0.18.0
|
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
|
|
|
38
38
|
|
|
39
39
|
- [Install](#install)
|
|
40
40
|
- [Quickstart](#quickstart)
|
|
41
|
+
- [Authenticating requests](#authenticating-requests)
|
|
41
42
|
- [Core concepts](#core-concepts)
|
|
42
43
|
- [The run loop and the AG-UI client](#the-run-loop-and-the-ag-ui-client)
|
|
43
44
|
- [Stopping a run](#stopping-a-run)
|
|
@@ -103,7 +104,9 @@ Drop the element into your page and register the tools the agent may call:
|
|
|
103
104
|
|
|
104
105
|
const chat = document.querySelector("ag-ui-chat");
|
|
105
106
|
|
|
106
|
-
// Extra request headers (e.g. CSRF) sent
|
|
107
|
+
// Extra request headers (e.g. CSRF), sent with every request the element makes.
|
|
108
|
+
// For a credential that rotates, set chat.getHeaders instead - it is consulted
|
|
109
|
+
// per request. See "Authenticating requests".
|
|
107
110
|
chat.headers = { "X-CSRFToken": getCsrfToken() };
|
|
108
111
|
|
|
109
112
|
// A non-destructive tool: fills a text field with a typing animation.
|
|
@@ -138,7 +141,9 @@ Drop the element into your page and register the tools the agent may call:
|
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
That's the whole integration: an `endpoint` attribute pointing at your AG-UI server, optional
|
|
141
|
-
`headers`, and the tools you want the agent to be able to invoke in the browser.
|
|
144
|
+
`headers`, and the tools you want the agent to be able to invoke in the browser. If your API is on
|
|
145
|
+
another origin, add `credentials="include"` too; see
|
|
146
|
+
[Authenticating requests](#authenticating-requests).
|
|
142
147
|
|
|
143
148
|
### Attributes and properties
|
|
144
149
|
|
|
@@ -147,14 +152,15 @@ That's the whole integration: an `endpoint` attribute pointing at your AG-UI ser
|
|
|
147
152
|
| Attribute | Property | Notes |
|
|
148
153
|
| --- | --- | --- |
|
|
149
154
|
| `endpoint` | `endpoint` | The AG-UI endpoint URL. Required to send. Reflecting getter + setter. |
|
|
155
|
+
| `credentials` | `credentials` | Cookie policy for every request the element makes: `omit` / `same-origin` / `include`. Unset means the browser default (`same-origin`), which sends no cookies cross-origin. See [Authenticating requests](#authenticating-requests). |
|
|
150
156
|
| `title-text` | — | Header label; defaults to `"Assistant"`. The only **observed** attribute (live-updates the header). |
|
|
151
157
|
| `data-tool-display` | `toolDisplay` | Tool-call card detail: `inline` / `minimal` / `compact` / `full` (default `full`). |
|
|
152
158
|
| `data-text-animation` | — | Incoming-text reveal: `none` (default) / `fade` / `word`. |
|
|
153
159
|
| `data-prompt-chips` | — | Present (bare, or any value but `"false"`) to surface skills as chips. |
|
|
154
160
|
| `data-slash-commands` | — | Present (bare, or any value but `"false"`) to enable the `/`-command palette. |
|
|
155
161
|
| `data-skills` | — | Inline JSON skill catalog. |
|
|
156
|
-
| `data-skills-url` | — | URL of a JSON skill catalog (fetched with
|
|
157
|
-
| `data-tools-url` | — | URL of a server tool-label catalog (`[{ name, summary, description? }]`), fetched with
|
|
162
|
+
| `data-skills-url` | — | URL of a JSON skill catalog (fetched with the element's headers and cookie policy). |
|
|
163
|
+
| `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. |
|
|
158
164
|
| `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
|
|
159
165
|
| `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). |
|
|
160
166
|
| `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's 📎 picker + drag-and-drop. |
|
|
@@ -182,11 +188,15 @@ brand `<img>` or `<svg>` rather than only restyling the character:
|
|
|
182
188
|
| `density` | — | CSS-only: `comfortable` (default) / `compact`. |
|
|
183
189
|
| `placement` | — | CSS-only: `floating` (default) / `bottom-left` / `side` / `sidebar` / `full` / `page` / `embedded`. |
|
|
184
190
|
|
|
185
|
-
**Properties** (JS only, not attributes): `headers`, `allowImages`, `autoConfirm`,
|
|
191
|
+
**Properties** (JS only, not attributes): `headers`, `getHeaders`, `allowImages`, `autoConfirm`,
|
|
186
192
|
`confirmPredicate`, `askUser`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`,
|
|
187
193
|
`getPageMap`, `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`,
|
|
188
194
|
`navigationResult`, `skillContext`, `toolSummaries`, `strings`, `resolvePageTarget`, plus the
|
|
189
|
-
mirrors `endpoint` / `toolDisplay` / `collapsed`.
|
|
195
|
+
mirrors `endpoint` / `toolDisplay` / `collapsed` / `credentials`.
|
|
196
|
+
|
|
197
|
+
`headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
|
|
198
|
+
run; `getHeaders` is the one to use for a credential that rotates. See
|
|
199
|
+
[Authenticating requests](#authenticating-requests).
|
|
190
200
|
|
|
191
201
|
`allowImages` (default `false`) re-enables `<img>` in rendered assistant markdown.
|
|
192
202
|
It is off by default because a model-controlled image URL is fetched by the browser
|
|
@@ -209,7 +219,8 @@ keyboard focus and styleable via the `code-copy` part. Override its labels with
|
|
|
209
219
|
the `copyCode` / `copied` / `copyFailed` strings.
|
|
210
220
|
|
|
211
221
|
**Methods**: `registerTool`, `registerPageState`, `setSkills`, `sendMessage`, `attachFile`,
|
|
212
|
-
`appendMessage`, `newChat`, `setCollapsed`, `toggleCollapsed
|
|
222
|
+
`appendMessage`, `newChat`, `setCollapsed`, `toggleCollapsed`, `toggleTheme`, `openThreads`,
|
|
223
|
+
`openCheckpoints`, `reload`.
|
|
213
224
|
|
|
214
225
|
### Sending from your own UI
|
|
215
226
|
|
|
@@ -243,6 +254,137 @@ mock AG-UI server.
|
|
|
243
254
|
|
|
244
255
|
---
|
|
245
256
|
|
|
257
|
+
## Authenticating requests
|
|
258
|
+
|
|
259
|
+
The element talks to more than one endpoint. Beyond the AG-UI run itself, it may fetch the thread
|
|
260
|
+
index and a thread's messages, the tool-label and skill catalogs, the run index, and it may POST an
|
|
261
|
+
upload or a voice clip. **Every one of them is authenticated the same way**, by the element rather
|
|
262
|
+
than by the agent — so configuring authentication on a custom `agentFactory` authenticates the run
|
|
263
|
+
and nothing else, and the history drawer comes back empty because its request was anonymous.
|
|
264
|
+
|
|
265
|
+
| Request | Endpoint | Transport |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| The agent run | `endpoint` | `fetch` (SSE), via `agentFactory` |
|
|
268
|
+
| Thread index / a thread's messages / rename / delete | `data-threads-url` | `fetch` |
|
|
269
|
+
| Tool-label catalog | `data-tools-url` | `fetch` |
|
|
270
|
+
| Skill catalog | `data-skills-url` | `fetch` |
|
|
271
|
+
| Run index | `data-runs-url` | `fetch` |
|
|
272
|
+
| Voice transcription | `data-transcribe-url` | `fetch` |
|
|
273
|
+
| File upload | `data-attachments-url` | `XMLHttpRequest` (for progress events) |
|
|
274
|
+
|
|
275
|
+
### `headers` and `getHeaders`
|
|
276
|
+
|
|
277
|
+
`headers` is a plain record sent with every request above:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
chat.headers = { "X-CSRFToken": getCsrfToken() };
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
It is read at request time, but only an assignment changes it — so a token captured there is pinned
|
|
284
|
+
until you remember to assign again. For anything that rotates (a short-lived JWT, a re-issued CSRF
|
|
285
|
+
token) set **`getHeaders`** instead, a function consulted immediately before every request:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Because it is called per request, a token refreshed between two requests reaches the second one —
|
|
292
|
+
including mid-conversation, on the cached agent's own stream.
|
|
293
|
+
|
|
294
|
+
The two **compose**: they are merged per key, `getHeaders` winning, so a fixed header and a rotating
|
|
295
|
+
one can be configured independently and neither silently drops the other.
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
chat.headers = { "X-Client": "admin" };
|
|
299
|
+
chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
|
|
300
|
+
// every request: X-Client: admin + a freshly-read Authorization
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Cross-origin cookies (`credentials`)
|
|
304
|
+
|
|
305
|
+
If your API is on a different origin from the page — `app.example.com` calling `api.example.com`
|
|
306
|
+
counts, subdomains are cross-origin — the browser's default of `same-origin` sends **no cookies at
|
|
307
|
+
all**. The requests still go out; they arrive unauthenticated, and the server answers `401` while
|
|
308
|
+
looking perfectly configured. Set the cookie policy explicitly:
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<ag-ui-chat endpoint="https://api.example.com/agent/" credentials="include"></ag-ui-chat>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
chat.credentials = "include"; // mirrors the attribute
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
It takes `fetch`'s own three modes — `omit`, `same-origin`, `include` — applies to every request in
|
|
319
|
+
the table above, and is read per request, so a late assignment applies to everything after it.
|
|
320
|
+
Anything else is rejected where you wrote it: an unknown value assigned as a property throws, and an
|
|
321
|
+
unknown value in the attribute is reported to the console and ignored, rather than becoming a `401`
|
|
322
|
+
later on.
|
|
323
|
+
|
|
324
|
+
The server has to agree: `Access-Control-Allow-Credentials: true` and a concrete
|
|
325
|
+
`Access-Control-Allow-Origin` (the wildcard is invalid with credentials).
|
|
326
|
+
|
|
327
|
+
One asymmetry: uploads use `XMLHttpRequest` for real progress events, and its cookie switch is
|
|
328
|
+
two-state. `include` turns it on; every other value leaves it off. `omit` therefore cannot suppress
|
|
329
|
+
cookies on a *same-origin* upload — supply your own `uploadHandler` if that matters.
|
|
330
|
+
|
|
331
|
+
### Framework hosts: configure before you insert
|
|
332
|
+
|
|
333
|
+
`headers`, `getHeaders` and `credentials` are read when a request is made, so they can be set at any
|
|
334
|
+
time. Several other things are read once, while the element **connects**: `strings`, `uploadHandler`
|
|
335
|
+
and `transcribeHandler` (they decide whether the attach and voice affordances exist at all) and
|
|
336
|
+
every chrome-building `data-*` attribute — and the catalogs and thread history are requested at, or
|
|
337
|
+
just after, that same moment.
|
|
338
|
+
|
|
339
|
+
React attaches `ref`s *after* it inserts the node, which puts the canonical integration on the wrong
|
|
340
|
+
side of that boundary. Create the element, configure it, then append:
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
function Assistant() {
|
|
344
|
+
const host = useRef(null);
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
defineAgUiChat();
|
|
348
|
+
const chat = document.createElement("ag-ui-chat");
|
|
349
|
+
|
|
350
|
+
// Configure first - every one of these is read as the element connects.
|
|
351
|
+
chat.setAttribute("endpoint", "/agent/");
|
|
352
|
+
chat.setAttribute("data-threads-url", "/agent/threads/");
|
|
353
|
+
chat.credentials = "include";
|
|
354
|
+
chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
|
|
355
|
+
chat.registerTool(myTool);
|
|
356
|
+
|
|
357
|
+
// ...then insert it.
|
|
358
|
+
host.current.appendChild(chat);
|
|
359
|
+
return () => chat.remove();
|
|
360
|
+
}, []);
|
|
361
|
+
|
|
362
|
+
return <div ref={host} />;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Writing it as `<ag-ui-chat ref={...} />` in JSX and configuring in the ref callback mostly works —
|
|
367
|
+
the catalog requests are held back one microtask precisely so a ref assigned in the same commit is
|
|
368
|
+
honoured — but the thread-history request is **not** deferred (a deferred replay could land after a
|
|
369
|
+
`sendMessage()` and duplicate the transcript), so that one goes out with whatever was configured at
|
|
370
|
+
insertion.
|
|
371
|
+
|
|
372
|
+
If your credentials can only arrive later still — an awaited token, a passive effect — call
|
|
373
|
+
**`reload()`** once they land:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
const token = await auth.login();
|
|
377
|
+
chat.getHeaders = () => ({ Authorization: `Bearer ${token}` });
|
|
378
|
+
await chat.reload();
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`reload()` re-runs everything the element loads on startup (tool catalog, skills, thread history)
|
|
382
|
+
with the configuration as it then stands. It is a reload, not a merge: the in-flight run is
|
|
383
|
+
cancelled and the transcript is rebuilt from the persisted history, so call it when configuration
|
|
384
|
+
lands rather than between turns.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
246
388
|
## Core concepts
|
|
247
389
|
|
|
248
390
|
### The run loop and the AG-UI client
|
|
@@ -432,19 +574,58 @@ human-readable speed (configurable; pass small/zero durations in tests):
|
|
|
432
574
|
- `highlightThenClick(el, { highlightMs })` / `pressThenClick(el, options)` — outline/press an
|
|
433
575
|
element, pause, then click.
|
|
434
576
|
- `selectOption(el, value)` / `toggleControl(el, checked)` — animate a `<select>` / checkbox.
|
|
435
|
-
- `scrollIntoCenterView(el)`
|
|
436
|
-
|
|
437
|
-
|
|
577
|
+
- `scrollIntoCenterView(el, { settleMs })` — scrolls the element to the vertical centre and
|
|
578
|
+
resolves once the scroll has **settled**, so the ring that follows is drawn where the user is
|
|
579
|
+
looking rather than mid-glide. Awaiting is optional; the scroll is requested synchronously
|
|
580
|
+
either way. Settlement is `scrollend` where the browser has it, a short probe when nothing
|
|
581
|
+
actually moved (the element was already in view), and a 600 ms cap otherwise.
|
|
582
|
+
- `flash(el, { flashMs, color })` / `focusWithFlash(el, { flashMs, color, focus })` — ring the
|
|
583
|
+
element so the user can find it. The ring is an `outline`, not a `box-shadow`, because a shadow
|
|
584
|
+
paints outside the border box and any `overflow: hidden` ancestor sharing the element's box (a
|
|
585
|
+
card, a table cell) clips it away entirely. It holds for **1200 ms** by default and fades out
|
|
586
|
+
over the last third — a 200 ms blink is not long enough to be *found* by someone who does not
|
|
587
|
+
yet know where to look. The colour comes from the target's own `--ag-ui-accent` (so a themed
|
|
588
|
+
page is flashed in its own colour), or from `color`.
|
|
589
|
+
`flash` leaves focus alone; `focusWithFlash` moves it — see "Flash versus focus" below.
|
|
590
|
+
- `prefersReducedMotion()` — honoured by every primitive that *moves* something: the hold delays
|
|
591
|
+
in `pressThenClick` / `selectOption` / `toggleControl` collapse to instant, `scrollIntoCenterView`
|
|
592
|
+
jumps instead of gliding and settles immediately, and the flash drops its fade while still
|
|
593
|
+
holding the ring for its full duration. Reduced motion asks for no animation, not for no
|
|
594
|
+
feedback. `typeInto` and `highlightThenClick` are the exceptions: they keep their
|
|
595
|
+
explicit-duration contract, so pass `charDelayMs: 0` / `highlightMs: 0` yourself if you want
|
|
596
|
+
them instant.
|
|
438
597
|
|
|
439
598
|
The **DOM-driver** primitives ([`dom_driver.ts`](src/dom/dom_driver.ts)) compose those into the
|
|
440
599
|
operations a tool handler typically wants:
|
|
441
600
|
|
|
442
|
-
- `fillField(el, value, options)` — scroll to, focus-flash, and type into a text field.
|
|
601
|
+
- `fillField(el, value, options)` — scroll to, focus-flash, and type into a text field. The
|
|
602
|
+
flash defaults to `flashMs: 0` here: the field is about to be typed into, which is its own
|
|
603
|
+
highlight. Pass `flashMs` (and optionally `color`) to ring it first.
|
|
443
604
|
- `clickElement(el, options)` / `pressButton(el, options)` — scroll to, highlight/press, and click.
|
|
444
605
|
- `selectControl(el, value)` / `toggleCheckbox(el, checked)` — animate a `<select>` / checkbox.
|
|
445
606
|
- `setControlValue(el, value)` — set a `<select>` or checkbox without animation, dispatching
|
|
446
607
|
`input`/`change`.
|
|
447
608
|
|
|
609
|
+
Every driver primitive **awaits the scroll** before it animates. A smooth scroll is not awaitable
|
|
610
|
+
on its own, so a highlight fired straight after `scrollIntoView` could be applied and removed
|
|
611
|
+
while the element was still travelling — visible to nobody. Budget up to ~600 ms of settle time
|
|
612
|
+
per action in a browser without `scrollend`; an element already in view costs ~100 ms.
|
|
613
|
+
|
|
614
|
+
**Flash versus focus.** `focusWithFlash` does what its name says: it moves keyboard focus. That is
|
|
615
|
+
rarely what you want just to *point at* something — it takes focus off the composer, can fire blur
|
|
616
|
+
validation on whatever the user was mid-edit in, and can close an open menu. Reach for `flash(el)`
|
|
617
|
+
to highlight, and keep `focusWithFlash(el)` for the case where the agent is about to type. Either
|
|
618
|
+
way you can be explicit with `focus`:
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
await flash(el); // highlight, focus untouched
|
|
622
|
+
await focusWithFlash(el); // highlight and take focus
|
|
623
|
+
await focusWithFlash(el, { focus: false }); // same as flash(el)
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`focusWithFlash` focuses with `preventScroll: true`, so it cannot fight a smooth scroll that is
|
|
627
|
+
still in flight.
|
|
628
|
+
|
|
448
629
|
The native-setter helpers ([`native_setter.ts`](src/dom/native_setter.ts)) — `setNativeValue` /
|
|
449
630
|
`setNativeChecked` — set a control through its native prototype setter so React-controlled inputs
|
|
450
631
|
register the change.
|
|
@@ -574,10 +755,15 @@ puts it. The element probes its own geometry and reflects the result as
|
|
|
574
755
|
`data-resize-anchor` (e.g. `bottom-right` means those two edges are fixed), which
|
|
575
756
|
is what positions the grip.
|
|
576
757
|
|
|
577
|
-
A drag writes `--ag-ui-width` / `--ag-ui-height` on the host
|
|
578
|
-
properties
|
|
579
|
-
|
|
580
|
-
|
|
758
|
+
A drag writes `--ag-ui-width` / `--ag-ui-height` on the host as custom
|
|
759
|
+
properties.
|
|
760
|
+
|
|
761
|
+
⚠ **That alone does not leave placement in charge** — an inline custom property
|
|
762
|
+
still outranks a `:host([placement=…])` rule setting the same property. So the
|
|
763
|
+
component enforces the split directly: **a placement owns the axes it fixes**,
|
|
764
|
+
and a dragged or persisted size is only ever applied to the ones it leaves free.
|
|
765
|
+
Switching placement hands the owned axes back. Without that, a height dragged
|
|
766
|
+
while floating capped a docked sidebar that had asked for `100vh`.
|
|
581
767
|
|
|
582
768
|
⚠ **A host rule that sizes the element wins over both.** `ag-ui-chat { flex: 1 }`
|
|
583
769
|
stretches the panel to its container and the dragged width has no visible
|
|
@@ -914,6 +1100,11 @@ Client-side `accept` / size checks are an instant-feedback nicety — **the serv
|
|
|
914
1100
|
authoritative**. Refs persist on the message, so a restored conversation re-renders its chips.
|
|
915
1101
|
Without the attribute the affordance stays hidden and the chat is text-only.
|
|
916
1102
|
|
|
1103
|
+
The built-in handler sends the element's `headers` / `getHeaders` with every upload, and honours
|
|
1104
|
+
`credentials="include"` — with the caveat that it is an `XMLHttpRequest` (for real progress
|
|
1105
|
+
events), whose cookie switch is two-state: `include` turns it on, every other value leaves it off.
|
|
1106
|
+
See [Authenticating requests](#authenticating-requests).
|
|
1107
|
+
|
|
917
1108
|
**Swapping the upload transport.** The built-in multipart `POST` is just the default
|
|
918
1109
|
`uploadHandler`. Set your own to use a different transport — a resumable
|
|
919
1110
|
[`tus-js-client`](https://github.com/tus/tus-js-client) adapter, direct-to-S3 multipart, etc.
|
|
@@ -1033,10 +1224,10 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1033
1224
|
| `DEFAULT_UI_STRINGS` | const | The English defaults (the override floor). |
|
|
1034
1225
|
| `mergeUiStrings(overrides)` | function | Merge a partial override over the defaults. |
|
|
1035
1226
|
| `renderMarkdown(text)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
|
|
1036
|
-
| `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
|
|
1227
|
+
| `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `flash` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
|
|
1037
1228
|
| `fillField` / `clickElement` / `pressButton` / `selectControl` / `setControlValue` / `toggleCheckbox` | function | DOM-driver primitives. |
|
|
1038
1229
|
| `setNativeValue` / `setNativeChecked` | function | Set a control via its native prototype setter (React-controlled inputs). |
|
|
1039
|
-
| `TypeOptions` / `HighlightClickOptions` / `PressOptions` / `SelectOptions` / `ToggleOptions` / `FlashOptions` / `FillFieldOptions` / `TextLikeElement` | type | Primitive option shapes. |
|
|
1230
|
+
| `TypeOptions` / `HighlightClickOptions` / `PressOptions` / `SelectOptions` / `ToggleOptions` / `FlashOptions` / `ScrollOptions` / `FillFieldOptions` / `TextLikeElement` | type | Primitive option shapes. |
|
|
1040
1231
|
|
|
1041
1232
|
### Constants
|
|
1042
1233
|
|
|
@@ -1058,8 +1249,24 @@ re-export point. Internal modules import from leaf paths.
|
|
|
1058
1249
|
## Theming, density, and placement
|
|
1059
1250
|
|
|
1060
1251
|
The chat shell is styled inside its Shadow DOM and exposes a large set of `--ag-ui-*` CSS custom
|
|
1061
|
-
properties
|
|
1062
|
-
|
|
1252
|
+
properties (colors, status, surface, spacing, layout), so you theme it from outside without
|
|
1253
|
+
piercing the shadow boundary. Set them anywhere above the element and they inherit in — on the
|
|
1254
|
+
element itself, on a wrapper, or on `:root` for a whole page. The closest declaration wins, the
|
|
1255
|
+
way any inherited CSS property behaves:
|
|
1256
|
+
|
|
1257
|
+
```css
|
|
1258
|
+
/* All three work. The most specific one that applies wins. */
|
|
1259
|
+
:root { --ag-ui-accent: #4f46e5; } /* whole page */
|
|
1260
|
+
.chat-dock { --ag-ui-accent: #0f766e; } /* one region */
|
|
1261
|
+
ag-ui-chat { --ag-ui-accent: #b91c1c; } /* one widget */
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
> Until 0.20.x the defaults were declared on `:host`, which set them *on the element* — and an
|
|
1265
|
+
> element's own value beats anything inherited from an ancestor, so only the `ag-ui-chat { … }`
|
|
1266
|
+
> form did anything and the wrapper form silently did nothing. The defaults now sit behind an
|
|
1267
|
+
> internal alias, so all three forms work.
|
|
1268
|
+
|
|
1269
|
+
A few of the knobs:
|
|
1063
1270
|
|
|
1064
1271
|
```css
|
|
1065
1272
|
ag-ui-chat {
|
|
@@ -1076,6 +1283,45 @@ ag-ui-chat {
|
|
|
1076
1283
|
}
|
|
1077
1284
|
```
|
|
1078
1285
|
|
|
1286
|
+
### Where to put the variables
|
|
1287
|
+
|
|
1288
|
+
There is one vocabulary — the `--ag-ui-*` names above — and it works from any ancestor. What
|
|
1289
|
+
differs is only *which* declaration wins, and that is ordinary CSS inheritance:
|
|
1290
|
+
|
|
1291
|
+
```css
|
|
1292
|
+
/* A whole page or design-system scope. */
|
|
1293
|
+
:root { --ag-ui-accent: var(--brand-600); --ag-ui-radius: 4px; }
|
|
1294
|
+
|
|
1295
|
+
/* One region — the widget picks this up through the wrapper. */
|
|
1296
|
+
aside.support-dock { --ag-ui-accent: #0f766e; }
|
|
1297
|
+
|
|
1298
|
+
/* One widget. Beats both of the above, because it targets the element. */
|
|
1299
|
+
ag-ui-chat#support { --ag-ui-accent: #b91c1c; }
|
|
1300
|
+
|
|
1301
|
+
/* Set at runtime with el.style.setProperty(...) — an inline style beats all of the above. */
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
Two that do not work:
|
|
1305
|
+
|
|
1306
|
+
```css
|
|
1307
|
+
/* ::part() reaches structural elements, not variables — a custom property set
|
|
1308
|
+
here applies to that part's own subtree, not to the whole shell. */
|
|
1309
|
+
ag-ui-chat::part(panel) { --ag-ui-accent: #b91c1c; }
|
|
1310
|
+
|
|
1311
|
+
/* The internal --_* aliases are private and unversioned; they are renamed
|
|
1312
|
+
without notice. Always set the public --ag-ui-* name. */
|
|
1313
|
+
ag-ui-chat { --_accent: #b91c1c; }
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
The preset attributes below sit *underneath* anything you declare: a `theme="dark"` widget still
|
|
1317
|
+
honours an explicit `--ag-ui-bg` from your CSS, so you can adopt a preset and correct one token
|
|
1318
|
+
rather than re-declaring the whole palette.
|
|
1319
|
+
|
|
1320
|
+
`--ag-ui-accent` reaches further than the widget: it also colours the rings the DOM driver draws
|
|
1321
|
+
on **your** page. Each primitive reads it from the computed style of the element it is about to
|
|
1322
|
+
touch, so setting it on `:root` (or on any ancestor of the elements the agent drives) themes the
|
|
1323
|
+
highlights too. Without it they fall back to the package indigo.
|
|
1324
|
+
|
|
1079
1325
|
For the common cases there are three CSS-reactive **preset attributes** (no JS API), so you don't
|
|
1080
1326
|
have to hand-tune the variables:
|
|
1081
1327
|
|
|
@@ -1132,6 +1378,26 @@ the skills UI (`skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skil
|
|
|
1132
1378
|
`drawer-row-delete`, `drawer-rename-input`, `drawer-confirm`, `drawer-confirm-label`,
|
|
1133
1379
|
`drawer-confirm-yes`, `drawer-confirm-no`).
|
|
1134
1380
|
|
|
1381
|
+
> **Hiding `::part(header)` hides the controls inside it.** The history, checkpoints, new-chat,
|
|
1382
|
+
> theme and collapse buttons are all children of the header, so a host that renders its own title
|
|
1383
|
+
> bar and does `ag-ui-chat::part(header) { display: none }` loses thread switching entirely. Every
|
|
1384
|
+
> one of them has an imperative equivalent, so your own chrome can drive them:
|
|
1385
|
+
>
|
|
1386
|
+
> | Control | Method |
|
|
1387
|
+
> | --- | --- |
|
|
1388
|
+
> | History drawer | `chat.openThreads()` |
|
|
1389
|
+
> | Checkpoints panel | `chat.openCheckpoints()` |
|
|
1390
|
+
> | New chat | `chat.newChat()` |
|
|
1391
|
+
> | Collapse | `chat.toggleCollapsed()` / `chat.setCollapsed(bool)` |
|
|
1392
|
+
> | Theme toggle | `chat.toggleTheme()` |
|
|
1393
|
+
>
|
|
1394
|
+
> ```js
|
|
1395
|
+
> myHeaderButton.onclick = () => chat.openThreads();
|
|
1396
|
+
> ```
|
|
1397
|
+
>
|
|
1398
|
+
> The built-in buttons call exactly these methods, so the two routes cannot drift. If you only want
|
|
1399
|
+
> to restyle the header, prefer `::part(header)` styling or the `header-actions` slot over hiding it.
|
|
1400
|
+
|
|
1135
1401
|
Coarse **slots** let you replace whole regions with your own markup (project light-DOM children
|
|
1136
1402
|
with a matching `slot=`):
|
|
1137
1403
|
|
|
@@ -1155,7 +1421,7 @@ with a matching `slot=`):
|
|
|
1155
1421
|
Give the header a brand icon with either the `icon` slot (any markup) or the `data-icon-url`
|
|
1156
1422
|
convenience attribute (an `<img>`); the slot wins when both are set, and with neither the header
|
|
1157
1423
|
stays icon-less. The same icon seam feeds the collapsed sidebar rail. Size it via
|
|
1158
|
-
`--ag-ui-icon-size` (default `22px`).
|
|
1424
|
+
`--ag-ui-icon-size` (default `22px`) and round it with `--ag-ui-icon-radius` (default `4px`).
|
|
1159
1425
|
|
|
1160
1426
|
```html
|
|
1161
1427
|
<ag-ui-chat endpoint="/agent/" data-icon-url="/logo.png"></ag-ui-chat>
|