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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,166 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.33.0] — 2026-09-02
11
+
12
+ ### Added
13
+
14
+ - **The panel resizes from every edge and every corner**, not only one grip.
15
+ The model is that the edge a grip does not drag is the one that stays put, so
16
+ a grip names its own edge and no layout can invert it.
17
+
18
+ A drag on the edge the layout was *holding still* moves the panel as well as
19
+ resizing it -- a floating panel pinned bottom-right could not grow rightward,
20
+ because its right edge is what the placement fixed. Those grips take the
21
+ position over by writing `--ag-ui-inset`; a grip on a free edge still writes
22
+ nothing but the size, so a host positioning the panel with its own rule keeps
23
+ it until someone drags the edge it was holding.
24
+
25
+ Exactly one grip stays in the tab order -- the corner opposite the pinned one,
26
+ so an arrow key changes the size and never the position. Eight separators
27
+ between the transcript and the composer would be a keyboard obstacle rather
28
+ than keyboard parity, and one grip already reaches both axes. Every grip has
29
+ its own part, and the hit areas are sized by `--ag-ui-grip-corner` and
30
+ `--ag-ui-grip-edge`.
31
+
32
+ Each grip draws a short pill centred on its edge -- a dot in a corner --
33
+ shown on hover and focus as well as during a drag, since a mark that appears
34
+ only once you are already dragging never told anyone the grips were there.
35
+ Filling the whole handle was what the single corner grip did, and at 14px
36
+ square nobody saw it; on a strip running the length of an edge the same fill
37
+ is a square-ended bar stopping short of the corner radius, which reads as a
38
+ border the panel grew. It was reported as one. The mark is sized by
39
+ `--ag-ui-grip-mark-length` and `--ag-ui-grip-mark-thickness`.
40
+
41
+ - **Files can be pasted into the composer**, alongside the picker and a drop --
42
+ a screenshot straight from the clipboard, or a file copied in the file
43
+ manager. Text pastes carry no files, so ordinary pasting is untouched, and the
44
+ default is prevented only when the clipboard carries no text: copying a rich
45
+ selection containing an image puts both on the clipboard, and swallowing the
46
+ words someone meant to paste is the worse of the two failures.
47
+
48
+ - **The collapsed launcher can be dragged anywhere on screen, and the panel
49
+ opens into the clearest space around it.** Per axis, the element compares the
50
+ room a panel would have on either side of the launcher and pins the side with
51
+ more of it, so a launcher dropped top-left opens down and to the right. What
52
+ it compares is the room the *panel* would get rather than which half of the
53
+ screen the launcher is in; those disagree either side of centre, and only the
54
+ first is about whether the panel fits.
55
+
56
+ Where the panel fits on neither side -- the middle of a short viewport -- the
57
+ launcher still keeps its position: the panel is clamped into the viewport and
58
+ the launcher's own inset carries the difference, which is why it can end up
59
+ outside its own host box. Nothing clips it there, and it keeps its pointer
60
+ events.
61
+
62
+ The position persists per tab beside the collapsed, theme and size
63
+ preferences, arrow keys move it from the keyboard, and
64
+ `data-launcher-drag="false"` opts out -- as does any placement that already
65
+ places the launcher itself. An undragged launcher is untouched: with nothing
66
+ stored the element writes no position at all, and feeding the geometry the
67
+ resting corner reproduces the existing default unchanged.
68
+
69
+ Two things worth carrying. A drag ends in a `click` the browser synthesises,
70
+ which would expand the panel the user was only moving -- but suppressing it
71
+ by arming a flag also swallows `Enter` on the focused button, assistive
72
+ activation, and a host's own `click()`, none of which can be the tail of a
73
+ drag and all of which are the only way in without a pointer. The suppression
74
+ is therefore narrowed to clicks carrying a click count. And the launcher is
75
+ scaled in four separate states, so `getBoundingClientRect` reports a box a few
76
+ pixels off in every one of them; the size comes from `offsetWidth` and the
77
+ position from the rect's centre, which a centred scale cannot move.
78
+
79
+ - **Copy puts the message on the clipboard in both flavours**, so a table
80
+ pastes as a table. `text/html` carries the markup for a target that reads it;
81
+ the plain flavour is serialised structurally -- tab separated table rows,
82
+ which is what a spreadsheet splits on, and list items that keep their markers.
83
+
84
+ It was `textContent`, which is the obvious source and loses every piece of
85
+ structure the message had: descendants concatenated with no separator, so a
86
+ table arrived as one run of cells with the headers welded to the first row.
87
+
88
+ A host driving its own bar opts in by passing the new `html` alongside `text`;
89
+ with `text` alone the clipboard gets plain text only, as before.
90
+
91
+ - **What is drawn on the accent and danger fills is now a variable**
92
+ (`--ag-ui-on-accent`, `--ag-ui-on-danger`). Eight rules had white hardcoded
93
+ against those two fills, so theming the accent to anything pale produced
94
+ white-on-pale on the send button, both approval buttons, the confirmation
95
+ card and the checkpoint row, with no way to correct it.
96
+
97
+ - **The disclosure marks are variables too** (`--ag-ui-disclosure-collapsed`,
98
+ `--ag-ui-disclosure-expanded`). The tool-status marks already were, so a host
99
+ re-theming the marks changed half of them and ended up with two vocabularies
100
+ in one transcript.
101
+
102
+ ### Fixed
103
+
104
+ - **The anchor probe inverted at the size clamps.** The element learns which
105
+ edges its layout holds still by changing its own size by a pixel and seeing
106
+ what moved, and it grew. Growing cannot answer the question at a size already
107
+ resting against `max-width` or `max-height`: the box does not change, no edge
108
+ moves, and every clamped axis reported the edge that did *not* move -- which
109
+ is the free one. The grip landed on the corner the drag travels by, with the
110
+ direction inverted.
111
+
112
+ It needed no unusual setup to reach. The default panel is 380px wide against
113
+ a max-width of `100vw - 48px`, so **any viewport under 428px was born
114
+ clamped**. The probe now shrinks, which always moves an edge because it is
115
+ measured from the box's used width rather than from whatever was asked for,
116
+ and falls back to growing for an axis a host rule gives a minimum.
117
+
118
+ - **A pasted blob with no filename** reached the upload as an empty `filename`
119
+ and showed in the tray as a chip with no label. It is given a name.
120
+
121
+ - **A sub-agent failure whose message the server left empty settled the row to
122
+ a blank line**, reading as a delegation that said nothing rather than one
123
+ that failed. The fallback for it existed in the string table, documented as
124
+ covering exactly this, and was wired to nothing.
125
+
126
+ - **Copying an answer that contained a code block copied the word Copy with
127
+ it.** The code blocks' copy buttons are appended *inside* their own `pre`, so
128
+ they are descendants of the message, and reading `textContent` picked their
129
+ label up mid-sentence. Both clipboard flavours now come from a copy of the
130
+ message with the component's own buttons removed.
131
+
132
+ - **The message action controls were about 20px square**, under the 24px that
133
+ makes a control reliably tappable, and marked with text glyphs -- the copy
134
+ mark in particular has no font behind it on most systems, so it rendered as
135
+ a mark nobody could name on a target nobody could hit. They are now sized
136
+ from `--ag-ui-action-size` with a 24px floor and drawn with the same icon set
137
+ as the rest of the component.
138
+
139
+ - **An icon-only action was unnamed for anyone using a keyboard.** The label was
140
+ carried by `title`, which browsers never show on focus. Each control now draws
141
+ its own label on hover and on focus alike, and the icon holder has its own
142
+ part so a host can swap the mark.
143
+
144
+ ### Changed
145
+
146
+ - **The resize grip is placed from the corner the element chose, when it chose
147
+ one.** With a dragged position the pinned edges are known rather than probed,
148
+ so `#measureAnchor` is skipped. The probe nudges the size by a pixel and reads
149
+ which edges moved, which cannot work at a size already resting against
150
+ `max-width` or `max-height` -- every clamped axis reads as pinned on the wrong
151
+ side. That is a pre-existing defect on the measured path and is untouched here.
152
+
153
+ - **The demo's delegation scenario now streams the carrier a current server
154
+ writes.** It still emitted the five-phase `ag_ui.subagent` `CUSTOM` lifecycle
155
+ that 0.32.0 moved onto the protocol's `SUBAGENT_STARTED` / `_FINISHED` /
156
+ `_ERROR` — so the playground demonstrated only the shape kept for
157
+ back-compatibility, and the component's own showcase was the one place the new
158
+ wire could not be seen.
159
+
160
+ Found by driving the demo in a browser rather than by reading it. Worth noting
161
+ that the *rendering was identical either way*, which is the tolerance working
162
+ as intended and also the reason a passing look at the page proves nothing about
163
+ which wire produced it: the check that separates them is reading the stream.
164
+
165
+ The legacy shape stays accepted — `subAgentUpdate` still narrows all five
166
+ phases for a server one release behind, covered by
167
+ `tests/subagent_update.test.ts`. It is a tolerance, not something a showcase
168
+ should teach.
169
+
10
170
  ## [0.32.0] — 2026-08-31
11
171
 
12
172
  ### Added
@@ -2649,7 +2809,8 @@ hosts that both arrange the page the way it expects.
2649
2809
  ### Notes
2650
2810
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
2651
2811
 
2652
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.32.0...HEAD
2812
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.33.0...HEAD
2813
+ [0.33.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.32.0...v0.33.0
2653
2814
  [0.32.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.31.1...v0.32.0
2654
2815
  [0.31.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.31.0...v0.31.1
2655
2816
  [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` |