@artooi/ag-ui-web-component 0.32.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,197 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.33.1] — 2026-09-03
11
+
12
+ ### Fixed
13
+
14
+ - **A markdown table in an answer scrolled, at last.** The stylesheet has always
15
+ said a wide table should scroll inside its own box, and it never could:
16
+ `.message` set `word-break: break-word`, which is the legacy spelling of
17
+ "break anywhere", and breaking anywhere drops the **min-content width** of
18
+ every descendant to a single character. A table's column algorithm takes
19
+ min-content as an input, so the table always fitted `max-width: 100%`, the
20
+ `overflow-x: auto` beneath it never had anything to scroll, and the columns
21
+ absorbed the pressure by rendering one letter per line instead. A seven-column
22
+ header row came out **162px tall**.
23
+
24
+ It is now `overflow-wrap: break-word`, which breaks a word only when it would
25
+ otherwise overflow its line and leaves min-content alone. Same protection
26
+ against a long unbroken token blowing out a bubble -- which is all the
27
+ declaration was ever for -- and the table is free to be wider than the panel
28
+ and scroll. Measured at the default 380px panel: the header row drops from
29
+ 162px to 50px, the table from 524px to 189px, the narrowest column from 36px
30
+ to 63px, and `scrollWidth` finally exceeds `clientWidth` (563 against 266).
31
+
32
+ Reported against 0.32.0 and present at least as far back as 0.27.0, so this is
33
+ not a recent regression. Thanks to the TrustPoint team for the measurements
34
+ and the revert control, which is what pinned the cause to one declaration.
35
+
36
+ The three other `word-break: break-word` declarations are untouched: they are
37
+ on `.thoughts-body`, `.tool-call-result` and `.confirm-args`, all of which are
38
+ filled with `textContent` and so cannot contain a table.
39
+
40
+
41
+ ## [0.33.0] — 2026-09-02
42
+
43
+ ### Added
44
+
45
+ - **The panel resizes from every edge and every corner**, not only one grip.
46
+ The model is that the edge a grip does not drag is the one that stays put, so
47
+ a grip names its own edge and no layout can invert it.
48
+
49
+ A drag on the edge the layout was *holding still* moves the panel as well as
50
+ resizing it -- a floating panel pinned bottom-right could not grow rightward,
51
+ because its right edge is what the placement fixed. Those grips take the
52
+ position over by writing `--ag-ui-inset`; a grip on a free edge still writes
53
+ nothing but the size, so a host positioning the panel with its own rule keeps
54
+ it until someone drags the edge it was holding.
55
+
56
+ Exactly one grip stays in the tab order -- the corner opposite the pinned one,
57
+ so an arrow key changes the size and never the position. Eight separators
58
+ between the transcript and the composer would be a keyboard obstacle rather
59
+ than keyboard parity, and one grip already reaches both axes. Every grip has
60
+ its own part, and the hit areas are sized by `--ag-ui-grip-corner` and
61
+ `--ag-ui-grip-edge`.
62
+
63
+ Each grip draws a short pill centred on its edge -- a dot in a corner --
64
+ shown on hover and focus as well as during a drag, since a mark that appears
65
+ only once you are already dragging never told anyone the grips were there.
66
+ Filling the whole handle was what the single corner grip did, and at 14px
67
+ square nobody saw it; on a strip running the length of an edge the same fill
68
+ is a square-ended bar stopping short of the corner radius, which reads as a
69
+ border the panel grew. It was reported as one. The mark is sized by
70
+ `--ag-ui-grip-mark-length` and `--ag-ui-grip-mark-thickness`.
71
+
72
+ - **Files can be pasted into the composer**, alongside the picker and a drop --
73
+ a screenshot straight from the clipboard, or a file copied in the file
74
+ manager. Text pastes carry no files, so ordinary pasting is untouched, and the
75
+ default is prevented only when the clipboard carries no text: copying a rich
76
+ selection containing an image puts both on the clipboard, and swallowing the
77
+ words someone meant to paste is the worse of the two failures.
78
+
79
+ - **The collapsed launcher can be dragged anywhere on screen, and the panel
80
+ opens into the clearest space around it.** Per axis, the element compares the
81
+ room a panel would have on either side of the launcher and pins the side with
82
+ more of it, so a launcher dropped top-left opens down and to the right. What
83
+ it compares is the room the *panel* would get rather than which half of the
84
+ screen the launcher is in; those disagree either side of centre, and only the
85
+ first is about whether the panel fits.
86
+
87
+ Where the panel fits on neither side -- the middle of a short viewport -- the
88
+ launcher still keeps its position: the panel is clamped into the viewport and
89
+ the launcher's own inset carries the difference, which is why it can end up
90
+ outside its own host box. Nothing clips it there, and it keeps its pointer
91
+ events.
92
+
93
+ The position persists per tab beside the collapsed, theme and size
94
+ preferences, arrow keys move it from the keyboard, and
95
+ `data-launcher-drag="false"` opts out -- as does any placement that already
96
+ places the launcher itself. An undragged launcher is untouched: with nothing
97
+ stored the element writes no position at all, and feeding the geometry the
98
+ resting corner reproduces the existing default unchanged.
99
+
100
+ Two things worth carrying. A drag ends in a `click` the browser synthesises,
101
+ which would expand the panel the user was only moving -- but suppressing it
102
+ by arming a flag also swallows `Enter` on the focused button, assistive
103
+ activation, and a host's own `click()`, none of which can be the tail of a
104
+ drag and all of which are the only way in without a pointer. The suppression
105
+ is therefore narrowed to clicks carrying a click count. And the launcher is
106
+ scaled in four separate states, so `getBoundingClientRect` reports a box a few
107
+ pixels off in every one of them; the size comes from `offsetWidth` and the
108
+ position from the rect's centre, which a centred scale cannot move.
109
+
110
+ - **Copy puts the message on the clipboard in both flavours**, so a table
111
+ pastes as a table. `text/html` carries the markup for a target that reads it;
112
+ the plain flavour is serialised structurally -- tab separated table rows,
113
+ which is what a spreadsheet splits on, and list items that keep their markers.
114
+
115
+ It was `textContent`, which is the obvious source and loses every piece of
116
+ structure the message had: descendants concatenated with no separator, so a
117
+ table arrived as one run of cells with the headers welded to the first row.
118
+
119
+ A host driving its own bar opts in by passing the new `html` alongside `text`;
120
+ with `text` alone the clipboard gets plain text only, as before.
121
+
122
+ - **What is drawn on the accent and danger fills is now a variable**
123
+ (`--ag-ui-on-accent`, `--ag-ui-on-danger`). Eight rules had white hardcoded
124
+ against those two fills, so theming the accent to anything pale produced
125
+ white-on-pale on the send button, both approval buttons, the confirmation
126
+ card and the checkpoint row, with no way to correct it.
127
+
128
+ - **The disclosure marks are variables too** (`--ag-ui-disclosure-collapsed`,
129
+ `--ag-ui-disclosure-expanded`). The tool-status marks already were, so a host
130
+ re-theming the marks changed half of them and ended up with two vocabularies
131
+ in one transcript.
132
+
133
+ ### Fixed
134
+
135
+ - **The anchor probe inverted at the size clamps.** The element learns which
136
+ edges its layout holds still by changing its own size by a pixel and seeing
137
+ what moved, and it grew. Growing cannot answer the question at a size already
138
+ resting against `max-width` or `max-height`: the box does not change, no edge
139
+ moves, and every clamped axis reported the edge that did *not* move -- which
140
+ is the free one. The grip landed on the corner the drag travels by, with the
141
+ direction inverted.
142
+
143
+ It needed no unusual setup to reach. The default panel is 380px wide against
144
+ a max-width of `100vw - 48px`, so **any viewport under 428px was born
145
+ clamped**. The probe now shrinks, which always moves an edge because it is
146
+ measured from the box's used width rather than from whatever was asked for,
147
+ and falls back to growing for an axis a host rule gives a minimum.
148
+
149
+ - **A pasted blob with no filename** reached the upload as an empty `filename`
150
+ and showed in the tray as a chip with no label. It is given a name.
151
+
152
+ - **A sub-agent failure whose message the server left empty settled the row to
153
+ a blank line**, reading as a delegation that said nothing rather than one
154
+ that failed. The fallback for it existed in the string table, documented as
155
+ covering exactly this, and was wired to nothing.
156
+
157
+ - **Copying an answer that contained a code block copied the word Copy with
158
+ it.** The code blocks' copy buttons are appended *inside* their own `pre`, so
159
+ they are descendants of the message, and reading `textContent` picked their
160
+ label up mid-sentence. Both clipboard flavours now come from a copy of the
161
+ message with the component's own buttons removed.
162
+
163
+ - **The message action controls were about 20px square**, under the 24px that
164
+ makes a control reliably tappable, and marked with text glyphs -- the copy
165
+ mark in particular has no font behind it on most systems, so it rendered as
166
+ a mark nobody could name on a target nobody could hit. They are now sized
167
+ from `--ag-ui-action-size` with a 24px floor and drawn with the same icon set
168
+ as the rest of the component.
169
+
170
+ - **An icon-only action was unnamed for anyone using a keyboard.** The label was
171
+ carried by `title`, which browsers never show on focus. Each control now draws
172
+ its own label on hover and on focus alike, and the icon holder has its own
173
+ part so a host can swap the mark.
174
+
175
+ ### Changed
176
+
177
+ - **The resize grip is placed from the corner the element chose, when it chose
178
+ one.** With a dragged position the pinned edges are known rather than probed,
179
+ so `#measureAnchor` is skipped. The probe nudges the size by a pixel and reads
180
+ which edges moved, which cannot work at a size already resting against
181
+ `max-width` or `max-height` -- every clamped axis reads as pinned on the wrong
182
+ side. That is a pre-existing defect on the measured path and is untouched here.
183
+
184
+ - **The demo's delegation scenario now streams the carrier a current server
185
+ writes.** It still emitted the five-phase `ag_ui.subagent` `CUSTOM` lifecycle
186
+ that 0.32.0 moved onto the protocol's `SUBAGENT_STARTED` / `_FINISHED` /
187
+ `_ERROR` — so the playground demonstrated only the shape kept for
188
+ back-compatibility, and the component's own showcase was the one place the new
189
+ wire could not be seen.
190
+
191
+ Found by driving the demo in a browser rather than by reading it. Worth noting
192
+ that the *rendering was identical either way*, which is the tolerance working
193
+ as intended and also the reason a passing look at the page proves nothing about
194
+ which wire produced it: the check that separates them is reading the stream.
195
+
196
+ The legacy shape stays accepted — `subAgentUpdate` still narrows all five
197
+ phases for a server one release behind, covered by
198
+ `tests/subagent_update.test.ts`. It is a tolerance, not something a showcase
199
+ should teach.
200
+
10
201
  ## [0.32.0] — 2026-08-31
