@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 +194 -1
- package/README.md +182 -23
- package/dist/ag-ui-web-component.bundle.js +360 -121
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +1275 -390
- package/dist/index.js.map +4 -4
- package/dist/ui/clamp_launcher.d.ts +15 -0
- package/dist/ui/clamp_launcher.d.ts.map +1 -0
- package/dist/ui/copy_payload.d.ts +30 -0
- package/dist/ui/copy_payload.d.ts.map +1 -0
- package/dist/ui/launcher_drag.d.ts +43 -0
- package/dist/ui/launcher_drag.d.ts.map +1 -0
- package/dist/ui/launcher_placement.d.ts +64 -0
- package/dist/ui/launcher_placement.d.ts.map +1 -0
- package/dist/ui/message_actions.d.ts +30 -2
- package/dist/ui/message_actions.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +32 -26
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +12 -0
- package/src/core/ag_ui_chat.ts +480 -24
- package/src/ui/clamp_launcher.ts +20 -0
- package/src/ui/copy_payload.ts +156 -0
- package/src/ui/launcher_drag.ts +182 -0
- package/src/ui/launcher_placement.ts +143 -0
- package/src/ui/message_actions.ts +93 -19
- package/src/ui/resize_handle.ts +109 -73
- package/src/ui/styles.ts +299 -82
- package/src/version.ts +1 -1
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.
|
|
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
|
|
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
|
|
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
|
|
1292
|
-
|
|
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
|
|
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"`
|
|
1297
|
-
owns the height
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
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
|
|
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` |
|