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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/README.md +404 -28
  3. package/dist/ag-ui-web-component.bundle.js +781 -451
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +29 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +93 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/create_http_agent.d.ts +9 -0
  10. package/dist/core/create_http_agent.d.ts.map +1 -1
  11. package/dist/core/remote_conversation_store.d.ts +8 -1
  12. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  13. package/dist/core/run_index.d.ts +8 -1
  14. package/dist/core/run_index.d.ts.map +1 -1
  15. package/dist/core/transcribe_audio.d.ts +7 -0
  16. package/dist/core/transcribe_audio.d.ts.map +1 -1
  17. package/dist/core/upload_attachment.d.ts +9 -0
  18. package/dist/core/upload_attachment.d.ts.map +1 -1
  19. package/dist/core/utils.d.ts +12 -0
  20. package/dist/core/utils.d.ts.map +1 -0
  21. package/dist/dom/animations.d.ts +51 -11
  22. package/dist/dom/animations.d.ts.map +1 -1
  23. package/dist/dom/dom_driver.d.ts +12 -7
  24. package/dist/dom/dom_driver.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1275 -520
  28. package/dist/index.js.map +3 -3
  29. package/dist/ui/styles.d.ts +1 -1
  30. package/dist/ui/styles.d.ts.map +1 -1
  31. package/dist/ui/ui_strings.d.ts +8 -1
  32. package/dist/ui/ui_strings.d.ts.map +1 -1
  33. package/dist/ui/voice_input.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/constants.ts +35 -0
  36. package/src/core/ag_ui_chat.ts +452 -55
  37. package/src/core/create_http_agent.ts +14 -3
  38. package/src/core/remote_conversation_store.ts +25 -6
  39. package/src/core/run_index.ts +27 -5
  40. package/src/core/transcribe_audio.ts +20 -5
  41. package/src/core/upload_attachment.ts +13 -0
  42. package/src/core/utils.ts +18 -0
  43. package/src/dom/animations.ts +175 -31
  44. package/src/dom/dom_driver.ts +18 -12
  45. package/src/index.ts +4 -0
  46. package/src/ui/styles.ts +751 -421
  47. package/src/ui/ui_strings.ts +9 -1
  48. package/src/ui/voice_input.ts +7 -1
  49. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var ELEMENT_TAG = "ag-ui-chat";
3
3
  var SUBMIT_EVENT = "ag-ui-submit";
4
4
  var TOGGLE_EVENT = "ag-ui-toggle";
5
+ var UNREAD_EVENT = "ag-ui-unread";
5
6
  var STATE_EVENT = "ag-ui-state";
6
7
  var ATTACHMENT_EVENT = "ag-ui-attachments";
