@artooi/ag-ui-web-component 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +501 -1
  2. package/README.md +232 -13
  3. package/dist/ag-ui-web-component.bundle.js +614 -96
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +76 -3
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +37 -2
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/dom/animations.d.ts +14 -0
  10. package/dist/dom/animations.d.ts.map +1 -1
  11. package/dist/dom/highlight_overlay.d.ts +47 -0
  12. package/dist/dom/highlight_overlay.d.ts.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1886 -320
  16. package/dist/index.js.map +4 -4
  17. package/dist/tools/chat_surface_tools.d.ts +96 -0
  18. package/dist/tools/chat_surface_tools.d.ts.map +1 -0
  19. package/dist/tools/page_action_tools.d.ts +2 -0
  20. package/dist/tools/page_action_tools.d.ts.map +1 -1
  21. package/dist/ui/clamp_launcher.d.ts +10 -5
  22. package/dist/ui/clamp_launcher.d.ts.map +1 -1
  23. package/dist/ui/clamp_panel.d.ts +2 -2
  24. package/dist/ui/clamp_panel.d.ts.map +1 -1
  25. package/dist/ui/launcher_drag.d.ts +2 -2
  26. package/dist/ui/launcher_drag.d.ts.map +1 -1
  27. package/dist/ui/launcher_placement.d.ts +14 -1
  28. package/dist/ui/launcher_placement.d.ts.map +1 -1
  29. package/dist/ui/panel_drag.d.ts.map +1 -1
  30. package/dist/ui/place_widget.d.ts +9 -1
  31. package/dist/ui/place_widget.d.ts.map +1 -1
  32. package/dist/ui/run_notice.d.ts +14 -3
  33. package/dist/ui/run_notice.d.ts.map +1 -1
  34. package/dist/ui/styles.d.ts +1 -1
  35. package/dist/ui/styles.d.ts.map +1 -1
  36. package/dist/ui/thread_drawer.d.ts +21 -0
  37. package/dist/ui/thread_drawer.d.ts.map +1 -1
  38. package/dist/ui/ui_strings.d.ts +16 -0
  39. package/dist/ui/ui_strings.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/constants.ts +82 -3
  42. package/src/core/ag_ui_chat.ts +965 -55
  43. package/src/dom/animations.ts +30 -0
  44. package/src/dom/highlight_overlay.ts +256 -0
  45. package/src/index.ts +12 -0
  46. package/src/tools/chat_surface_tools.ts +207 -0
  47. package/src/tools/page_action_tools.ts +2 -0
  48. package/src/ui/clamp_launcher.ts +25 -7
  49. package/src/ui/clamp_panel.ts +10 -4
  50. package/src/ui/launcher_drag.ts +11 -2
  51. package/src/ui/launcher_placement.ts +34 -8
  52. package/src/ui/panel_drag.ts +4 -0
  53. package/src/ui/place_widget.ts +11 -3
  54. package/src/ui/run_notice.ts +32 -3
  55. package/src/ui/styles.ts +563 -45
  56. package/src/ui/thread_drawer.ts +138 -8
  57. package/src/ui/ui_strings.ts +24 -0
  58. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,505 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.35.0] — 2026-09-04
