@artooi/ag-ui-web-component 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/README.md +404 -28
  3. package/dist/ag-ui-web-component.bundle.js +781 -451
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +29 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +93 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/create_http_agent.d.ts +9 -0
  10. package/dist/core/create_http_agent.d.ts.map +1 -1
  11. package/dist/core/remote_conversation_store.d.ts +8 -1
  12. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  13. package/dist/core/run_index.d.ts +8 -1
  14. package/dist/core/run_index.d.ts.map +1 -1
  15. package/dist/core/transcribe_audio.d.ts +7 -0
  16. package/dist/core/transcribe_audio.d.ts.map +1 -1
  17. package/dist/core/upload_attachment.d.ts +9 -0
  18. package/dist/core/upload_attachment.d.ts.map +1 -1
  19. package/dist/core/utils.d.ts +12 -0
  20. package/dist/core/utils.d.ts.map +1 -0
  21. package/dist/dom/animations.d.ts +51 -11
  22. package/dist/dom/animations.d.ts.map +1 -1
  23. package/dist/dom/dom_driver.d.ts +12 -7
  24. package/dist/dom/dom_driver.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1275 -520
  28. package/dist/index.js.map +3 -3
  29. package/dist/ui/styles.d.ts +1 -1
  30. package/dist/ui/styles.d.ts.map +1 -1
  31. package/dist/ui/ui_strings.d.ts +8 -1
  32. package/dist/ui/ui_strings.d.ts.map +1 -1
  33. package/dist/ui/voice_input.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/constants.ts +35 -0
  36. package/src/core/ag_ui_chat.ts +452 -55
  37. package/src/core/create_http_agent.ts +14 -3
  38. package/src/core/remote_conversation_store.ts +25 -6
  39. package/src/core/run_index.ts +27 -5
  40. package/src/core/transcribe_audio.ts +20 -5
  41. package/src/core/upload_attachment.ts +13 -0
  42. package/src/core/utils.ts +18 -0
  43. package/src/dom/animations.ts +175 -31
  44. package/src/dom/dom_driver.ts +18 -12
  45. package/src/index.ts +4 -0
  46. package/src/ui/styles.ts +751 -421
  47. package/src/ui/ui_strings.ts +9 -1
  48. package/src/ui/voice_input.ts +7 -1
  49. package/src/version.ts +1 -1
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)
@@ -46,6 +47,7 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
46
47
  - [DOM-driver and animation primitives](#dom-driver-and-animation-primitives)
47
48
  - [Page-action tools](#page-action-tools)
48
49
  - [New chat and collapse](#new-chat-and-collapse)
50
+ - [Collapsing to the launcher](#collapsing-to-the-launcher)
49
51
  - [Tool-call display modes](#tool-call-display-modes)
50
52
  - [Markdown rendering](#markdown-rendering)
51
53
  - [Run notices: compaction and agent skills](#run-notices-compaction-and-agent-skills)
@@ -103,7 +105,9 @@ Drop the element into your page and register the tools the agent may call:
103
105
 
104
106
  const chat = document.querySelector("ag-ui-chat");
105
107
 
106
- // Extra request headers (e.g. CSRF) sent to the AG-UI endpoint.
108
+ // Extra request headers (e.g. CSRF), sent with every request the element makes.
109
+ // For a credential that rotates, set chat.getHeaders instead - it is consulted
110
+ // per request. See "Authenticating requests".
107
111
  chat.headers = { "X-CSRFToken": getCsrfToken() };
108
112
 
109
113
  // A non-destructive tool: fills a text field with a typing animation.
@@ -138,7 +142,9 @@ Drop the element into your page and register the tools the agent may call:
138
142
  ```
139
143
 
140
144
  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.
145
+ `headers`, and the tools you want the agent to be able to invoke in the browser. If your API is on
146
+ another origin, add `credentials="include"` too; see
147
+ [Authenticating requests](#authenticating-requests).
142
148
 
143
149
  ### Attributes and properties
144
150
 
@@ -147,27 +153,31 @@ That's the whole integration: an `endpoint` attribute pointing at your AG-UI ser
147
153
  | Attribute | Property | Notes |
148
154
  | --- | --- | --- |
149
155
  | `endpoint` | `endpoint` | The AG-UI endpoint URL. Required to send. Reflecting getter + setter. |
156
+ | `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
157
  | `title-text` | — | Header label; defaults to `"Assistant"`. The only **observed** attribute (live-updates the header). |
151
158
  | `data-tool-display` | `toolDisplay` | Tool-call card detail: `inline` / `minimal` / `compact` / `full` (default `full`). |
152
159
  | `data-text-animation` | — | Incoming-text reveal: `none` (default) / `fade` / `word`. |
153
160
  | `data-prompt-chips` | — | Present (bare, or any value but `"false"`) to surface skills as chips. |
154
161
  | `data-slash-commands` | — | Present (bare, or any value but `"false"`) to enable the `/`-command palette. |
155
162
  | `data-skills` | — | Inline JSON skill catalog. |
156
- | `data-skills-url` | — | URL of a JSON skill catalog (fetched with `headers`). |
157
- | `data-tools-url` | — | URL of a server tool-label catalog (`[{ name, summary, description? }]`), fetched with `headers`; labels tool-call cards for server-side tools. |
163
+ | `data-skills-url` | — | URL of a JSON skill catalog (fetched with the element's headers and cookie policy). |
164
+ | `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
165
  | `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
159
166
  | `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
- | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's 📎 picker + drag-and-drop. |
167
+ | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker + drag-and-drop. |
161
168
  | `data-attachment-accept` | — | `<input accept>` list for client-side type filtering (e.g. `image/*,.pdf`). The server stays authoritative. |
162
169
  | `data-attachment-max-bytes` | — | Client-side upload size cap in bytes (default 10 MiB; `0` disables). The server stays authoritative. |
163
- | `data-transcribe-url` | — | URL of the voice-transcription endpoint (django-ag-ui's `TranscribeView`); reveals the composer's 🎤 mic button. See [Voice input](#voice-input). |
170
+ | `data-transcribe-url` | — | URL of the voice-transcription endpoint (django-ag-ui's `TranscribeView`); reveals the composer's mic button. See [Voice input](#voice-input). |
164
171
  | `data-theme-toggle` | — | Boolean: show a built-in header light⇄dark toggle (persists per tab). Off by default. See [Theme toggle](#theme-toggle). |
165
172
  | `data-strings` | `strings` | Partial JSON override of the UI string table (localization). The property wins key-by-key over the attribute; see [Internationalization](#internationalization-i18n). |
166
- | `data-icon-url` | — | Header (and sidebar-rail) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
173
+ | `data-icon-url` | — | Header (and launcher) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
174
+ | `data-launcher-icon-url` | — | Icon image URL for the collapsed launcher only, when it should differ from the header's. Falls back to `data-icon-url`; a slotted `slot="launcher"` wins over both. |
175
+ | `data-unread-badge` | — | **On by default.** `="false"` hides the launcher's unread badge; the count and the `ag-ui-unread` event keep running. See [Collapsing to the launcher](#collapsing-to-the-launcher). |
167
176
 
168
177
  Each header control also takes its own icon slot — `icon-history`, `icon-checkpoints`,
169
178
  `icon-new`, `icon-collapse` — with the built-in glyph as the fallback, so a host can project a
170
- brand `<img>` or `<svg>` rather than only restyling the character:
179
+ brand `<img>` or `<svg>` rather than only restyling the character. The composer's glyphs work the
180
+ same way: `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`.
171
181
 
172
182
  ```html
173
183
  <ag-ui-chat endpoint="/agent/">
@@ -177,16 +187,20 @@ brand `<img>` or `<svg>` rather than only restyling the character:
177
187
  | `data-page-actions` | — | Opt-in built-in page-action tools: a comma list of `scroll` / `drag` (e.g. `"scroll,drag"`). See [Page-action tools](#page-action-tools). |
178
188
  | `data-side` | — | CSS-only, for `placement="sidebar"`: which edge it docks to — `right` (default) / `left`. |
179
189
  | `data-answer-well` | — | CSS-only boolean: box each assistant turn (its text, tool cards, and thinking) in one bordered "well". Off by default. See [The answer well](#the-answer-well). |
180
- | `collapsed` | `collapsed` | Reflected boolean; collapses the widget (to a rail under `placement="sidebar"`). Persisted per-tab in `sessionStorage`. |
190
+ | `collapsed` | `collapsed` | Reflected boolean; collapses the widget to its [launcher](#collapsing-to-the-launcher) (a rail under `placement="sidebar"`, the header bar under `embedded` / `page`). Persisted per-tab in `sessionStorage`. |
181
191
  | `theme` | — | CSS-only: `light` (default) / `dark` / `auto` / `code`. |
182
192
  | `density` | — | CSS-only: `comfortable` (default) / `compact`. |
183
193
  | `placement` | — | CSS-only: `floating` (default) / `bottom-left` / `side` / `sidebar` / `full` / `page` / `embedded`. |
184
194
 
185
- **Properties** (JS only, not attributes): `headers`, `allowImages`, `autoConfirm`,
195
+ **Properties** (JS only, not attributes): `headers`, `getHeaders`, `allowImages`, `autoConfirm`,
186
196
  `confirmPredicate`, `askUser`, `agentFactory`, `getTools`, `getContext`, `routeMap`, `navigate`,
187
197
  `getPageMap`, `autoInjectPageMap`, `conversationStore`, `uploadHandler`, `transcribeHandler`,
188
198
  `navigationResult`, `skillContext`, `toolSummaries`, `strings`, `resolvePageTarget`, plus the
189
- mirrors `endpoint` / `toolDisplay` / `collapsed`.
199
+ mirrors `endpoint` / `toolDisplay` / `collapsed` / `credentials`.
200
+
201
+ `headers` and `getHeaders` authenticate **every** request the element makes, not only the agent
202
+ run; `getHeaders` is the one to use for a credential that rotates. See
203
+ [Authenticating requests](#authenticating-requests).
190
204
 
191
205
  `allowImages` (default `false`) re-enables `<img>` in rendered assistant markdown.
192
206
  It is off by default because a model-controlled image URL is fetched by the browser
@@ -209,7 +223,8 @@ keyboard focus and styleable via the `code-copy` part. Override its labels with
209
223
  the `copyCode` / `copied` / `copyFailed` strings.
210
224
 
211
225
  **Methods**: `registerTool`, `registerPageState`, `setSkills`, `sendMessage`, `attachFile`,
212
- `appendMessage`, `newChat`, `setCollapsed`, `toggleCollapsed`.
226
+ `appendMessage`, `newChat`, `setCollapsed`, `toggleCollapsed`, `toggleTheme`, `openThreads`,
227
+ `openCheckpoints`, `reload`.
213
228
 
214
229
  ### Sending from your own UI
215
230
 
@@ -243,6 +258,137 @@ mock AG-UI server.
243
258
 
244
259
  ---
245
260
 
261
+ ## Authenticating requests
262
+
263
+ The element talks to more than one endpoint. Beyond the AG-UI run itself, it may fetch the thread
264
+ index and a thread's messages, the tool-label and skill catalogs, the run index, and it may POST an
265
+ upload or a voice clip. **Every one of them is authenticated the same way**, by the element rather
266
+ than by the agent — so configuring authentication on a custom `agentFactory` authenticates the run
267
+ and nothing else, and the history drawer comes back empty because its request was anonymous.
268
+
269
+ | Request | Endpoint | Transport |
270
+ | --- | --- | --- |
271
+ | The agent run | `endpoint` | `fetch` (SSE), via `agentFactory` |
272
+ | Thread index / a thread's messages / rename / delete | `data-threads-url` | `fetch` |
273
+ | Tool-label catalog | `data-tools-url` | `fetch` |
274
+ | Skill catalog | `data-skills-url` | `fetch` |
275
+ | Run index | `data-runs-url` | `fetch` |
276
+ | Voice transcription | `data-transcribe-url` | `fetch` |
277
+ | File upload | `data-attachments-url` | `XMLHttpRequest` (for progress events) |
278
+
279
+ ### `headers` and `getHeaders`
280
+
281
+ `headers` is a plain record sent with every request above:
282
+
283
+ ```js
284
+ chat.headers = { "X-CSRFToken": getCsrfToken() };
285
+ ```
286
+
287
+ It is read at request time, but only an assignment changes it — so a token captured there is pinned
288
+ until you remember to assign again. For anything that rotates (a short-lived JWT, a re-issued CSRF
289
+ token) set **`getHeaders`** instead, a function consulted immediately before every request:
290
+
291
+ ```js
292
+ chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
293
+ ```
294
+
295
+ Because it is called per request, a token refreshed between two requests reaches the second one —
296
+ including mid-conversation, on the cached agent's own stream.
297
+
298
+ The two **compose**: they are merged per key, `getHeaders` winning, so a fixed header and a rotating
299
+ one can be configured independently and neither silently drops the other.
300
+
301
+ ```js
302
+ chat.headers = { "X-Client": "admin" };
303
+ chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
304
+ // every request: X-Client: admin + a freshly-read Authorization
305
+ ```
306
+
307
+ ### Cross-origin cookies (`credentials`)
308
+
309
+ If your API is on a different origin from the page — `app.example.com` calling `api.example.com`
310
+ counts, subdomains are cross-origin — the browser's default of `same-origin` sends **no cookies at
311
+ all**. The requests still go out; they arrive unauthenticated, and the server answers `401` while
312
+ looking perfectly configured. Set the cookie policy explicitly:
313
+
314
+ ```html
315
+ <ag-ui-chat endpoint="https://api.example.com/agent/" credentials="include"></ag-ui-chat>
316
+ ```
317
+
318
+ ```js
319
+ chat.credentials = "include"; // mirrors the attribute
320
+ ```
321
+
322
+ It takes `fetch`'s own three modes — `omit`, `same-origin`, `include` — applies to every request in
323
+ the table above, and is read per request, so a late assignment applies to everything after it.
324
+ Anything else is rejected where you wrote it: an unknown value assigned as a property throws, and an
325
+ unknown value in the attribute is reported to the console and ignored, rather than becoming a `401`
326
+ later on.
327
+
328
+ The server has to agree: `Access-Control-Allow-Credentials: true` and a concrete
329
+ `Access-Control-Allow-Origin` (the wildcard is invalid with credentials).
330
+
331
+ One asymmetry: uploads use `XMLHttpRequest` for real progress events, and its cookie switch is
332
+ two-state. `include` turns it on; every other value leaves it off. `omit` therefore cannot suppress
333
+ cookies on a *same-origin* upload — supply your own `uploadHandler` if that matters.
334
+
335
+ ### Framework hosts: configure before you insert
336
+
337
+ `headers`, `getHeaders` and `credentials` are read when a request is made, so they can be set at any
338
+ time. Several other things are read once, while the element **connects**: `strings`, `uploadHandler`
339
+ and `transcribeHandler` (they decide whether the attach and voice affordances exist at all) and
340
+ every chrome-building `data-*` attribute — and the catalogs and thread history are requested at, or
341
+ just after, that same moment.
342
+
343
+ React attaches `ref`s *after* it inserts the node, which puts the canonical integration on the wrong
344
+ side of that boundary. Create the element, configure it, then append:
345
+
346
+ ```jsx
347
+ function Assistant() {
348
+ const host = useRef(null);
349
+
350
+ useEffect(() => {
351
+ defineAgUiChat();
352
+ const chat = document.createElement("ag-ui-chat");
353
+
354
+ // Configure first - every one of these is read as the element connects.
355
+ chat.setAttribute("endpoint", "/agent/");
356
+ chat.setAttribute("data-threads-url", "/agent/threads/");
357
+ chat.credentials = "include";
358
+ chat.getHeaders = () => ({ Authorization: `Bearer ${auth.accessToken()}` });
359
+ chat.registerTool(myTool);
360
+
361
+ // ...then insert it.
362
+ host.current.appendChild(chat);
363
+ return () => chat.remove();
364
+ }, []);
365
+
366
+ return <div ref={host} />;
367
+ }
368
+ ```
369
+
370
+ Writing it as `<ag-ui-chat ref={...} />` in JSX and configuring in the ref callback mostly works —
371
+ the catalog requests are held back one microtask precisely so a ref assigned in the same commit is
372
+ honoured — but the thread-history request is **not** deferred (a deferred replay could land after a
373
+ `sendMessage()` and duplicate the transcript), so that one goes out with whatever was configured at
374
+ insertion.
375
+
376
+ If your credentials can only arrive later still — an awaited token, a passive effect — call
377
+ **`reload()`** once they land:
378
+
379
+ ```js
380
+ const token = await auth.login();
381
+ chat.getHeaders = () => ({ Authorization: `Bearer ${token}` });
382
+ await chat.reload();
383
+ ```
384
+
385
+ `reload()` re-runs everything the element loads on startup (tool catalog, skills, thread history)
386
+ with the configuration as it then stands. It is a reload, not a merge: the in-flight run is
387
+ cancelled and the transcript is rebuilt from the persisted history, so call it when configuration
388
+ lands rather than between turns.
389
+
390
+ ---
391
+
246
392
  ## Core concepts
247
393
 
248
394
  ### The run loop and the AG-UI client
@@ -432,19 +578,58 @@ human-readable speed (configurable; pass small/zero durations in tests):
432
578
  - `highlightThenClick(el, { highlightMs })` / `pressThenClick(el, options)` — outline/press an
433
579
  element, pause, then click.
434
580
  - `selectOption(el, value)` / `toggleControl(el, checked)` — animate a `<select>` / checkbox.
435
- - `scrollIntoCenterView(el)` / `focusWithFlash(el, { flashMs })`.
436
- - `prefersReducedMotion()` honoured throughout so animations collapse to instant when the user
437
- asks for reduced motion.
581
+ - `scrollIntoCenterView(el, { settleMs })` scrolls the element to the vertical centre and
582
+ resolves once the scroll has **settled**, so the ring that follows is drawn where the user is
583
+ looking rather than mid-glide. Awaiting is optional; the scroll is requested synchronously
584
+ either way. Settlement is `scrollend` where the browser has it, a short probe when nothing
585
+ actually moved (the element was already in view), and a 600 ms cap otherwise.
586
+ - `flash(el, { flashMs, color })` / `focusWithFlash(el, { flashMs, color, focus })` — ring the
587
+ element so the user can find it. The ring is an `outline`, not a `box-shadow`, because a shadow
588
+ paints outside the border box and any `overflow: hidden` ancestor sharing the element's box (a
589
+ card, a table cell) clips it away entirely. It holds for **1200 ms** by default and fades out
590
+ over the last third — a 200 ms blink is not long enough to be *found* by someone who does not
591
+ yet know where to look. The colour comes from the target's own `--ag-ui-accent` (so a themed
592
+ page is flashed in its own colour), or from `color`.
593
+ `flash` leaves focus alone; `focusWithFlash` moves it — see "Flash versus focus" below.
594
+ - `prefersReducedMotion()` — honoured by every primitive that *moves* something: the hold delays
595
+ in `pressThenClick` / `selectOption` / `toggleControl` collapse to instant, `scrollIntoCenterView`
596
+ jumps instead of gliding and settles immediately, and the flash drops its fade while still
597
+ holding the ring for its full duration. Reduced motion asks for no animation, not for no
598
+ feedback. `typeInto` and `highlightThenClick` are the exceptions: they keep their
599
+ explicit-duration contract, so pass `charDelayMs: 0` / `highlightMs: 0` yourself if you want
600
+ them instant.
438
601
 
439
602
  The **DOM-driver** primitives ([`dom_driver.ts`](src/dom/dom_driver.ts)) compose those into the
440
603
  operations a tool handler typically wants:
441
604
 
442
- - `fillField(el, value, options)` — scroll to, focus-flash, and type into a text field.
605
+ - `fillField(el, value, options)` — scroll to, focus-flash, and type into a text field. The
606
+ flash defaults to `flashMs: 0` here: the field is about to be typed into, which is its own
607
+ highlight. Pass `flashMs` (and optionally `color`) to ring it first.
443
608
  - `clickElement(el, options)` / `pressButton(el, options)` — scroll to, highlight/press, and click.
444
609
  - `selectControl(el, value)` / `toggleCheckbox(el, checked)` — animate a `<select>` / checkbox.
445
610
  - `setControlValue(el, value)` — set a `<select>` or checkbox without animation, dispatching
446
611
  `input`/`change`.
447
612
 
613
+ Every driver primitive **awaits the scroll** before it animates. A smooth scroll is not awaitable
614
+ on its own, so a highlight fired straight after `scrollIntoView` could be applied and removed
615
+ while the element was still travelling — visible to nobody. Budget up to ~600 ms of settle time
616
+ per action in a browser without `scrollend`; an element already in view costs ~100 ms.
617
+
618
+ **Flash versus focus.** `focusWithFlash` does what its name says: it moves keyboard focus. That is
619
+ rarely what you want just to *point at* something — it takes focus off the composer, can fire blur
620
+ validation on whatever the user was mid-edit in, and can close an open menu. Reach for `flash(el)`
621
+ to highlight, and keep `focusWithFlash(el)` for the case where the agent is about to type. Either
622
+ way you can be explicit with `focus`:
623
+
624
+ ```js
625
+ await flash(el); // highlight, focus untouched
626
+ await focusWithFlash(el); // highlight and take focus
627
+ await focusWithFlash(el, { focus: false }); // same as flash(el)
628
+ ```
629
+
630
+ `focusWithFlash` focuses with `preventScroll: true`, so it cannot fight a smooth scroll that is
631
+ still in flight.
632
+
448
633
  The native-setter helpers ([`native_setter.ts`](src/dom/native_setter.ts)) — `setNativeValue` /
449
634
  `setNativeChecked` — set a control through its native prototype setter so React-controlled inputs
450
635
  register the change.
@@ -505,6 +690,71 @@ chat.toggleCollapsed();
505
690
  chat.addEventListener("ag-ui-toggle", (e) => console.log(e.detail.collapsed));
506
691
  ```
507
692
 
693
+ ### Collapsing to the launcher
694
+
695
+ A collapsed widget shrinks to a round **floating launcher** in the corner it already occupies: the
696
+ panel scales down into it and fades, the launcher grows out of the same point, and clicking the
697
+ launcher reverses it. Only `transform` and `opacity` animate, so the motion is compositor-only and
698
+ never reflows your page. Two placements collapse to something else instead — `sidebar` slides out
699
+ to its [edge rail](#sidebar-placement), and `embedded` / `page` keep the header bar, since one is
700
+ laid out by your page and the other is a full-screen route.
701
+
702
+ The launcher's mark comes from the same seam as the header icon, most specific first: a slotted
703
+ `slot="launcher"` child, then `data-launcher-icon-url`, then `data-icon-url`, then the built-in
704
+ speech bubble.
705
+
706
+ ```html
707
+ <ag-ui-chat endpoint="/agent/" data-launcher-icon-url="/mark.svg"></ag-ui-chat>
708
+
709
+ <!-- or any markup at all -->
710
+ <ag-ui-chat endpoint="/agent/">
711
+ <svg slot="launcher" width="26" height="26"><!-- ... --></svg>
712
+ </ag-ui-chat>
713
+ ```
714
+
715
+ ```css
716
+ ag-ui-chat {
717
+ --ag-ui-launcher-size: 56px;
718
+ --ag-ui-launcher-radius: 50%; /* 12px for a squircle */
719
+ --ag-ui-launcher-bg: #14532d; /* defaults to the header background */
720
+ --ag-ui-launcher-fg: #ffffff;
721
+ --ag-ui-launcher-icon-size: 26px;
722
+ --ag-ui-launcher-inset: auto 0 0 auto; /* which corner of the widget's box */
723
+ }
724
+ ```
725
+
726
+ > **The collapsed host keeps its box.** Animating the element's own width and height would animate
727
+ > layout; instead the box stays put with `pointer-events: none`, and the launcher takes the clicks.
728
+ > A host measuring `getBoundingClientRect()` on a collapsed widget still sees the panel's
729
+ > footprint — nothing there paints or takes input.
730
+
731
+ #### The unread badge
732
+
733
+ A collapsed widget is the one state where an answer can arrive with nothing on screen to say so,
734
+ so the launcher carries a count of the answers that finished while it was closed (capped at `9+`).
735
+ Expanding — or `newChat()` — marks them read. It is the only affordance here that is **on by
736
+ default**; `data-unread-badge="false"` turns the badge off.
737
+
738
+ The count is also the launcher's accessible name (`Expand — 2 unread`, from the `expandUnread`
739
+ string), because a coloured dot says nothing to a screen reader.
740
+
741
+ ```js
742
+ chat.unread; // 2
743
+
744
+ // Every change, whether or not the badge renders it — so a host that hides the
745
+ // badge can put the count in its own chrome.
746
+ chat.addEventListener("ag-ui-unread", (e) => setDockBadge(e.detail.unread));
747
+ ```
748
+
749
+ ```css
750
+ ag-ui-chat {
751
+ --ag-ui-badge-bg: #b91c1c; /* defaults to --ag-ui-danger */
752
+ --ag-ui-badge-fg: #ffffff;
753
+ --ag-ui-badge-size: 18px;
754
+ --ag-ui-badge-font-size: 11px;
755
+ }
756
+ ```
757
+
508
758
  ---
509
759
 
510
760
  ## Tool-call display modes
@@ -919,6 +1169,11 @@ Client-side `accept` / size checks are an instant-feedback nicety — **the serv
919
1169
  authoritative**. Refs persist on the message, so a restored conversation re-renders its chips.
920
1170
  Without the attribute the affordance stays hidden and the chat is text-only.
921
1171
 
1172
+ The built-in handler sends the element's `headers` / `getHeaders` with every upload, and honours
1173
+ `credentials="include"` — with the caveat that it is an `XMLHttpRequest` (for real progress
1174
+ events), whose cookie switch is two-state: `include` turns it on, every other value leaves it off.
1175
+ See [Authenticating requests](#authenticating-requests).
1176
+
922
1177
  **Swapping the upload transport.** The built-in multipart `POST` is just the default
923
1178
  `uploadHandler`. Set your own to use a different transport — a resumable
924
1179
  [`tus-js-client`](https://github.com/tus/tus-js-client) adapter, direct-to-S3 multipart, etc.
@@ -963,6 +1218,7 @@ re-export point. Internal modules import from leaf paths.
963
1218
  | `MessageRole` | type | Role of a rendered chat message. |
964
1219
  | `SubmitDetail` | type | `detail` shape of the submit event. |
965
1220
  | `ToggleDetail` | type | `detail` shape of the `ag-ui-toggle` event (`{ collapsed }`). |
1221
+ | `UnreadDetail` | type | `detail` shape of the `ag-ui-unread` event (`{ unread }`). |
966
1222
 
967
1223
  ### AG-UI client & agent
968
1224
 
@@ -1038,10 +1294,10 @@ re-export point. Internal modules import from leaf paths.
1038
1294
  | `DEFAULT_UI_STRINGS` | const | The English defaults (the override floor). |
1039
1295
  | `mergeUiStrings(overrides)` | function | Merge a partial override over the defaults. |
1040
1296
  | `renderMarkdown(text)` | function | Render sanitized markdown/HTML (marked + DOMPurify). |
1041
- | `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
1297
+ | `typeInto` / `highlightThenClick` / `pressThenClick` / `selectOption` / `toggleControl` / `scrollIntoCenterView` / `flash` / `focusWithFlash` / `prefersReducedMotion` | function | Animation primitives. |
1042
1298
  | `fillField` / `clickElement` / `pressButton` / `selectControl` / `setControlValue` / `toggleCheckbox` | function | DOM-driver primitives. |
1043
1299
  | `setNativeValue` / `setNativeChecked` | function | Set a control via its native prototype setter (React-controlled inputs). |
1044
- | `TypeOptions` / `HighlightClickOptions` / `PressOptions` / `SelectOptions` / `ToggleOptions` / `FlashOptions` / `FillFieldOptions` / `TextLikeElement` | type | Primitive option shapes. |
1300
+ | `TypeOptions` / `HighlightClickOptions` / `PressOptions` / `SelectOptions` / `ToggleOptions` / `FlashOptions` / `ScrollOptions` / `FillFieldOptions` / `TextLikeElement` | type | Primitive option shapes. |
1045
1301
 
1046
1302
  ### Constants
1047
1303
 
@@ -1050,6 +1306,7 @@ re-export point. Internal modules import from leaf paths.
1050
1306
  | `ELEMENT_TAG` | The registered tag name (`ag-ui-chat`). |
1051
1307
  | `SUBMIT_EVENT` | The submit CustomEvent name. |
1052
1308
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1309
+ | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1053
1310
  | `MESSAGE_ROLE` | Message role constants. |
1054
1311
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1055
1312
  | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
@@ -1063,8 +1320,24 @@ re-export point. Internal modules import from leaf paths.
1063
1320
  ## Theming, density, and placement
1064
1321
 
1065
1322
  The chat shell is styled inside its Shadow DOM and exposes a large set of `--ag-ui-*` CSS custom
1066
- properties on `:host` (colors, status, surface, spacing, layout), so you theme it from outside
1067
- without piercing the shadow boundary. A few of the knobs:
1323
+ properties (colors, status, surface, spacing, layout), so you theme it from outside without
1324
+ piercing the shadow boundary. Set them anywhere above the element and they inherit in — on the
1325
+ element itself, on a wrapper, or on `:root` for a whole page. The closest declaration wins, the
1326
+ way any inherited CSS property behaves:
1327
+
1328
+ ```css
1329
+ /* All three work. The most specific one that applies wins. */
1330
+ :root { --ag-ui-accent: #4f46e5; } /* whole page */
1331
+ .chat-dock { --ag-ui-accent: #0f766e; } /* one region */
1332
+ ag-ui-chat { --ag-ui-accent: #b91c1c; } /* one widget */
1333
+ ```
1334
+
1335
+ > Until 0.20.x the defaults were declared on `:host`, which set them *on the element* — and an
1336
+ > element's own value beats anything inherited from an ancestor, so only the `ag-ui-chat { … }`
1337
+ > form did anything and the wrapper form silently did nothing. The defaults now sit behind an
1338
+ > internal alias, so all three forms work.
1339
+
1340
+ A few of the knobs:
1068
1341
 
1069
1342
  ```css
1070
1343
  ag-ui-chat {
@@ -1081,6 +1354,45 @@ ag-ui-chat {
1081
1354
  }
1082
1355
  ```
1083
1356
 
1357
+ ### Where to put the variables
1358
+
1359
+ There is one vocabulary — the `--ag-ui-*` names above — and it works from any ancestor. What
1360
+ differs is only *which* declaration wins, and that is ordinary CSS inheritance:
1361
+
1362
+ ```css
1363
+ /* A whole page or design-system scope. */
1364
+ :root { --ag-ui-accent: var(--brand-600); --ag-ui-radius: 4px; }
1365
+
1366
+ /* One region — the widget picks this up through the wrapper. */
1367
+ aside.support-dock { --ag-ui-accent: #0f766e; }
1368
+
1369
+ /* One widget. Beats both of the above, because it targets the element. */
1370
+ ag-ui-chat#support { --ag-ui-accent: #b91c1c; }
1371
+
1372
+ /* Set at runtime with el.style.setProperty(...) — an inline style beats all of the above. */
1373
+ ```
1374
+
1375
+ Two that do not work:
1376
+
1377
+ ```css
1378
+ /* ::part() reaches structural elements, not variables — a custom property set
1379
+ here applies to that part's own subtree, not to the whole shell. */
1380
+ ag-ui-chat::part(panel) { --ag-ui-accent: #b91c1c; }
1381
+
1382
+ /* The internal --_* aliases are private and unversioned; they are renamed
1383
+ without notice. Always set the public --ag-ui-* name. */
1384
+ ag-ui-chat { --_accent: #b91c1c; }
1385
+ ```
1386
+
1387
+ The preset attributes below sit *underneath* anything you declare: a `theme="dark"` widget still
1388
+ honours an explicit `--ag-ui-bg` from your CSS, so you can adopt a preset and correct one token
1389
+ rather than re-declaring the whole palette.
1390
+
1391
+ `--ag-ui-accent` reaches further than the widget: it also colours the rings the DOM driver draws
1392
+ on **your** page. Each primitive reads it from the computed style of the element it is about to
1393
+ touch, so setting it on `:root` (or on any ancestor of the elements the agent drives) themes the
1394
+ highlights too. Without it they fall back to the package indigo.
1395
+
1084
1396
  For the common cases there are three CSS-reactive **preset attributes** (no JS API), so you don't
1085
1397
  have to hand-tune the variables:
1086
1398
 
@@ -1123,20 +1435,40 @@ Available parts: `panel`, `header`, `title`, `icon`, `header-controls`, `header-
1123
1435
  `-args` / `-actions` / `-button` / `-cancel` / `-confirm`),
1124
1436
  `approval` (plus `approval-body` / `-actions` / `-button` / `-approve` / `-deny`),
1125
1437
  `question` (plus `question-body` / `-options` / `-choice` / `-choice-text` / `-radio` / `-input` /
1126
- `-actions` / `-button`), `composer`, `input`, `send`,
1438
+ `-actions` / `-button`), `composer` (plus `composer-surface` / `composer-tools`), `input`, `send`,
1127
1439
  `attach-button`, `voice-button`,
1128
1440
  the attachment chips — `attachment-tray` and `attachment-chips` (the read-only chips on sent
1129
1441
  bubbles) with the shared chip parts `attachment-chip` (plus `-icon` / `-name` / `-size` / `-bar` /
1130
1442
  `-bar-fill` / `-retry` / `-remove`),
1131
1443
  the skills UI (`skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`,
1132
1444
  `skill-item-desc`, and the missing-placeholder `skill-hint`),
1133
- `launcher`, `launcher-icon`, and the drawer parts
1445
+ `launcher`, `launcher-icon`, `launcher-badge`, and the drawer parts
1134
1446
  (`drawer`, `drawer-backdrop`, `drawer-panel`, `drawer-header`, `drawer-title`, `drawer-new`,
1135
1447
  `drawer-list`, `drawer-empty`, `drawer-row`, `drawer-row-select`, `drawer-row-title`,
1136
1448
  `drawer-row-time`, `drawer-row-preview`, `drawer-row-actions`, `drawer-row-rename`,
1137
1449
  `drawer-row-delete`, `drawer-rename-input`, `drawer-confirm`, `drawer-confirm-label`,
1138
1450
  `drawer-confirm-yes`, `drawer-confirm-no`).
1139
1451
 
1452
+ > **Hiding `::part(header)` hides the controls inside it.** The history, checkpoints, new-chat,
1453
+ > theme and collapse buttons are all children of the header, so a host that renders its own title
1454
+ > bar and does `ag-ui-chat::part(header) { display: none }` loses thread switching entirely. Every
1455
+ > one of them has an imperative equivalent, so your own chrome can drive them:
1456
+ >
1457
+ > | Control | Method |
1458
+ > | --- | --- |
1459
+ > | History drawer | `chat.openThreads()` |
1460
+ > | Checkpoints panel | `chat.openCheckpoints()` |
1461
+ > | New chat | `chat.newChat()` |
1462
+ > | Collapse | `chat.toggleCollapsed()` / `chat.setCollapsed(bool)` |
1463
+ > | Theme toggle | `chat.toggleTheme()` |
1464
+ >
1465
+ > ```js
1466
+ > myHeaderButton.onclick = () => chat.openThreads();
1467
+ > ```
1468
+ >
1469
+ > The built-in buttons call exactly these methods, so the two routes cannot drift. If you only want
1470
+ > to restyle the header, prefer `::part(header)` styling or the `header-actions` slot over hiding it.
1471
+
1140
1472
  Coarse **slots** let you replace whole regions with your own markup (project light-DOM children
1141
1473
  with a matching `slot=`):
1142
1474
 
@@ -1146,7 +1478,10 @@ with a matching `slot=`):
1146
1478
  | `header-actions` | Extra controls between the title and the built-in buttons. |
1147
1479
  | `empty` | The empty-state shown before any message. |
1148
1480
  | `footer` | Below the composer. |
1149
- | `launcher` | The collapsed sidebar rail's content. |
1481
+ | `launcher` | The collapsed widget's mark — the floating launcher, or the sidebar rail. |
1482
+ | `icon-send` / `icon-stop` | The composer button's two glyphs (idle and mid-run). |
1483
+ | `icon-attach` / `icon-voice` | The paperclip and mic glyphs. |
1484
+ | `icon-history` / `icon-checkpoints` / `icon-new` / `icon-collapse` | The header controls' glyphs. |
1150
1485
 
1151
1486
  ```html
1152
1487
  <ag-ui-chat endpoint="/agent/">
@@ -1160,7 +1495,7 @@ with a matching `slot=`):
1160
1495
  Give the header a brand icon with either the `icon` slot (any markup) or the `data-icon-url`
1161
1496
  convenience attribute (an `<img>`); the slot wins when both are set, and with neither the header
1162
1497
  stays icon-less. The same icon seam feeds the collapsed sidebar rail. Size it via
1163
- `--ag-ui-icon-size` (default `22px`).
1498
+ `--ag-ui-icon-size` (default `22px`) and round it with `--ag-ui-icon-radius` (default `4px`).
1164
1499
 
1165
1500
  ```html
1166
1501
  <ag-ui-chat endpoint="/agent/" data-icon-url="/logo.png"></ag-ui-chat>
@@ -1169,9 +1504,11 @@ stays icon-less. The same icon seam feeds the collapsed sidebar rail. Size it vi
1169
1504
  ### Sidebar placement
1170
1505
 
1171
1506
  `placement="sidebar"` is a full-height **docked** panel that slides open/closed and collapses to a
1172
- slim **icon rail** (rather than the floating launcher). It docks right by default; `data-side="left"`
1173
- docks it left. Collapse state reuses the `collapsed` attribute (persisted per-tab), and the rail
1174
- carries `aria-expanded`. The slide honours `prefers-reduced-motion`.
1507
+ slim **icon rail** rather than the [floating launcher](#collapsing-to-the-launcher) the same
1508
+ element, shaped by the placement. It docks right by default; `data-side="left"` docks it left. The
1509
+ panel slides out through the edge it docks against. Collapse state reuses the `collapsed` attribute
1510
+ (persisted per-tab), and the rail carries `aria-expanded`. The slide honours
1511
+ `prefers-reduced-motion`.
1175
1512
 
1176
1513
  ```html
1177
1514
  <ag-ui-chat endpoint="/agent/" placement="sidebar" data-side="left"></ag-ui-chat>
@@ -1215,9 +1552,48 @@ web component handles the `REASONING_*` event family (and the deprecated `THINKI
1215
1552
  `@ag-ui/client` maps onto it), so no client config is needed — the thoughts appear whenever the
1216
1553
  server forwards reasoning.
1217
1554
 
1555
+ ### The composer
1556
+
1557
+ The composer is one bordered surface (part `composer-surface`) that owns the border and the focus
1558
+ ring: the field sits on top and grows with what is typed until it hits its ceiling and scrolls,
1559
+ and a tool row (part `composer-tools`) sits underneath with the paperclip and mic as quiet icon
1560
+ buttons on the left and a circular **Send** on the right. Send is icon-only — its accessible name
1561
+ still comes from the `send` / `stop` [strings](#internationalization-i18n) — and it becomes the
1562
+ Stop control mid-run by swapping its glyph, so nothing moves when a run starts.
1563
+
1564
+ ```css
1565
+ ag-ui-chat {
1566
+ --ag-ui-composer-radius: 14px;
1567
+ --ag-ui-composer-max-height: 40vh; /* where the growing field starts scrolling */
1568
+ --ag-ui-tool-btn-size: 30px; /* the paperclip / mic hit targets */
1569
+ --ag-ui-send-size: 30px; /* the send circle */
1570
+ --ag-ui-glyph-size: 18px;
1571
+ --ag-ui-glyph-stroke: 1.75;
1572
+ }
1573
+ ```
1574
+
1575
+ Every glyph is a slot with the built-in mark as its fallback (`icon-send`, `icon-stop`,
1576
+ `icon-attach`, `icon-voice`), so projecting your own icon set never means restyling a character.
1577
+
1578
+ ### Motion
1579
+
1580
+ One duration and two curves drive every collapse, expand and slide-over, so the whole widget
1581
+ settles as one thing:
1582
+
1583
+ ```css
1584
+ ag-ui-chat {
1585
+ --ag-ui-motion: 0.28s;
1586
+ --ag-ui-ease: cubic-bezier(0.32, 0.72, 0, 1); /* the settle */
1587
+ --ag-ui-ease-pop: cubic-bezier(0.34, 1.36, 0.64, 1); /* the arrival, with overshoot */
1588
+ }
1589
+ ```
1590
+
1591
+ Under `prefers-reduced-motion: reduce` the duration collapses to a single frame: states still
1592
+ change, nothing travels. Set `--ag-ui-motion: 0s` to switch the animation off outright.
1593
+
1218
1594
  ### Voice input
1219
1595
 
1220
- Set `data-transcribe-url` (django-ag-ui's `TranscribeView`) to reveal a 🎤 mic button in the
1596
+ Set `data-transcribe-url` (django-ag-ui's `TranscribeView`) to reveal a mic button in the
1221
1597
  composer (part `voice-button`). Click it to record via `MediaRecorder`, click again to stop — the
1222
1598
  clip is POSTed to the endpoint and the returned transcript is dropped into the textarea. Swap the
1223
1599
  transport with a custom `transcribeHandler` — `(audio: Blob) => Promise<string>` — to use a