11
202
 
12
203
  ### Added
@@ -2649,7 +2840,9 @@ hosts that both arrange the page the way it expects.
2649
2840
  ### Notes
2650
2841
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
2651
2842
 
2652
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.32.0...HEAD
2843
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.33.1...HEAD
2844
+ [0.33.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.33.0...v0.33.1
2845
+ [0.33.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.32.0...v0.33.0
2653
2846
  [0.32.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.31.1...v0.32.0
2654
2847
  [0.31.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.31.0...v0.31.1
2655
2848
  [0.31.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.30.0...v0.31.0
package/README.md CHANGED
@@ -174,7 +174,7 @@ another origin, add `credentials="include"` too; see
174
174
  | `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
175
175
  | `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`. |
176
176
  | `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). |
177
- | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker + drag-and-drop. |
177
+ | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker, drag-and-drop, and paste. |
178
178
  | `data-attachment-accept` | — | `<input accept>` list for client-side type filtering (e.g. `image/*,.pdf`). The server stays authoritative. |
179
179
  | `data-attachment-max-bytes` | — | Client-side upload size cap in bytes (default 10 MiB; `0` disables). The server stays authoritative. |
180
180
  | `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). |
@@ -183,6 +183,7 @@ another origin, add `credentials="include"` too; see
183
183
  | `data-icon-url` | — | Header (and launcher) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
184
184
  | `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. |
185
185
  | `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). |
186
+ | `data-launcher-drag` | — | **On by default.** `="false"` leaves the collapsed launcher wherever your CSS puts it. Otherwise it can be dragged anywhere on screen and the panel opens into the clearest space. See [Moving the launcher](#moving-the-launcher). |
186
187
  | `data-quote-selection` | — | **On by default.** `="false"` stops the transcript offering to quote a selection. `quote()` keeps working either way. See [Quoting a selection](#quoting-a-selection). |
187
188
  | `data-message-actions` | — | **All on by default.** A comma list of the actions a finished answer keeps: `copy` / `retry` / `feedback` (e.g. `"copy,retry"`). `="false"` removes the row entirely. See [Message actions](#message-actions-copy-retry-feedback). |
188
189
  | `data-max-tool-rounds` | — | Upper bound on frontend tool-call → re-run rounds within one send (default 10; a value below 1 is ignored). Raise it for a page-driving agent whose turn takes many small steps. See [The run loop](#the-run-loop-and-the-ag-ui-client). |
@@ -211,8 +212,8 @@ The element observes two groups of attributes, and they behave differently once
211
212
  Nothing outside those groups is observed: a CSS-only attribute (`theme`, `density`, `data-side`,
212
213
  `data-answer-well`) is read by the stylesheet rather than by script, and `endpoint`,
213
214
  `data-tool-display`, `data-text-animation`, `data-runs-url`, `data-page-actions`,
214
- `data-message-actions`, `data-max-tool-rounds`, `data-unread-badge` and `data-quote-selection` are
215
- re-read at each use, so a late write to any of those simply takes effect. The one attribute in
215
+ `data-message-actions`, `data-max-tool-rounds`, `data-unread-badge`, `data-launcher-drag` and
216
+ `data-quote-selection` are re-read at each use, so a late write to any of those simply takes effect. The one attribute in
216
217
  neither camp is `data-launcher-icon-url`: it is read while the element connects, like the group
217
218
  below, but is not observed, so a late write is inert and says nothing.
218
219
 
@@ -968,6 +969,34 @@ default**; `data-unread-badge="false"` turns the badge off.
968
969
  The count is also the launcher's accessible name (`Expand — 2 unread`, from the `expandUnread`
969
970
  string), because a coloured dot says nothing to a screen reader.
970
971
 
972
+ #### Moving the launcher
973
+
974
+ The collapsed launcher can be dragged anywhere on screen, and it stays there — per tab, in
975
+ `sessionStorage`, namespaced per element exactly like the collapsed, theme and size preferences.
976
+ Arrow keys move it from the keyboard (`Shift` for a larger step). `data-launcher-drag="false"`
977
+ turns it off; so does any placement that already places the launcher itself — `sidebar` collapses
978
+ it to a full-height rail, `embedded` and `page` hide it and keep their header bar, and a
979
+ full-bleed panel has no clear space to open into.
980
+
981
+ **The panel then opens into whichever side of the launcher has more room.** For each axis the
982
+ element compares the space a panel would have on either side of the launcher and pins the side
983
+ with more of it, so a launcher dragged to the top-left opens down and to the right. What it
984
+ compares is the room the *panel* would get, not which half of the screen the launcher is in —
985
+ those give different answers either side of centre, and only the first one is about whether the
986
+ panel fits.
987
+
988
+ A launcher parked where the panel fits neither way — the middle of a short viewport — keeps its
989
+ position anyway: the panel is clamped into the viewport and the launcher carries the difference,
990
+ which is why it can end up sitting outside its own host box. Nothing clips it there.
991
+
992
+ A drag writes `--ag-ui-inset` and `--ag-ui-launcher-inset` on the host, and an inline custom
993
+ property outranks your stylesheet's rule for the same one — the same trade a dragged size makes
994
+ against a placement. Switching to a placement that places itself hands both back.
995
+
996
+ > **An undragged launcher is untouched.** With nothing stored, the element writes neither
997
+ > property and your CSS decides, exactly as before. The geometry is built so that feeding it the
998
+ > resting position reproduces the default `auto 24px 24px auto` unchanged.
999
+
971
1000
  ```js
972
1001
  chat.unread; // 2
973
1002
 
@@ -1288,22 +1317,48 @@ server's text.
1288
1317
 
1289
1318
  ## Resizing the panel
1290
1319
 
1291
- The panel carries a drag handle on its leading corner (or leading edge, docked),
1292
- so a reader can widen it without the host having to re-theme anything.
1320
+ The panel carries a grip on **every edge and every corner**, so it can be
1321
+ dragged from whichever side you are already near.
1293
1322
 
1294
- - `placement="full"` / `placement="page"` get **no handle** — a full-bleed layout
1323
+ - `placement="full"` / `placement="page"` get **no grips** — a full-bleed layout
1295
1324
  is `100vw`/`100vh` by definition, so there is nothing to drag.
1296
- - `placement="sidebar"` / `placement="side"` get **width only**; the placement
1297
- owns the height.
1298
- - Everything else resizes on both axes.
1299
-
1300
- **The grip sits at the corner your layout grows toward, and the component
1301
- measures which one that is.** A resize has to be computed from the edge that
1302
- stays still, and that belongs to *your* CSS rather than to `placement` a
1303
- floating panel is pinned bottom-right, an embedded one goes wherever the page
1304
- puts it. The element probes its own geometry and reflects the result as
1305
- `data-resize-anchor` (e.g. `bottom-right` means those two edges are fixed), which
1306
- is what positions the grip.
1325
+ - `placement="sidebar"` / `placement="side"` keep only the two vertical edges;
1326
+ the placement owns the height, so a horizontal edge or a corner would
1327
+ advertise a drag that does nothing.
1328
+ - Everything else gets all eight.
1329
+
1330
+ **The edge a grip does not drag is the one that stays put.** That is the whole
1331
+ model: the left grip moves the left edge and holds the right, the right grip
1332
+ does the reverse, a corner does both axes. A grip names its own edge, so no
1333
+ layout can invert it.
1334
+
1335
+ ### Dragging the edge your layout was holding still
1336
+
1337
+ A floating panel pinned bottom-right cannot grow rightward on its own — its
1338
+ right edge is what the placement fixed. So **a drag on a pinned edge moves the
1339
+ panel as well as resizing it**, and the component takes the position over by
1340
+ writing `--ag-ui-inset`, the same ownership a dragged launcher takes.
1341
+
1342
+ A grip on a free edge writes nothing but the size, exactly as before, so a host
1343
+ positioning the panel with its own rule keeps that rule until someone drags the
1344
+ edge it was holding.
1345
+
1346
+ Which edges those are belongs to *your* CSS rather than to `placement` — a
1347
+ floating panel a host right-aligns is anchored bottom-left — so the element
1348
+ probes its own geometry and reflects the result as `data-resize-anchor` (e.g.
1349
+ `bottom-right` means those two edges are fixed). Nothing in the stylesheet reads
1350
+ it any more; the element uses it to decide what a drag on a pinned edge costs,
1351
+ and which grip carries the keyboard.
1352
+
1353
+ > **The probe shrinks rather than grows.** Growing by a pixel cannot answer the
1354
+ > question at a size already resting against `max-width` or `max-height`: the
1355
+ > box does not change, no edge moves, and every clamped axis reads as pinned on
1356
+ > the side that is actually free. That was reachable with no user action at all
1357
+ > — the default panel is 380px wide against a max-width of `100vw - 48px`, so
1358
+ > any viewport under 428px was born clamped, with the grip on the wrong corner
1359
+ > and the drag inverted.
1360
+
1361
+ ### Sizes, keyboard, and parts
1307
1362
 
1308
1363
  A drag writes `--ag-ui-width` / `--ag-ui-height` on the host as custom
1309
1364
  properties.
@@ -1323,8 +1378,31 @@ container.
1323
1378
 
1324
1379
  The size persists per tab (`sessionStorage`, namespaced per element like the
1325
1380
  collapsed and theme preferences) and is restored before the first paint.
1326
- Arrow keys resize from the keyboard (`Shift` for a larger step); style the grip
1327
- via the `resize-handle` part.
1381
+
1382
+ **Exactly one grip is in the tab order** — the corner diagonally opposite the
1383
+ pinned one, so an arrow key changes the size and never the position. Eight
1384
+ separators between the transcript and the composer would be a keyboard
1385
+ obstacle rather than keyboard parity, and one grip already reaches both axes.
1386
+ Arrow keys resize from it (`Shift` for a larger step).
1387
+
1388
+ Each grip draws a short pill centred on its edge — a dot in a corner — on hover
1389
+ and focus as well as during a drag. **The hit area and the mark are separate on
1390
+ purpose**: the area is the whole strip, so the grip is easy to catch, while the
1391
+ mark stays small enough that it cannot be read as a border and never meets the
1392
+ panel's corner radius.
1393
+
1394
+ Every grip has its own part (`resize-handle-left`, `resize-handle-bottom-right`,
1395
+ and so on, plus `resize-handle` on all of them). Hit areas and marks are sized
1396
+ separately, so a coarser pointer can get a bigger target without a heavier mark.
1397
+
1398
+ ```css
1399
+ ag-ui-chat {
1400
+ --ag-ui-grip-corner: 20px; /* the corner squares */
1401
+ --ag-ui-grip-edge: 10px; /* the edge strips */
1402
+ --ag-ui-grip-mark-length: 28px; /* the pill drawn inside them */
1403
+ --ag-ui-grip-mark-thickness: 3px;
1404
+ }
1405
+ ```
1328
1406
 
1329
1407
  ---
1330
1408
 
@@ -1472,6 +1550,53 @@ the actions to keep, or `="false"` for none at all:
1472
1550
  The default is `copy,retry`. Those two work with nothing wired; the rating pair
1473
1551
  needs a listener, so it is asked for rather than assumed.
1474
1552
 
1553
+ ### What Copy puts on the clipboard
1554
+
1555
+ Both flavours of the message: `text/plain` for anywhere, and `text/html` for a
1556
+ target that understands it. A table therefore pastes into a spreadsheet as
1557
+ columns and into a document as a table.
1558
+
1559
+ The plain flavour is serialised structurally rather than read off `textContent`,
1560
+ which is the obvious source and loses everything: it concatenates descendants
1561
+ with no separator, so a table arrives as one unbroken run of cells with the
1562
+ headers welded to the first row. Rows are tab separated because that is what a
1563
+ spreadsheet splits on; list items keep their bullets and numbers; a code block
1564
+ keeps its own whitespace.
1565
+
1566
+ Both flavours are taken from a copy of the message with the component's own
1567
+ buttons removed. The code blocks' copy buttons live *inside* their `pre`, so
1568
+ they are descendants of the message: before this, copying an answer containing a
1569
+ code block copied the word Copy along with it.
1570
+
1571
+ A host that drives its own bar gets the same by passing `html` alongside `text`
1572
+ to `attachMessageActions`; with `text` alone the clipboard gets plain text only,
1573
+ exactly as before.
1574
+
1575
+ ### Sizing and theming the row
1576
+
1577
+ The controls are icon-only, so the whole box is the target. It is sized from
1578
+ `--ag-ui-action-size`, with a floor rather than a fixed height so the compact
1579
+ density cannot shrink it below the 24px that keeps it reliably tappable.
1580
+
1581
+ Each control draws its own label on hover **and on keyboard focus**. That is not
1582
+ the `title` attribute repeated: a `title` never appears on focus, so an
1583
+ icon-only button is unnamed for anyone tabbing to it. On a touch device, where
1584
+ there is no hover to reveal it and it would sit over the answer, it is not
1585
+ drawn at all.
1586
+
1587
+ ```css
1588
+ ag-ui-chat {
1589
+ --ag-ui-action-size: 28px; /* the control box; floors at 24px */
1590
+ --ag-ui-action-icon-size: 15px; /* the mark inside it */
1591
+ --ag-ui-tooltip-bg: #1f2430;
1592
+ --ag-ui-tooltip-fg: #f5f6fa;
1593
+ }
1594
+ ```
1595
+
1596
+ To swap a mark for your own, style its part — `message-action-icon-copy` and its
1597
+ siblings. A slot would be the better channel and cannot be used: these repeat
1598
+ once per message, and a named slot can only be filled once.
1599
+
1475
1600
  It is per-action rather than one switch because the three disappear for
1476
1601
  different reasons. Thumbs are only useful to a host listening for
1477
1602
  `ag-ui-feedback`, and two buttons that lead nowhere are worse than none. Retry
@@ -2031,13 +2156,30 @@ error — a history affordance that fails is empty, not broken.
2031
2156
  ## File uploads
2032
2157
 
2033
2158
  Set **`data-attachments-url`** (django-ag-ui's `AttachmentsView`) to let the user attach files
2034
- to a message. A button and drag-and-drop appear on the composer; each picked file uploads
2159
+ to a message. A button, drag-and-drop, and **paste** appear on the composer; each picked file uploads
2035
2160
  out-of-band (multipart, with the element's `headers`) and shows a chip in a pending tray —
2036
2161
  `uploading` (with a progress bar) → `ready`, or `error` with a retry. On send, the ready files'
2037
2162
  **refs** ride on the user bubble as read-only chips and the agent reads their contents
2038
2163
  server-side via the `read_attachment` tool. The wire stays vanilla AG-UI: only lightweight refs
2039
2164
  (`{ id, name, mime, size }`) travel, never the bytes.
2040
2165
 
2166
+ ### Pasting
2167
+
2168
+ A paste carrying files puts them on the tray, the same way the picker and a drop
2169
+ do — a screenshot straight from the clipboard, or a file copied in the file
2170
+ manager.
2171
+
2172
+ Two rules keep it from stealing a paste that was never about files. Text pastes
2173
+ carry no files at all, so ordinary pasting is untouched. And the default is
2174
+ prevented **only when the clipboard carries no text**: copying a rich selection
2175
+ that happens to contain an image puts both on the clipboard, and swallowing the
2176
+ words someone meant to paste in order to attach a picture they did not is the
2177
+ worse of the two failures.
2178
+
2179
+ A pasted blob that arrives with no filename — some engines hand one over that
2180
+ way — is given one, rather than reaching the server as an empty `filename` and
2181
+ showing in the tray as a chip with no label.
2182
+
2041
2183
  ```html
2042
2184
  <ag-ui-chat
2043
2185
  endpoint="/agent/"
@@ -2155,7 +2297,7 @@ re-export point. Internal modules import from leaf paths.
2155
2297
  | `suggestionPrompts` | function | The usable prompts in a `suggestions` activity's content, bounded and trimmed. |
2156
2298
  | `attachMessageActions` | function | Give a finished bubble its action row (copy, and feedback when a handler is passed). |
2157
2299
  | `messageActionBar` | function | The empty action row for a bubble, created if it has none — the shared shell both callers use. |
2158
- | `MessageActionsOptions` | type | What `attachMessageActions` takes: `strings`, a `text` source, an optional `onFeedback`. |
2300
+ | `MessageActionsOptions` | type | What `attachMessageActions` takes: `strings`, a `text` source, an optional `html` source for the rich clipboard flavour, an optional `onFeedback`. |
2159
2301
  | `ActivityRenderer` | type | Draws one activity from its `content`. Pure: it runs again on every restore. |
2160
2302
  | `ActivityRegistration` | type | What `registerActivityRenderer` takes: `type`, `render`, and an optional `removedNotice`. |
2161
2303
  | `createStateHookTools(binding)` / `StateHook` | deprecated | The former names for `createPageStateTools` / `PageState`. |
@@ -2293,6 +2435,11 @@ ag-ui-chat {
2293
2435
  --ag-ui-fg: #1a1a2e;
2294
2436
  --ag-ui-radius: 12px;
2295
2437
 
2438
+ /* What is drawn on top of the accent and danger fills. Change these with
2439
+ the fills: a pale accent leaves white-on-pale everywhere they are used. */
2440
+ --ag-ui-on-accent: #ffffff;
2441
+ --ag-ui-on-danger: #ffffff;
2442
+
2296
2443
  /* Layout */
2297
2444
  --ag-ui-width: 380px;
2298
2445
  --ag-ui-height: 560px;
@@ -2301,6 +2448,18 @@ ag-ui-chat {
2301
2448
  }
2302
2449
  ```
2303
2450
 
2451
+ Marks are variables too, so one vocabulary covers a re-theme rather than
2452
+ leaving half the transcript in the built-in set: `--ag-ui-tool-icon-done` /
2453
+ `-error` / `-declined` for tool status, and `--ag-ui-disclosure-collapsed` /
2454
+ `--ag-ui-disclosure-expanded` for every expandable row.
2455
+
2456
+ ```css
2457
+ ag-ui-chat {
2458
+ --ag-ui-disclosure-collapsed: "+";
2459
+ --ag-ui-disclosure-expanded: "-";
2460
+ }
2461
+ ```
2462
+
2304
2463
  ### Where to put the variables
2305
2464
 
2306
2465
  There is one vocabulary — the `--ag-ui-*` names above — and it works from any ancestor. What
@@ -2387,13 +2546,13 @@ component sets, so a new one cannot ship undocumented.
2387
2546
 
2388
2547
  | Feature | Parts |
2389
2548
  | --- | --- |
2390
- | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `jump-latest`, `resize-handle` |
2549
+ | Shell | `panel`, `header`, `title`, `icon`, `header-controls`, `messages`, `empty`, `pending`, `stopped`, `jump-latest`, and one per resize grip: `resize-handle` plus `resize-handle-top`, `resize-handle-bottom`, `resize-handle-left`, `resize-handle-right`, `resize-handle-top-left`, `resize-handle-top-right`, `resize-handle-bottom-left`, `resize-handle-bottom-right` |
2391
2550
  | Header buttons | `header-button` on each, plus `history-button`, `checkpoints-button`, `new-button`, `collapse-button`, `theme-toggle` |
2392
2551
  | Collapsed widget | `launcher`, `launcher-icon`, `launcher-badge` |
2393
2552
  | Answers | `answer` (the per-turn group), `message` (plus `message-user`, `message-assistant`), `code-copy` |
2394
2553
  | Reasoning | `thoughts`, `thoughts-toggle`, `thoughts-body`, `thoughts-label` |
2395
2554
  | Follow-up suggestions | `suggestions`, `suggestion-chip` |
2396
- | Message actions | `message-actions`, `message-action` (plus `message-action-retry`, `message-action-copy`, `message-action-up`, `message-action-down`) |
2555
+ | Message actions | `message-actions`, `message-action` (plus `message-action-retry`, `message-action-copy`, `message-action-up`, `message-action-down`), and the icon holder inside each: `message-action-icon` (plus `message-action-icon-retry`, `message-action-icon-copy`, `message-action-icon-up`, `message-action-icon-down`) |
2397
2556
  | Run notices | `run-notice` (plus `run-notice-interrupted`, `run-notice-attachment-pending`, `run-notice-compaction`, `run-notice-skill`, `run-notice-history-replaced`, `run-notice-chart-undrawable`), `run-notice-icon`, `run-notice-text` |
2398
2557
  | Tool cards | `tool-card`, `tool-card-head`, `tool-card-icon`, `tool-card-name`, `tool-card-status`, `tool-card-decision`, `tool-card-toggle`, `tool-card-body`, `tool-card-section` (plus `tool-card-args-section`, `tool-card-result-section`), `tool-card-section-label` (plus `tool-card-args-label`, `tool-card-result-label`), `tool-card-args`, `tool-card-result`, `tool-card-approval`, `tool-card-subagent` |
2399
2558
  | Delegated sub-agents | `subagent`, `subagent-row`, `subagent-icon`, `subagent-status`, `subagent-steps`, `subagent-step`, `subagent-step-icon`, `subagent-step-name` |