11
+
12
+ ### Added
13
+
14
+ - **Enter during a run queues instead of doing nothing.** A second run cannot
15
+ start while one is in flight -- it would orphan the first, which is
16
+ unabortable, and the second's settle sweep would corrupt the first's still
17
+ pending tool cards -- so that key did nothing at all, silently, and the text
18
+ sat in the box.
19
+
20
+ What is waiting now shows above the composer as chips, each of which takes its
21
+ message back when pressed, and the next is sent when the run settles. Stopping
22
+ the run discards them: sending into a conversation someone has just stopped is
23
+ the opposite of what stopping meant. Not sending it is not the same as
24
+ destroying it, though -- a queued message has already left the composer, so
25
+ it goes to the front of the arrow-key recall history rather than nowhere.
26
+ Text only -- an attachment is settled state the tray already holds, and the
27
+ composer has no second copy of it.
28
+
29
+ - **A very long paste becomes an attachment rather than a wall of text.** At
30
+ 5000 characters, and only where uploads are configured -- without somewhere
31
+ for it to go, quietly dropping a paste for being long is far worse than an
32
+ awkward composer. The field is capped at `40vh`, so a paste that size is
33
+ already taller than the box holding it: the reader cannot see what they
34
+ pasted, cannot edit around it, and sends one enormous turn.
35
+
36
+ Nothing is lost by removing the chip, because the text is still on the
37
+ clipboard -- which is why this needs no undo of its own.
38
+ `data-paste-attach` takes `off` or a character count.
39
+
40
+ - **The conversation list filters itself** once there are eight or more
41
+ conversations in it -- below that a search box is a control asking to be used
42
+ on a list you can already read in one glance. It matches the title **and** the
43
+ preview, because the title is often the model's one-line summary and the
44
+ phrase you remember is as likely to be inside the conversation as on it, and
45
+ it filters what the drawer already holds rather than going back to the server
46
+ for a list that is already in memory. A filter that matches nothing says so,
47
+ rather than reusing the sentence for having no conversations at all.
48
+
49
+ - **`data-starters` offers prompts on an empty transcript.** A JSON array of
50
+ strings, drawn as chips that send themselves. Different from the suggestion
51
+ chips a run pushes, which are follow-ups to something already said -- these
52
+ answer the blank page, and they are the host's rather than the model's,
53
+ because only the host knows what its page is for. They are fallback content
54
+ for `slot="empty"`, so slotting your own replaces them entirely, and they
55
+ share the four-prompt and 120-character limits with the pushed chips.
56
+
57
+ - **Up and Down walk back through what you have already sent**, from an empty
58
+ composer with the skills palette closed. The shape every shell and every
59
+ coding agent uses; the conditions are what make it safe, because an arrow
60
+ inside text is how you move the caret. Arrowing forward past the newest turn
61
+ empties the box again, so the way out is the key that got you in, and typing
62
+ hands the composer back so the next walk starts from the newest turn.
63
+
64
+ - **On a full page, the conversation list docks beside the transcript instead of
65
+ covering it.** From 900px of panel width under `placement="page"`: no
66
+ backdrop, no focus trap, and `role="region"` rather than a modal dialog, with
67
+ the transcript shifted over rather than hidden. Covering the conversation to
68
+ show the list of conversations hides the thing you are trying to get back to,
69
+ and a dedicated route is the one surface with width to spare.
70
+
71
+ Narrower than that, or under any other placement, it stays the slide-over it
72
+ was -- a few hundred pixels of panel with a list docked into it leaves a column
73
+ of transcript narrower than the messages in it. Width alone is not the test:
74
+ an app shell can hand `embedded` a page-sized box, and that box is still a
75
+ column of somebody's layout.
76
+
77
+ `--ag-ui-threads-rail-width` sets the docked width; the host carries
78
+ `data-threads-docked` while it is showing. `closeThreads()` joins
79
+ `openThreads()`.
80
+
81
+ - **The agent can move the panel it is speaking from.** Four tools behind a new
82
+ `chat` token on `data-page-actions`: `read_chat_surface`, `move_chat`,
83
+ `minimise_chat` and `restore_chat`, plus the `describeSurface()` and
84
+ `moveTo(corner)` methods behind them.
85
+
86
+ This is the affordance nobody else can offer. Every other assistant's chat is
87
+ a surface of its own, so it has nothing to be in the way *of*; this one is
88
+ mounted in the page the user is working in.
89
+
90
+ **They report what happened, not what was asked.** A panel that fills the
91
+ screen has nowhere to move to, and a placement that places itself owns its
92
+ position, so `move_chat` answers `moved: false` with the reason and what would
93
+ work instead. `read_chat_surface` lets the agent ask before it acts rather
94
+ than learn through a failure. `moveTo` claims the axes the same way a user
95
+ drag does -- the launcher travels with the panel, the corner it opens from is
96
+ re-picked, and switching placement hands everything back.
97
+
98
+ None is stamped `x-destructive`: moving a window destroys nothing, and a
99
+ confirmation card in front of it would be worse than the move. **What it gets
100
+ instead is a notice and an undo.** A panel that rearranges itself
101
+ mid-conversation has to be both visible and reversible, so `move_chat` and
102
+ `minimise_chat` write a run notice with an Undo beside it that puts the panel
103
+ back exactly where it was -- inset, launcher inset and the corner it opens
104
+ from, which are one decision rather than three. A host calling `moveTo` or
105
+ `setCollapsed` itself gets no notice: it is arranging its own page and does
106
+ not need telling what it just did.
107
+
108
+ Run notices may now carry that one control, and only ever an undo. Anything
109
+ the user has to *decide* is a confirmation card; a notice reports something
110
+ already done.
111
+
112
+ - **`showHighlightOverlay(el, options)`** -- ring a host-page element from an
113
+ overlay drawn *outside* it, optionally dimming everything else (`scrim`) or
114
+ flowing a gradient round it (`gradient`). Returns a function that removes it.
115
+ `flash` and `focusWithFlash` accept both and route through it when either is
116
+ asked for; the plain outline is unchanged and still the default.
117
+
118
+ These are one mechanism rather than two features, and the reason is the same
119
+ in both directions. The flat ring is an `outline` on the element deliberately,
120
+ because a `box-shadow` is clipped away by any `overflow: hidden` ancestor
121
+ sharing the target's box while the helper still reports success -- but an
122
+ outline takes a *colour*, there is no `outline-image`, and anything else that
123
+ can carry a gradient is a property of the target and lands back inside
124
+ whatever is clipping it. Dimming everything else needs a surface larger than
125
+ the target, which is the same problem from the other side.
126
+
127
+ **It is inert.** The overlay takes no pointer event at the cut-out or anywhere
128
+ else: a dim that swallows clicks is a modal the user did not open, and
129
+ `highlightThenClick` has to reach the control it just pointed at. It follows
130
+ the target on scroll and resize, and under reduced motion the gradient is
131
+ still drawn but stops travelling. Themed with `--ag-ui-highlight-scrim` and
132
+ `--ag-ui-highlight-gradient`.
133
+
134
+ - **Touch affordances, decided by the pointer rather than the width.** The eight
135
+ resize grips are hidden -- a 6px edge strip is not a control, it is a trap that
136
+ eats a scroll -- and the action, tool and send buttons go to 44px, which is
137
+ what iOS and Android ask for and what 28-30px missed.
138
+
139
+ The composer states at least 16px. iOS Safari zooms the page when a control
140
+ under that takes focus, which drags the whole fixed panel with it and leaves
141
+ the user pinching back out of a chat they only wanted to type into; the
142
+ composer inherits the widget font, which is 14px by default and 13px at
143
+ compact density.
144
+
145
+ Every scroll container also contains its own overscroll, so reaching the end
146
+ of the transcript no longer scrolls the page behind it.
147
+
148
+ - **`data-small-viewport="off"`** keeps the desktop layout at every width. Every
149
+ value the small-viewport override sets is a token a host can re-state; the
150
+ trigger is a media query, which cannot read one -- so without this the
151
+ breakpoint was the only part of the placement model a consumer could not reach.
152
+
153
+ - **A small-viewport layout.** At 600px wide and below, every placement but
154
+ `embedded` becomes one full-bleed shape: edge to edge, no radius, no shadow,
155
+ no resize grips. There was previously no width-based behaviour of any kind, so
156
+ a phone got the desktop's 380x560 floating panel clamped against the screen
157
+ edges -- and that clamp had already produced one shipped defect, because any
158
+ viewport under 428px was born against it.
159
+
160
+ A phone is not an eighth placement, it is an override that collapses the
161
+ others onto one of them. `embedded` is exempt: it sits in a box the host sized
162
+ and placed, and only the host knows whether that column should become the
163
+ whole screen. The corner placements still rest at their launcher, so a
164
+ full-bleed panel is something the user opens rather than something they are
165
+ given.
166
+
167
+ The breakpoint is a width rather than a pointer test. A touch laptop is
168
+ coarse-pointered and wide; a narrow desktop window is fine-pointered and
169
+ small.
170
+
171
+ - **Tell the widget which edges of the viewport your own chrome already
172
+ occupies**, with `--ag-ui-viewport-inset-top` / `-right` / `-bottom` / `-left`.
173
+ A fixed placement covers the viewport it is given and knows nothing about your
174
+ sticky header, so it came down on top of it.
175
+
176
+ Reserving that space was already possible and was the wrong shape of work:
177
+ `--ag-ui-inset` is one four-value shorthand and every placement has a different
178
+ default, so a host restated it per placement family and then kept
179
+ `--ag-ui-height` and `--ag-ui-max-height` in step by hand. Forgetting the
180
+ height half overflowed the panel off the bottom of the screen with nothing to
181
+ say so. Now `page` and `full` inset by all four edges, `sidebar` and `side` by
182
+ three, `floating` and `bottom-left` add them to their own margins, and every
183
+ height follows centrally.
184
+
185
+ They take `env(safe-area-inset-*)` verbatim. `--ag-ui-viewport-height` and
186
+ `--ag-ui-viewport-width` state the usable box outright for the case no
187
+ viewport-percentage length describes -- an on-screen keyboard changes neither
188
+ `vh` nor `dvh` nor `svh`, so a full-bleed panel on a phone has to be told.
189
+
190
+ The playground's own config bar was the first consumer: it replaced
191
+ twenty-five lines with one declaration.
192
+
193
+ ### Changed
194
+
195
+ - **Four documented placements instead of seven.** `floating`, `sidebar`, `page`
196
+ and `embedded` are the four shapes that differ structurally: a corner panel, a
197
+ docked rail, a surface that owns the screen, and a thing in your layout.
198
+
199
+ `bottom-left`, `side` and `full` **still parse and still work**, and are no
200
+ longer documented. Each turned out to be a variant of one of the four rather
201
+ than a shape of its own: `full` is `page` with `--ag-ui-content-max-width:
202
+ none`, `bottom-left` is `floating` with a different `--ag-ui-inset`, and `side`
203
+ is `sidebar` collapsing to the floating launcher instead of an edge rail.
204
+ Nothing warns and no markup breaks -- `placement` is a public attribute in
205
+ hand-written HTML with no build step to catch a removed value, so removing one
206
+ would fail silently and visually. There is simply less to choose between.
207
+
208
+ - **The corner placements now rest at their launcher on a first visit.** An
209
+ unconfigured widget mounted open, so a visitor's first page load put a 380x560
210
+ panel over the host page's own bottom-right corner, uninvited. Every corner
211
+ chat in the field rests closed and treats opening as something the user does.
212
+
213
+ A stored choice still wins in both directions, so nobody who opened the panel
214
+ finds it closed. Only `floating` and `bottom-left` are affected -- the
215
+ placements that place themselves are unchanged, because a host that docks a
216
+ sidebar or embeds the widget in its own layout has already decided it belongs
217
+ on screen. `data-start-open` restores the previous behaviour.
218
+
219
+ **This is a visible change for any host that mounts the widget without a
220
+ placement**, which is the default, and it is the reason for the attribute.
221
+
222
+ - **Where the widget sits, how big it is and which theme it wears now outlive
223
+ the tab.** These went to `sessionStorage` beside the transcript and inherited
224
+ its lifetime without earning it: the transcript is per-tab on purpose -- two
225
+ tabs are two conversations -- while a user who dragged the panel clear of
226
+ their own interface did it again in the next tab, and again after every
227
+ restart.
228
+
229
+ They are written to `localStorage` and to the per-tab store, and read from the
230
+ durable one first. The second write is not redundancy: a privacy mode can deny
231
+ `localStorage` while allowing `sessionStorage`, and losing the durable copy
232
+ should degrade to the previous behaviour rather than to no persistence at all.
233
+ An existing per-tab value is still honoured, so nothing resets on upgrade.
234
+
235
+ **Whether the widget is currently open stays per-tab.** That is a statement
236
+ about this tab rather than a preference, and carrying it across would pop the
237
+ panel open in every new tab because it was opened once somewhere else.
238
+
239
+ - **The page placement no longer collapses.** It is a dedicated route rather
240
+ than a panel on someone else's page, so there was no "away" for it to go to:
241
+ collapsing left a strip of application chrome fixed over a route that no
242
+ longer had an owner, under the one placement that also hides the launcher.
243
+
244
+ Removing the control was not enough on its own. The state has three other
245
+ ways in -- the `collapsed` property, the attribute, and a value restored from
246
+ per-tab storage that was written under a different placement -- and the
247
+ storage key is namespaced per instance, not per placement. A tab that
248
+ collapsed a floating panel and later loaded the same instance as a page would
249
+ have restored a state with no control and no launcher to undo it. So the
250
+ property and the restore are gated, switching into the placement releases a
251
+ collapsed panel, and the stylesheet neutralises the state for the one path
252
+ that reaches none of those: an attribute written straight onto the element.
253
+
254
+ A control removed from the interface is not a state removed from the model.
255
+
256
+ The embedded placement is unchanged and still collapses to its header bar,
257
+ which is an ordinary accordion for a panel that sits in a page's own flow.
258
+
259
+ - **`--ag-ui-edge-gutter` and `--ag-ui-keyboard-inset`.** The first is the gap a
260
+ resting floating panel keeps from the box the host left free, and the size cap
261
+ now subtracts the same one rather than restating it. The second overrides the
262
+ lift an on-screen keyboard earns: the widget publishes what it measured, and a
263
+ host that states this outranks it -- the two-token shape the measured viewport
264
+ height already had, and the inset half was missing.
265
+
266
+ - **`createChatSurfaceTools` and its types are exported.** `moveTo` and
267
+ `describeSurface` are public methods, so a TypeScript consumer could call them
268
+ and had nowhere to import `ChatCorner` or `ChatSurfaceReport` from. The report
269
+ also gained `draggable`, and an origin on its `viewport`, so an agent reasoning
270
+ about the room beside the panel is not mixing two coordinate frames.
271
+
272
+ ### Fixed
273
+
274
+ - **One screen-edge bound, instead of four answers to one question.** A drag
275
+ stopped the panel at 0, a resize at 0, a restore at the 24px resting gutter,
276
+ and the launcher at 8 -- so a panel dragged flush leapt a whole gutter inward
277
+ the next time anything re-placed it (a resize, an expand, a reload), and a
278
+ bubble held clear of the boundary sat beside a panel welded to it. Every
279
+ gesture now stops on the same line, 8px in, which is the bound the launcher
280
+ already had and it has it for the same reason: a rounded box with a drop
281
+ shadow sitting on the boundary has its shadow cut and its curve running into
282
+ the edge, and on a rounded screen or under a scrollbar it is genuinely
283
+ clipped. The 24px gutter keeps its own job -- where an untouched panel
284
+ *rests*, and what the size cap subtracts -- and is no longer a limit on where
285
+ a person may put one.
286
+
287
+ - **A floating panel grown to its cap sat outside the screen.** The cap was the
288
+ whole usable box while the resting inset had already spent a gutter on the
289
+ anchored corner, so the far edge landed exactly one gutter outside it -- a top
290
+ of -24 on an ordinary window, taking the header and every control in it off
291
+ the screen. With a reserved header it sat inside that instead, which is the
292
+ one thing reserving it is meant to prevent.
293
+
294
+ - **The corner a panel opened from ignored the edges a host had reserved.** The
295
+ probe compared the room on each side using the usable box's extents as though
296
+ it started at the origin, while the launcher's coordinates were the screen's
297
+ -- understating the room one way and overstating it the other by the same
298
+ reserved inset, which is enough to invert the choice rather than shade it. The
299
+ agent's own `moveTo` had the matching bug, and sent the panel under the very
300
+ chrome the reservation exists to keep it out of.
301
+
302
+ - **The reserved edges are read as lengths.** They were parsed out of the custom
303
+ property directly, which returns the token stream rather than a value: a host
304
+ stating `4rem` reserved four pixels, and one stating
305
+ `calc(56px + env(safe-area-inset-top))` -- the natural spelling of what the
306
+ token's own documentation recommends -- reserved `NaN`, which made every clamp
307
+ `NaN` and dropped the panel to its static position.
308
+
309
+ - **A stated position is measured from the layout viewport.** It was written
310
+ against the visual one, which is the box the widget is *clamped* into rather
311
+ than the box CSS resolves a fixed element's inset against. The two come apart
312
+ exactly when it matters: with a keyboard open, a panel the clamp had just held
313
+ inside the visible band was written back out behind the keyboard.
314
+
315
+ - **Undoing an agent's move erased the position it had stored.** The undo put
316
+ the panel back and then wrote the moved position to storage again, because the
317
+ helper it called returns early when there is nothing to store. The panel
318
+ returned and the next resize or reload sent it back to the corner the user had
319
+ just rejected -- and since that store now outlives the tab, so did the move.
320
+
321
+ - **The composer's history is cleared with the conversation.** Starting a new
322
+ chat, switching threads or changing `user-key` wiped the transcript and left
323
+ every turn the previous principal had typed one ArrowUp away in the composer.
324
+
325
+ - **A conversation filter that hides itself stops filtering.** A list that fell
326
+ below the threshold while a query matched nothing showed "no conversations
327
+ match that" over conversations that were right there, with no control left on
328
+ screen to clear -- and reopening the drawer did not help, only a new one would.
329
+
330
+ - **`move_chat` checks the corner it was given.** `required` in a schema is
331
+ advisory; a corner that is not one matched none of the edge tests, sent the
332
+ panel bottom-right, and was reported as a success for the corner asked for.
333
+
334
+ - **The agent's minimise offers no undo.** The notice is written into the
335
+ transcript and the collapse it describes is what hides the transcript, so the
336
+ control could only be reached from a state where it had nothing left to do.
337
+
338
+ - **A docked conversation list re-decides on a resize.** Docking is a width
339
+ decision taken only on the way in, so a window narrowed with the list open
340
+ kept a rail sized for the wide layout, with no focus trap and no backdrop.
341
+
342
+ - **The theme toggle is drawn rather than typed.** It was the moon and sun
343
+ emoji, and an emoji is painted by the platform's own font at its own weight
344
+ and colour, with ink that routinely runs outside the box laid out for it -- so
345
+ it clipped, and it was the one gold glyph in a row of white strokes. It is an
346
+ SVG path like every neighbour now.
347
+
348
+ - **The launcher keeps a little clear of the screen edge.** Flush against the
349
+ boundary, a circle with a drop shadow reads as clipped whether or not a pixel
350
+ is missing -- and on a rounded display, or under a scrollbar, it is. Not the
351
+ panel's 24px gutter, which is deliberately refused here because it would put
352
+ the corners people drag to out of reach; the margin is given up entirely on a
353
+ viewport too small to hold it.
354
+
355
+ - **The header's controls were sized by their contents**, so each came out as
356
+ wide as the glyph inside it -- five buttons of four different widths, four
357
+ pixels apart, with the theme toggle (the one emoji among the line icons) the
358
+ widest and looking like it had run into its neighbour. They are square and
359
+ equal now, from `--ag-ui-header-btn-size`, spaced by `--ag-ui-header-gap`.
360
+
361
+ Both go to 44px on a coarse pointer, which the earlier touch pass missed --
362
+ it raised the message, tool and send controls and left the header at half
363
+ their size, despite it being the only way to reach history, a new chat or the
364
+ collapse.
365
+
366
+ - **A resized panel and a dragged one disagreed about where the screen ends.**
367
+ The size cap subtracted a gutter that the drag's own limit did not, so the two
368
+ named different edges -- and once the size was capped a grip could no longer
369
+ grow the panel, which left the pull to land on the position instead and took
370
+ the whole panel down the screen. Pulling the other way stopped a gutter short
371
+ of an edge a drag could reach, leaving a band you could drag into but not
372
+ resize into, whose height changed with whatever the host had reserved.
373
+
374
+ The gutter belongs in the resting inset, which is what holds an untouched
375
+ panel clear of the edge; the cap is now simply what the host left free, which
376
+ is the limit the drag already used.
377
+
378
+ **It showed on one axis only, and the arithmetic says why**: the default panel
379
+ is 560 tall against a cap of the viewport minus 48 -- on an 800px screen with
380
+ a header reserved, 72px of headroom, so the height hit its cap almost at once.
381
+ The default width is 380 against a cap near 1230, which is 850px of headroom
382
+ nothing ever reaches. Same rule on both axes; only the vertical one was ever
383
+ felt.
384
+
385
+ - **Resizing had none of the restrictions dragging has.** A grip could be pulled
386
+ past the edges the host reserved, and pulling the *anchored* edge -- the
387
+ bottom, under the default placement -- wrote a negative inset and moved the
388
+ whole panel down the screen instead of growing it. Each edge is now bounded to
389
+ what the host left free: the gesture carries on and the panel simply stops.
390
+
391
+ A resize also persisted the size the pointer asked for rather than the one the
392
+ panel got, so the next mount restored a size it never had -- the same
393
+ disagreement between apply and commit that made the header drag jump.
394
+
395
+ - **Dragging the panel or the launcher jumped, and stopped short of every
396
+ edge.** Reported from a phone and reproduced on a desktop: the widget
397
+ followed the pointer until it passed roughly the middle of the screen, then
398
+ leapt by about the height of the host's own header bar -- and it could not be
399
+ dragged flush to any side.
400
+
401
+ Three separate causes, all of them in this release's own new work:
402
+
403
+ A CSS `inset` on a fixed element is measured from the **real** viewport
404
+ edges, and the new `--ag-ui-viewport-inset-*` support had them measured from
405
+ the box the host left free. Halfway across the screen is where the expand
406
+ corner flips -- the same point stops being written as a `top` and starts
407
+ being written as a `bottom` -- so that is where the difference appeared, as a
408
+ leap of exactly the reserved edge.
409
+
410
+ The 24px gutter a placement rests a panel at was being enforced against a
411
+ drag, which is what made it feel stuck short of every side. Staying on screen
412
+ is the part that matters, and that is still enforced.
413
+
414
+ Releasing the drag moved the launcher again, because the commit recomputed
415
+ the same sum the last move had already applied. Both now come from one
416
+ place, so the release changes nothing by construction.
417
+
418
+ Also fixed while in there: a `pointercancel` now ends a drag. On touch that
419
+ is routine rather than exceptional -- the browser takes the pointer back for
420
+ a scroll or a system gesture and never sends `pointerup` -- and without it the
421
+ move listeners stayed attached and the widget kept following a finger that
422
+ had stopped.
423
+
424
+ - **The highlight overlay could not be themed by any host that themes the
425
+ widget.** It is appended to the document body so it can escape the clipping it
426
+ exists to avoid, so a `var()` in its own inline style resolved against the
427
+ *body's* cascade -- a host setting `--ag-ui-accent` or
428
+ `--ag-ui-highlight-scrim` on `ag-ui-chat` or a wrapper, which is how everything
429
+ else here is themed, never reached it. Every token is now read from the
430
+ element being pointed at, which is where the flat ring has always read its
431
+ accent.
432
+
433
+ Its inline styles also beat any rule a host could write, and `::part` does not
434
+ reach the light DOM, so this was the one surface with no way in at all. The
435
+ ring width, the gradient's speed and the stacking order are now tokens too
436
+ (`--ag-ui-highlight-ring-width`, `--ag-ui-highlight-flow-ms`,
437
+ `--ag-ui-highlight-z-index`), with `ringWidth` and `flowMs` options over them.
438
+
439
+ - **A panel could be dragged somewhere it could not be rescued from.** The
440
+ clamps that keep it on screen measured a viewport starting at the top-left of
441
+ the display, so a panel dragged upward settled happily underneath a host's
442
+ sticky header -- and collapsing it, the one thing a user tries, replaced an
443
+ unreachable panel with an unreachable launcher. They now clamp against the
444
+ box the host actually left, reserved edges included.
445
+
446
+ - **The sidebar's edge rail ran under reserved chrome, and looked like a
447
+ stripe.** It asked for `100vh` and pinned its own bottom, so it ignored those
448
+ same reserved edges -- and the icon lives at the top of the rail, which made
449
+ the one control that reopens the panel the first thing to disappear behind a
450
+ header.
451
+
452
+ It has also been redesigned. A screen-high slab of accent carrying one small
453
+ icon is the widest collapsed state the widget has and the one that said least
454
+ about itself; it now reads as the docked edge of a panel -- the surface the
455
+ panel is made of, a border on the side it docks against, the accent kept for
456
+ the icon, and the widget's own title set down the rail as a caption
457
+ (`rail-label` part).
458
+
459
+ - **A panel no longer sizes itself to screen space the on-screen keyboard is
460
+ covering.** No CSS length describes this: a keyboard has no effect on any
461
+ viewport-percentage unit, so `100vh`, `100dvh` and `100svh` are the same
462
+ number with it up as without. A full-bleed panel sized from one of them put
463
+ its own composer behind the keyboard being typed into.
464
+
465
+ The element now tracks `visualViewport` and publishes both the visible height
466
+ and how much is hidden below it. The height shrinks the panel; the hidden gap
467
+ lifts anything anchored to the bottom, which a shorter panel does not do on
468
+ its own -- a floating widget is positioned against the layout viewport, so its
469
+ bottom edge and the launcher at that corner stayed behind the keyboard
470
+ whatever height it had. A host's own `--ag-ui-viewport-height` still outranks
471
+ the measurement, and nothing is written at all while the two viewports agree.
472
+
473
+ The same measurement fixes the clamps, which were computing which corner to
474
+ open into using space that was off the screen.
475
+
476
+ - **The chat-history list could not be dismissed under the embedded
477
+ placement.** The drawer closes when its backdrop is clicked, which is enough
478
+ wherever a strip of backdrop is showing -- but `embedded` widens the panel to
479
+ the full width of the host's box on purpose, so there was none left to hit.
480
+ What remained was Escape, which is invisible, picking a row, and New chat,
481
+ which replaces the conversation you opened the list to get back to.
482
+
483
+ The drawer header now carries a close control, exposed as the `drawer-close`
484
+ part. It is a third control in a row built for two, so it was measured at the
485
+ narrow end: at a 220px panel nothing overflows and every control stays inside
486
+ it. The title takes the slack and truncates rather than pushing them out.
487
+
488
+ - **The documented way to make a sidebar push content instead of overlaying it
489
+ did not work.** Setting `--ag-ui-position: static` and placing the element in
490
+ your own layout is what the README offers, and it put the panel at the
491
+ document origin rather than in the box you gave it -- measured 1631px above
492
+ its own host on a scrolled page, and pinned to the document's left edge rather
493
+ than the host's when docked left.
494
+
495
+ The panel is taken out of flow so the collapse can slide it out at full width,
496
+ which needs the host to be a containing block. The host was one only by
497
+ accident, because it is `position: fixed` by default; a static element
498
+ establishes nothing, so the panel resolved against the initial containing
499
+ block instead. The sidebar host now contains its own layout whatever its
500
+ position, which is a no-op in the default overlay case.
501
+
502
+ It looked correct wherever it was first tried: a full-height column at the top
503
+ of an unscrolled document is exactly where the two answers coincide. The
504
+ stylesheet's own tests could not see it either -- they match strings against
505
+ the source, and every declaration involved was already correct on its own. The
506
+ regression test measures rects in a real browser, with the host offset from
507
+ the viewport and the page scrolled.
508
+
10
509
  ## [0.34.0] — 2026-09-03
11
510
 
12
511
  ### Added
@@ -2901,7 +3400,8 @@ hosts that both arrange the page the way it expects.
2901
3400
  ### Notes
2902
3401
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
2903
3402
 
2904
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.34.0...HEAD
3403
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.35.0...HEAD
3404
+ [0.35.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.34.0...v0.35.0
2905
3405
  [0.34.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.33.1...v0.34.0
2906
3406
  [0.33.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.33.0...v0.33.1
2907
3407
  [0.33.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.32.0...v0.33.0