7
8
  var MESSAGE_ROLE = {
@@ -34,6 +35,11 @@ var TOOL_DISPLAY = {
34
35
  };
35
36
  var COMPACTION_ACTIVITY_TYPE = "compaction";
36
37
  var LOAD_CAPABILITY_TOOL = "load_capability";
38
+ var ICON_SEND = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19.5V5m-6.5 6.5L12 5l6.5 6.5"/></svg>`;
39
+ var ICON_STOP = `<svg class="glyph glyph--solid" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2.5"/></svg>`;
40
+ var ICON_ATTACH = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 8.5V15a5 5 0 0 1-10 0V7a3 3 0 0 1 6 0v7.5a1 1 0 0 1-2 0V8.5"/></svg>`;
41
+ var ICON_VOICE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V7a3 3 0 0 1 3-3z"/><path d="M5 11v1a7 7 0 0 0 14 0v-1"/><path d="M12 19v3"/></svg>`;
42
+ var ICON_LAUNCHER = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H9l-5 4z"/></svg>`;
37
43
 
38
44
  // src/skills/fill_template.ts
39
45
  var PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
@@ -134,11 +140,29 @@ function setNativeChecked(el, checked) {
134
140
 
135
141
  // src/dom/animations.ts
136
142
  var ACCENT = "#4f46e5";
143
+ var ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
144
+ var ACCENT_PROPERTY = "--ag-ui-accent";
137
145
  function delay(ms) {
138
146
  return new Promise((resolve) => {
139
147
  setTimeout(resolve, ms);
140
148
  });
141
149
  }
150
+ function prefersReducedMotion() {
151
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
152
+ }
153
+ function motionDelay(ms) {
154
+ if (ms <= 0 || prefersReducedMotion()) {
155
+ return Promise.resolve();
156
+ }
157
+ return delay(ms);
158
+ }
159
+ function accentColor(el, fallback) {
160
+ const themed = window.getComputedStyle(el).getPropertyValue(ACCENT_PROPERTY).trim();
161
+ return themed === "" ? fallback : themed;
162
+ }
163
+ function ringShadow(el) {
164
+ return `0 0 0 3px ${accentColor(el, ACCENT_HALO)}`;
165
+ }
142
166
  async function typeInto(el, value, options = {}) {
143
167
  const charDelayMs = options.charDelayMs ?? 35;
144
168
  setNativeValue(el, "");
@@ -156,33 +180,75 @@ async function highlightThenClick(el, options = {}) {
156
180
  const highlightMs = options.highlightMs ?? 280;
157
181
  const previousOutline = el.style.outline;
158
182
  const previousOffset = el.style.outlineOffset;
159
- el.style.outline = `2px solid ${ACCENT}`;
183
+ el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
160
184
  el.style.outlineOffset = "2px";
161
185
  await delay(highlightMs);
162
186
  el.style.outline = previousOutline;
163
187
  el.style.outlineOffset = previousOffset;
164
188
  el.click();
165
189
  }
166
- function scrollIntoCenterView(el) {
167
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
190
+ var SCROLL_SETTLE_MS = 600;
191
+ var SCROLL_START_MS = 100;
192
+ function scrollIntoCenterView(el, options = {}) {
193
+ const reduced = prefersReducedMotion();
194
+ el.scrollIntoView({
195
+ block: "center",
196
+ inline: "nearest",
197
+ behavior: reduced ? "auto" : "smooth"
198
+ });
199
+ if (reduced) {
200
+ return Promise.resolve();
201
+ }
202
+ return new Promise((resolve) => {
203
+ let timer;
204
+ const settle = () => {
205
+ clearTimeout(timer);
206
+ document.removeEventListener("scroll", started, true);
207
+ document.removeEventListener("scrollend", settle, true);
208
+ resolve();
209
+ };
210
+ const started = () => {
211
+ document.removeEventListener("scroll", started, true);
212
+ clearTimeout(timer);
213
+ timer = setTimeout(settle, options.settleMs ?? SCROLL_SETTLE_MS);
214
+ };
215
+ timer = setTimeout(settle, SCROLL_START_MS);
216
+ document.addEventListener("scroll", started, true);
217
+ document.addEventListener("scrollend", settle, true);
218
+ });
168
219
  }
169
- async function focusWithFlash(el, options = {}) {
170
- const flashMs = options.flashMs ?? 200;
171
- el.focus();
172
- const previousShadow = el.style.boxShadow;
173
- el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
174
- await delay(flashMs);
175
- el.style.boxShadow = previousShadow;
220
+ var FLASH_MS = 1200;
221
+ var FLASH_FADE_RATIO = 1 / 3;
222
+ async function flashRing(el, options, focusByDefault) {
223
+ if (options.focus ?? focusByDefault) {
224
+ el.focus({ preventScroll: true });
225
+ }
226
+ const flashMs = options.flashMs ?? FLASH_MS;
227
+ if (flashMs <= 0) {
228
+ return;
229
+ }
230
+ const previousOutline = el.style.outline;
231
+ const previousOffset = el.style.outlineOffset;
232
+ const previousTransition = el.style.transition;
233
+ const color = options.color ?? accentColor(el, ACCENT);
234
+ el.style.outline = `3px solid ${color}`;
235
+ el.style.outlineOffset = "2px";
236
+ const fadeMs = prefersReducedMotion() ? 0 : Math.round(flashMs * FLASH_FADE_RATIO);
237
+ await delay(flashMs - fadeMs);
238
+ if (fadeMs > 0) {
239
+ el.style.transition = `outline-color ${fadeMs}ms ease-out`;
240
+ el.style.outline = "3px solid transparent";
241
+ await delay(fadeMs);
242
+ }
243
+ el.style.outline = previousOutline;
244
+ el.style.outlineOffset = previousOffset;
245
+ el.style.transition = previousTransition;
176
246
  }
177
- var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
178
- function prefersReducedMotion() {
179
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
247
+ function flash(el, options = {}) {
248
+ return flashRing(el, options, false);
180
249
  }
181
- function motionDelay(ms) {
182
- if (ms <= 0 || prefersReducedMotion()) {
183
- return Promise.resolve();
184
- }
185
- return delay(ms);
250
+ function focusWithFlash(el, options = {}) {
251
+ return flashRing(el, options, true);
186
252
  }
187
253
  async function pressThenClick(el, options = {}) {
188
254
  const pressMs = options.pressMs ?? 140;
@@ -191,7 +257,7 @@ async function pressThenClick(el, options = {}) {
191
257
  const previousShadow = el.style.boxShadow;
192
258
  el.style.transition = "transform 80ms ease";
193
259
  el.style.transform = "scale(0.96)";
194
- el.style.boxShadow = RING;
260
+ el.style.boxShadow = ringShadow(el);
195
261
  await motionDelay(pressMs);
196
262
  el.style.transform = previousTransform;
197
263
  el.style.transition = previousTransition;
@@ -214,7 +280,7 @@ async function selectOption(el, value, options = {}) {
214
280
  const highlightMs = options.highlightMs ?? 220;
215
281
  const previousOutline = el.style.outline;
216
282
  const previousOffset = el.style.outlineOffset;
217
- el.style.outline = `2px solid ${ACCENT}`;
283
+ el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
218
284
  el.style.outlineOffset = "2px";
219
285
  await motionDelay(highlightMs);
220
286
  setNativeValue(el, option.value);
@@ -226,7 +292,7 @@ async function selectOption(el, value, options = {}) {
226
292
  async function toggleControl(el, checked, options = {}) {
227
293
  const flashMs = options.flashMs ?? 200;
228
294
  const previousShadow = el.style.boxShadow;
229
- el.style.boxShadow = RING;
295
+ el.style.boxShadow = ringShadow(el);
230
296
  await motionDelay(flashMs);
231
297
  setNativeChecked(el, checked);
232
298
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -449,6 +515,7 @@ var DEFAULT_UI_STRINGS = {
449
515
  newChat: "New chat",
450
516
  collapse: "Collapse",
451
517
  expand: "Expand",
518
+ expandUnread: "Expand \u2014 {count} unread",
452
519
  toggleTheme: "Toggle theme",
453
520
  copyCode: "Copy",
454
521
  copied: "Copied",
@@ -4401,151 +4468,216 @@ var SkillsMenu = class {
4401
4468
 
4402
4469
  // src/ui/styles.ts
4403
4470
  var STYLES = `
4471
+ /* \u2500\u2500 Token defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4472
+ Every public --ag-ui-* token is read here into a private --_* alias, and
4473
+ only the alias is used by the rules below.
4474
+
4475
+ The indirection is what makes ancestor theming work. Declaring the public
4476
+ name on :host would set it *on the host element*, and a value on an element
4477
+ always beats one inherited from an ancestor \u2014 so a page that put the tokens
4478
+ on a wrapper would see no effect at all, which is the one thing the API is
4479
+ documented to support. Reading the public name with the default as a var()
4480
+ fallback leaves it undeclared on the element, so an ancestor's value is
4481
+ inherited normally while a value aimed at the element still wins over it.
4482
+
4483
+ Two invariants hold this together:
4484
+ 1. No rule outside this file's :host blocks may reference a public name
4485
+ directly. A public name read outside these blocks resolves to nothing and
4486
+ silently drops its declaration rather than erroring, so the miss shows up
4487
+ as a missing colour, not a failure.
4488
+ 2. Every --_* alias in use is declared here. Aliases inherit like any custom
4489
+ property, so an undeclared one would pick up a same-named property from
4490
+ the host page; declaring it on :host shields the shadow tree from that. */
4404
4491
  :host {
4405
4492
  /* Colors */
4406
- --ag-ui-bg: #ffffff;
4407
- --ag-ui-fg: #1a1a2e;
4408
- --ag-ui-accent: #4f46e5;
4409
- --ag-ui-user-bg: #4f46e5;
4410
- --ag-ui-user-fg: #ffffff;
4411
- --ag-ui-assistant-bg: #f1f1f6;
4412
- --ag-ui-hover: #e7e7ee;
4413
- --ag-ui-input-bg: var(--ag-ui-bg);
4414
- --ag-ui-tool-bg: var(--ag-ui-assistant-bg);
4415
- --ag-ui-tool-fg: var(--ag-ui-accent);
4416
- --ag-ui-header-bg: var(--ag-ui-accent);
4417
- --ag-ui-header-fg: #ffffff;
4418
- --ag-ui-border: #e2e2ec;
4419
- --ag-ui-radius: 12px;
4493
+ --_bg: var(--ag-ui-bg, #ffffff);
4494
+ --_fg: var(--ag-ui-fg, #1a1a2e);
4495
+ --_accent: var(--ag-ui-accent, #4f46e5);
4496
+ --_user-bg: var(--ag-ui-user-bg, #4f46e5);
4497
+ --_user-fg: var(--ag-ui-user-fg, #ffffff);
4498
+ --_assistant-bg: var(--ag-ui-assistant-bg, #f1f1f6);
4499
+ --_hover: var(--ag-ui-hover, #e7e7ee);
4500
+ --_input-bg: var(--ag-ui-input-bg, var(--_bg));
4501
+ --_tool-bg: var(--ag-ui-tool-bg, var(--_assistant-bg));
4502
+ --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
4503
+ --_header-bg: var(--ag-ui-header-bg, var(--_accent));
4504
+ --_header-fg: var(--ag-ui-header-fg, #ffffff);
4505
+ --_border: var(--ag-ui-border, #e2e2ec);
4506
+ --_radius: var(--ag-ui-radius, 12px);
4507
+
4508
+ /* Body text and raised chrome. Both are referenced by the code-block copy
4509
+ button; neither had a default before, so the declarations reading them
4510
+ were dropped and the button fell back to the inherited colour over a
4511
+ transparent box. The defaults below restate exactly that, so this is a
4512
+ rename with no repaint \u2014 see the note on .code-copy. */
4513
+ --_text: var(--ag-ui-text, var(--_fg));
4514
+ --_surface: var(--ag-ui-surface, transparent);
4420
4515
 
4421
4516
  /* Status accents for tool-call cards. */
4422
- --ag-ui-success: #15803d;
4423
- --ag-ui-danger: #b91c1c;
4424
- --ag-ui-muted: #6b7280;
4517
+ --_success: var(--ag-ui-success, #15803d);
4518
+ --_danger: var(--ag-ui-danger, #b91c1c);
4519
+ --_muted: var(--ag-ui-muted, #6b7280);
4425
4520
 
4426
4521
  /* Tool-call status icon glyphs (override to re-theme) + spinner speed.
4427
4522
  The pending state is the animated ring; the settled states use these. */
4428
- --ag-ui-tool-icon-done: "\u2713";
4429
- --ag-ui-tool-icon-error: "\u2715";
4430
- --ag-ui-tool-icon-declined: "\u2298";
4431
- --ag-ui-tool-spin-duration: 0.7s;
4523
+ --_tool-icon-done: var(--ag-ui-tool-icon-done, "\u2713");
4524
+ --_tool-icon-error: var(--ag-ui-tool-icon-error, "\u2715");
4525
+ --_tool-icon-declined: var(--ag-ui-tool-icon-declined, "\u2298");
4526
+ --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);
4432
4527
 
4433
4528
  /* Answer well (opt-in via data-answer-well) \u2014 boxes a whole assistant turn. */
4434
- --ag-ui-well-bg: transparent;
4435
- --ag-ui-well-border: var(--ag-ui-border);
4529
+ --_well-bg: var(--ag-ui-well-bg, transparent);
4530
+ --_well-border: var(--ag-ui-well-border, var(--_border));
4436
4531
 
4437
4532
  /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */
4438
- --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
4439
- --ag-ui-font: inherit;
4440
- --ag-ui-font-size: 14px;
4441
- --ag-ui-code-font: ui-monospace, "SF Mono", Menlo, monospace;
4533
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));
4534
+ --_font: var(--ag-ui-font, inherit);
4535
+ --_font-size: var(--ag-ui-font-size, 14px);
4536
+ --_code-font: var(--ag-ui-code-font, ui-monospace, "SF Mono", Menlo, monospace);
4537
+
4538
+ /* Header / launcher icon box. */
4539
+ --_icon-size: var(--ag-ui-icon-size, 22px);
4540
+ --_icon-radius: var(--ag-ui-icon-radius, 4px);
4541
+
4542
+ /* Composer \u2014 one bordered surface holding the field and its tool row. */
4543
+ --_composer-radius: var(--ag-ui-composer-radius, 14px);
4544
+ --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
4545
+ --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
4546
+ --_send-size: var(--ag-ui-send-size, 30px);
4547
+ --_glyph-size: var(--ag-ui-glyph-size, 18px);
4548
+ --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
4549
+
4550
+ /* Unread badge on the launcher. */
4551
+ --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
4552
+ --_badge-fg: var(--ag-ui-badge-fg, #ffffff);
4553
+ --_badge-size: var(--ag-ui-badge-size, 18px);
4554
+ --_badge-font-size: var(--ag-ui-badge-font-size, 11px);
4555
+
4556
+ /* The floating launcher a collapsed widget shrinks to. */
4557
+ --_launcher-size: var(--ag-ui-launcher-size, 56px);
4558
+ --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
4559
+ --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
4560
+ --_launcher-radius: var(--ag-ui-launcher-radius, 50%);
4561
+ --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
4562
+ --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
4563
+
4564
+ /* Motion. One duration and two curves drive every collapse, expand and
4565
+ slide-over, so the whole widget accelerates and settles as one thing.
4566
+ The default curve is a long-tailed ease-out (a fast start that decelerates
4567
+ into place); the pop curve overshoots slightly, for something arriving. */
4568
+ --_motion: var(--ag-ui-motion, 0.28s);
4569
+ --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
4570
+ --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
4442
4571
 
4443
4572
  /* Spacing \u2014 the density preset overrides these. */
4444
- --ag-ui-space: 10px;
4445
- --ag-ui-pad: 16px;
4446
- --ag-ui-msg-pad: 8px 12px;
4447
- --ag-ui-msg-radius: 14px;
4573
+ --_space: var(--ag-ui-space, 10px);
4574
+ --_pad: var(--ag-ui-pad, 16px);
4575
+ --_msg-pad: var(--ag-ui-msg-pad, 8px 12px);
4576
+ --_msg-radius: var(--ag-ui-msg-radius, 14px);
4448
4577
 
4449
4578
  /* Layout \u2014 override from outside to dock the widget anywhere.
4450
4579
  Set --ag-ui-position: static (and place this element in your own
4451
4580
  grid/flex layout) to embed it in the page flow instead of floating. */
4452
- --ag-ui-position: fixed;
4453
- --ag-ui-z-index: 2147483000;
4454
- --ag-ui-width: 380px;
4455
- --ag-ui-height: 560px;
4456
- --ag-ui-inset: auto 24px 24px auto;
4457
- --ag-ui-max-width: calc(100vw - 48px);
4458
- --ag-ui-max-height: calc(100vh - 48px);
4581
+ --_position: var(--ag-ui-position, fixed);
4582
+ --_z-index: var(--ag-ui-z-index, 2147483000);
4583
+ --_width: var(--ag-ui-width, 380px);
4584
+ --_height: var(--ag-ui-height, 560px);
4585
+ --_inset: var(--ag-ui-inset, auto 24px 24px auto);
4586
+ --_max-width: var(--ag-ui-max-width, calc(100vw - 48px));
4587
+ --_max-height: var(--ag-ui-max-height, calc(100vh - 48px));
4459
4588
  /* Reading-column width for placement="page" (full-bleed, centred content). */
4460
- --ag-ui-content-max-width: 820px;
4461
-
4462
- position: var(--ag-ui-position);
4463
- inset: var(--ag-ui-inset);
4464
- z-index: var(--ag-ui-z-index);
4465
- width: var(--ag-ui-width);
4466
- max-width: var(--ag-ui-max-width);
4467
- height: var(--ag-ui-height);
4468
- max-height: var(--ag-ui-max-height);
4589
+ --_content-max-width: var(--ag-ui-content-max-width, 820px);
4590
+ /* Slim rail the sidebar placement collapses to. Only that placement reads
4591
+ it, but it is declared here so invariant 2 holds for every alias. */
4592
+ --_rail-width: var(--ag-ui-rail-width, 52px);
4593
+
4594
+ position: var(--_position);
4595
+ inset: var(--_inset);
4596
+ z-index: var(--_z-index);
4597
+ width: var(--_width);
4598
+ max-width: var(--_max-width);
4599
+ height: var(--_height);
4600
+ max-height: var(--_max-height);
4469
4601
  display: flex;
4470
- font-family: var(--ag-ui-font);
4471
- font-size: var(--ag-ui-font-size);
4472
- color: var(--ag-ui-fg);
4602
+ font-family: var(--_font);
4603
+ font-size: var(--_font-size);
4604
+ color: var(--_fg);
4473
4605
  }
4474
4606
 
4475
4607
  /* \u2500\u2500 Themes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4476
4608
  Themes only re-set the colour variables; layout/spacing are unaffected.
4477
4609
  theme="auto" follows the OS via prefers-color-scheme. */
4478
4610
  :host([theme="dark"]) {
4479
- --ag-ui-bg: #15151f;
4480
- --ag-ui-fg: #e8e8f2;
4481
- --ag-ui-assistant-bg: #25253a;
4482
- --ag-ui-hover: #303049;
4483
- --ag-ui-header-bg: #1f1f30;
4484
- --ag-ui-header-fg: #e8e8f2;
4485
- --ag-ui-border: #33334a;
4486
- --ag-ui-muted: #9aa0b4;
4487
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
4611
+ --_bg: var(--ag-ui-bg, #15151f);
4612
+ --_fg: var(--ag-ui-fg, #e8e8f2);
4613
+ --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
4614
+ --_hover: var(--ag-ui-hover, #303049);
4615
+ --_header-bg: var(--ag-ui-header-bg, #1f1f30);
4616
+ --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
4617
+ --_border: var(--ag-ui-border, #33334a);
4618
+ --_muted: var(--ag-ui-muted, #9aa0b4);
4619
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
4488
4620
  }
4489
4621
 
4490
4622
  @media (prefers-color-scheme: dark) {
4491
4623
  :host([theme="auto"]) {
4492
- --ag-ui-bg: #15151f;
4493
- --ag-ui-fg: #e8e8f2;
4494
- --ag-ui-assistant-bg: #25253a;
4495
- --ag-ui-hover: #303049;
4496
- --ag-ui-header-bg: #1f1f30;
4497
- --ag-ui-header-fg: #e8e8f2;
4498
- --ag-ui-border: #33334a;
4499
- --ag-ui-muted: #9aa0b4;
4500
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
4624
+ --_bg: var(--ag-ui-bg, #15151f);
4625
+ --_fg: var(--ag-ui-fg, #e8e8f2);
4626
+ --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
4627
+ --_hover: var(--ag-ui-hover, #303049);
4628
+ --_header-bg: var(--ag-ui-header-bg, #1f1f30);
4629
+ --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
4630
+ --_border: var(--ag-ui-border, #33334a);
4631
+ --_muted: var(--ag-ui-muted, #9aa0b4);
4632
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
4501
4633
  }
4502
4634
  }
4503
4635
 
4504
4636
  /* A terminal-flavoured "code" theme: dark, monospace, green accent. */
4505
4637
  :host([theme="code"]) {
4506
- --ag-ui-bg: #0d1117;
4507
- --ag-ui-fg: #c9d1d9;
4508
- --ag-ui-accent: #3fb950;
4509
- --ag-ui-user-bg: #238636;
4510
- --ag-ui-assistant-bg: #161b22;
4511
- --ag-ui-hover: #21262d;
4512
- --ag-ui-header-bg: #010409;
4513
- --ag-ui-header-fg: #c9d1d9;
4514
- --ag-ui-border: #30363d;
4515
- --ag-ui-muted: #8b949e;
4516
- --ag-ui-font: var(--ag-ui-code-font);
4517
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
4638
+ --_bg: var(--ag-ui-bg, #0d1117);
4639
+ --_fg: var(--ag-ui-fg, #c9d1d9);
4640
+ --_accent: var(--ag-ui-accent, #3fb950);
4641
+ --_user-bg: var(--ag-ui-user-bg, #238636);
4642
+ --_assistant-bg: var(--ag-ui-assistant-bg, #161b22);
4643
+ --_hover: var(--ag-ui-hover, #21262d);
4644
+ --_header-bg: var(--ag-ui-header-bg, #010409);
4645
+ --_header-fg: var(--ag-ui-header-fg, #c9d1d9);
4646
+ --_border: var(--ag-ui-border, #30363d);
4647
+ --_muted: var(--ag-ui-muted, #8b949e);
4648
+ --_font: var(--ag-ui-font, var(--_code-font));
4649
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.6));
4518
4650
  }
4519
4651
 
4520
4652
  /* \u2500\u2500 Density \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4521
4653
  :host([density="compact"]) {
4522
- --ag-ui-font-size: 13px;
4523
- --ag-ui-space: 6px;
4524
- --ag-ui-pad: 10px;
4525
- --ag-ui-msg-pad: 5px 9px;
4526
- --ag-ui-msg-radius: 10px;
4654
+ --_font-size: var(--ag-ui-font-size, 13px);
4655
+ --_space: var(--ag-ui-space, 6px);
4656
+ --_pad: var(--ag-ui-pad, 10px);
4657
+ --_msg-pad: var(--ag-ui-msg-pad, 5px 9px);
4658
+ --_msg-radius: var(--ag-ui-msg-radius, 10px);
4527
4659
  }
4528
4660
 
4529
4661
  /* \u2500\u2500 Placement presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4530
4662
  :host([placement="bottom-left"]) {
4531
- --ag-ui-inset: auto auto 24px 24px;
4663
+ --_inset: var(--ag-ui-inset, auto auto 24px 24px);
4532
4664
  }
4533
4665
 
4534
4666
  :host([placement="side"]) {
4535
- --ag-ui-inset: 0 0 0 auto;
4536
- --ag-ui-width: 420px;
4537
- --ag-ui-height: 100vh;
4538
- --ag-ui-max-height: 100vh;
4539
- --ag-ui-radius: 0;
4667
+ --_inset: var(--ag-ui-inset, 0 0 0 auto);
4668
+ --_width: var(--ag-ui-width, 420px);
4669
+ --_height: var(--ag-ui-height, 100vh);
4670
+ --_max-height: var(--ag-ui-max-height, 100vh);
4671
+ --_radius: var(--ag-ui-radius, 0);
4540
4672
  }
4541
4673
 
4542
4674
  :host([placement="full"]) {
4543
- --ag-ui-inset: 0;
4544
- --ag-ui-width: 100vw;
4545
- --ag-ui-height: 100vh;
4546
- --ag-ui-max-width: 100vw;
4547
- --ag-ui-max-height: 100vh;
4548
- --ag-ui-radius: 0;
4675
+ --_inset: var(--ag-ui-inset, 0);
4676
+ --_width: var(--ag-ui-width, 100vw);
4677
+ --_height: var(--ag-ui-height, 100vh);
4678
+ --_max-width: var(--ag-ui-max-width, 100vw);
4679
+ --_max-height: var(--ag-ui-max-height, 100vh);
4680
+ --_radius: var(--ag-ui-radius, 0);
4549
4681
  }
4550
4682
 
4551
4683
  /* Page: full-bleed background with a centred reading column. Unlike
@@ -4554,85 +4686,20 @@ var STYLES = `
4554
4686
  padding on the scroll area + composer (no per-row wrapper), so user pills
4555
4687
  still right-align and the assistant well spans the column. */
4556
4688
  :host([placement="page"]) {
4557
- --ag-ui-inset: 0;
4558
- --ag-ui-width: 100vw;
4559
- --ag-ui-height: 100vh;
4560
- --ag-ui-max-width: 100vw;
4561
- --ag-ui-max-height: 100vh;
4562
- --ag-ui-radius: 0;
4689
+ --_inset: var(--ag-ui-inset, 0);
4690
+ --_width: var(--ag-ui-width, 100vw);
4691
+ --_height: var(--ag-ui-height, 100vh);
4692
+ --_max-width: var(--ag-ui-max-width, 100vw);
4693
+ --_max-height: var(--ag-ui-max-height, 100vh);
4694
+ --_radius: var(--ag-ui-radius, 0);
4563
4695
  }
4564
4696
 
4565
4697
  :host([placement="page"]) .messages {
4566
- padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
4567
- }
4568
-
4569
- :host([placement="page"]) /* Resize handle: a corner grip on a floating panel, an edge grip on a docked
4570
- one. Absent entirely where the placement is full-bleed, since there is
4571
- nothing to drag. */
4572
- .resize-handle {
4573
- position: absolute;
4574
- z-index: 2;
4575
- background: transparent;
4576
- touch-action: none;
4577
- }
4578
-
4579
- .resize-handle:focus-visible {
4580
- outline: 2px solid var(--ag-ui-accent);
4581
- outline-offset: -2px;
4698
+ padding-inline: max(var(--_pad), calc((100% - var(--_content-max-width)) / 2));
4582
4699
  }
4583
4700
 
4584
- /* The grip sits at the corner the panel grows toward. Which corner that is
4585
- depends on the host's layout, not on placement, so the element measures it
4586
- and stamps data-resize-anchor with the edges that stay put.
4587
-
4588
- Default (bottom-right pinned, the floating case) puts the grip top-left. */
4589
- .resize-handle {
4590
- top: 0;
4591
- left: 0;
4592
- width: 14px;
4593
- height: 14px;
4594
- cursor: nwse-resize;
4595
- }
4596
-
4597
- :host([data-resize-anchor~="left"]) .resize-handle {
4598
- left: auto;
4599
- right: 0;
4600
- }
4601
-
4602
- :host([data-resize-anchor~="top"]) .resize-handle {
4603
- top: auto;
4604
- bottom: 0;
4605
- }
4606
-
4607
- /* One axis flipped means the drag runs along the other diagonal. */
4608
- :host([data-resize-anchor="top-right"]) .resize-handle,
4609
- :host([data-resize-anchor="bottom-left"]) .resize-handle {
4610
- cursor: nesw-resize;
4611
- }
4612
-
4613
- /* Docked: the placement owns the height, so only the inner edge is the user's. */
4614
- :host([placement="sidebar"]) .resize-handle,
4615
- :host([placement="side"]) .resize-handle {
4616
- top: 0;
4617
- bottom: auto;
4618
- width: 8px;
4619
- height: 100%;
4620
- cursor: ew-resize;
4621
- }
4622
-
4623
- /* Full-bleed: nothing to drag. */
4624
- :host([placement="full"]) .resize-handle,
4625
- :host([placement="page"]) .resize-handle {
4626
- display: none;
4627
- }
4628
-
4629
- .resize-handle[data-dragging] {
4630
- background: var(--ag-ui-accent);
4631
- opacity: 0.35;
4632
- }
4633
-
4634
- .input-row {
4635
- padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
4701
+ :host([placement="page"]) .input-row {
4702
+ padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
4636
4703
  }
4637
4704
 
4638
4705
  /* The rows between the message list and the composer (skill chips, the
@@ -4641,12 +4708,12 @@ var STYLES = `
4641
4708
  margin-based, so each gets its own inline axis nudged by the same gutter. */
4642
4709
  :host([placement="page"]) .skill-chips,
4643
4710
  :host([placement="page"]) .attachment-tray {
4644
- padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
4711
+ padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
4645
4712
  }
4646
4713
 
4647
4714
  :host([placement="page"]) .skill-palette,
4648
4715
  :host([placement="page"]) .skill-hint {
4649
- margin-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
4716
+ margin-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
4650
4717
  }
4651
4718
 
4652
4719
  /* In the reading column the assistant well uses the full width; the user
@@ -4661,75 +4728,179 @@ var STYLES = `
4661
4728
  --ag-ui-position: static (and place this element in your own layout) for a
4662
4729
  host-managed push instead. */
4663
4730
  :host([placement="sidebar"]) {
4664
- --ag-ui-inset: 0 0 0 auto;
4665
- --ag-ui-width: 420px;
4666
- --ag-ui-height: 100vh;
4667
- --ag-ui-max-height: 100vh;
4668
- --ag-ui-radius: 0;
4669
- --ag-ui-rail-width: 52px;
4670
- transition: width 0.28s ease;
4731
+ --_inset: var(--ag-ui-inset, 0 0 0 auto);
4732
+ --_width: var(--ag-ui-width, 420px);
4733
+ --_height: var(--ag-ui-height, 100vh);
4734
+ --_max-height: var(--ag-ui-max-height, 100vh);
4735
+ --_radius: var(--ag-ui-radius, 0);
4736
+ transition: width var(--_motion) var(--_ease);
4671
4737
  }
4672
4738
 
4673
4739
  :host([placement="sidebar"][data-side="left"]) {
4674
- --ag-ui-inset: 0 auto 0 0;
4740
+ --_inset: var(--ag-ui-inset, 0 auto 0 0);
4675
4741
  }
4676
4742
 
4743
+ /* The docked panel is pinned to the edge it docks against rather than filling
4744
+ the host as a flex child. Collapsing shrinks the host to the rail width, and
4745
+ a flex child would be squashed to 52px on the way out instead of sliding out
4746
+ at full width. */
4677
4747
  :host([placement="sidebar"]) .chat {
4678
- transition: transform 0.28s ease;
4748
+ position: absolute;
4749
+ inset: 0 0 0 auto;
4750
+ width: var(--_width);
4751
+ transform-origin: right center;
4752
+ }
4753
+
4754
+ :host([placement="sidebar"][data-side="left"]) .chat {
4755
+ inset: 0 auto 0 0;
4756
+ transform-origin: left center;
4679
4757
  }
4680
4758
 
4681
- /* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
4682
- reveal the rail. Higher specificity than the generic collapse rules, so it
4683
- wins regardless of source order. */
4759
+ /* Collapsed sidebar: shrink the host to the rail width and slide the panel out
4760
+ through the edge it docks against. Higher specificity than the generic
4761
+ collapse rules, so it wins regardless of source order. */
4684
4762
  :host([placement="sidebar"][collapsed]) {
4685
- width: var(--ag-ui-rail-width);
4763
+ width: var(--_rail-width);
4686
4764
  height: 100vh;
4687
4765
  max-height: 100vh;
4688
4766
  bottom: 0;
4767
+ pointer-events: auto;
4689
4768
  }
4690
4769
 
4691
4770
  :host([placement="sidebar"][collapsed]) .chat {
4692
- display: none;
4771
+ transform: translateX(100%);
4693
4772
  }
4694
4773
 
4695
- /* The rail is a sibling of the panel (so it survives the panel being hidden);
4696
- shown only for a collapsed sidebar. */
4697
- .rail {
4698
- display: none;
4774
+ :host([placement="sidebar"][data-side="left"][collapsed]) .chat {
4775
+ transform: translateX(-100%);
4776
+ }
4777
+
4778
+ @media (prefers-reduced-motion: reduce) {
4779
+ :host([placement="sidebar"]),
4780
+ :host([placement="sidebar"]) .chat {
4781
+ transition: none;
4782
+ }
4783
+ }
4784
+
4785
+ /* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4786
+ What a collapsed widget shrinks to: a round floating button by default, the
4787
+ sidebar's slim edge rail under that placement. A sibling of the panel, so it
4788
+ survives the panel being hidden, and the only part of a collapsed widget
4789
+ that takes pointer events. */
4790
+ .launcher {
4791
+ display: flex;
4792
+ position: absolute;
4793
+ inset: var(--_launcher-inset);
4794
+ align-items: center;
4795
+ justify-content: center;
4796
+ width: var(--_launcher-size);
4797
+ height: var(--_launcher-size);
4798
+ padding: 0;
4699
4799
  border: none;
4800
+ border-radius: var(--_launcher-radius);
4801
+ background: var(--_launcher-bg);
4802
+ color: var(--_launcher-fg);
4803
+ box-shadow: var(--_shadow);
4700
4804
  font: inherit;
4805
+ cursor: pointer;
4806
+ pointer-events: auto;
4807
+ opacity: 0;
4808
+ transform: scale(0.4);
4809
+ visibility: hidden;
4810
+ transition:
4811
+ opacity var(--_motion) var(--_ease),
4812
+ transform var(--_motion) var(--_ease-pop),
4813
+ visibility var(--_motion) var(--_ease);
4814
+ }
4815
+
4816
+ /* The launcher grows out of the corner the panel shrank into.
4817
+ It is laid out at rest rather than display:none, which is what lets it
4818
+ animate in *and* out: an element that was not rendered has no before-change
4819
+ style to transition from, and one whose display flips to none cannot
4820
+ transition at all. visibility keeps it unpaintable, untabbable and
4821
+ unclickable in between -- the expanded panel's own controls sit under it and
4822
+ must stay reachable. */
4823
+ :host([collapsed]) .launcher {
4824
+ opacity: 1;
4825
+ transform: none;
4826
+ visibility: visible;
4701
4827
  }
4702
4828
 
4703
- :host([placement="sidebar"][collapsed]) .rail {
4704
- display: flex;
4829
+ :host([collapsed]) .launcher:hover {
4830
+ transform: scale(1.06);
4831
+ }
4832
+
4833
+ :host([collapsed]) .launcher:active {
4834
+ transform: scale(0.94);
4835
+ }
4836
+
4837
+ .launcher .icon-holder {
4838
+ width: var(--_launcher-icon-size);
4839
+ height: var(--_launcher-icon-size);
4840
+ }
4841
+
4842
+ /* The unread badge rides the launcher's top-right corner. The ring in the
4843
+ widget's own background is what separates it from the launcher underneath,
4844
+ whatever colour the host themes either one. */
4845
+ .launcher-badge {
4705
4846
  position: absolute;
4847
+ top: -2px;
4848
+ right: -2px;
4849
+ display: inline-flex;
4850
+ align-items: center;
4851
+ justify-content: center;
4852
+ box-sizing: border-box;
4853
+ min-width: var(--_badge-size);
4854
+ height: var(--_badge-size);
4855
+ padding: 0 5px;
4856
+ border-radius: 999px;
4857
+ background: var(--_badge-bg);
4858
+ color: var(--_badge-fg);
4859
+ font-size: var(--_badge-font-size);
4860
+ font-weight: 600;
4861
+ line-height: 1;
4862
+ box-shadow: 0 0 0 2px var(--_bg);
4863
+ }
4864
+
4865
+ .launcher-badge[hidden] {
4866
+ display: none;
4867
+ }
4868
+
4869
+ /* The sidebar collapses to an edge rail instead: full height, square, flush
4870
+ against the dock. It slides in with the panel rather than popping. */
4871
+ :host([placement="sidebar"][collapsed]) .launcher {
4706
4872
  inset: 0;
4873
+ width: auto;
4874
+ height: auto;
4707
4875
  align-items: flex-start;
4708
- justify-content: center;
4709
4876
  padding-top: 16px;
4710
- border: 1px solid var(--ag-ui-border);
4711
- background: var(--ag-ui-header-bg);
4712
- color: var(--ag-ui-header-fg);
4713
- cursor: pointer;
4877
+ border: 1px solid var(--_border);
4878
+ border-radius: 0;
4879
+ background: var(--_header-bg);
4880
+ color: var(--_header-fg);
4881
+ box-shadow: none;
4882
+ transform: none;
4714
4883
  }
4715
4884
 
4885
+ /* Every transition in this file is timed by --_motion, so collapsing it to a
4886
+ frame is the whole reduced-motion story: states still change (and display
4887
+ still flips at the end of its discrete transition), nothing travels. */
4716
4888
  @media (prefers-reduced-motion: reduce) {
4717
- :host([placement="sidebar"]),
4718
- :host([placement="sidebar"]) .chat {
4719
- transition: none;
4889
+ :host {
4890
+ --_motion: var(--ag-ui-motion, 0.001s);
4720
4891
  }
4721
4892
  }
4722
4893
 
4723
4894
  /* Embedded: drop the floating chrome and the high z-index stacking context so
4724
4895
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
4725
4896
  :host([placement="embedded"]) {
4726
- --ag-ui-position: static;
4727
- --ag-ui-width: 100%;
4728
- --ag-ui-height: 100%;
4729
- --ag-ui-max-width: 100%;
4730
- --ag-ui-max-height: 100%;
4731
- --ag-ui-shadow: none;
4732
- --ag-ui-z-index: auto;
4897
+ --_position: var(--ag-ui-position, static);
4898
+ --_width: var(--ag-ui-width, 100%);
4899
+ --_height: var(--ag-ui-height, 100%);
4900
+ --_max-width: var(--ag-ui-max-width, 100%);
4901
+ --_max-height: var(--ag-ui-max-height, 100%);
4902
+ --_shadow: var(--ag-ui-shadow, none);
4903
+ --_z-index: var(--ag-ui-z-index, auto);
4733
4904
  }
4734
4905
 
4735
4906
  .chat {
@@ -4738,10 +4909,10 @@ var STYLES = `
4738
4909
  flex-direction: column;
4739
4910
  flex: 1;
4740
4911
  min-height: 0;
4741
- background: var(--ag-ui-bg);
4742
- border: 1px solid var(--ag-ui-border);
4743
- border-radius: var(--ag-ui-radius);
4744
- box-shadow: var(--ag-ui-shadow);
4912
+ background: var(--_bg);
4913
+ border: 1px solid var(--_border);
4914
+ border-radius: var(--_radius);
4915
+ box-shadow: var(--_shadow);
4745
4916
  overflow: hidden;
4746
4917
  }
4747
4918
 
@@ -4751,9 +4922,9 @@ var STYLES = `
4751
4922
  justify-content: space-between;
4752
4923
  gap: 8px;
4753
4924
  padding: 10px 14px;
4754
- border-bottom: 1px solid var(--ag-ui-border);
4755
- background: var(--ag-ui-header-bg);
4756
- color: var(--ag-ui-header-fg);
4925
+ border-bottom: 1px solid var(--_border);
4926
+ background: var(--_header-bg);
4927
+ color: var(--_header-fg);
4757
4928
  }
4758
4929
 
4759
4930
  .header-title {
@@ -4772,8 +4943,8 @@ var STYLES = `
4772
4943
  align-items: center;
4773
4944
  justify-content: center;
4774
4945
  flex: none;
4775
- width: var(--ag-ui-icon-size, 22px);
4776
- height: var(--ag-ui-icon-size, 22px);
4946
+ width: var(--_icon-size);
4947
+ height: var(--_icon-size);
4777
4948
  line-height: 1;
4778
4949
  }
4779
4950
 
@@ -4781,7 +4952,7 @@ var STYLES = `
4781
4952
  width: 100%;
4782
4953
  height: 100%;
4783
4954
  object-fit: contain;
4784
- border-radius: var(--ag-ui-icon-radius, 4px);
4955
+ border-radius: var(--_icon-radius);
4785
4956
  }
4786
4957
 
4787
4958
  .header-controls {
@@ -4807,36 +4978,87 @@ var STYLES = `
4807
4978
  background: rgba(255, 255, 255, 0.18);
4808
4979
  }
4809
4980
 
4810
- /* Collapsed: keep just the header bar \u2014 hide the whole body, and let the host
4811
- shrink to the header. (A host can also fully hide the element itself.) */
4981
+ /* \u2500\u2500 Collapse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4982
+ Collapsing shrinks the widget to the round floating launcher: the panel
4983
+ scales down toward the launcher's corner and fades out, the launcher pops in
4984
+ from that same point. Both halves are transform and opacity only, so the
4985
+ morph runs on the compositor and never reflows the host page.
4986
+
4987
+ The host box keeps its expanded size. Animating it would animate layout, and
4988
+ a dragged --ag-ui-width would then fight the launcher's own size. Nothing
4989
+ paints there once the panel is gone, so the box only has to stop swallowing
4990
+ clicks: pointer events go to none, and the launcher takes them back.
4991
+
4992
+ Two placements collapse to something else and restore what they need:
4993
+ "sidebar" slides to its rail (below), while "embedded" and "page" keep the
4994
+ original header bar \u2014 embedded is laid out by the host page, where a
4995
+ floating circle would escape the layout, and page is a full-screen route
4996
+ with no corner to float in. */
4812
4997
  :host([collapsed]) {
4998
+ pointer-events: none;
4999
+ }
5000
+
5001
+ :host([collapsed]) .chat {
5002
+ opacity: 0;
5003
+ transform: scale(0.94);
5004
+ visibility: hidden;
5005
+ }
5006
+
5007
+ /* visibility is what keeps the panel out of the tab order and the a11y tree at
5008
+ rest without display:none killing the transition. It interpolates so that any
5009
+ progress below 1 still counts as visible: the panel stays on screen for the
5010
+ whole collapse and flips hidden exactly at the end, and on expand it is
5011
+ visible from the first frame. */
5012
+ .chat {
5013
+ transform-origin: bottom right;
5014
+ transition:
5015
+ opacity var(--_motion) var(--_ease),
5016
+ transform var(--_motion) var(--_ease),
5017
+ visibility var(--_motion) var(--_ease);
5018
+ }
5019
+
5020
+ :host([placement="bottom-left"]) .chat {
5021
+ transform-origin: bottom left;
5022
+ }
5023
+
5024
+ /* The two in-flow placements keep the original collapse: hide the body, let
5025
+ the host shrink to the header bar. */
5026
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) {
4813
5027
  height: auto;
4814
5028
  max-height: none;
5029
+ pointer-events: auto;
4815
5030
  }
4816
5031
 
4817
- :host([collapsed]) .messages,
4818
- :host([collapsed]) .input-row,
4819
- :host([collapsed]) .skill-chips,
4820
- :host([collapsed]) .skill-palette,
4821
- :host([collapsed]) .skill-hint {
4822
- display: none;
5032
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .chat {
5033
+ opacity: 1;
5034
+ transform: none;
5035
+ visibility: visible;
4823
5036
  }
4824
5037
 
4825
- /* Edge-docked placements pin top *and* bottom, which would otherwise keep the
4826
- panel full-height when collapsed; unpin the bottom so it shrinks to the
4827
- header. (Floating/bottom-left pin only the bottom, so they already shrink.) */
4828
- :host([collapsed][placement="side"]),
4829
- :host([collapsed][placement="full"]) {
4830
- bottom: auto;
5038
+ /* These two keep the header bar, so the launcher must stay out of the way \u2014
5039
+ an embedded host is position: static, which would otherwise leave an
5040
+ absolutely-positioned circle to escape the layout entirely and land against
5041
+ whatever the page happens to position. */
5042
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .launcher {
5043
+ visibility: hidden;
5044
+ opacity: 0;
5045
+ }
5046
+
5047
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
5048
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
5049
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
5050
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-palette,
5051
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-hint {
5052
+ display: none;
4831
5053
  }
4832
5054
 
4833
5055
  .messages {
4834
5056
  flex: 1;
4835
5057
  overflow-y: auto;
4836
- padding: var(--ag-ui-pad);
5058
+ padding: var(--_pad);
4837
5059
  display: flex;
4838
5060
  flex-direction: column;
4839
- gap: var(--ag-ui-space);
5061
+ gap: var(--_space);
4840
5062
  }
4841
5063
 
4842
5064
  /* Empty-state region (slot): centred while it's the only thing in the
@@ -4844,7 +5066,7 @@ var STYLES = `
4844
5066
  .empty {
4845
5067
  margin: auto;
4846
5068
  text-align: center;
4847
- color: var(--ag-ui-muted);
5069
+ color: var(--_muted);
4848
5070
  }
4849
5071
 
4850
5072
  .empty[hidden] {
@@ -4860,22 +5082,22 @@ var STYLES = `
4860
5082
  .answer {
4861
5083
  display: flex;
4862
5084
  flex-direction: column;
4863
- gap: var(--ag-ui-space);
5085
+ gap: var(--_space);
4864
5086
  align-self: stretch;
4865
5087
  min-width: 0;
4866
5088
  }
4867
5089
 
4868
5090
  :host([data-answer-well]) .answer {
4869
- padding: var(--ag-ui-pad);
4870
- background: var(--ag-ui-well-bg);
4871
- border: 1px solid var(--ag-ui-well-border);
4872
- border-radius: var(--ag-ui-radius);
5091
+ padding: var(--_pad);
5092
+ background: var(--_well-bg);
5093
+ border: 1px solid var(--_well-border);
5094
+ border-radius: var(--_radius);
4873
5095
  }
4874
5096
 
4875
5097
  .message {
4876
5098
  max-width: 80%;
4877
- padding: var(--ag-ui-msg-pad);
4878
- border-radius: var(--ag-ui-msg-radius);
5099
+ padding: var(--_msg-pad);
5100
+ border-radius: var(--_msg-radius);
4879
5101
  line-height: 1.4;
4880
5102
  white-space: pre-wrap;
4881
5103
  word-break: break-word;
@@ -4913,14 +5135,14 @@ var STYLES = `
4913
5135
 
4914
5136
  .message--user {
4915
5137
  align-self: flex-end;
4916
- background: var(--ag-ui-user-bg);
4917
- color: var(--ag-ui-user-fg);
5138
+ background: var(--_user-bg);
5139
+ color: var(--_user-fg);
4918
5140
  border-bottom-right-radius: 4px;
4919
5141
  }
4920
5142
 
4921
5143
  .message--assistant {
4922
5144
  align-self: flex-start;
4923
- background: var(--ag-ui-assistant-bg);
5145
+ background: var(--_assistant-bg);
4924
5146
  border-bottom-left-radius: 4px;
4925
5147
  /* Assistant bubbles hold rendered markdown/HTML, so collapse the source
4926
5148
  whitespace the renderer leaves between block tags. */
@@ -4950,7 +5172,7 @@ var STYLES = `
4950
5172
  }
4951
5173
 
4952
5174
  .message--assistant a {
4953
- color: var(--ag-ui-accent);
5175
+ color: var(--_accent);
4954
5176
  text-decoration: underline;
4955
5177
  }
4956
5178
 
@@ -4965,8 +5187,8 @@ var STYLES = `
4965
5187
  .message--assistant pre {
4966
5188
  padding: 8px 10px;
4967
5189
  overflow: auto;
4968
- background: var(--ag-ui-bg);
4969
- border: 1px solid var(--ag-ui-border);
5190
+ background: var(--_bg);
5191
+ border: 1px solid var(--_border);
4970
5192
  border-radius: 6px;
4971
5193
  }
4972
5194
 
@@ -4982,6 +5204,13 @@ var STYLES = `
4982
5204
  position: relative;
4983
5205
  }
4984
5206
 
5207
+ /* This button is the only reader of --ag-ui-surface and --ag-ui-text, and
5208
+ neither had a default until the alias layer gave them one. Both references
5209
+ used to resolve to nothing, dropping the declaration: the background fell
5210
+ back to the initial transparent, and the hover/copied colour to the
5211
+ inherited body colour. The defaults chosen upstream (transparent, and the
5212
+ body foreground) reproduce that, so the rename repaints nothing. A raised
5213
+ surface behind the button is a separate design question. */
4985
5214
  .code-copy {
4986
5215
  position: absolute;
4987
5216
  top: 4px;
@@ -4990,9 +5219,9 @@ var STYLES = `
4990
5219
  font: inherit;
4991
5220
  font-size: 0.75em;
4992
5221
  line-height: 1.6;
4993
- color: var(--ag-ui-muted);
4994
- background: var(--ag-ui-surface);
4995
- border: 1px solid var(--ag-ui-border);
5222
+ color: var(--_muted);
5223
+ background: var(--_surface);
5224
+ border: 1px solid var(--_border);
4996
5225
  border-radius: 4px;
4997
5226
  cursor: pointer;
4998
5227
  opacity: 0;
@@ -5007,24 +5236,24 @@ var STYLES = `
5007
5236
  }
5008
5237
 
5009
5238
  .code-copy:hover {
5010
- color: var(--ag-ui-text);
5011
- background: var(--ag-ui-hover);
5239
+ color: var(--_text);
5240
+ background: var(--_hover);
5012
5241
  }
5013
5242
 
5014
5243
  .code-copy[data-state="copied"] {
5015
5244
  opacity: 1;
5016
- color: var(--ag-ui-text);
5245
+ color: var(--_text);
5017
5246
  }
5018
5247
 
5019
5248
  .code-copy[data-state="failed"] {
5020
5249
  opacity: 1;
5021
- color: var(--ag-ui-danger, var(--ag-ui-text));
5250
+ color: var(--_danger, var(--_text));
5022
5251
  }
5023
5252
 
5024
5253
  .message--assistant blockquote {
5025
5254
  padding-left: 10px;
5026
- border-left: 3px solid var(--ag-ui-border);
5027
- color: var(--ag-ui-muted);
5255
+ border-left: 3px solid var(--_border);
5256
+ color: var(--_muted);
5028
5257
  }
5029
5258
 
5030
5259
  /* Markdown tables. table/thead/tbody/tr/th/td are all in
@@ -5045,13 +5274,13 @@ var STYLES = `
5045
5274
  .message--assistant th,
5046
5275
  .message--assistant td {
5047
5276
  padding: 6px 10px;
5048
- border: 1px solid var(--ag-ui-border);
5277
+ border: 1px solid var(--_border);
5049
5278
  text-align: left;
5050
5279
  vertical-align: top;
5051
5280
  }
5052
5281
 
5053
5282
  .message--assistant th {
5054
- background: var(--ag-ui-hover);
5283
+ background: var(--_hover);
5055
5284
  font-weight: 600;
5056
5285
  }
5057
5286
 
@@ -5061,7 +5290,7 @@ var STYLES = `
5061
5290
  gap: 4px;
5062
5291
  align-items: center;
5063
5292
  padding: 12px 14px;
5064
- background: var(--ag-ui-assistant-bg);
5293
+ background: var(--_assistant-bg);
5065
5294
  border-radius: 14px;
5066
5295
  border-bottom-left-radius: 4px;
5067
5296
  }
@@ -5070,7 +5299,7 @@ var STYLES = `
5070
5299
  width: 6px;
5071
5300
  height: 6px;
5072
5301
  border-radius: 50%;
5073
- background: var(--ag-ui-muted);
5302
+ background: var(--_muted);
5074
5303
  animation: ag-ui-pending 1.2s infinite ease-in-out both;
5075
5304
  }
5076
5305
 
@@ -5103,7 +5332,7 @@ var STYLES = `
5103
5332
  flex-direction: column;
5104
5333
  gap: 4px;
5105
5334
  font-size: 12px;
5106
- color: var(--ag-ui-muted);
5335
+ color: var(--_muted);
5107
5336
  }
5108
5337
 
5109
5338
  .thoughts-toggle {
@@ -5114,7 +5343,7 @@ var STYLES = `
5114
5343
  font: inherit;
5115
5344
  font-size: 12px;
5116
5345
  font-weight: 600;
5117
- color: var(--ag-ui-muted);
5346
+ color: var(--_muted);
5118
5347
  cursor: pointer;
5119
5348
  }
5120
5349
 
@@ -5139,7 +5368,7 @@ var STYLES = `
5139
5368
  .thoughts-body {
5140
5369
  margin: 0;
5141
5370
  padding: 4px 0 4px 10px;
5142
- border-left: 2px solid var(--ag-ui-border);
5371
+ border-left: 2px solid var(--_border);
5143
5372
  max-height: 220px;
5144
5373
  overflow: auto;
5145
5374
  white-space: pre-wrap;
@@ -5168,9 +5397,9 @@ var STYLES = `
5168
5397
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
5169
5398
  padding: 8px 10px;
5170
5399
  border-radius: 8px;
5171
- background: var(--ag-ui-tool-bg);
5172
- border: 1px solid var(--ag-ui-border);
5173
- color: var(--ag-ui-tool-fg);
5400
+ background: var(--_tool-bg);
5401
+ border: 1px solid var(--_border);
5402
+ color: var(--_tool-fg);
5174
5403
  }
5175
5404
 
5176
5405
  .tool-call-head {
@@ -5203,10 +5432,10 @@ var STYLES = `
5203
5432
 
5204
5433
  /* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
5205
5434
  .tool-call[data-status="pending"] .tool-call-icon {
5206
- border: 2px solid var(--ag-ui-muted);
5435
+ border: 2px solid var(--_muted);
5207
5436
  border-top-color: transparent;
5208
5437
  border-radius: 50%;
5209
- animation: ag-ui-tool-spin var(--ag-ui-tool-spin-duration) linear infinite;
5438
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
5210
5439
  }
5211
5440
 
5212
5441
  @keyframes ag-ui-tool-spin {
@@ -5215,18 +5444,18 @@ var STYLES = `
5215
5444
 
5216
5445
  /* Settled: a themeable glyph coloured by outcome. */
5217
5446
  .tool-call[data-status="done"] .tool-call-icon::before {
5218
- content: var(--ag-ui-tool-icon-done);
5219
- color: var(--ag-ui-success);
5447
+ content: var(--_tool-icon-done);
5448
+ color: var(--_success);
5220
5449
  }
5221
5450
 
5222
5451
  .tool-call[data-status="error"] .tool-call-icon::before {
5223
- content: var(--ag-ui-tool-icon-error);
5224
- color: var(--ag-ui-danger);
5452
+ content: var(--_tool-icon-error);
5453
+ color: var(--_danger);
5225
5454
  }
5226
5455
 
5227
5456
  .tool-call[data-status="declined"] .tool-call-icon::before {
5228
- content: var(--ag-ui-tool-icon-declined);
5229
- color: var(--ag-ui-muted);
5457
+ content: var(--_tool-icon-declined);
5458
+ color: var(--_muted);
5230
5459
  }
5231
5460
 
5232
5461
  @media (prefers-reduced-motion: reduce) {
@@ -5252,19 +5481,19 @@ var STYLES = `
5252
5481
  font-size: 11px;
5253
5482
  font-weight: 600;
5254
5483
  background: rgba(127, 127, 127, 0.16);
5255
- color: var(--ag-ui-muted);
5484
+ color: var(--_muted);
5256
5485
  }
5257
5486
 
5258
5487
  .tool-call[data-status="done"] .tool-call-status {
5259
- color: var(--ag-ui-success);
5488
+ color: var(--_success);
5260
5489
  }
5261
5490
 
5262
5491
  .tool-call[data-status="error"] .tool-call-status {
5263
- color: var(--ag-ui-danger);
5492
+ color: var(--_danger);
5264
5493
  }
5265
5494
 
5266
5495
  .tool-call[data-status="declined"] .tool-call-status {
5267
- color: var(--ag-ui-muted);
5496
+ color: var(--_muted);
5268
5497
  }
5269
5498
 
5270
5499
  .tool-call-body {
@@ -5286,7 +5515,7 @@ var STYLES = `
5286
5515
  font-weight: 700;
5287
5516
  letter-spacing: 0.06em;
5288
5517
  text-transform: uppercase;
5289
- color: var(--ag-ui-muted);
5518
+ color: var(--_muted);
5290
5519
  }
5291
5520
 
5292
5521
  /* The record of a human decision on a gated call. An approved call used to
@@ -5294,7 +5523,7 @@ var STYLES = `
5294
5523
  .skill-item-token {
5295
5524
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
5296
5525
  font-size: 0.92em;
5297
- color: var(--ag-ui-accent);
5526
+ color: var(--_accent);
5298
5527
  margin-right: 6px;
5299
5528
  }
5300
5529
 
@@ -5302,7 +5531,7 @@ var STYLES = `
5302
5531
  flex: none;
5303
5532
  font-size: 11px;
5304
5533
  font-style: italic;
5305
- color: var(--ag-ui-muted);
5534
+ color: var(--_muted);
5306
5535
  }
5307
5536
 
5308
5537
  .tool-call-args,
@@ -5311,12 +5540,12 @@ var STYLES = `
5311
5540
  padding: 6px 8px;
5312
5541
  max-height: 160px;
5313
5542
  overflow: auto;
5314
- background: var(--ag-ui-bg);
5315
- border: 1px solid var(--ag-ui-border);
5543
+ background: var(--_bg);
5544
+ border: 1px solid var(--_border);
5316
5545
  border-radius: 6px;
5317
5546
  white-space: pre-wrap;
5318
5547
  word-break: break-word;
5319
- color: var(--ag-ui-fg);
5548
+ color: var(--_fg);
5320
5549
  }
5321
5550
 
5322
5551
  /* Display modes are pure visibility over one DOM shape, selected from the host
@@ -5363,7 +5592,7 @@ var STYLES = `
5363
5592
  background: none;
5364
5593
  font: inherit;
5365
5594
  font-weight: 600;
5366
- color: var(--ag-ui-accent);
5595
+ color: var(--_accent);
5367
5596
  cursor: pointer;
5368
5597
  }
5369
5598
 
@@ -5386,13 +5615,12 @@ var STYLES = `
5386
5615
  }
5387
5616
 
5388
5617
  .resize-handle:focus-visible {
5389
- outline: 2px solid var(--ag-ui-accent);
5618
+ outline: 2px solid var(--_accent);
5390
5619
  outline-offset: -2px;
5391
5620
  }
5392
5621
 
5393
5622
  /* The grip sits on the corner opposite the panel's anchor, because a resize
5394
- measures from whichever edge is not moving. Selected from the host attribute,
5395
- so switching placement moves the grip immediately.
5623
+ measures from whichever edge is not moving.
5396
5624
 
5397
5625
  Default is floating: pinned bottom-right, so the grip is top-left. */
5398
5626
  .resize-handle {
@@ -5403,7 +5631,9 @@ var STYLES = `
5403
5631
  cursor: nwse-resize;
5404
5632
  }
5405
5633
 
5406
- /* Embedded sits in normal flow, pinned top-left, so it grows bottom-right. */
5634
+ /* Placement is only the opening guess, and these two rules are all it is good
5635
+ for. Embedded sits in normal flow, pinned top-left, so it grows bottom-right;
5636
+ bottom-left is pinned there, so its free corner is top-right. */
5407
5637
  :host([placement="embedded"]) .resize-handle {
5408
5638
  top: auto;
5409
5639
  left: auto;
@@ -5412,16 +5642,64 @@ var STYLES = `
5412
5642
  cursor: nwse-resize;
5413
5643
  }
5414
5644
 
5415
- /* Pinned bottom-left, so the free corner is top-right. */
5416
5645
  :host([placement="bottom-left"]) .resize-handle {
5417
5646
  left: auto;
5418
5647
  right: 0;
5419
5648
  cursor: nesw-resize;
5420
5649
  }
5421
5650
 
5422
- /* Docked: the placement owns the height, so only the inner edge is the user's. */
5651
+ /* Then the measurement corrects it. Which edges are held still belongs to the
5652
+ host's layout rather than to placement -- a floating panel a host right-aligns
5653
+ is anchored bottom-left, not bottom-right -- so the element measures the edges
5654
+ that stay put and stamps them here as "<y>-<x>". Equal specificity to the
5655
+ rules above, so source order is what lets the measured value win.
5656
+
5657
+ These must not be written as [data-resize-anchor~="left"]: the stamped value
5658
+ is one hyphenated token, and ~= matches whitespace-separated words, so such a
5659
+ selector cannot ever match. It did not, which left the grip drawn at the
5660
+ corner that moves while the cursor pointed along the right diagonal.
5661
+
5662
+ Each rule also sets both sides of its axis rather than only the side it
5663
+ moves. One that flipped a single way could not undo a placement guess that
5664
+ had flipped the other, which put the grip on the anchored corner for an
5665
+ embedded panel its host right-aligns. */
5666
+ :host([data-resize-anchor$="-left"]) .resize-handle {
5667
+ left: auto;
5668
+ right: 0;
5669
+ }
5670
+
5671
+ :host([data-resize-anchor$="-right"]) .resize-handle {
5672
+ left: 0;
5673
+ right: auto;
5674
+ }
5675
+
5676
+ :host([data-resize-anchor^="top-"]) .resize-handle {
5677
+ top: auto;
5678
+ bottom: 0;
5679
+ }
5680
+
5681
+ :host([data-resize-anchor^="bottom-"]) .resize-handle {
5682
+ top: 0;
5683
+ bottom: auto;
5684
+ }
5685
+
5686
+ /* One axis flipped means the drag runs along the other diagonal. */
5687
+ :host([data-resize-anchor="top-left"]) .resize-handle,
5688
+ :host([data-resize-anchor="bottom-right"]) .resize-handle {
5689
+ cursor: nwse-resize;
5690
+ }
5691
+
5692
+ :host([data-resize-anchor="top-right"]) .resize-handle,
5693
+ :host([data-resize-anchor="bottom-left"]) .resize-handle {
5694
+ cursor: nesw-resize;
5695
+ }
5696
+
5697
+ /* Docked: the placement owns the height, so only the inner edge is the user's --
5698
+ an edge, not a corner, so this outranks the measured anchor by coming after. */
5423
5699
  :host([placement="sidebar"]) .resize-handle,
5424
5700
  :host([placement="side"]) .resize-handle {
5701
+ top: 0;
5702
+ bottom: auto;
5425
5703
  width: 8px;
5426
5704
  height: 100%;
5427
5705
  cursor: ew-resize;
@@ -5440,106 +5718,191 @@ var STYLES = `
5440
5718
  }
5441
5719
 
5442
5720
  .resize-handle[data-dragging] {
5443
- background: var(--ag-ui-accent);
5721
+ background: var(--_accent);
5444
5722
  opacity: 0.35;
5445
5723
  }
5446
5724
 
5725
+ /* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5726
+ One surface owns the border, the background and the focus ring; the field and
5727
+ its tool row sit inside it. The row used to be four siblings stretched to the
5728
+ textarea's height, which gave a paperclip the same visual weight as the field
5729
+ and turned Send into a full-height slab. */
5447
5730
  .input-row {
5448
5731
  display: flex;
5449
- gap: 8px;
5450
5732
  padding: 12px;
5451
- border-top: 1px solid var(--ag-ui-border);
5733
+ border-top: 1px solid var(--_border);
5452
5734
  }
5453
5735
 
5454
- .input {
5736
+ .composer {
5455
5737
  flex: 1;
5738
+ min-width: 0;
5739
+ display: flex;
5740
+ flex-direction: column;
5741
+ gap: 4px;
5742
+ padding: 6px 6px 6px 10px;
5743
+ background: var(--_input-bg);
5744
+ border: 1px solid var(--_border);
5745
+ border-radius: var(--_composer-radius);
5746
+ transition: border-color var(--_motion) var(--_ease);
5747
+ }
5748
+
5749
+ .composer:focus-within {
5750
+ border-color: var(--_accent);
5751
+ }
5752
+
5753
+ /* The field grows with its content (sized by #autoGrow) up to the ceiling,
5754
+ then scrolls. border-box keeps that measurement stable: with content-box the
5755
+ padding would be added to every scrollHeight read and the field would creep
5756
+ taller on each keystroke. */
5757
+ .input {
5758
+ box-sizing: border-box;
5456
5759
  resize: none;
5457
- background: var(--ag-ui-input-bg);
5458
- border: 1px solid var(--ag-ui-border);
5459
- border-radius: 8px;
5460
- padding: 8px 10px;
5760
+ max-height: var(--_composer-max-height);
5761
+ overflow-y: auto;
5762
+ padding: 6px 4px 2px;
5763
+ background: transparent;
5764
+ border: none;
5461
5765
  font: inherit;
5462
5766
  color: inherit;
5463
5767
  outline: none;
5464
5768
  }
5465
5769
 
5466
- .input:focus {
5467
- border-color: var(--ag-ui-accent);
5770
+ .composer-tools {
5771
+ display: flex;
5772
+ align-items: center;
5773
+ gap: 2px;
5468
5774
  }
5469
5775
 
5470
- .send {
5776
+ /* Icon buttons: quiet at rest, so the field is what the eye lands on. */
5777
+ .attach-btn,
5778
+ .voice-btn {
5779
+ display: inline-flex;
5780
+ align-items: center;
5781
+ justify-content: center;
5782
+ width: var(--_tool-btn-size);
5783
+ height: var(--_tool-btn-size);
5784
+ padding: 0;
5471
5785
  border: none;
5472
5786
  border-radius: 8px;
5473
- padding: 0 16px;
5474
- background: var(--ag-ui-accent);
5475
- color: #ffffff;
5787
+ background: transparent;
5788
+ color: var(--_muted);
5476
5789
  font: inherit;
5477
- font-weight: 600;
5790
+ line-height: 1;
5478
5791
  cursor: pointer;
5792
+ transition:
5793
+ background-color var(--_motion) var(--_ease),
5794
+ color var(--_motion) var(--_ease);
5479
5795
  }
5480
5796
 
5481
- .send:disabled {
5482
- opacity: 0.5;
5483
- cursor: default;
5797
+ .attach-btn:hover,
5798
+ .voice-btn:hover {
5799
+ background: var(--_hover);
5800
+ color: var(--_fg);
5484
5801
  }
5485
5802
 
5486
- /* The composer button doubles as the Stop control while a run is in flight. */
5487
- .send[data-state="running"] {
5488
- background: var(--ag-ui-muted);
5803
+ .attach-btn:disabled,
5804
+ .voice-btn:disabled {
5805
+ cursor: default;
5806
+ opacity: 0.6;
5489
5807
  }
5490
5808
 
5491
- /* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5492
- /* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
5493
- .attach-btn {
5494
- border: 1px solid var(--ag-ui-border);
5495
- border-radius: 8px;
5496
- padding: 0 10px;
5497
- background: var(--ag-ui-input-bg);
5498
- color: inherit;
5809
+ /* Send closes the row on the right: a circle, the only filled control in the
5810
+ composer, so "the thing that acts" reads at a glance. */
5811
+ .send {
5812
+ margin-left: auto;
5813
+ display: inline-flex;
5814
+ align-items: center;
5815
+ justify-content: center;
5816
+ flex: none;
5817
+ width: var(--_send-size);
5818
+ height: var(--_send-size);
5819
+ padding: 0;
5820
+ border: none;
5821
+ border-radius: 50%;
5822
+ background: var(--_accent);
5823
+ color: #ffffff;
5499
5824
  font: inherit;
5825
+ line-height: 1;
5500
5826
  cursor: pointer;
5827
+ transition:
5828
+ background-color var(--_motion) var(--_ease),
5829
+ transform var(--_motion) var(--_ease-pop);
5501
5830
  }
5502
5831
 
5503
- .attach-btn:hover {
5504
- border-color: var(--ag-ui-accent);
5832
+ .send:hover {
5833
+ transform: scale(1.08);
5505
5834
  }
5506
5835
 
5507
- .attach-input {
5836
+ .send:active {
5837
+ transform: scale(0.92);
5838
+ }
5839
+
5840
+ .send:disabled {
5841
+ opacity: 0.5;
5842
+ cursor: default;
5843
+ transform: none;
5844
+ }
5845
+
5846
+ /* The composer button doubles as the Stop control while a run is in flight \u2014
5847
+ same circle, different glyph, so nothing moves when a run starts. */
5848
+ .send[data-state="running"] {
5849
+ background: var(--_muted);
5850
+ }
5851
+
5852
+ .send[data-state="idle"] .send-stop,
5853
+ .send[data-state="running"] .send-send {
5508
5854
  display: none;
5509
5855
  }
5510
5856
 
5511
- /* The \u{1F3A4} mic button; shown only once #wireVoice mounts it. */
5512
- .voice-slot {
5513
- display: contents;
5857
+ /* Glyphs \u2014 one class, painted from the button's own colour so every state
5858
+ (hover, recording, running) carries the icon with it. */
5859
+ .glyph {
5860
+ width: var(--_glyph-size);
5861
+ height: var(--_glyph-size);
5862
+ fill: none;
5863
+ stroke: currentColor;
5864
+ stroke-width: var(--_glyph-stroke);
5865
+ stroke-linecap: round;
5866
+ stroke-linejoin: round;
5514
5867
  }
5515
5868
 
5516
- .voice-btn {
5517
- border: 1px solid var(--ag-ui-border);
5518
- border-radius: 8px;
5519
- padding: 0 10px;
5520
- background: var(--ag-ui-input-bg);
5521
- color: inherit;
5522
- font: inherit;
5523
- cursor: pointer;
5869
+ .glyph--solid {
5870
+ fill: currentColor;
5871
+ stroke: none;
5524
5872
  }
5525
5873
 
5526
- .voice-btn:hover {
5527
- border-color: var(--ag-ui-accent);
5874
+ /* In an icon holder (header brand, launcher) the glyph takes the holder's size
5875
+ rather than the composer's. */
5876
+ .icon-holder .glyph {
5877
+ width: 100%;
5878
+ height: 100%;
5528
5879
  }
5529
5880
 
5530
- .voice-btn:disabled {
5531
- cursor: default;
5532
- opacity: 0.6;
5881
+ /* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5882
+ /* The picker button sits in the composer's tool row; hidden until
5883
+ data-attachments-url. */
5884
+ .attach-input {
5885
+ display: none;
5886
+ }
5887
+
5888
+ /* The mic button's mount point; filled only once #wireVoice mounts the
5889
+ control. */
5890
+ .voice-slot {
5891
+ display: contents;
5533
5892
  }
5534
5893
 
5535
5894
  /* Recording: a red tint + a gentle pulse so it's clearly "live". */
5536
5895
  .voice-btn[data-state="recording"] {
5537
- border-color: var(--ag-ui-danger);
5538
- background: var(--ag-ui-danger);
5896
+ background: var(--_danger);
5539
5897
  color: #ffffff;
5540
5898
  animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
5541
5899
  }
5542
5900
 
5901
+ .voice-btn[data-state="recording"]:hover {
5902
+ background: var(--_danger);
5903
+ color: #ffffff;
5904
+ }
5905
+
5543
5906
  @keyframes ag-ui-voice-pulse {
5544
5907
  0%, 100% { opacity: 1; }
5545
5908
  50% { opacity: 0.6; }
@@ -5578,10 +5941,10 @@ var STYLES = `
5578
5941
  max-width: 100%;
5579
5942
  margin: 2px 0;
5580
5943
  padding: 3px 10px;
5581
- border: 1px dashed var(--ag-ui-border);
5944
+ border: 1px dashed var(--_border);
5582
5945
  border-radius: 999px;
5583
5946
  background: transparent;
5584
- color: var(--ag-ui-muted);
5947
+ color: var(--_muted);
5585
5948
  font-size: 0.8em;
5586
5949
  line-height: 1.4;
5587
5950
  }
@@ -5602,16 +5965,16 @@ var STYLES = `
5602
5965
  gap: 6px;
5603
5966
  max-width: 100%;
5604
5967
  padding: 4px 8px;
5605
- border: 1px solid var(--ag-ui-border);
5968
+ border: 1px solid var(--_border);
5606
5969
  border-radius: 999px;
5607
- background: var(--ag-ui-assistant-bg);
5970
+ background: var(--_assistant-bg);
5608
5971
  font-size: 0.85em;
5609
5972
  position: relative;
5610
5973
  }
5611
5974
 
5612
5975
  .attachment-chip--error {
5613
- border-color: var(--ag-ui-danger);
5614
- color: var(--ag-ui-danger);
5976
+ border-color: var(--_danger);
5977
+ color: var(--_danger);
5615
5978
  }
5616
5979
 
5617
5980
  .attachment-chip-name {
@@ -5622,12 +5985,12 @@ var STYLES = `
5622
5985
  }
5623
5986
 
5624
5987
  .attachment-chip-size {
5625
- color: var(--ag-ui-muted);
5988
+ color: var(--_muted);
5626
5989
  white-space: nowrap;
5627
5990
  }
5628
5991
 
5629
5992
  .attachment-chip--error .attachment-chip-size {
5630
- color: var(--ag-ui-danger);
5993
+ color: var(--_danger);
5631
5994
  }
5632
5995
 
5633
5996
  /* The progress bar fills as the file uploads. */
@@ -5635,13 +5998,13 @@ var STYLES = `
5635
5998
  flex-basis: 100%;
5636
5999
  height: 3px;
5637
6000
  border-radius: 2px;
5638
- background: var(--ag-ui-border);
6001
+ background: var(--_border);
5639
6002
  overflow: hidden;
5640
6003
  }
5641
6004
 
5642
6005
  .attachment-chip-bar-fill {
5643
6006
  height: 100%;
5644
- background: var(--ag-ui-accent);
6007
+ background: var(--_accent);
5645
6008
  transition: width 0.15s ease;
5646
6009
  }
5647
6010
 
@@ -5663,14 +6026,14 @@ var STYLES = `
5663
6026
 
5664
6027
  /* A subtle outline while a file is dragged over the shell. */
5665
6028
  .chat--dragover {
5666
- outline: 2px dashed var(--ag-ui-accent);
6029
+ outline: 2px dashed var(--_accent);
5667
6030
  outline-offset: -4px;
5668
6031
  }
5669
6032
 
5670
6033
  /* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
5671
6034
  .stopped-note {
5672
6035
  align-self: flex-start;
5673
- color: var(--ag-ui-muted);
6036
+ color: var(--_muted);
5674
6037
  font-size: 12px;
5675
6038
  padding: 2px 4px;
5676
6039
  }
@@ -5683,14 +6046,14 @@ var STYLES = `
5683
6046
  flex-direction: column;
5684
6047
  gap: 8px;
5685
6048
  padding: 12px;
5686
- background: var(--ag-ui-bg);
5687
- border: 1px solid var(--ag-ui-accent);
6049
+ background: var(--_bg);
6050
+ border: 1px solid var(--_accent);
5688
6051
  border-radius: 10px;
5689
6052
  }
5690
6053
 
5691
6054
  .confirm[data-resolved] {
5692
6055
  opacity: 0.7;
5693
- border-color: var(--ag-ui-border);
6056
+ border-color: var(--_border);
5694
6057
  }
5695
6058
 
5696
6059
  .confirm-body {
@@ -5704,7 +6067,7 @@ var STYLES = `
5704
6067
  overflow: auto;
5705
6068
  font-size: 12px;
5706
6069
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
5707
- background: var(--ag-ui-assistant-bg);
6070
+ background: var(--_assistant-bg);
5708
6071
  border-radius: 8px;
5709
6072
  white-space: pre-wrap;
5710
6073
  word-break: break-word;
@@ -5717,14 +6080,14 @@ var STYLES = `
5717
6080
  }
5718
6081
 
5719
6082
  .confirm-btn {
5720
- border: 1px solid var(--ag-ui-border);
6083
+ border: 1px solid var(--_border);
5721
6084
  border-radius: 8px;
5722
6085
  padding: 8px 14px;
5723
6086
  font: inherit;
5724
6087
  font-weight: 600;
5725
6088
  cursor: pointer;
5726
- background: var(--ag-ui-bg);
5727
- color: var(--ag-ui-fg);
6089
+ background: var(--_bg);
6090
+ color: var(--_fg);
5728
6091
  }
5729
6092
 
5730
6093
  .confirm-btn:disabled {
@@ -5733,8 +6096,8 @@ var STYLES = `
5733
6096
  }
5734
6097
 
5735
6098
  .confirm-btn--confirm {
5736
- border-color: var(--ag-ui-accent);
5737
- background: var(--ag-ui-accent);
6099
+ border-color: var(--_accent);
6100
+ background: var(--_accent);
5738
6101
  color: #ffffff;
5739
6102
  }
5740
6103
 
@@ -5746,14 +6109,14 @@ var STYLES = `
5746
6109
  flex-direction: column;
5747
6110
  gap: 8px;
5748
6111
  padding: 12px;
5749
- background: var(--ag-ui-bg);
5750
- border: 1px solid var(--ag-ui-accent);
6112
+ background: var(--_bg);
6113
+ border: 1px solid var(--_accent);
5751
6114
  border-radius: 10px;
5752
6115
  }
5753
6116
 
5754
6117
  .approval[data-resolved] {
5755
6118
  opacity: 0.7;
5756
- border-color: var(--ag-ui-border);
6119
+ border-color: var(--_border);
5757
6120
  }
5758
6121
 
5759
6122
  .approval-body {
@@ -5767,14 +6130,14 @@ var STYLES = `
5767
6130
  }
5768
6131
 
5769
6132
  .approval-btn {
5770
- border: 1px solid var(--ag-ui-border);
6133
+ border: 1px solid var(--_border);
5771
6134
  border-radius: 8px;
5772
6135
  padding: 8px 14px;
5773
6136
  font: inherit;
5774
6137
  font-weight: 600;
5775
6138
  cursor: pointer;
5776
- background: var(--ag-ui-bg);
5777
- color: var(--ag-ui-fg);
6139
+ background: var(--_bg);
6140
+ color: var(--_fg);
5778
6141
  }
5779
6142
 
5780
6143
  .approval-btn:disabled {
@@ -5783,8 +6146,8 @@ var STYLES = `
5783
6146
  }
5784
6147
 
5785
6148
  .approval-btn--approve {
5786
- border-color: var(--ag-ui-accent);
5787
- background: var(--ag-ui-accent);
6149
+ border-color: var(--_accent);
6150
+ background: var(--_accent);
5788
6151
  color: #ffffff;
5789
6152
  }
5790
6153
 
@@ -5796,14 +6159,14 @@ var STYLES = `
5796
6159
  flex-direction: column;
5797
6160
  gap: 8px;
5798
6161
  padding: 12px;
5799
- background: var(--ag-ui-bg);
5800
- border: 1px solid var(--ag-ui-accent);
6162
+ background: var(--_bg);
6163
+ border: 1px solid var(--_accent);
5801
6164
  border-radius: 10px;
5802
6165
  }
5803
6166
 
5804
6167
  .question[data-resolved] {
5805
6168
  opacity: 0.7;
5806
- border-color: var(--ag-ui-border);
6169
+ border-color: var(--_border);
5807
6170
  }
5808
6171
 
5809
6172
  .question-body {
@@ -5828,9 +6191,9 @@ var STYLES = `
5828
6191
  width: 100%;
5829
6192
  padding: 8px 10px;
5830
6193
  font: inherit;
5831
- color: var(--ag-ui-fg);
5832
- background: var(--ag-ui-bg);
5833
- border: 1px solid var(--ag-ui-border);
6194
+ color: var(--_fg);
6195
+ background: var(--_bg);
6196
+ border: 1px solid var(--_border);
5834
6197
  border-radius: 8px;
5835
6198
  }
5836
6199
 
@@ -5844,13 +6207,13 @@ var STYLES = `
5844
6207
  }
5845
6208
 
5846
6209
  .question-btn {
5847
- border: 1px solid var(--ag-ui-accent);
6210
+ border: 1px solid var(--_accent);
5848
6211
  border-radius: 8px;
5849
6212
  padding: 8px 14px;
5850
6213
  font: inherit;
5851
6214
  font-weight: 600;
5852
6215
  cursor: pointer;
5853
- background: var(--ag-ui-accent);
6216
+ background: var(--_accent);
5854
6217
  color: #ffffff;
5855
6218
  }
5856
6219
 
@@ -5868,18 +6231,18 @@ var STYLES = `
5868
6231
  }
5869
6232
 
5870
6233
  .skill-chip {
5871
- border: 1px solid var(--ag-ui-border);
6234
+ border: 1px solid var(--_border);
5872
6235
  border-radius: 999px;
5873
6236
  padding: 4px 12px;
5874
6237
  font: inherit;
5875
6238
  font-size: 0.9em;
5876
6239
  cursor: pointer;
5877
- background: var(--ag-ui-assistant-bg);
5878
- color: var(--ag-ui-fg);
6240
+ background: var(--_assistant-bg);
6241
+ color: var(--_fg);
5879
6242
  }
5880
6243
 
5881
6244
  .skill-chip:hover {
5882
- border-color: var(--ag-ui-accent);
6245
+ border-color: var(--_accent);
5883
6246
  }
5884
6247
 
5885
6248
  .skill-palette {
@@ -5888,8 +6251,8 @@ var STYLES = `
5888
6251
  flex-direction: column;
5889
6252
  max-height: 220px;
5890
6253
  overflow: auto;
5891
- background: var(--ag-ui-bg);
5892
- border: 1px solid var(--ag-ui-border);
6254
+ background: var(--_bg);
6255
+ border: 1px solid var(--_border);
5893
6256
  border-radius: 8px;
5894
6257
  box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);
5895
6258
  }
@@ -5905,11 +6268,11 @@ var STYLES = `
5905
6268
  font: inherit;
5906
6269
  text-align: left;
5907
6270
  cursor: pointer;
5908
- color: var(--ag-ui-fg);
6271
+ color: var(--_fg);
5909
6272
  }
5910
6273
 
5911
6274
  .skill-item[aria-selected="true"] {
5912
- background: var(--ag-ui-assistant-bg);
6275
+ background: var(--_assistant-bg);
5913
6276
  }
5914
6277
 
5915
6278
  .skill-item-title {
@@ -5918,7 +6281,7 @@ var STYLES = `
5918
6281
 
5919
6282
  .skill-item-desc {
5920
6283
  font-size: 0.85em;
5921
- color: var(--ag-ui-muted);
6284
+ color: var(--_muted);
5922
6285
  }
5923
6286
 
5924
6287
  /* The hint sits directly above the composer's top border, so a zero bottom
@@ -5927,19 +6290,38 @@ var STYLES = `
5927
6290
  margin: 8px 12px;
5928
6291
  font-size: 0.85em;
5929
6292
  line-height: 1.4;
5930
- color: var(--ag-ui-danger);
6293
+ color: var(--_danger);
5931
6294
  }
5932
6295
 
5933
- /* Chat-history drawer \u2014 a slide-over within the chat panel. */
6296
+ /* Chat-history drawer \u2014 a slide-over within the chat panel.
6297
+ The hidden attribute stays the single source of truth for open/closed (no
6298
+ JS-driven animation state): display is transitioned discretely, so on close
6299
+ the drawer stays displayed for the whole exit and is removed only at the end,
6300
+ which is what lets the backdrop fade and the panel slide out. */
5934
6301
  .drawer {
5935
6302
  position: absolute;
5936
6303
  inset: 0;
5937
6304
  z-index: 5;
5938
6305
  display: flex;
6306
+ transition: visibility var(--_motion) var(--_ease);
5939
6307
  }
5940
6308
 
6309
+ /* Closed. The overlay keeps its box (display, not none) so the backdrop and
6310
+ panel inside it stay rendered and can transition both ways; visibility is
6311
+ what takes the whole subtree out of the tab order, the a11y tree and hit
6312
+ testing at rest, and it holds off until the slide has finished. */
5941
6313
  .drawer[hidden] {
5942
- display: none;
6314
+ display: flex;
6315
+ visibility: hidden;
6316
+ pointer-events: none;
6317
+ }
6318
+
6319
+ .drawer[hidden] .drawer-backdrop {
6320
+ opacity: 0;
6321
+ }
6322
+
6323
+ .drawer[hidden] .drawer-panel {
6324
+ transform: translateX(-100%);
5943
6325
  }
5944
6326
 
5945
6327
  .checkpoints {
@@ -5951,16 +6333,27 @@ var STYLES = `
5951
6333
  flex-direction: column;
5952
6334
  gap: 0.25rem;
5953
6335
  padding: 0.5rem;
5954
- border: 1px solid var(--ag-ui-border);
6336
+ border: 1px solid var(--_border);
5955
6337
  border-radius: 0.5rem;
5956
- background: var(--ag-ui-assistant-bg);
6338
+ background: var(--_assistant-bg);
5957
6339
  box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
5958
6340
  max-height: 60%;
5959
6341
  overflow-y: auto;
6342
+ transform-origin: top center;
6343
+ transition:
6344
+ opacity var(--_motion) var(--_ease),
6345
+ transform var(--_motion) var(--_ease),
6346
+ visibility var(--_motion) var(--_ease);
5960
6347
  }
5961
6348
 
6349
+ /* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
6350
+ popover can animate open and closed. */
5962
6351
  .checkpoints[hidden] {
5963
- display: none;
6352
+ display: flex;
6353
+ visibility: hidden;
6354
+ pointer-events: none;
6355
+ opacity: 0;
6356
+ transform: scale(0.96) translateY(-6px);
5964
6357
  }
5965
6358
 
5966
6359
  .checkpoints-title {
@@ -5984,7 +6377,7 @@ var STYLES = `
5984
6377
  }
5985
6378
 
5986
6379
  .checkpoint-row:hover {
5987
- background: var(--ag-ui-hover);
6380
+ background: var(--_hover);
5988
6381
  }
5989
6382
 
5990
6383
  .checkpoint-label {
@@ -5999,7 +6392,7 @@ var STYLES = `
5999
6392
  font-size: 0.6875rem;
6000
6393
  padding: 0 0.375rem;
6001
6394
  border-radius: 999px;
6002
- background: var(--ag-ui-hover);
6395
+ background: var(--_hover);
6003
6396
  opacity: 0.8;
6004
6397
  }
6005
6398
 
@@ -6008,20 +6401,22 @@ var STYLES = `
6008
6401
  font-size: 0.75rem;
6009
6402
  cursor: pointer;
6010
6403
  padding: 0.125rem 0.5rem;
6011
- border: 1px solid var(--ag-ui-border);
6404
+ border: 1px solid var(--_border);
6012
6405
  border-radius: 0.375rem;
6013
6406
  background: transparent;
6014
6407
  color: inherit;
6015
6408
  }
6016
6409
 
6017
6410
  .checkpoint-action:hover {
6018
- background: var(--ag-ui-hover);
6411
+ background: var(--_hover);
6019
6412
  }
6020
6413
 
6021
6414
  .drawer-backdrop {
6022
6415
  position: absolute;
6023
6416
  inset: 0;
6024
6417
  background: rgba(20, 20, 50, 0.32);
6418
+ opacity: 1;
6419
+ transition: opacity var(--_motion) var(--_ease);
6025
6420
  }
6026
6421
 
6027
6422
  .drawer-panel {
@@ -6030,19 +6425,21 @@ var STYLES = `
6030
6425
  flex-direction: column;
6031
6426
  width: min(300px, 85%);
6032
6427
  height: 100%;
6033
- background: var(--ag-ui-bg);
6034
- border-right: 1px solid var(--ag-ui-border);
6035
- box-shadow: var(--ag-ui-shadow);
6428
+ background: var(--_bg);
6429
+ border-right: 1px solid var(--_border);
6430
+ box-shadow: var(--_shadow);
6036
6431
  overflow: hidden;
6432
+ transform: none;
6433
+ transition: transform var(--_motion) var(--_ease);
6037
6434
  }
6038
6435
 
6039
6436
  .drawer-header {
6040
6437
  display: flex;
6041
6438
  align-items: center;
6042
6439
  justify-content: space-between;
6043
- gap: var(--ag-ui-space);
6044
- padding: var(--ag-ui-pad);
6045
- border-bottom: 1px solid var(--ag-ui-border);
6440
+ gap: var(--_space);
6441
+ padding: var(--_pad);
6442
+ border-bottom: 1px solid var(--_border);
6046
6443
  }
6047
6444
 
6048
6445
  .drawer-title {
@@ -6050,10 +6447,10 @@ var STYLES = `
6050
6447
  }
6051
6448
 
6052
6449
  .drawer-new {
6053
- border: 1px solid var(--ag-ui-border);
6054
- border-radius: var(--ag-ui-radius);
6055
- background: var(--ag-ui-bg);
6056
- color: var(--ag-ui-accent);
6450
+ border: 1px solid var(--_border);
6451
+ border-radius: var(--_radius);
6452
+ background: var(--_bg);
6453
+ color: var(--_accent);
6057
6454
  padding: 4px 10px;
6058
6455
  font: inherit;
6059
6456
  font-size: 0.85em;
@@ -6067,19 +6464,19 @@ var STYLES = `
6067
6464
  }
6068
6465
 
6069
6466
  .drawer-empty {
6070
- padding: var(--ag-ui-pad);
6467
+ padding: var(--_pad);
6071
6468
  font-size: 0.9em;
6072
- color: var(--ag-ui-muted);
6469
+ color: var(--_muted);
6073
6470
  }
6074
6471
 
6075
6472
  .drawer-row {
6076
6473
  display: flex;
6077
6474
  align-items: stretch;
6078
- border-bottom: 1px solid var(--ag-ui-border);
6475
+ border-bottom: 1px solid var(--_border);
6079
6476
  }
6080
6477
 
6081
6478
  .drawer-row--active {
6082
- background: var(--ag-ui-assistant-bg);
6479
+ background: var(--_assistant-bg);
6083
6480
  }
6084
6481
 
6085
6482
  .drawer-row-select {
@@ -6106,12 +6503,12 @@ var STYLES = `
6106
6503
 
6107
6504
  .drawer-row-time {
6108
6505
  font-size: 0.72em;
6109
- color: var(--ag-ui-muted);
6506
+ color: var(--_muted);
6110
6507
  }
6111
6508
 
6112
6509
  .drawer-row-preview {
6113
6510
  font-size: 0.8em;
6114
- color: var(--ag-ui-muted);
6511
+ color: var(--_muted);
6115
6512
  overflow: hidden;
6116
6513
  white-space: nowrap;
6117
6514
  text-overflow: ellipsis;
@@ -6128,7 +6525,7 @@ var STYLES = `
6128
6525
  .drawer-row-delete {
6129
6526
  border: none;
6130
6527
  background: none;
6131
- color: var(--ag-ui-muted);
6528
+ color: var(--_muted);
6132
6529
  font-size: 0.9em;
6133
6530
  padding: 4px;
6134
6531
  cursor: pointer;
@@ -6139,10 +6536,10 @@ var STYLES = `
6139
6536
  min-width: 0;
6140
6537
  margin: 6px 10px;
6141
6538
  padding: 4px 8px;
6142
- border: 1px solid var(--ag-ui-accent);
6539
+ border: 1px solid var(--_accent);
6143
6540
  border-radius: 6px;
6144
- background: var(--ag-ui-input-bg);
6145
- color: var(--ag-ui-fg);
6541
+ background: var(--_input-bg);
6542
+ color: var(--_fg);
6146
6543
  font: inherit;
6147
6544
  }
6148
6545
 
@@ -6155,13 +6552,13 @@ var STYLES = `
6155
6552
  }
6156
6553
 
6157
6554
  .drawer-confirm-label {
6158
- color: var(--ag-ui-danger);
6555
+ color: var(--_danger);
6159
6556
  }
6160
6557
 
6161
6558
  .drawer-confirm-yes {
6162
6559
  border: none;
6163
6560
  border-radius: 6px;
6164
- background: var(--ag-ui-danger);
6561
+ background: var(--_danger);
6165
6562
  color: #ffffff;
6166
6563
  padding: 3px 10px;
6167
6564
  font: inherit;
@@ -6169,7 +6566,7 @@ var STYLES = `
6169
6566
  }
6170
6567
 
6171
6568
  .drawer-confirm-no {
6172
- border: 1px solid var(--ag-ui-border);
6569
+ border: 1px solid var(--_border);
6173
6570
  border-radius: 6px;
6174
6571
  background: none;
6175
6572
  color: inherit;
@@ -6663,7 +7060,10 @@ var VoiceInput = class {
6663
7060
  this.element.type = "button";
6664
7061
  this.element.className = "voice-btn";
6665
7062
  this.element.setAttribute("part", "voice-button");
6666
- this.element.textContent = "\u{1F3A4}";
7063
+ const glyph = document.createElement("slot");
7064
+ glyph.name = "icon-voice";
7065
+ glyph.innerHTML = ICON_VOICE;
7066
+ this.element.append(glyph);
6667
7067
  this.#setState("idle");
6668
7068
  this.element.addEventListener("click", () => {
6669
7069
  void this.toggle();
@@ -7232,6 +7632,13 @@ function truncate(text2, limit) {
7232
7632
 
7233
7633
  // src/core/create_http_agent.ts
7234
7634
  import { HttpAgent } from "@ag-ui/client";
7635
+
7636
+ // src/core/utils.ts
7637
+ function withCredentials(init, credentials) {
7638
+ return credentials === void 0 ? init : { ...init, credentials };
7639
+ }
7640
+
7641
+ // src/core/create_http_agent.ts
7235
7642
  function createHttpAgent(options) {
7236
7643
  return new HttpAgent({
7237
7644
  url: options.endpoint,
@@ -7242,17 +7649,18 @@ function createHttpAgent(options) {
7242
7649
  // "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
7243
7650
  // a free function with the correct receiver. The wrapper also overlays
7244
7651
  // `getHeaders()` per request, so header rotation (CSRF, short-lived JWT)
7245
- // reaches the stream even though the agent instance is cached.
7652
+ // reaches the stream even though the agent instance is cached, and applies
7653
+ // the configured cookie policy — the agent's own config has no seam for it.
7246
7654
  fetch: (url, init) => {
7247
7655
  const fresh = options.getHeaders?.();
7248
7656
  if (fresh === void 0) {
7249
- return fetch(url, init);
7657
+ return fetch(url, withCredentials(init, options.credentials));
7250
7658
  }
7251
7659
  const headers = new Headers(init?.headers);
7252
7660
  for (const [name, value] of Object.entries(fresh)) {
7253
7661
  headers.set(name, value);
7254
7662
  }
7255
- return fetch(url, { ...init, headers });
7663
+ return fetch(url, withCredentials({ ...init, headers }, options.credentials));
7256
7664
  },
7257
7665
  // Spread conditionally: under `exactOptionalPropertyTypes` an explicit
7258
7666
  // `undefined` is not assignable to these optional config fields.
@@ -7266,12 +7674,14 @@ var RemoteConversationStore = class {
7266
7674
  #url;
7267
7675
  #headers;
7268
7676
  #local;
7677
+ #credentials;
7269
7678
  #dropped = /* @__PURE__ */ new Set();
7270
7679
  #renamed = /* @__PURE__ */ new Map();
7271
- constructor(url, headers = () => ({}), local = new SessionStorageStore()) {
7680
+ constructor(url, headers = () => ({}), local = new SessionStorageStore(), credentials = () => void 0) {
7272
7681
  this.#url = url.endsWith("/") ? url : `${url}/`;
7273
7682
  this.#headers = headers;
7274
7683
  this.#local = local;
7684
+ this.#credentials = credentials;
7275
7685
  }
7276
7686
  threadId() {
7277
7687
  return this.#local.threadId();
@@ -7349,7 +7759,7 @@ var RemoteConversationStore = class {
7349
7759
  /** GET that resolves to the `Response`, or `null` on a network error. */
7350
7760
  async #get(url) {
7351
7761
  try {
7352
- return await fetch(url, { headers: this.#headers() });
7762
+ return await fetch(url, withCredentials({ headers: this.#headers() }, this.#credentials()));
7353
7763
  } catch {
7354
7764
  return null;
7355
7765
  }
@@ -7358,11 +7768,17 @@ var RemoteConversationStore = class {
7358
7768
  async #mutate(threadId, method, body) {
7359
7769
  const headers = this.#headers();
7360
7770
  try {
7361
- await fetch(`${this.#url}${encodeURIComponent(threadId)}/`, {
7362
- method,
7363
- headers: body === void 0 ? headers : { ...headers, "content-type": "application/json" },
7364
- body: body === void 0 ? null : JSON.stringify(body)
7365
- });
7771
+ await fetch(
7772
+ `${this.#url}${encodeURIComponent(threadId)}/`,
7773
+ withCredentials(
7774
+ {
7775
+ method,
7776
+ headers: body === void 0 ? headers : { ...headers, "content-type": "application/json" },
7777
+ body: body === void 0 ? null : JSON.stringify(body)
7778
+ },
7779
+ this.#credentials()
7780
+ )
7781
+ );
7366
7782
  } catch {
7367
7783
  }
7368
7784
  }
@@ -7372,9 +7788,11 @@ var RemoteConversationStore = class {
7372
7788
  var RunIndex = class {
7373
7789
  #url;
7374
7790
  #headers;
7375
- constructor(url, headers = () => ({})) {
7791
+ #credentials;
7792
+ constructor(url, headers = () => ({}), credentials = () => void 0) {
7376
7793
  this.#url = url.endsWith("/") ? url : `${url}/`;
7377
7794
  this.#headers = headers;
7795
+ this.#credentials = credentials;
7378
7796
  }
7379
7797
  /**
7380
7798
  * The user's runs, or `[]` when the endpoint is unreachable or answers with
@@ -7384,10 +7802,16 @@ var RunIndex = class {
7384
7802
  */
7385
7803
  async list() {
7386
7804
  try {
7387
- const response = await fetch(this.#url, {
7388
- method: "GET",
7389
- headers: { Accept: "application/json", ...this.#headers() }
7390
- });
7805
+ const response = await fetch(
7806
+ this.#url,
7807
+ withCredentials(
7808
+ {
7809
+ method: "GET",
7810
+ headers: { Accept: "application/json", ...this.#headers() }
7811
+ },
7812
+ this.#credentials()
7813
+ )
7814
+ );
7391
7815
  if (!response.ok) {
7392
7816
  return [];
7393
7817
  }
@@ -7426,11 +7850,17 @@ var RunIndex = class {
7426
7850
  async function transcribeAudio(audio, options) {
7427
7851
  const form = new FormData();
7428
7852
  form.append("audio", audio, "recording.webm");
7429
- const response = await fetch(options.url, {
7430
- method: "POST",
7431
- headers: { ...options.headers ?? {} },
7432
- body: form
7433
- });
7853
+ const response = await fetch(
7854
+ options.url,
7855
+ withCredentials(
7856
+ {
7857
+ method: "POST",
7858
+ headers: { ...options.headers ?? {} },
7859
+ body: form
7860
+ },
7861
+ options.credentials
7862
+ )
7863
+ );
7434
7864
  if (!response.ok) {
7435
7865
  throw new Error(await errorMessage(response));
7436
7866
  }
@@ -7458,6 +7888,7 @@ function uploadAttachment(file, options) {
7458
7888
  form.append("file", file);
7459
7889
  const xhr = new XMLHttpRequest();
7460
7890
  xhr.open("POST", options.url);
7891
+ xhr.withCredentials = options.credentials === "include";
7461
7892
  for (const [key, value] of Object.entries(options.headers ?? {})) {
7462
7893
  xhr.setRequestHeader(key, value);
7463
7894
  }
@@ -7531,14 +7962,42 @@ var CONNECT_TIME_ATTRIBUTES = [
7531
7962
  "data-strings",
7532
7963
  "data-icon-url"
7533
7964
  ];
7965
+ var CREDENTIALS_MODES = ["omit", "same-origin", "include"];
7966
+ function isCredentialsMode(value) {
7967
+ return CREDENTIALS_MODES.includes(value);
7968
+ }
7534
7969
  var COLLAPSED_KEY = "ag-ui-chat:collapsed";
7535
7970
  var SIZE_KEY = "ag-ui-chat:size";
7536
7971
  var THEME_KEY = "ag-ui-chat:theme";
7537
7972
  var AgUiChat = class extends HTMLElement {
7538
7973
  /** Agent factory; override to inject a custom or fake agent (tests). */
7539
7974
  agentFactory = createHttpAgent;
7540
- /** Extra HTTP headers for the AG-UI endpoint (e.g. CSRF). */
7975
+ /**
7976
+ * Static extra HTTP headers, sent with **every** request this element makes —
7977
+ * the agent run, the thread index and its messages, the tool and skill
7978
+ * catalogs, the run index, uploads and transcription.
7979
+ *
7980
+ * Right for values fixed for the element's lifetime. A credential that
7981
+ * rotates (a short-lived JWT, a re-issued CSRF token) belongs in
7982
+ * {@link getHeaders} instead: this is read at request time, but only a
7983
+ * re-assignment updates it, so a token captured here is pinned until the host
7984
+ * remembers to assign again.
7985
+ */
7541
7986
  headers = {};
7987
+ /**
7988
+ * Live header source, consulted immediately before every request — the way to
7989
+ * supply rotating credentials.
7990
+ *
7991
+ * Set it to a function and each request calls it afresh: a token refreshed by
7992
+ * the host between two requests reaches the second one, with nothing to
7993
+ * re-assign and nothing to keep in sync.
7994
+ *
7995
+ * Composes with {@link headers} rather than replacing it: the two are merged
7996
+ * per key with `getHeaders()` winning, so a static `X-Client` and a rotating
7997
+ * `Authorization` can be configured independently and neither silently drops
7998
+ * the other.
7999
+ */
8000
+ getHeaders = null;
7542
8001
  /**
7543
8002
  * Permit `<img>` in rendered assistant markdown. **Off by default**: a
7544
8003
  * model-controlled image URL is fetched with no user interaction, which
@@ -7721,8 +8180,12 @@ var AgUiChat = class extends HTMLElement {
7721
8180
  #attachSlot;
7722
8181
  /** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
7723
8182
  #themeToggle;
7724
- /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
7725
- #rail;
8183
+ /** What the collapsed widget shrinks to: the floating launcher, or the sidebar rail. */
8184
+ #launcher;
8185
+ /** The launcher's unread badge; hidden at zero, and when the host opts out. */
8186
+ #badge;
8187
+ // Answers that finished while the widget was collapsed. Expanding clears it.
8188
+ #unread = 0;
7726
8189
  /** Empty-state region at the top of the message list; hidden once anything renders. */
7727
8190
  #emptyWrap;
7728
8191
  /** Upload tray; created on connect only when `data-attachments-url` is set. */
@@ -7795,7 +8258,8 @@ var AgUiChat = class extends HTMLElement {
7795
8258
  this.#attachSlot = document.createElement("div");
7796
8259
  this.#voiceSlot = document.createElement("span");
7797
8260
  this.#themeToggle = document.createElement("button");
7798
- this.#rail = document.createElement("button");
8261
+ this.#launcher = document.createElement("button");
8262
+ this.#badge = document.createElement("span");
7799
8263
  this.#emptyWrap = document.createElement("div");
7800
8264
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
7801
8265
  this.#drawer = new ThreadDrawer({
@@ -7825,7 +8289,11 @@ var AgUiChat = class extends HTMLElement {
7825
8289
  return null;
7826
8290
  }
7827
8291
  if (this.#runIndex === null) {
7828
- this.#runIndex = new RunIndex(url, () => this.headers);
8292
+ this.#runIndex = new RunIndex(
8293
+ url,
8294
+ () => this.#requestHeaders(),
8295
+ () => this.#requestCredentials()
8296
+ );
7829
8297
  }
7830
8298
  return this.#runIndex;
7831
8299
  }
@@ -7854,11 +8322,13 @@ var AgUiChat = class extends HTMLElement {
7854
8322
  return;
7855
8323
  }
7856
8324
  this.#input.value = "";
8325
+ this.#autoGrow();
7857
8326
  const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
7858
8327
  const agent = this.agentFactory({
7859
8328
  endpoint,
7860
- headers: this.headers,
7861
- getHeaders: () => this.headers,
8329
+ headers: this.#requestHeaders(),
8330
+ getHeaders: () => this.#requestHeaders(),
8331
+ ...this.#credentialsOption(),
7862
8332
  threadId: this.#threadId,
7863
8333
  // The seed the endpoints assume: nothing. The snapshot is the history.
7864
8334
  initialMessages: []
@@ -7881,9 +8351,17 @@ var AgUiChat = class extends HTMLElement {
7881
8351
  }
7882
8352
  /** Attributes the element reacts to after it has been connected. */
7883
8353
  static get observedAttributes() {
7884
- return ["title-text", "placement", ...CONNECT_TIME_ATTRIBUTES];
8354
+ return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
7885
8355
  }
7886
8356
  attributeChangedCallback(name, previous, value) {
8357
+ if (name === "credentials") {
8358
+ if (value !== null && !isCredentialsMode(value)) {
8359
+ console.error(
8360
+ `<ag-ui-chat>: credentials="${value}" is not a fetch credentials mode (${CREDENTIALS_MODES.join(" / ")}) \u2014 it is being ignored, so requests use the browser default and cross-origin cookies will not be sent.`
8361
+ );
8362
+ }
8363
+ return;
8364
+ }
7887
8365
  if (name === "placement") {
7888
8366
  this.#releaseOwnedAxes();
7889
8367
  requestAnimationFrame(() => this.#syncResizeAnchor());
@@ -8074,6 +8552,72 @@ var AgUiChat = class extends HTMLElement {
8074
8552
  set endpoint(value) {
8075
8553
  this.setAttribute("endpoint", value);
8076
8554
  }
8555
+ /**
8556
+ * Cookie policy for **every** request this element makes, as `fetch`'s own
8557
+ * `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
8558
+ * the `credentials` attribute, so markup embeds can set it without script.
8559
+ *
8560
+ * `null` (the default) leaves the browser's default of `same-origin` in
8561
+ * place. That default sends **no cookies at all** when the endpoints live on
8562
+ * a different origin from the page — app.example.com calling
8563
+ * api.example.com is cross-origin — and the request goes out anonymously
8564
+ * rather than failing, so the symptom is a 401 from a server that looks
8565
+ * correctly configured. A cookie-authenticated cross-origin deployment wants
8566
+ * `"include"`, plus `Access-Control-Allow-Credentials: true` and a concrete
8567
+ * (non-wildcard) `Access-Control-Allow-Origin` on the server.
8568
+ *
8569
+ * Read per request, so a late assignment applies to everything after it.
8570
+ * `"omit"` cannot be honoured by the built-in **upload** transport, which is
8571
+ * an `XMLHttpRequest` and only has a two-state cookie switch; every other
8572
+ * endpoint honours all three modes.
8573
+ */
8574
+ get credentials() {
8575
+ const attr = this.getAttribute("credentials");
8576
+ return attr !== null && isCredentialsMode(attr) ? attr : null;
8577
+ }
8578
+ set credentials(value) {
8579
+ if (value === null) {
8580
+ this.removeAttribute("credentials");
8581
+ return;
8582
+ }
8583
+ if (!isCredentialsMode(value)) {
8584
+ throw new TypeError(
8585
+ `<ag-ui-chat>: credentials must be one of ${CREDENTIALS_MODES.map((mode) => `"${mode}"`).join(", ")} (got ${JSON.stringify(value)}).`
8586
+ );
8587
+ }
8588
+ this.setAttribute("credentials", value);
8589
+ }
8590
+ /**
8591
+ * The headers for the request about to go out: the static {@link headers}
8592
+ * with {@link getHeaders}'s live values overlaid, per key.
8593
+ *
8594
+ * Every request site goes through here, so "how this element authenticates"
8595
+ * is one answer rather than one per endpoint.
8596
+ */
8597
+ #requestHeaders() {
8598
+ return { ...this.headers, ...this.getHeaders?.() };
8599
+ }
8600
+ /** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
8601
+ #requestCredentials() {
8602
+ return this.credentials ?? void 0;
8603
+ }
8604
+ /**
8605
+ * The `credentials` entry for an {@link AgentFactory} call, or nothing at all.
8606
+ *
8607
+ * Spread rather than assigned: `exactOptionalPropertyTypes` rejects an
8608
+ * explicit `credentials: undefined`, and a factory should see the field
8609
+ * absent — not present-and-empty — when no policy is configured. The agent
8610
+ * reads it when it is built (first send, thread switch, continuation), by
8611
+ * which time any host configuration has landed.
8612
+ */
8613
+ #credentialsOption() {
8614
+ const credentials = this.#requestCredentials();
8615
+ return credentials === void 0 ? {} : { credentials };
8616
+ }
8617
+ /** The `fetch` init for the element's own plain GETs (catalogs). */
8618
+ #fetchInit() {
8619
+ return withCredentials({ headers: this.#requestHeaders() }, this.#requestCredentials());
8620
+ }
8077
8621
  /**
8078
8622
  * How much detail tool-call cards show, from the `data-tool-display`
8079
8623
  * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
@@ -8109,9 +8653,8 @@ var AgUiChat = class extends HTMLElement {
8109
8653
  if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
8110
8654
  this.setAttribute("collapsed", "");
8111
8655
  }
8112
- this.#syncRail();
8656
+ this.#syncLauncher();
8113
8657
  this.#initSkills();
8114
- void this.#fetchToolCatalog();
8115
8658
  if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
8116
8659
  this.conversationStore = new SessionStorageStore(this.#storageNs);
8117
8660
  }
@@ -8119,9 +8662,62 @@ var AgUiChat = class extends HTMLElement {
8119
8662
  this.#wireAttachments();
8120
8663
  this.#wireVoice();
8121
8664
  this.#threadId = this.conversationStore.threadId();
8665
+ queueMicrotask(() => this.#startup());
8122
8666
  void this.#rehydrate();
8123
8667
  this.#connected = true;
8124
8668
  }
8669
+ /**
8670
+ * The catalog requests the element issues on startup: the tool labels
8671
+ * (`data-tools-url`) and the backend skills (`data-skills-url`).
8672
+ *
8673
+ * Deliberately one microtask behind `connectedCallback`. A host that
8674
+ * configures the element through a framework ref necessarily does so *after*
8675
+ * inserting the node — React attaches refs and runs layout effects in the
8676
+ * same commit as the insertion, but strictly afterwards — so a request issued
8677
+ * from `connectedCallback` itself goes out before `headers`,
8678
+ * {@link getHeaders} or {@link credentials} exist, and comes back 401 in a
8679
+ * way that reads as a server fault rather than a mis-timed assignment. A
8680
+ * microtask lands after that commit and still before paint.
8681
+ *
8682
+ * Two things it is **not**. It is not a fix for configuration that arrives
8683
+ * later than the commit (a passive `useEffect`, an awaited token fetch):
8684
+ * configure before insertion (`createElement` → configure → `append`) or call
8685
+ * {@link reload} once configured, because a longer timer would hide that race
8686
+ * rather than close it. And it deliberately excludes the *history* replay,
8687
+ * which stays in `connectedCallback`: the replay renders into the transcript,
8688
+ * so deferring it lets a `sendMessage()` issued in the same task land first
8689
+ * and the replay then duplicate it. The thread history is therefore the one
8690
+ * request that can still go out before a ref is attached — {@link reload}
8691
+ * covers it.
8692
+ */
8693
+ #startup() {
8694
+ if (!this.#connected) {
8695
+ return;
8696
+ }
8697
+ void this.#fetchToolCatalog();
8698
+ void this.#fetchSkills();
8699
+ }
8700
+ /**
8701
+ * Re-run everything the element loads on startup — the tool-label catalog,
8702
+ * the backend skill catalog and the thread's history — with the transport
8703
+ * configuration as it stands now.
8704
+ *
8705
+ * This is the answer for a host that can only configure the element after the
8706
+ * fact (a token fetched in a passive effect, an async auth handshake): the
8707
+ * startup requests already went out with whatever was set then, and this says
8708
+ * "try again, properly authenticated" without removing and re-inserting the
8709
+ * node.
8710
+ *
8711
+ * A reload, not a merge — the in-flight run is cancelled and the transcript
8712
+ * is rebuilt from the persisted history, so anything streamed since is
8713
+ * dropped. Call it once, when configuration lands; not between turns.
8714
+ */
8715
+ async reload() {
8716
+ this.#cancelRun();
8717
+ this.#resetState();
8718
+ this.#setRunning(false);
8719
+ await Promise.all([this.#fetchToolCatalog(), this.#fetchSkills(), this.#rehydrate()]);
8720
+ }
8125
8721
  /**
8126
8722
  * Tear down live resources when the element leaves the DOM (a removed node, a
8127
8723
  * client-side route swap): cancel the in-flight run so its SSE stream closes,
@@ -8203,7 +8799,13 @@ var AgUiChat = class extends HTMLElement {
8203
8799
  if (url === null) {
8204
8800
  return null;
8205
8801
  }
8206
- return (file, onProgress, signal) => uploadAttachment(file, { url, headers: this.headers, onProgress, signal });
8802
+ return (file, onProgress, signal) => uploadAttachment(file, {
8803
+ url,
8804
+ headers: this.#requestHeaders(),
8805
+ ...this.#credentialsOption(),
8806
+ onProgress,
8807
+ signal
8808
+ });
8207
8809
  }
8208
8810
  /**
8209
8811
  * Reveal the composer's 🎤 mic button when transcription is possible — either
@@ -8230,7 +8832,11 @@ var AgUiChat = class extends HTMLElement {
8230
8832
  if (url === null) {
8231
8833
  return null;
8232
8834
  }
8233
- return (audio) => transcribeAudio(audio, { url, headers: this.headers });
8835
+ return (audio) => transcribeAudio(audio, {
8836
+ url,
8837
+ headers: this.#requestHeaders(),
8838
+ ...this.#credentialsOption()
8839
+ });
8234
8840
  }
8235
8841
  /** Drop a voice transcript into the composer (appended to any typed text). */
8236
8842
  #insertVoiceText(text2) {
@@ -8305,8 +8911,9 @@ Use the read_attachment tool with an id to read a file's contents.`
8305
8911
  if (url !== null) {
8306
8912
  this.conversationStore = new RemoteConversationStore(
8307
8913
  url,
8308
- () => this.headers,
8309
- this.conversationStore
8914
+ () => this.#requestHeaders(),
8915
+ this.conversationStore,
8916
+ () => this.#requestCredentials()
8310
8917
  );
8311
8918
  }
8312
8919
  }
@@ -8317,7 +8924,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8317
8924
  return;
8318
8925
  }
8319
8926
  try {
8320
- const response = await fetch(url, { headers: this.headers });
8927
+ const response = await fetch(url, this.#fetchInit());
8321
8928
  this.#toolCatalog = parseToolCatalog(await response.json());
8322
8929
  } catch {
8323
8930
  }
@@ -8330,13 +8937,16 @@ Use the read_attachment tool with an id to read a file's contents.`
8330
8937
  this.#clientSkills = skills;
8331
8938
  this.#recomputeSkills();
8332
8939
  }
8333
- /** Wire the skill surfaces: opt-in flags, embedded catalog, optional fetch. */
8940
+ /**
8941
+ * Wire the skill surfaces: opt-in flags and the embedded catalog. The backend
8942
+ * catalog is fetched from `#startup`, a microtask later, so it carries the
8943
+ * host's transport configuration.
8944
+ */
8334
8945
  #initSkills() {
8335
8946
  this.#skillsMenu.enableChips(this.#flag("data-prompt-chips"));
8336
8947
  this.#skillsMenu.enableSlash(this.#flag("data-slash-commands"));
8337
8948
  this.#embedSkills = this.#readEmbeddedSkills();
8338
8949
  this.#recomputeSkills();
8339
- void this.#fetchSkills();
8340
8950
  }
8341
8951
  /** Parse the inline `data-skills` JSON catalog (empty when absent/malformed). */
8342
8952
  #readEmbeddedSkills() {
@@ -8357,7 +8967,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8357
8967
  return;
8358
8968
  }
8359
8969
  try {
8360
- const response = await fetch(url, { headers: this.headers });
8970
+ const response = await fetch(url, this.#fetchInit());
8361
8971
  this.#backendSkills = parseSkills(await response.json());
8362
8972
  this.#recomputeSkills();
8363
8973
  } catch {
@@ -8401,12 +9011,14 @@ Use the read_attachment tool with an id to read a file's contents.`
8401
9011
  this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
8402
9012
  this.#skillHint.hidden = false;
8403
9013
  this.#input.value = text2;
9014
+ this.#autoGrow();
8404
9015
  this.#input.focus();
8405
9016
  this.#selectFirstPlaceholder(text2);
8406
9017
  return;
8407
9018
  }
8408
9019
  this.#skillHint.hidden = true;
8409
9020
  this.#input.value = text2;
9021
+ this.#autoGrow();
8410
9022
  if (skill.sendImmediately === false) {
8411
9023
  this.#input.focus();
8412
9024
  return;
@@ -8444,7 +9056,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8444
9056
  this.removeAttribute("collapsed");
8445
9057
  }
8446
9058
  sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
8447
- this.#syncRail();
9059
+ this.#setUnread(0);
8448
9060
  this.dispatchEvent(
8449
9061
  new CustomEvent(TOGGLE_EVENT, {
8450
9062
  detail: { collapsed },
@@ -8453,6 +9065,15 @@ Use the read_attachment tool with an id to read a file's contents.`
8453
9065
  })
8454
9066
  );
8455
9067
  }
9068
+ /**
9069
+ * Answers that finished while the widget was collapsed, and that the user has
9070
+ * therefore not seen. Expanding (or {@link newChat}) clears it. The launcher's
9071
+ * badge renders this; {@link UNREAD_EVENT} announces every change, so a host
9072
+ * chrome can render its own instead.
9073
+ */
9074
+ get unread() {
9075
+ return this.#unread;
9076
+ }
8456
9077
  /** Flip the collapsed state. Bound to the built-in header toggle. */
8457
9078
  toggleCollapsed() {
8458
9079
  this.setCollapsed(!this.collapsed);
@@ -8610,6 +9231,32 @@ Use the read_attachment tool with an id to read a file's contents.`
8610
9231
  const dark = this.getAttribute("theme") === "dark";
8611
9232
  this.#themeToggle.textContent = dark ? "\u2600\uFE0F" : "\u{1F319}";
8612
9233
  }
9234
+ /**
9235
+ * Open the thread-history drawer: the imperative route to the control that
9236
+ * renders as `::part(history-button)`.
9237
+ *
9238
+ * A host that hides `::part(header)` to render its own title bar hides the
9239
+ * history, new-chat and collapse buttons with it — and thread switching then
9240
+ * has no route at all, because those controls live inside the header. Each of
9241
+ * them has a method, so a host chrome can rebuild the set: this one,
9242
+ * {@link openCheckpoints}, {@link newChat}, {@link toggleCollapsed} and
9243
+ * {@link toggleTheme}.
9244
+ */
9245
+ openThreads() {
9246
+ void this.#refreshDrawer();
9247
+ this.#drawer.open();
9248
+ }
9249
+ /**
9250
+ * Open the checkpoints panel (the `::part(checkpoints-button)` route).
9251
+ *
9252
+ * It lists the runs the `data-runs-url` server reports as continuable;
9253
+ * without that attribute the built-in button is never rendered and this opens
9254
+ * an empty panel.
9255
+ */
9256
+ openCheckpoints() {
9257
+ void this.#refreshCheckpoints();
9258
+ this.#checkpoints.open();
9259
+ }
8613
9260
  /**
8614
9261
  * Start a fresh conversation: forget the persisted history, drop the
8615
9262
  * in-memory run state, clear the transcript, and mint a new thread id.
@@ -8620,6 +9267,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8620
9267
  this.#resetState();
8621
9268
  this.#threadId = this.conversationStore.threadId();
8622
9269
  this.#setRunning(false);
9270
+ this.#setUnread(0);
8623
9271
  }
8624
9272
  /** Drop the in-memory run + transcript, leaving the thread id untouched. */
8625
9273
  #resetState() {
@@ -8880,15 +9528,9 @@ Use the read_attachment tool with an id to read a file's contents.`
8880
9528
  controls.className = "header-controls";
8881
9529
  controls.setAttribute("part", "header-controls");
8882
9530
  const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
8883
- history.addEventListener("click", () => {
8884
- void this.#refreshDrawer();
8885
- this.#drawer.open();
8886
- });
9531
+ history.addEventListener("click", () => this.openThreads());
8887
9532
  const checkpoints = this.#headerButton("checkpoints", this.#strings.checkpoints, "\u2B6F");
8888
- checkpoints.addEventListener("click", () => {
8889
- void this.#refreshCheckpoints();
8890
- this.#checkpoints.open();
8891
- });
9533
+ checkpoints.addEventListener("click", () => this.openCheckpoints());
8892
9534
  const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
8893
9535
  newChat.addEventListener("click", () => this.newChat());
8894
9536
  const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
@@ -8924,17 +9566,27 @@ Use the read_attachment tool with an id to read a file's contents.`
8924
9566
  const inputRow = document.createElement("div");
8925
9567
  inputRow.className = "input-row";
8926
9568
  inputRow.setAttribute("part", "composer");
9569
+ const composer = document.createElement("div");
9570
+ composer.className = "composer";
9571
+ composer.setAttribute("part", "composer-surface");
9572
+ const tools = document.createElement("div");
9573
+ tools.className = "composer-tools";
9574
+ tools.setAttribute("part", "composer-tools");
8927
9575
  this.#input.className = "input";
8928
9576
  this.#input.setAttribute("part", "input");
8929
9577
  this.#input.setAttribute("aria-label", this.#strings.message);
8930
- this.#input.rows = 2;
9578
+ this.#input.rows = 1;
8931
9579
  this.#input.placeholder = this.#strings.inputPlaceholder;
8932
9580
  this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
8933
9581
  this.#input.addEventListener("input", () => this.#onInput());
8934
9582
  this.#send.className = "send";
8935
9583
  this.#send.type = "button";
8936
9584
  this.#send.setAttribute("part", "send");
8937
- this.#send.textContent = this.#strings.send;
9585
+ this.#send.append(
9586
+ this.#glyphSlot("icon-send", "send-send", ICON_SEND),
9587
+ this.#glyphSlot("icon-stop", "send-stop", ICON_STOP)
9588
+ );
9589
+ this.#send.title = this.#strings.send;
8938
9590
  this.#send.setAttribute("aria-label", this.#strings.send);
8939
9591
  this.#send.dataset["state"] = "idle";
8940
9592
  this.#send.addEventListener("click", () => {
@@ -8950,7 +9602,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8950
9602
  this.#attachButton.className = "attach-btn";
8951
9603
  this.#attachButton.type = "button";
8952
9604
  this.#attachButton.setAttribute("part", "attach-button");
8953
- this.#attachButton.textContent = "\u{1F4CE}";
9605
+ this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
8954
9606
  this.#attachButton.title = this.#strings.attachFiles;
8955
9607
  this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
8956
9608
  this.#attachButton.hidden = true;
@@ -8964,7 +9616,9 @@ Use the read_attachment tool with an id to read a file's contents.`
8964
9616
  this.#voiceSlot.className = "voice-slot";
8965
9617
  const footer = document.createElement("slot");
8966
9618
  footer.name = "footer";
8967
- inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
9619
+ tools.append(this.#attachButton, this.#voiceSlot, this.#send);
9620
+ composer.append(this.#input, tools);
9621
+ inputRow.append(composer, this.#fileInput);
8968
9622
  this.#chat.append(
8969
9623
  header,
8970
9624
  this.#messages,
@@ -8977,12 +9631,19 @@ Use the read_attachment tool with an id to read a file's contents.`
8977
9631
  this.#drawer.element,
8978
9632
  this.#checkpoints.element
8979
9633
  );
8980
- this.#rail.className = "rail";
8981
- this.#rail.type = "button";
8982
- this.#rail.setAttribute("part", "launcher");
8983
- this.#rail.setAttribute("aria-label", this.#strings.expand);
8984
- this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
8985
- this.#rail.addEventListener("click", () => this.setCollapsed(false));
9634
+ this.#launcher.className = "launcher";
9635
+ this.#launcher.type = "button";
9636
+ this.#launcher.setAttribute("part", "launcher");
9637
+ this.#launcher.setAttribute("aria-label", this.#strings.expand);
9638
+ this.#badge.className = "launcher-badge";
9639
+ this.#badge.setAttribute("part", "launcher-badge");
9640
+ this.#badge.setAttribute("aria-hidden", "true");
9641
+ this.#badge.hidden = true;
9642
+ this.#launcher.append(
9643
+ this.#iconElement("launcher", "launcher-icon", ICON_LAUNCHER, this.#launcherIconUrl()),
9644
+ this.#badge
9645
+ );
9646
+ this.#launcher.addEventListener("click", () => this.setCollapsed(false));
8986
9647
  this.#chat.append(
8987
9648
  createResizeHandle({
8988
9649
  axis: () => this.#resizeAxis(),
@@ -8996,7 +9657,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8996
9657
  label: this.#strings.resizePanel
8997
9658
  })
8998
9659
  );
8999
- this.#root.append(style, this.#chat, this.#rail);
9660
+ this.#root.append(style, this.#chat, this.#launcher);
9000
9661
  }
9001
9662
  /**
9002
9663
  * Build a header control button: a named slot a host can project markup into,
@@ -9023,16 +9684,37 @@ Use the read_attachment tool with an id to read a file's contents.`
9023
9684
  return button2;
9024
9685
  }
9025
9686
  /**
9026
- * An icon holder wrapping a `<slot>` so a host can project custom markup; with
9027
- * a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
9687
+ * A `<slot>` a host can project its own mark into, falling back to one of the
9688
+ * built-in glyphs. The markup is an author-written constant, never user or
9689
+ * server data, so it is assigned directly rather than sanitised.
9690
+ */
9691
+ #glyphSlot(slotName, className, markup) {
9692
+ const slot = document.createElement("slot");
9693
+ slot.name = slotName;
9694
+ slot.className = className;
9695
+ slot.innerHTML = markup;
9696
+ return slot;
9697
+ }
9698
+ /**
9699
+ * The launcher's own image URL. `data-launcher-icon-url` lets the collapsed
9700
+ * button carry a different mark from the header's — a product logo reads at
9701
+ * 22px in a header bar but rarely at 26px in a circle — and falls back to the
9702
+ * header icon so a single `data-icon-url` still feeds both.
9703
+ */
9704
+ #launcherIconUrl() {
9705
+ return this.getAttribute("data-launcher-icon-url") ?? this.getAttribute("data-icon-url");
9706
+ }
9707
+ /**
9708
+ * An icon holder wrapping a `<slot>` so a host can project custom markup;
9709
+ * with an `<img>` as the slot's fallback when an icon URL is configured, or
9710
+ * the given glyph markup when it is not.
9028
9711
  */
9029
- #iconElement(slotName, part, fallbackGlyph) {
9712
+ #iconElement(slotName, part, fallbackGlyph, iconUrl = this.getAttribute("data-icon-url")) {
9030
9713
  const holder = document.createElement("span");
9031
9714
  holder.className = "icon-holder";
9032
9715
  holder.setAttribute("part", part);
9033
9716
  const slot = document.createElement("slot");
9034
9717
  slot.name = slotName;
9035
- const iconUrl = this.getAttribute("data-icon-url");
9036
9718
  if (iconUrl !== null) {
9037
9719
  const img = document.createElement("img");
9038
9720
  img.className = "icon-img";
@@ -9040,14 +9722,63 @@ Use the read_attachment tool with an id to read a file's contents.`
9040
9722
  img.alt = "";
9041
9723
  slot.append(img);
9042
9724
  } else if (fallbackGlyph !== null) {
9043
- slot.append(document.createTextNode(fallbackGlyph));
9725
+ slot.innerHTML = fallbackGlyph;
9044
9726
  }
9045
9727
  holder.append(slot);
9046
9728
  return holder;
9047
9729
  }
9048
- /** Reflect the collapsed state on the rail's `aria-expanded`. */
9049
- #syncRail() {
9050
- this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
9730
+ /**
9731
+ * Reflect the collapsed state and the unread count on the launcher.
9732
+ *
9733
+ * The count is also the launcher's accessible name: a badge that only exists
9734
+ * as a coloured dot says nothing to a screen reader, and "Expand" alone would
9735
+ * be a lie once answers are waiting behind it.
9736
+ */
9737
+ #syncLauncher() {
9738
+ this.#launcher.setAttribute("aria-expanded", String(!this.collapsed));
9739
+ const unread = this.#unread;
9740
+ this.#badge.textContent = unread > 9 ? "9+" : String(unread);
9741
+ this.#badge.hidden = unread === 0 || !this.#badgeEnabled();
9742
+ const label = this.#badge.hidden ? this.#strings.expand : this.#strings.expandUnread.replace("{count}", String(unread));
9743
+ this.#launcher.setAttribute("aria-label", label);
9744
+ this.#launcher.title = label;
9745
+ }
9746
+ /**
9747
+ * The unread badge, unlike every other affordance here, is on by default:
9748
+ * a collapsed widget is the one state where an answer can arrive with nothing
9749
+ * on screen to say so. `data-unread-badge="false"` turns it off for a host
9750
+ * that drives its own chrome from the `ag-ui-unread` event.
9751
+ */
9752
+ #badgeEnabled() {
9753
+ return this.getAttribute("data-unread-badge") !== "false";
9754
+ }
9755
+ /**
9756
+ * Set the unread count, repaint the badge, and tell the host.
9757
+ *
9758
+ * The count is kept whether or not the badge renders it, so `unread` stays
9759
+ * truthful for a host chrome and switching the badge on mid-session doesn't
9760
+ * start from a number that was never counted.
9761
+ */
9762
+ #setUnread(count) {
9763
+ this.#unread = count;
9764
+ this.#syncLauncher();
9765
+ this.dispatchEvent(
9766
+ new CustomEvent(UNREAD_EVENT, {
9767
+ detail: { unread: count },
9768
+ bubbles: true,
9769
+ composed: true
9770
+ })
9771
+ );
9772
+ }
9773
+ /**
9774
+ * Count an answer the user cannot have seen: one that finished while the
9775
+ * widget was collapsed. Expanding is what marks them read.
9776
+ */
9777
+ #noteUnread() {
9778
+ if (!this.collapsed) {
9779
+ return;
9780
+ }
9781
+ this.#setUnread(this.#unread + 1);
9051
9782
  }
9052
9783
  /** Hide the empty-state region once the message list holds anything else. */
9053
9784
  #updateEmptyState() {
@@ -9057,6 +9788,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9057
9788
  #onInput() {
9058
9789
  this.#skillsMenu.onInput(this.#input.value);
9059
9790
  this.#skillHint.hidden = true;
9791
+ this.#autoGrow();
9060
9792
  }
9061
9793
  #onKeydown(event) {
9062
9794
  if (this.#skillsMenu.onKeydown(event)) {
@@ -9083,14 +9815,32 @@ Use the read_attachment tool with an id to read a file's contents.`
9083
9815
  this.#confirmAbort?.abort();
9084
9816
  this.#client?.cancel();
9085
9817
  }
9086
- /** Swap the composer button between Send (idle) and Stop (running). */
9818
+ /**
9819
+ * Swap the composer button between Send (idle) and Stop (running).
9820
+ *
9821
+ * The glyph is swapped by CSS from `data-state` — both are mounted — so this
9822
+ * only has to move the accessible name, which is the button's whole label now
9823
+ * that it carries no text.
9824
+ */
9087
9825
  #setRunning(running) {
9088
9826
  this.#running = running;
9089
9827
  const label = running ? this.#strings.stop : this.#strings.send;
9090
- this.#send.textContent = label;
9828
+ this.#send.title = label;
9091
9829
  this.#send.setAttribute("aria-label", label);
9092
9830
  this.#send.dataset["state"] = running ? "running" : "idle";
9093
9831
  }
9832
+ /**
9833
+ * Size the field to its content: one row when empty, growing with what is
9834
+ * typed until the CSS ceiling takes over and it scrolls.
9835
+ *
9836
+ * Resetting to `auto` first is what makes it shrink again — `scrollHeight`
9837
+ * never reports less than the current height, so measuring without the reset
9838
+ * would ratchet the composer taller and never back down.
9839
+ */
9840
+ #autoGrow() {
9841
+ this.#input.style.height = "auto";
9842
+ this.#input.style.height = `${this.#input.scrollHeight}px`;
9843
+ }
9094
9844
  async #submit() {
9095
9845
  if (this.#running) {
9096
9846
  return;
@@ -9101,6 +9851,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9101
9851
  return;
9102
9852
  }
9103
9853
  this.#input.value = "";
9854
+ this.#autoGrow();
9104
9855
  if (this.#attachTray?.hasPending() === true) {
9105
9856
  this.#appendNotice(
9106
9857
  "\u{1F4CE}",
@@ -9189,11 +9940,12 @@ Use the read_attachment tool with an id to read a file's contents.`
9189
9940
  if (this.#client === null) {
9190
9941
  const agent = this.agentFactory({
9191
9942
  endpoint: this.endpoint,
9192
- headers: this.headers,
9943
+ headers: this.#requestHeaders(),
9193
9944
  // Live getter: the client is built once and cached, but a rotated
9194
9945
  // token must still reach every request — the factory's fetch wrapper
9195
9946
  // re-reads this on each call.
9196
- getHeaders: () => this.headers,
9947
+ getHeaders: () => this.#requestHeaders(),
9948
+ ...this.#credentialsOption(),
9197
9949
  threadId: this.#threadId,
9198
9950
  initialMessages: this.#initialMessages,
9199
9951
  initialState: this.#sharedState
@@ -9368,6 +10120,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9368
10120
  }
9369
10121
  attachCopyButtons(bubble, this.#strings);
9370
10122
  this.#streamingBubble = null;
10123
+ this.#noteUnread();
9371
10124
  },
9372
10125
  onToolCall: (call) => {
9373
10126
  this.#hidePending();
@@ -9563,24 +10316,24 @@ function defineAgUiChat() {
9563
10316
 
9564
10317
  // src/dom/dom_driver.ts
9565
10318
  async function fillField(el, value, options = {}) {
9566
- scrollIntoCenterView(el);
9567
- await focusWithFlash(el, { flashMs: options.flashMs ?? 0 });
10319
+ await scrollIntoCenterView(el);
10320
+ await focusWithFlash(el, { ...options, flashMs: options.flashMs ?? 0 });
9568
10321
  await typeInto(el, value, options);
9569
10322
  }
9570
10323
  async function clickElement(el, options = {}) {
9571
- scrollIntoCenterView(el);
10324
+ await scrollIntoCenterView(el);
9572
10325
  await highlightThenClick(el, options);
9573
10326
  }
9574
10327
  async function pressButton(el, options = {}) {
9575
- scrollIntoCenterView(el);
10328
+ await scrollIntoCenterView(el);
9576
10329
  await pressThenClick(el, options);
9577
10330
  }
9578
10331
  async function selectControl(el, value, options = {}) {
9579
- scrollIntoCenterView(el);
10332
+ await scrollIntoCenterView(el);
9580
10333
  await selectOption(el, value, options);
9581
10334
  }
9582
10335
  async function toggleCheckbox(el, checked, options = {}) {
9583
- scrollIntoCenterView(el);
10336
+ await scrollIntoCenterView(el);
9584
10337
  await toggleControl(el, checked, options);
9585
10338
  }
9586
10339
  function setControlValue(el, value) {
@@ -9594,7 +10347,7 @@ function setControlValue(el, value) {
9594
10347
  }
9595
10348
 
9596
10349
  // src/version.ts
9597
- var VERSION = "0.20.1";
10350
+ var VERSION = "0.22.0";
9598
10351
  export {
9599
10352
  ATTACHMENT_EVENT,
9600
10353
  AgUiChat,
@@ -9618,6 +10371,7 @@ export {
9618
10371
  TOOL_CALL_STATUS,
9619
10372
  TOOL_DISPLAY,
9620
10373
  ToolCallCard,
10374
+ UNREAD_EVENT,
9621
10375
  VERSION,
9622
10376
  X_CONFIRM_KEY,
9623
10377
  X_DESTRUCTIVE_KEY,
@@ -9632,6 +10386,7 @@ export {
9632
10386
  createStateHookTools,
9633
10387
  defineAgUiChat,
9634
10388
  fillField,
10389
+ flash,
9635
10390
  focusWithFlash,
9636
10391
  highlightThenClick,
9637
10392
  isDestructive,