@artooi/ag-ui-web-component 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +171 -15
  3. package/dist/ag-ui-web-component.bundle.js +591 -176
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +27 -2
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +36 -3
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/events/bar_height_detail.d.ts +6 -0
  10. package/dist/core/events/bar_height_detail.d.ts.map +1 -0
  11. package/dist/dom/highlight_overlay.d.ts +2 -1
  12. package/dist/dom/highlight_overlay.d.ts.map +1 -1
  13. package/dist/dom/ring_color.d.ts +8 -0
  14. package/dist/dom/ring_color.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +926 -52
  18. package/dist/index.js.map +4 -4
  19. package/dist/tools/chat_surface_tools.d.ts +8 -1
  20. package/dist/tools/chat_surface_tools.d.ts.map +1 -1
  21. package/dist/ui/history/conversation_history.d.ts +16 -3
  22. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  23. package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
  24. package/dist/ui/placement/dock_styles.d.ts +2 -0
  25. package/dist/ui/placement/dock_styles.d.ts.map +1 -0
  26. package/dist/ui/placement/panel_placement.d.ts +11 -0
  27. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  28. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  29. package/dist/ui/placement/surface_for.d.ts +21 -17
  30. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  31. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
  32. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  33. package/dist/ui/shell/build_launcher.d.ts +15 -4
  34. package/dist/ui/shell/build_launcher.d.ts.map +1 -1
  35. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  36. package/dist/ui/styles.d.ts.map +1 -1
  37. package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/constants.ts +30 -2
  40. package/src/core/ag_ui_chat.ts +326 -26
  41. package/src/core/events/bar_height_detail.ts +5 -0
  42. package/src/dom/highlight_overlay.ts +2 -1
  43. package/src/dom/ring_color.ts +141 -4
  44. package/src/index.ts +4 -0
  45. package/src/tools/chat_surface_tools.ts +18 -8
  46. package/src/ui/history/conversation_history.ts +51 -7
  47. package/src/ui/placement/collapse_styles.ts +248 -2
  48. package/src/ui/placement/dock_styles.ts +91 -0
  49. package/src/ui/placement/panel_placement.ts +71 -3
  50. package/src/ui/placement/small_viewport_styles.ts +9 -0
  51. package/src/ui/placement/surface_for.ts +29 -18
  52. package/src/ui/progress/tool_group_styles.ts +49 -0
  53. package/src/ui/progress/tool_step_styles.ts +5 -3
  54. package/src/ui/shell/build_launcher.ts +32 -5
  55. package/src/ui/shell/token_styles.ts +28 -1
  56. package/src/ui/styles.ts +4 -0
  57. package/src/ui/themes/scheme_styles.ts +10 -1
  58. 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 BAR_HEIGHT_EVENT = "ag-ui-bar-height";
5
6
  var UNREAD_EVENT = "ag-ui-unread";
6
7
  var STATE_EVENT = "ag-ui-state";
7
8
  var ATTACHMENT_EVENT = "ag-ui-attachments";
@@ -62,6 +63,12 @@ var TOOL_DISPLAY = {
62
63
  COMPACT: "compact",
63
64
  FULL: "full"
64
65
  };
66
+ var TOOL_GROUP_DISPLAY = {
67
+ INLINE: "inline",
68
+ MINIMAL: "minimal",
69
+ COMPACT: "compact",
70
+ FULL: "full"
71
+ };
65
72
  var COMPACTION_ACTIVITY_TYPE = "compaction";
66
73
  var LOAD_CAPABILITY_TOOL = "load_capability";
67
74
  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>`;
@@ -277,7 +284,7 @@ function createChatSurfaceTools(surface) {
277
284
  if (!report.movable) {
278
285
  return {
279
286
  moved: false,
280
- reason: report.fullBleed ? "the panel fills the screen, so there is nowhere to move it to" : report.draggable === false ? "this page has turned off moving the panel" : `the "${report.placement ?? ""}" placement owns the panel's position`,
287
+ reason: report.fullBleed ? "the panel fills the screen, so there is nowhere to move it to" : report.collapseTo === "bar" || report.collapseTo === "none" ? "the panel is docked: it rises from the page's bottom bar and stays there" : report.draggable === false ? "this page has turned off moving the panel" : `the "${report.placement ?? ""}" placement owns the panel's position`,
281
288
  suggestion: report.collapsible ? "minimise_chat" : null
282
289
  };
283
290
  }
@@ -286,7 +293,7 @@ function createChatSurfaceTools(surface) {
286
293
  },
287
294
  {
288
295
  name: "minimise_chat",
289
- description: "Collapse your own panel to its launcher, so the user can see the whole page. The launcher stays visible and reopens it. Answers `minimised: false` when the placement has no collapsed state, which is the case for a full-page chat.",
296
+ description: "Collapse your own panel, so the user can see the whole page. The user reopens it from where it collapsed to (a launcher, a rail, a bottom bar or the page's own control). Answers `minimised: false` when the placement has no collapsed state, which is the case for a full-page chat.",
290
297
  parameters: {
291
298
  type: "object",
292
299
  properties: {},
@@ -298,7 +305,7 @@ function createChatSurfaceTools(surface) {
298
305
  if (!report.collapsible) {
299
306
  return {
300
307
  minimised: false,
301
- reason: "this placement has no collapsed state, so there is no launcher to return to"
308
+ reason: "this placement has no collapsed state, so there is nothing to minimise to"
302
309
  };
303
310
  }
304
311
  surface.setCollapsed(true, { announce: true });
@@ -403,13 +410,94 @@ function ringColor(target, chat) {
403
410
  if (accent !== "") {
404
411
  return accent;
405
412
  }
406
- const chatAccent = owner === null ? "" : resolveOn(owner, CHAT_ACCENT);
407
- return chatAccent === "" ? PACKAGE_ACCENT : chatAccent;
413
+ if (owner === null) {
414
+ return PACKAGE_ACCENT;
415
+ }
416
+ const chatAccent = resolveOn(owner, CHAT_ACCENT);
417
+ return chatAccent === "" ? PACKAGE_ACCENT : legible(owner, chatAccent, target);
418
+ }
419
+ var RING_CONTRAST = 3;
420
+ var STEP = 0.1;
421
+ function legible(chat, accent, target) {
422
+ const paint = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
423
+ if (paint === null) {
424
+ return accent;
425
+ }
426
+ const bytes = (color) => {
427
+ paint.clearRect(0, 0, 1, 1);
428
+ paint.fillStyle = "rgb(0 0 0 / 0)";
429
+ paint.fillStyle = color;
430
+ paint.fillRect(0, 0, 1, 1);
431
+ const [r2 = 0, g2 = 0, b2 = 0, a3 = 0] = paint.getImageData(0, 0, 1, 1).data;
432
+ return [r2, g2, b2, a3 / 255];
433
+ };
434
+ const surface = surfaceBehind(target, bytes);
435
+ const holds = (color) => contrast(over(color, surface), surface) >= RING_CONTRAST;
436
+ const own = [accent, resolveOn(chat, CHAT_ACCENT, "light"), resolveOn(chat, CHAT_ACCENT, "dark")];
437
+ const fit = own.find((candidate) => holds(bytes(candidate)));
438
+ if (fit !== void 0) {
439
+ return fit;
440
+ }
441
+ const from = over(bytes(accent), surface);
442
+ const black = [0, 0, 0, 1];
443
+ const white = [255, 255, 255, 1];
444
+ const toward = contrast(black, surface) > contrast(white, surface) ? black : white;
445
+ let step = 1;
446
+ while (!holds(mix(from, toward, step * STEP))) {
447
+ step += 1;
448
+ }
449
+ const [r, g, b] = mix(from, toward, step * STEP);
450
+ return `rgb(${r}, ${g}, ${b})`;
451
+ }
452
+ function surfaceBehind(target, bytes) {
453
+ const layers = [];
454
+ for (let el2 = parentOf(target); el2 !== null; el2 = parentOf(el2)) {
455
+ const layer = bytes(window.getComputedStyle(el2).backgroundColor);
456
+ layers.push(layer);
457
+ if (layer[3] === 1) {
458
+ break;
459
+ }
460
+ }
461
+ const probe = document.createElement("span");
462
+ probe.hidden = true;
463
+ probe.style.color = "Canvas";
464
+ document.documentElement.appendChild(probe);
465
+ let surface = bytes(window.getComputedStyle(probe).color);
466
+ probe.remove();
467
+ for (const layer of layers.reverse()) {
468
+ surface = over(layer, surface);
469
+ }
470
+ return surface;
471
+ }
472
+ function parentOf(el2) {
473
+ if (el2.parentElement !== null) {
474
+ return el2.parentElement;
475
+ }
476
+ const root = el2.getRootNode();
477
+ return root instanceof ShadowRoot ? root.host : null;
478
+ }
479
+ function over(top, under) {
480
+ return mix(under, top, top[3]);
481
+ }
482
+ function mix(from, to, share) {
483
+ const channel = (index) => Math.round(from[index] + (to[index] - from[index]) * share);
484
+ return [channel(0), channel(1), channel(2), 1];
485
+ }
486
+ function luminance([r, g, b]) {
487
+ const linear = (channel) => {
488
+ const c = channel / 255;
489
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
490
+ };
491
+ return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
492
+ }
493
+ function contrast(a3, b) {
494
+ const [la, lb] = [luminance(a3), luminance(b)];
495
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
408
496
  }
409
497
  function readToken(el2, name) {
410
498
  return window.getComputedStyle(el2).getPropertyValue(name).trim();
411
499
  }
412
- function resolveOn(chat, name) {
500
+ function resolveOn(chat, name, scheme) {
413
501
  const value = readToken(chat, name);
414
502
  const root = chat.shadowRoot;
415
503
  if (value === "" || root === null) {
@@ -417,6 +505,9 @@ function resolveOn(chat, name) {
417
505
  }
418
506
  const probe = document.createElement("span");
419
507
  probe.hidden = true;
508
+ if (scheme !== void 0) {
509
+ probe.style.colorScheme = scheme;
510
+ }
420
511
  probe.style.color = value;
421
512
  if (probe.style.color === "") {
422
513
  return "";
@@ -4396,6 +4487,8 @@ var ConversationHistory = class {
4396
4487
  * Also what tells a pick refused for a load from one refused for a run.
4397
4488
  */
4398
4489
  #loading = null;
4490
+ /** The thread switch under way and the promise that settles it, or `null`. */
4491
+ #switching = null;
4399
4492
  constructor(host) {
4400
4493
  this.#host = host;
4401
4494
  }
@@ -4478,17 +4571,49 @@ var ConversationHistory = class {
4478
4571
  void this.#refreshCheckpoints();
4479
4572
  this.#host.checkpoints.open();
4480
4573
  }
4481
- /** Switch the active conversation to an existing thread and replay it. */
4482
- async switchThread(threadId) {
4574
+ /**
4575
+ * Switch the active conversation to an existing thread and replay it.
4576
+ *
4577
+ * A call for the thread a switch is already loading is handed that switch's
4578
+ * own promise, so it settles when the thread is drawn rather than at once,
4579
+ * before the replay.
4580
+ *
4581
+ * A store that rejects the load leaves the element on the thread it was on:
4582
+ * the switch has already cleared the transcript, so it points back at the
4583
+ * old thread and draws it again, saying nothing, and then rejects with the
4584
+ * store's error. Left as it was, the element named a thread it was not
4585
+ * showing and never reported it.
4586
+ */
4587
+ switchThread(threadId) {
4483
4588
  if (threadId === this.#threadId) {
4484
- return;
4589
+ return this.#switching?.threadId === threadId ? this.#switching.done : Promise.resolve();
4485
4590
  }
4591
+ const previous = this.#threadId;
4592
+ const done = this.#switchTo(threadId, previous).finally(() => {
4593
+ if (this.#switching === entry) {
4594
+ this.#switching = null;
4595
+ }
4596
+ });
4597
+ const entry = { threadId, done };
4598
+ this.#switching = entry;
4599
+ return done;
4600
+ }
4601
+ async #switchTo(threadId, previous) {
4486
4602
  this.#host.cancelRun();
4487
4603
  this.#host.resetState();
4488
4604
  this.#host.conversationStore().setActiveThread(threadId);
4489
4605
  this.#threadId = threadId;
4490
4606
  this.#host.setRunning(false);
4491
- await this.rehydrate("switch");
4607
+ try {
4608
+ await this.rehydrate("switch");
4609
+ } catch (error) {
4610
+ if (this.#threadId === threadId) {
4611
+ this.#threadId = previous;
4612
+ this.#host.conversationStore().setActiveThread(previous);
4613
+ await this.rehydrate(null).catch(() => void 0);
4614
+ }
4615
+ throw error;
4616
+ }
4492
4617
  }
4493
4618
  /** Rename a thread, then reload the list that shows its title. */
4494
4619
  renameThread(threadId, title) {
@@ -4685,7 +4810,8 @@ var ConversationHistory = class {
4685
4810
  * instead; see `#forgetStopped`.
4686
4811
  *
4687
4812
  * `reason` is what the thread event this restore ends with reports as why
4688
- * the conversation changed. A restore that is stopped reports nothing.
4813
+ * the conversation changed. A restore that is stopped reports nothing, and
4814
+ * neither does one given `null`, which draws the thread already reported.
4689
4815
  */
4690
4816
  async rehydrate(reason) {
4691
4817
  this.#generation += 1;
@@ -4740,7 +4866,9 @@ var ConversationHistory = class {
4740
4866
  if (checkpoint === null) {
4741
4867
  this.#noticeIfRunUnfinished(messages);
4742
4868
  }
4743
- this.#host.threadChanged(threadId, reason);
4869
+ if (reason !== null) {
4870
+ this.#host.threadChanged(threadId, reason);
4871
+ }
4744
4872
  if (checkpoint !== null) {
4745
4873
  if (!this.#restoring(generation)) {
4746
4874
  this.#forgetStopped(store, threadId, checkpoint);
@@ -5380,7 +5508,7 @@ function clampPanel(host, viewport, margin = EDGE_MARGIN) {
5380
5508
 
5381
5509
  // src/ui/placement/launcher_drag.ts
5382
5510
  var DRAG_THRESHOLD = 4;
5383
- var STEP = 16;
5511
+ var STEP2 = 16;
5384
5512
  var COARSE_STEP = 64;
5385
5513
  function enableLauncherDrag(launcher, options) {
5386
5514
  let suppressClick = false;
@@ -5462,7 +5590,7 @@ function enableLauncherDrag(launcher, options) {
5462
5590
  if (!options.enabled()) {
5463
5591
  return;
5464
5592
  }
5465
- const step = event.shiftKey ? COARSE_STEP : STEP;
5593
+ const step = event.shiftKey ? COARSE_STEP : STEP2;
5466
5594
  const rect = options.rect();
5467
5595
  let moved = null;
5468
5596
  if (event.key === "ArrowLeft") {
@@ -5589,7 +5717,7 @@ function onControl(event, handle) {
5589
5717
  // src/ui/placement/resize_handle.ts
5590
5718
  var MIN_WIDTH2 = 280;
5591
5719
  var MIN_HEIGHT2 = 240;
5592
- var STEP2 = 16;
5720
+ var STEP3 = 16;
5593
5721
  var COARSE_STEP2 = 64;
5594
5722
  function createResizeHandle(grip, options) {
5595
5723
  const handle = document.createElement("div");
@@ -5637,7 +5765,7 @@ function createResizeHandle(grip, options) {
5637
5765
  if (axis === "none" || !movable(grip, axis)) {
5638
5766
  return;
5639
5767
  }
5640
- const step = event.shiftKey ? COARSE_STEP2 : STEP2;
5768
+ const step = event.shiftKey ? COARSE_STEP2 : STEP3;
5641
5769
  const rect = options.rect();
5642
5770
  const delta = ARROWS[event.key];
5643
5771
  if (delta === void 0) {
@@ -5722,14 +5850,23 @@ var UNKNOWN = {
5722
5850
  resizeAxis: "both",
5723
5851
  startsCollapsed: false
5724
5852
  };
5725
- function surfaceFor(placement) {
5726
- return SURFACES.get(placement) ?? UNKNOWN;
5853
+ function surfaceFor(placement, collapseTo = null) {
5854
+ const surface = SURFACES.get(placement) ?? UNKNOWN;
5855
+ if (collapseTo === "bar" && surface.collapseTo === "launcher") {
5856
+ return { ...surface, collapseTo: "bar", launcherDraggable: false };
5857
+ }
5858
+ if (collapseTo === "none" && surface.collapseTo !== null) {
5859
+ return { ...surface, collapseTo: "none", launcherDraggable: false };
5860
+ }
5861
+ return surface;
5727
5862
  }
5728
5863
 
5729
5864
  // src/ui/placement/panel_placement.ts
5730
5865
  var SIZE_KEY = "ag-ui-chat:size";
5731
5866
  var DRAG_WIDTH = "--_drag-width";
5732
5867
  var DRAG_HEIGHT = "--_drag-height";
5868
+ var DOCK_BOTTOM = "--_dock-bottom";
5869
+ var DOCK_RIGHT = "--_dock-right";
5733
5870
  var LAUNCHER_KEY = "ag-ui-chat:launcher";
5734
5871
  function asPoint(value) {
5735
5872
  if (typeof value !== "object" || value === null) {
@@ -5905,6 +6042,7 @@ var PanelPlacement = class {
5905
6042
  const fullBleed = coversViewport(box, viewport);
5906
6043
  return {
5907
6044
  placement: this.#host.element.getAttribute("placement"),
6045
+ collapseTo: this.#surface().collapseTo,
5908
6046
  collapsed: this.#host.collapsed(),
5909
6047
  collapsible: this.#host.collapsible(),
5910
6048
  movable: this.#launcherDraggable() && !fullBleed,
@@ -5999,7 +6137,7 @@ var PanelPlacement = class {
5999
6137
  * per interaction, because `placement` is a live attribute.
6000
6138
  */
6001
6139
  #resizeAxis() {
6002
- return surfaceFor(this.#host.element.getAttribute("placement")).resizeAxis;
6140
+ return this.#surface().resizeAxis;
6003
6141
  }
6004
6142
  /**
6005
6143
  * Which edges the layout is holding still, by measuring rather than guessing:
@@ -6099,13 +6237,53 @@ var PanelPlacement = class {
6099
6237
  this.#host.element.style.removeProperty(DRAG_WIDTH);
6100
6238
  }
6101
6239
  }
6240
+ /** The surface the live placement and collapse-to attributes describe. */
6241
+ #surface() {
6242
+ const element = this.#host.element;
6243
+ return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
6244
+ }
6245
+ /** Whether the panel rises from the bottom bar or the host's anchor. */
6246
+ #docked() {
6247
+ const target = this.#surface().collapseTo;
6248
+ return target === "bar" || target === "none";
6249
+ }
6250
+ /**
6251
+ * Write where a docked panel stands, and return the bar's height.
6252
+ *
6253
+ * Under the bar it is the strip's border box, which is 0 whenever the strip
6254
+ * is not drawn (a phone with the panel open, where the panel is the screen).
6255
+ * Under a host anchor it is the anchor's top and right, measured against the
6256
+ * layout viewport a fixed element is placed in; an anchor of null leaves the
6257
+ * panel in its placement's own corner. Every other surface carries neither
6258
+ * property, so nothing stale is left behind by a live switch.
6259
+ */
6260
+ syncDock(bar, anchor) {
6261
+ const element = this.#host.element;
6262
+ const target = this.#surface().collapseTo;
6263
+ const height = target === "bar" ? bar.offsetHeight : 0;
6264
+ if (target === "bar") {
6265
+ element.style.setProperty(DOCK_BOTTOM, `${height}px`);
6266
+ } else if (target === "none" && anchor !== null) {
6267
+ const rect = anchor.getBoundingClientRect();
6268
+ const screen = this.#screen();
6269
+ element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, screen.height - rect.top)}px`);
6270
+ element.style.setProperty(DOCK_RIGHT, `${Math.max(0, screen.width - rect.right)}px`);
6271
+ } else {
6272
+ element.style.removeProperty(DOCK_BOTTOM);
6273
+ }
6274
+ if (target !== "none" || anchor === null) {
6275
+ element.style.removeProperty(DOCK_RIGHT);
6276
+ }
6277
+ element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
6278
+ return height;
6279
+ }
6102
6280
  /**
6103
6281
  * Whether this placement lets the launcher be dragged, read per interaction
6104
6282
  * because `placement` is a live attribute. `data-launcher-drag="false"` opts
6105
6283
  * a host out without giving up the launcher itself.
6106
6284
  */
6107
6285
  #launcherDraggable() {
6108
- return this.#host.element.getAttribute("data-launcher-drag") !== "false" && surfaceFor(this.#host.element.getAttribute("placement")).launcherDraggable;
6286
+ return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().launcherDraggable;
6109
6287
  }
6110
6288
  /**
6111
6289
  * The viewport the launcher and the panel both have to fit inside.
@@ -6463,6 +6641,17 @@ var PanelPlacement = class {
6463
6641
  */
6464
6642
  #applyResize(grip, box) {
6465
6643
  box = this.#withinViewport(box);
6644
+ if (this.#docked()) {
6645
+ const now = this.#host.element.getBoundingClientRect();
6646
+ box = {
6647
+ left: grip.x === this.#anchor.x ? now.left : box.left,
6648
+ right: grip.x === this.#anchor.x ? now.right : box.right,
6649
+ top: grip.y === this.#anchor.y ? now.top : box.top,
6650
+ bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom
6651
+ };
6652
+ this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
6653
+ return box;
6654
+ }
6466
6655
  this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
6467
6656
  if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
6468
6657
  return box;
@@ -8430,12 +8619,18 @@ var COLLAPSE_STYLES = `
8430
8619
  :host([collapsed][placement="embedded"]) .messages-wrap,
8431
8620
  :host([collapsed][placement="embedded"]) .messages,
8432
8621
  :host([collapsed][placement="embedded"]) .input-row,
8622
+ :host([collapsed][placement="embedded"]) slot[name="above-composer"],
8433
8623
  :host([collapsed][placement="embedded"]) .skill-chips,
8434
8624
  :host([collapsed][placement="embedded"]) .skill-palette,
8435
8625
  :host([collapsed][placement="embedded"]) .skill-hint {
8436
8626
  display: none;
8437
8627
  }
8438
8628
 
8629
+ /* The above-composer slot is in the list above because it is the host's content
8630
+ against the composer: a bare slot has no box of its own, so hiding the input
8631
+ row alone left the slotted row showing under the header bar. Its sibling slot
8632
+ below the composer is a footer and is left to the host. */
8633
+
8439
8634
  /* The page placement has no collapsed state, so it offers no control for one.
8440
8635
  A dedicated route has no "away" to go to: shrinking it left a strip of
8441
8636
  application chrome fixed over a route that no longer had an owner, and this
@@ -8463,6 +8658,334 @@ var COLLAPSE_STYLES = `
8463
8658
  transform: none;
8464
8659
  visibility: visible;
8465
8660
  }
8661
+
8662
+ /* \u2500\u2500 Bottom bar (collapse-to="bar") \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8663
+ The round launcher drawn as a strip along the bottom edge: the title, the
8664
+ unread badge and a running mark, with a history button beside it. The
8665
+ wrapper draws no box anywhere else, so the launcher lays out against the
8666
+ host as it always has; under the bar it is fixed to the bottom of the
8667
+ viewport, outside the host's box, which sits where the panel opens.
8668
+
8669
+ Here rather than with the launcher because it restyles the launcher's
8670
+ collapsed and pressed states, which this module follows in the cascade.
8671
+ The strip stays drawn while the panel is open on a wide viewport, as the
8672
+ way to minimise it; the phone block hides it then. */
8673
+ .dock-bar {
8674
+ display: contents;
8675
+ }
8676
+
8677
+ .bar-history {
8678
+ display: none;
8679
+ }
8680
+
8681
+ :host([data-collapsed-to="bar"]) .dock-bar {
8682
+ position: fixed;
8683
+ inset: auto var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left);
8684
+ display: flex;
8685
+ align-items: center;
8686
+ justify-content: flex-end;
8687
+ gap: 4px;
8688
+ box-sizing: border-box;
8689
+ min-height: 40px;
8690
+ padding: 4px 8px;
8691
+ border-top: 1px solid var(--_border);
8692
+ background: var(--_bg);
8693
+ color: var(--_fg);
8694
+ pointer-events: auto;
8695
+ }
8696
+
8697
+ /* A left-hand chat keeps its side: its controls and its panel at the left end. */
8698
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar {
8699
+ justify-content: flex-start;
8700
+ }
8701
+
8702
+ /* The launcher as the strip's title button. Its resting state hides it behind
8703
+ an open panel; the bar keeps it on screen, so every part of that state is
8704
+ restated, and the hover and press scales are cancelled at the weight of the
8705
+ rules that set them. The title truncates rather than taking a fixed width,
8706
+ leaving room beside it for whatever a later release adds to the row. */
8707
+ :host([data-collapsed-to="bar"]) .launcher {
8708
+ position: static;
8709
+ inset: auto;
8710
+ flex: 0 1 auto;
8711
+ justify-content: flex-start;
8712
+ gap: 8px;
8713
+ width: auto;
8714
+ min-width: 0;
8715
+ max-width: 100%;
8716
+ height: auto;
8717
+ min-height: 32px;
8718
+ padding: 4px 10px;
8719
+ border-radius: 8px;
8720
+ background: transparent;
8721
+ color: var(--_fg);
8722
+ box-shadow: none;
8723
+ touch-action: manipulation;
8724
+ opacity: 1;
8725
+ transform: none;
8726
+ visibility: visible;
8727
+ }
8728
+
8729
+ :host([data-collapsed-to="bar"]) .launcher:hover {
8730
+ background: var(--_wash);
8731
+ }
8732
+
8733
+ /* Bare at rest, like the history button beside it. The themes frame the
8734
+ round launcher with a border or a shadow at the weight of the rule above
8735
+ and later in the cascade, so the child combinator on the bar is there for
8736
+ weight, not scope: the launcher is always the bar's direct child. */
8737
+ :host([data-collapsed-to="bar"]) .dock-bar > .launcher {
8738
+ border: 0;
8739
+ box-shadow: none;
8740
+ }
8741
+
8742
+ :host([data-collapsed-to="bar"][collapsed]) .launcher:hover,
8743
+ :host([data-collapsed-to="bar"][collapsed]) .launcher:active {
8744
+ transform: none;
8745
+ }
8746
+
8747
+ :host([data-collapsed-to="bar"]) .launcher .icon-holder {
8748
+ flex: none;
8749
+ width: 18px;
8750
+ height: 18px;
8751
+ }
8752
+
8753
+ :host([data-collapsed-to="bar"]) .rail-label {
8754
+ display: block;
8755
+ min-width: 0;
8756
+ overflow: hidden;
8757
+ font-size: 0.9em;
8758
+ font-weight: 600;
8759
+ white-space: nowrap;
8760
+ text-overflow: ellipsis;
8761
+ }
8762
+
8763
+ :host([data-collapsed-to="bar"]) .launcher-badge {
8764
+ position: static;
8765
+ flex: none;
8766
+ box-shadow: none;
8767
+ }
8768
+
8769
+ /* The running mark: the element already reflects a run as data-running, so
8770
+ the bar shows one without a state of its own. */
8771
+ :host([data-collapsed-to="bar"][data-running]) .launcher::after {
8772
+ content: "";
8773
+ flex: none;
8774
+ width: 8px;
8775
+ height: 8px;
8776
+ border-radius: 50%;
8777
+ background: var(--_accent);
8778
+ animation: ag-ui-dock-running 1.2s ease-in-out infinite;
8779
+ }
8780
+
8781
+ @keyframes ag-ui-dock-running {
8782
+ 50% {
8783
+ opacity: 0.35;
8784
+ }
8785
+ }
8786
+
8787
+ @media (prefers-reduced-motion: reduce) {
8788
+ :host([data-collapsed-to="bar"][data-running]) .launcher::after {
8789
+ animation: none;
8790
+ }
8791
+ }
8792
+
8793
+ :host([data-collapsed-to="bar"]) .bar-history {
8794
+ display: inline-flex;
8795
+ flex: none;
8796
+ align-items: center;
8797
+ justify-content: center;
8798
+ width: 32px;
8799
+ height: 32px;
8800
+ padding: 0;
8801
+ border: 0;
8802
+ border-radius: 8px;
8803
+ background: transparent;
8804
+ color: var(--_muted);
8805
+ font: inherit;
8806
+ cursor: pointer;
8807
+ }
8808
+
8809
+ :host([data-collapsed-to="bar"]) .bar-history:hover {
8810
+ background: var(--_wash);
8811
+ color: var(--_fg);
8812
+ }
8813
+
8814
+ /* Both declare a display of their own, so each needs its own hidden rule. */
8815
+ :host([data-collapsed-to]) .dock-bar[hidden],
8816
+ :host([data-collapsed-to]) .bar-history[hidden] {
8817
+ display: none;
8818
+ }
8819
+
8820
+ /* History from the bar: the panel's own drawer, moved beside the bar while
8821
+ the panel is away, drawn as a popover standing on the bar's end. There is
8822
+ no backdrop; a press outside the bar closes it, as Escape does.
8823
+
8824
+ Its outer edge keeps the panel's gutter, so the popover and the panel line
8825
+ up on the side they share. The bar is already inset by the host's reserved
8826
+ edges, so the gutter alone is the whole offset from it, and the width cap
8827
+ leaves the gutter free on both sides. A phone keeps a narrow margin
8828
+ instead, in the block below: the panel there is the whole screen, so there
8829
+ is no edge to line up with. */
8830
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer {
8831
+ inset: auto var(--_edge-gutter) calc(100% + 8px) auto;
8832
+ width: min(320px, calc(var(--_viewport-width) - 2 * var(--_edge-gutter)));
8833
+ height: min(440px, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - 16px));
8834
+ border-radius: var(--_panel-radius);
8835
+ box-shadow: var(--_shadow);
8836
+ overflow: hidden;
8837
+ }
8838
+
8839
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar > .drawer {
8840
+ inset: auto auto calc(100% + 8px) var(--_edge-gutter);
8841
+ }
8842
+
8843
+ @media (max-width: 600px) {
8844
+ :host([data-collapsed-to="bar"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
8845
+ inset: auto 8px calc(100% + 8px) auto;
8846
+ width: min(320px, calc(100vw - 16px));
8847
+ }
8848
+
8849
+ :host([data-collapsed-to="bar"][placement="bottom-left"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
8850
+ inset: auto auto calc(100% + 8px) 8px;
8851
+ }
8852
+ }
8853
+
8854
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-backdrop {
8855
+ display: none;
8856
+ }
8857
+
8858
+ /* The popover closes in one step. The in-panel drawer slides its panel out
8859
+ and holds the overlay's visibility until the slide ends; here the overlay
8860
+ is the popover's own box, rounded and clipped, so the slide moved the list
8861
+ inside a box that stayed put (read as a shrink) and the box went only once
8862
+ the slide had finished. This outweighs the drawer's own hidden rule, so the
8863
+ box hides at once, and whatever the list inside does is out of sight. The
8864
+ way in needs no rule: the drawer is moved into the bar as it opens, and a
8865
+ moved element starts no transition. Nothing is left for reduced motion. */
8866
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer {
8867
+ transition: none;
8868
+ }
8869
+
8870
+ /* Border-box, so the list's own border sits inside the popover's clipped box
8871
+ rather than 2px past its bottom edge. */
8872
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-panel {
8873
+ box-sizing: border-box;
8874
+ width: 100%;
8875
+ border: 1px solid var(--_border);
8876
+ border-radius: inherit;
8877
+ box-shadow: none;
8878
+ }
8879
+
8880
+ /* \u2500\u2500 The host's own bar (collapse-to="none") \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8881
+ Collapsed is hidden: no launcher, no rail, no header bar. The host's control
8882
+ calls the public API, and the panel rises from its anchor (the dock module)
8883
+ or from the placement's corner. Each placement's own collapsed rule is
8884
+ overridden at its weight or above, later in the cascade. */
8885
+ :host([data-collapsed-to="none"][collapsed]) {
8886
+ pointer-events: none;
8887
+ }
8888
+
8889
+ :host([data-collapsed-to="none"][collapsed]) .launcher {
8890
+ display: none;
8891
+ }
8892
+
8893
+ :host([data-collapsed-to="none"][collapsed][placement="sidebar"]) {
8894
+ width: 0;
8895
+ }
8896
+
8897
+ :host([data-collapsed-to="none"][collapsed][placement="embedded"]) {
8898
+ display: none;
8899
+ }
8900
+ `;
8901
+
8902
+ // src/ui/placement/dock_styles.ts
8903
+ var DOCK_STYLES = `
8904
+ /* \u2500\u2500 Dock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8905
+ A docked panel rises from the bar along the bottom edge, or from the host's
8906
+ own control, instead of resting in a corner of its own. The element stamps
8907
+ what it collapses to as data-collapsed-to, and writes where the dock is as
8908
+ two private properties: --_dock-bottom, the bar's height or the distance
8909
+ from the layout viewport's bottom to the anchor's top, and --_dock-right,
8910
+ the anchor's distance from the right edge. Both are read with a 0px
8911
+ fallback, so a panel measured before the bar has laid out stands where a
8912
+ bar of no height would put it.
8913
+
8914
+ The cap follows the token block's rule: what the host left free less the
8915
+ one gutter the anchored edge spends, so a panel grown to the cap runs from
8916
+ just above the bar to the far edge. A host therefore needs no inset to keep
8917
+ the panel off its bar; --ag-ui-inset still wins when set.
8918
+
8919
+ These blocks restate tokens and nothing else, and sit ahead of the phone
8920
+ block at no more weight than it has, so on a phone the panel is the screen
8921
+ as every other placement is there. The bar measures 0 while it is not drawn
8922
+ (a phone with the panel open), which is what lets the side and full shapes
8923
+ below fall back to the whole height without a phone rule of their own.
8924
+
8925
+ Each placement keeps its own side and shape; only the bottom edge moves. */
8926
+ :host([data-collapsed-to="bar"]) {
8927
+ --_inset: var(
8928
+ --ag-ui-inset,
8929
+ auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
8930
+ calc(
8931
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
8932
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
8933
+ )
8934
+ auto
8935
+ );
8936
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - var(--_edge-gutter)));
8937
+ }
8938
+
8939
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) {
8940
+ --_inset: var(
8941
+ --ag-ui-inset,
8942
+ auto auto
8943
+ calc(
8944
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
8945
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
8946
+ )
8947
+ calc(var(--_edge-gutter) + var(--_viewport-inset-left))
8948
+ );
8949
+ }
8950
+
8951
+ /* The column and the full-screen panel stop at the bar rather than covering
8952
+ it, so the bar stays where the user minimises from. */
8953
+ :host([data-collapsed-to="bar"][placement="side"]) {
8954
+ --_inset: var(
8955
+ --ag-ui-inset,
8956
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
8957
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) auto
8958
+ );
8959
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
8960
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
8961
+ }
8962
+
8963
+ :host([data-collapsed-to="bar"][placement="full"]) {
8964
+ --_inset: var(
8965
+ --ag-ui-inset,
8966
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
8967
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) var(--_viewport-inset-left)
8968
+ );
8969
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
8970
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
8971
+ }
8972
+
8973
+ /* The host's own bar: the panel stands the anchor gap (8px unless the host
8974
+ says otherwise) above the anchor and lines up with its right edge. Only
8975
+ once the element found the anchor; until then, and when it never does, the
8976
+ placement's own corner stands.
8977
+
8978
+ The cap spends the larger of the gap and the gutter. The gutter alone is
8979
+ what this always spent, so the default is unchanged; but a host that opens
8980
+ the gap past the gutter to clear a tall toolbar would otherwise push a
8981
+ panel grown to the cap off the top of the window by the difference. */
8982
+ :host([data-collapsed-to="none"][data-dock-anchored]) {
8983
+ --_inset: var(--ag-ui-inset, auto var(--_dock-right, 0px) calc(var(--_dock-bottom, 0px) + var(--_anchor-gap)) auto);
8984
+ --_max-height: var(
8985
+ --ag-ui-max-height,
8986
+ calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - max(var(--_edge-gutter), var(--_anchor-gap)))
8987
+ );
8988
+ }
8466
8989
  `;
8467
8990
 
8468
8991
  // src/ui/placement/embedded_styles.ts
@@ -9085,6 +9608,15 @@ var SMALL_VIEWPORT_STYLES = `
9085
9608
  :host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
9086
9609
  display: none;
9087
9610
  }
9611
+
9612
+ /* The bottom bar shows only while collapsed. With the panel open the panel
9613
+ is the screen, its header's minimise control is the way back, and a
9614
+ second strip fixed to the bottom edge would sit under the keyboard while
9615
+ the composer has focus. Hidden by display, so the bar measures 0 and the
9616
+ host's padding goes with it. */
9617
+ :host([data-collapsed-to="bar"]:not([collapsed]):not([data-small-viewport="off"])) .dock-bar {
9618
+ display: none;
9619
+ }
9088
9620
  }
9089
9621
  `;
9090
9622
 
@@ -9216,6 +9748,9 @@ var CARD_MODES = [
9216
9748
  ].join(", ");
9217
9749
  var GROUPED = `:host(:where(:not(${CARD_MODES})))`;
9218
9750
  var CARDS = `:host(:is(${CARD_MODES}))`;
9751
+ function inner(mode) {
9752
+ return `:host(:where(:not(${CARD_MODES}))[data-tool-group-display="${mode}"]) .tool-group-list .tool-call`;
9753
+ }
9219
9754
  var TOOL_GROUP_STYLES = `
9220
9755
  /* \u2500\u2500 The folded tool-step group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
9221
9756
  One quiet row per run of consecutive calls, the cards one click down as
@@ -9400,6 +9935,43 @@ ${GROUPED} .tool-group .tool-call-subagent {
9400
9935
  flex-basis: 100%;
9401
9936
  }
9402
9937
 
9938
+ /* The inner display mode: how an opened group draws its calls. The same
9939
+ visibility rules the card modes apply to a flat card, so a call inside the
9940
+ group shows the regions the flat card of that mode shows. The flat inline
9941
+ mode's chrome rule is left out on purpose: a card in the group has already
9942
+ lost its box and is laid out as a row of the list, and that rule's padding
9943
+ and gap would undo the row. Selected from the host attribute, so changing it
9944
+ restyles groups already on screen. */
9945
+ ${inner("compact")}[data-expanded="false"] .tool-call-section {
9946
+ display: none;
9947
+ }
9948
+
9949
+ ${inner("inline")} .tool-call-section--args {
9950
+ display: none;
9951
+ }
9952
+
9953
+ ${inner("minimal")} .tool-call-toggle,
9954
+ ${inner("minimal")} .tool-call-body {
9955
+ display: none;
9956
+ }
9957
+
9958
+ /* A call waiting on an approval shows its arguments in every inner mode, as it
9959
+ does in every card mode, and for the same reason: the arguments are what
9960
+ tell three gated calls of one tool apart. These come after the hides above,
9961
+ which they tie with on weight. The arguments region only, never every
9962
+ section, since the result region carries the hidden attribute until a
9963
+ result exists and a display value here would override it. The arguments
9964
+ region carries it too when the call has none, so it is excluded while
9965
+ hidden rather than framed empty. */
9966
+ ${inner("minimal")}[data-status="deferred"] .tool-call-body {
9967
+ display: flex;
9968
+ }
9969
+
9970
+ ${inner("compact")}[data-status="deferred"] .tool-call-section--args:not([hidden]),
9971
+ ${inner("inline")}[data-status="deferred"] .tool-call-section--args:not([hidden]) {
9972
+ display: flex;
9973
+ }
9974
+
9403
9975
  /* Grouping is decided as each card is built, so a card mode set afterwards
9404
9976
  finds groups already drawn. It flattens them: the group and its list stop
9405
9977
  generating boxes, the row and the repeat ordinals go, and every card lays
@@ -9703,9 +10275,11 @@ var TOOL_STEP_STYLES = `
9703
10275
 
9704
10276
  /* The arguments region only, never every section: the result region carries the
9705
10277
  hidden attribute until a result exists, and a display value here overrides it,
9706
- framing an empty RESULT heading under the question. */
9707
- :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args,
9708
- :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args {
10278
+ framing an empty RESULT heading under the question. The arguments region is
10279
+ hidden too when the call has none, and is left alone then, or it would frame
10280
+ an empty ARGUMENTS heading. */
10281
+ :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]),
10282
+ :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]) {
9709
10283
  display: flex;
9710
10284
  }
9711
10285
 
@@ -10067,7 +10641,15 @@ var TOKEN_STYLES = `
10067
10641
  --_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
10068
10642
  --_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
10069
10643
  --_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
10070
- --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 55%, transparent));
10644
+ /* The focus ring is drawn on the panel, so it has to hold 3:1 against
10645
+ --_bg whatever accent a host brings (WCAG 2.4.13). Two parts accent to
10646
+ three parts ink does that at the worst case, an accent at the far end
10647
+ from the ink (white on a light panel, black on a dark one): the ink
10648
+ alone sits far above 3:1, and three fifths of it keeps the mix there
10649
+ while the hue still reads as the accent. Half and half fell to 2.3:1
10650
+ for black on the code theme. The accent over transparent, as before,
10651
+ measured only the accent, and a light host accent fell to 1.6:1. */
10652
+ --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 40%, var(--_fg)));
10071
10653
  /* The user's turn is the accent at a whisper. With Quiet's graphite accent
10072
10654
  that is a neutral wash; once a host sets a brand accent it becomes a tint
10073
10655
  of the brand, which is the one thing Soft does that Quiet keeps. */
@@ -10330,6 +10912,25 @@ var TOKEN_STYLES = `
10330
10912
  grid/flex layout) to embed it in the page flow instead of floating. */
10331
10913
  --_position: var(--ag-ui-position, fixed);
10332
10914
  --_z-index: var(--ag-ui-z-index, 2147483000);
10915
+ /* A size the user dragged, written inline on the host by the placement
10916
+ code, and read ahead of the host's tokens so the drag wins. There is no
10917
+ public token behind it; it is declared initial here only so a chat that
10918
+ was never dragged does not inherit one, from a dragged chat it is
10919
+ slotted into or from any page rule naming it. The inline value still
10920
+ beats this default. */
10921
+ --_drag-width: initial;
10922
+ --_drag-height: initial;
10923
+ /* Where a docked panel stands, written inline the same way by the placement
10924
+ code: the bottom bar's height, or the host anchor's distance from the
10925
+ bottom and right edges. Read only by the dock rules, each with a 0px
10926
+ fallback; declared initial so it never inherits either. */
10927
+ --_dock-bottom: initial;
10928
+ --_dock-right: initial;
10929
+ /* The gap between a panel and the host's own button it rises from under
10930
+ collapse-to="none". The button usually sits inside a taller toolbar, so a
10931
+ host sets this to the button's distance from the toolbar's top plus the
10932
+ clearance it wants, and the panel stands clear of the toolbar. */
10933
+ --_anchor-gap: var(--ag-ui-anchor-gap, 8px);
10333
10934
  --_width: var(--_drag-width, var(--ag-ui-width, 380px));
10334
10935
  --_height: var(--_drag-height, var(--ag-ui-height, 560px));
10335
10936
  /* The gutter a resting floating panel keeps between itself and the edge of
@@ -10945,9 +11546,18 @@ var SCHEME_STYLES = `
10945
11546
  dark) and beside a theme name (soft dark). auto follows the operating
10946
11547
  system through color-scheme itself, with no media query to keep in step.
10947
11548
  code is a dark-only theme and has no light half. */
11549
+ /* Important, so a scheme word holds against a host rule that sets the
11550
+ element's color-scheme (a page that follows its own dark mode). A rule
11551
+ from outside the shadow tree beats every :host rule at normal weight, and
11552
+ the theme toggle writes the word; without this its press changed nothing.
11553
+ code stays last, so code light is still dark. */
11554
+ :host([theme~="light"]) {
11555
+ color-scheme: light !important;
11556
+ }
11557
+
10948
11558
  :host([theme~="dark"]),
10949
11559
  :host([theme~="code"]) {
10950
- color-scheme: dark;
11560
+ color-scheme: dark !important;
10951
11561
  }
10952
11562
 
10953
11563
  :host([theme~="auto"]) {
@@ -11988,6 +12598,9 @@ var STYLES = [
11988
12598
  THEME_STYLES,
11989
12599
  PRESET_STYLES,
11990
12600
  PAGE_STYLES,
12601
+ // Ahead of the phone block and no heavier than it: a docked panel is still
12602
+ // full-bleed on a phone.
12603
+ DOCK_STYLES,
11991
12604
  SMALL_VIEWPORT_STYLES,
11992
12605
  SIDEBAR_STYLES,
11993
12606
  LAUNCHER_STYLES,
@@ -12117,7 +12730,7 @@ function buildHeader(options) {
12117
12730
 
12118
12731
  // src/ui/shell/build_launcher.ts
12119
12732
  function buildLauncher(options) {
12120
- const { launcher, badge, railLabel } = options;
12733
+ const { launcher, badge, railLabel, bar, barHistory } = options;
12121
12734
  launcher.className = "launcher";
12122
12735
  launcher.type = "button";
12123
12736
  launcher.setAttribute("part", "launcher");
@@ -12136,6 +12749,20 @@ function buildLauncher(options) {
12136
12749
  badge
12137
12750
  );
12138
12751
  launcher.addEventListener("click", options.onOpen, { signal: options.signal });
12752
+ bar.className = "dock-bar";
12753
+ bar.setAttribute("part", "bar");
12754
+ barHistory.className = "bar-history";
12755
+ barHistory.type = "button";
12756
+ barHistory.setAttribute("part", "bar-history");
12757
+ barHistory.title = options.strings.chatHistory;
12758
+ barHistory.setAttribute("aria-label", options.strings.chatHistory);
12759
+ const glyph = document.createElement("span");
12760
+ glyph.className = "bar-history-glyph";
12761
+ glyph.setAttribute("aria-hidden", "true");
12762
+ glyph.textContent = "\u2630";
12763
+ barHistory.replaceChildren(glyph);
12764
+ barHistory.addEventListener("click", options.onHistory, { signal: options.signal });
12765
+ bar.replaceChildren(launcher, barHistory);
12139
12766
  }
12140
12767
 
12141
12768
  // src/ui/shell/is_unread_badge_enabled.ts
@@ -12216,12 +12843,12 @@ ${listText(element, tag === "OL")}
12216
12843
 
12217
12844
  `;
12218
12845
  }
12219
- const inner = children(element);
12846
+ const inner2 = children(element);
12220
12847
  return BLOCK_TAGS.has(tag) ? `
12221
12848
 
12222
- ${inner}
12849
+ ${inner2}
12223
12850
 
12224
- ` : inner;
12851
+ ` : inner2;
12225
12852
  }
12226
12853
  function children(element) {
12227
12854
  let joined = "";
@@ -18582,6 +19209,14 @@ function withScheme(theme, scheme) {
18582
19209
  const names = theme.split(/\s+/).filter((word) => word !== "" && !SCHEME_WORDS.includes(word));
18583
19210
  return [...names, scheme].join(" ");
18584
19211
  }
19212
+ function findAnchor(root, selector) {
19213
+ try {
19214
+ const scope = root instanceof ShadowRoot ? root : document;
19215
+ return scope.querySelector(selector) ?? document.querySelector(selector);
19216
+ } catch {
19217
+ return null;
19218
+ }
19219
+ }
18585
19220
  var AgUiChat = class extends HTMLElement {
18586
19221
  /** Agent factory; override to inject a custom or fake agent (tests). */
18587
19222
  agentFactory = createHttpAgent;
@@ -18903,8 +19538,21 @@ var AgUiChat = class extends HTMLElement {
18903
19538
  #launcher;
18904
19539
  /** The launcher's unread badge; hidden at zero, and when the host opts out. */
18905
19540
  #badge;
18906
- /** The rail's vertical caption. Rendered only by the sidebar's edge rail. */
19541
+ /** The rail's vertical caption. Rendered only by the sidebar's edge rail and the bottom bar. */
18907
19542
  #railLabel = document.createElement("span");
19543
+ /**
19544
+ * The bottom bar under collapse-to="bar": the launcher drawn as a strip, and
19545
+ * the history button beside it. Outside the bar it draws no box of its own.
19546
+ */
19547
+ #dockBar = document.createElement("div");
19548
+ /** The bottom bar's history button, which opens the drawer as a popover. */
19549
+ #barHistory = document.createElement("button");
19550
+ /** The bar height last reported, so the event fires on a change only. */
19551
+ #barHeight = 0;
19552
+ /** The element the panel rises from under collapse-to="none", set as a property. */
19553
+ #anchor = null;
19554
+ /** Whether a missing anchor has been reported; once per anchor given. */
19555
+ #anchorWarned = false;
18908
19556
  // Answers that finished while the widget was collapsed. Expanding clears it.
18909
19557
  #unread = 0;
18910
19558
  /** Empty-state region at the top of the message list; hidden once anything renders. */
@@ -18965,6 +19613,7 @@ var AgUiChat = class extends HTMLElement {
18965
19613
  */
18966
19614
  #onViewportResize = () => {
18967
19615
  this.#placement.publishVisualViewport();
19616
+ this.#syncDock();
18968
19617
  if (this.#placement.dragging()) {
18969
19618
  return;
18970
19619
  }
@@ -18976,6 +19625,8 @@ var AgUiChat = class extends HTMLElement {
18976
19625
  #voice;
18977
19626
  /** Whether the element is currently in the DOM; gates the connect-time warning. */
18978
19627
  #connected = false;
19628
+ /** A {@link newChat} asked for before the element connected, to start at connect. */
19629
+ #newChatOnConnect = false;
18979
19630
  /**
18980
19631
  * Aborted when the element leaves the document, and replaced when it comes
18981
19632
  * back.
@@ -19226,7 +19877,7 @@ var AgUiChat = class extends HTMLElement {
19226
19877
  root: this.#root,
19227
19878
  connected: () => this.#connected,
19228
19879
  collapsed: () => this.collapsed,
19229
- collapsible: () => surfaceFor(this.getAttribute("placement")).collapseTo !== null,
19880
+ collapsible: () => this.#surface().collapseTo !== null,
19230
19881
  strings: () => this.#strings,
19231
19882
  readPreference: (base) => this.#storage.readPreference(base),
19232
19883
  writePreference: (base, value) => this.#storage.writePreference(base, value),
@@ -19302,7 +19953,7 @@ var AgUiChat = class extends HTMLElement {
19302
19953
  });
19303
19954
  this.#drawer = new ThreadDrawer({
19304
19955
  onSelect: (threadId) => {
19305
- void this.#history.switchThread(threadId);
19956
+ this.#history.switchThread(threadId).catch(() => void 0);
19306
19957
  },
19307
19958
  onNew: () => {
19308
19959
  this.newChat();
@@ -19395,6 +20046,8 @@ var AgUiChat = class extends HTMLElement {
19395
20046
  return [
19396
20047
  "title-text",
19397
20048
  "placement",
20049
+ "collapse-to",
20050
+ "anchor",
19398
20051
  "credentials",
19399
20052
  "user-key",
19400
20053
  "user-name",
@@ -19410,12 +20063,25 @@ var AgUiChat = class extends HTMLElement {
19410
20063
  }
19411
20064
  return;
19412
20065
  }
19413
- if (name === "placement") {
20066
+ if (name === "anchor") {
20067
+ this.#anchorWarned = false;
20068
+ this.#syncDock();
20069
+ return;
20070
+ }
20071
+ if (name === "placement" || name === "collapse-to") {
19414
20072
  this.#placement.releaseOwnedAxes();
19415
20073
  this.#placement.releaseLauncherPosition();
19416
- if (surfaceFor(this.getAttribute("placement")).collapseTo === null && this.collapsed) {
20074
+ if (this.#surface().collapseTo === null && this.collapsed) {
19417
20075
  this.setCollapsed(false);
19418
20076
  }
20077
+ this.#stampCollapseTarget();
20078
+ if (name === "collapse-to") {
20079
+ this.#syncLauncher();
20080
+ if (this.#surface().collapseTo !== "bar") {
20081
+ this.#homeDrawer();
20082
+ }
20083
+ }
20084
+ this.#syncDock();
19419
20085
  requestAnimationFrame(() => this.#placement.syncResizeAnchor());
19420
20086
  return;
19421
20087
  }
@@ -19672,6 +20338,26 @@ var AgUiChat = class extends HTMLElement {
19672
20338
  set toolDisplay(value) {
19673
20339
  this.setAttribute("data-tool-display", value);
19674
20340
  }
20341
+ /**
20342
+ * How an opened step group draws its calls, from the
20343
+ * `data-tool-group-display` attribute (`full` / `compact` / `minimal` /
20344
+ * `inline`). Defaults to `full`, for an absent attribute and for a value the
20345
+ * element does not know, which is what a group drew before the attribute.
20346
+ *
20347
+ * Read only while `toolDisplay` is `grouped`: under a card mode the groups
20348
+ * are flattened into that mode's cards. Applied by the shadow CSS from the
20349
+ * attribute itself, so changing it restyles groups already on screen.
20350
+ */
20351
+ get toolGroupDisplay() {
20352
+ const attr = this.getAttribute("data-tool-group-display");
20353
+ if (attr === TOOL_GROUP_DISPLAY.INLINE || attr === TOOL_GROUP_DISPLAY.MINIMAL || attr === TOOL_GROUP_DISPLAY.COMPACT) {
20354
+ return attr;
20355
+ }
20356
+ return TOOL_GROUP_DISPLAY.FULL;
20357
+ }
20358
+ set toolGroupDisplay(value) {
20359
+ this.setAttribute("data-tool-group-display", value);
20360
+ }
19675
20361
  /**
19676
20362
  * The host's own constructed stylesheets, adopted after the built-in one.
19677
20363
  *
@@ -19717,10 +20403,24 @@ var AgUiChat = class extends HTMLElement {
19717
20403
  this.#render();
19718
20404
  this.#drawer.setStrings(this.#strings);
19719
20405
  this.#checkpoints.setStrings(this.#strings);
19720
- if (surfaceFor(this.getAttribute("placement")).collapseTo !== null && this.#startsCollapsed()) {
20406
+ if (this.#surface().collapseTo !== null && this.#startsCollapsed()) {
19721
20407
  this.setAttribute("collapsed", "");
19722
20408
  }
19723
20409
  this.#syncLauncher();
20410
+ this.#stampCollapseTarget();
20411
+ const dockObserver = new ResizeObserver(() => this.#syncDock());
20412
+ dockObserver.observe(this.#dockBar);
20413
+ this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
20414
+ this.#syncDock();
20415
+ if (this.#surface().collapseTo === "none") {
20416
+ this.dispatchEvent(
20417
+ new CustomEvent(TOGGLE_EVENT, {
20418
+ detail: { collapsed: this.collapsed },
20419
+ bubbles: true,
20420
+ composed: true
20421
+ })
20422
+ );
20423
+ }
19724
20424
  this.#skills.init();
19725
20425
  const held = this.#principalKey;
19726
20426
  const changed = held !== null && held !== this.userKey;
@@ -19743,6 +20443,10 @@ var AgUiChat = class extends HTMLElement {
19743
20443
  void this.#history.refreshDrawer();
19744
20444
  }
19745
20445
  this.#connected = true;
20446
+ if (this.#newChatOnConnect) {
20447
+ this.#newChatOnConnect = false;
20448
+ this.newChat();
20449
+ }
19746
20450
  }
19747
20451
  /**
19748
20452
  * The catalog requests the element issues on startup: the tool labels
@@ -19875,15 +20579,15 @@ var AgUiChat = class extends HTMLElement {
19875
20579
  this.#threadStore = null;
19876
20580
  const url = this.getAttribute("data-threads-url");
19877
20581
  if (url !== null) {
19878
- const inner = this.conversationStore;
20582
+ const inner2 = this.conversationStore;
19879
20583
  const remote = new RemoteConversationStore(
19880
20584
  url,
19881
20585
  () => this.#headersFor(url),
19882
- inner,
20586
+ inner2,
19883
20587
  () => this.#requestCredentials(),
19884
20588
  this.getAttribute("data-threads-cache") !== "false"
19885
20589
  );
19886
- this.#threadStore = { remote, inner };
20590
+ this.#threadStore = { remote, inner: inner2 };
19887
20591
  this.conversationStore = remote;
19888
20592
  }
19889
20593
  }
@@ -19922,7 +20626,7 @@ var AgUiChat = class extends HTMLElement {
19922
20626
  * its own chrome.
19923
20627
  */
19924
20628
  setCollapsed(collapsed, options = {}) {
19925
- if (collapsed && surfaceFor(this.getAttribute("placement")).collapseTo === null) {
20629
+ if (collapsed && this.#surface().collapseTo === null) {
19926
20630
  return;
19927
20631
  }
19928
20632
  if (options.announce === true && collapsed && !this.collapsed) {
@@ -19934,8 +20638,10 @@ var AgUiChat = class extends HTMLElement {
19934
20638
  if (collapsed) {
19935
20639
  this.setAttribute("collapsed", "");
19936
20640
  } else {
20641
+ this.#homeDrawer();
19937
20642
  this.removeAttribute("collapsed");
19938
20643
  }
20644
+ this.#syncDock();
19939
20645
  writeStoredItem(this.#storage.key(COLLAPSED_KEY), collapsed ? "1" : "0");
19940
20646
  this.#setUnread(0);
19941
20647
  this.dispatchEvent(
@@ -19975,6 +20681,94 @@ var AgUiChat = class extends HTMLElement {
19975
20681
  get running() {
19976
20682
  return this.#running;
19977
20683
  }
20684
+ /**
20685
+ * The height of the bottom bar in CSS px, and 0 when none is drawn: under
20686
+ * any collapse-to but "bar", and on a phone while the panel is open, where
20687
+ * the panel is the screen. The bar is fixed over the page, so a host pads
20688
+ * its own page by this much; {@link BAR_HEIGHT_EVENT} says when it changes.
20689
+ */
20690
+ get barHeight() {
20691
+ return this.#barHeight;
20692
+ }
20693
+ /**
20694
+ * The element the panel rises from under collapse-to="none", right-aligned
20695
+ * to it: typically the host's own button in its own bar. Takes precedence
20696
+ * over the anchor attribute, which names one by selector. One that resolves
20697
+ * to nothing leaves the panel in its placement's own corner, with a console
20698
+ * warning.
20699
+ */
20700
+ get anchor() {
20701
+ return this.#anchor;
20702
+ }
20703
+ set anchor(value) {
20704
+ this.#anchor = value;
20705
+ this.#anchorWarned = false;
20706
+ this.#syncDock();
20707
+ }
20708
+ /** The surface the live placement and collapse-to attributes describe. */
20709
+ #surface() {
20710
+ return surfaceFor(this.getAttribute("placement"), this.getAttribute("collapse-to"));
20711
+ }
20712
+ /**
20713
+ * Stamp what the panel collapses to as data-collapsed-to, which is what the
20714
+ * stylesheet draws the bar and the hidden state from. Resolved here rather
20715
+ * than in CSS, because "bar" over a sidebar is its rail and "none" over a
20716
+ * page is no collapse at all, and the stylesheet would have to restate that
20717
+ * table.
20718
+ */
20719
+ #stampCollapseTarget() {
20720
+ const target = this.#surface().collapseTo;
20721
+ if (target === null) {
20722
+ this.removeAttribute("data-collapsed-to");
20723
+ } else {
20724
+ this.setAttribute("data-collapsed-to", target);
20725
+ }
20726
+ }
20727
+ /** Re-measure the bar or the anchor, and tell the host when the bar's height changed. */
20728
+ #syncDock() {
20729
+ if (!this.isConnected) {
20730
+ return;
20731
+ }
20732
+ const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
20733
+ if (height === this.#barHeight) {
20734
+ return;
20735
+ }
20736
+ this.#barHeight = height;
20737
+ this.dispatchEvent(
20738
+ new CustomEvent(BAR_HEIGHT_EVENT, {
20739
+ detail: { height },
20740
+ bubbles: true,
20741
+ composed: true
20742
+ })
20743
+ );
20744
+ }
20745
+ /**
20746
+ * The anchor in force under collapse-to="none": the property, else the
20747
+ * attribute's selector, looked up in the root this element lives in and
20748
+ * then the document, so a chat inside a framework's shadow root can name a
20749
+ * button beside it. Null under any other collapse-to, with nothing given,
20750
+ * or when what was given is not in the document -- reported once per anchor.
20751
+ */
20752
+ #resolveAnchor() {
20753
+ if (this.#surface().collapseTo !== "none") {
20754
+ return null;
20755
+ }
20756
+ const selector = this.getAttribute("anchor") ?? "";
20757
+ if (this.#anchor === null && selector === "") {
20758
+ return null;
20759
+ }
20760
+ const found = this.#anchor ?? findAnchor(this.getRootNode(), selector);
20761
+ if (found?.isConnected === true) {
20762
+ return found;
20763
+ }
20764
+ if (!this.#anchorWarned) {
20765
+ this.#anchorWarned = true;
20766
+ console.warn(
20767
+ `<ag-ui-chat>: the anchor ${this.#anchor === null ? `"${selector}"` : "element"} is not in the document, so the panel opens from its placement's own corner.`
20768
+ );
20769
+ }
20770
+ return null;
20771
+ }
19978
20772
  /**
19979
20773
  * Whether to mount collapsed.
19980
20774
  *
@@ -19997,7 +20791,7 @@ var AgUiChat = class extends HTMLElement {
19997
20791
  if (stored !== null) {
19998
20792
  return stored === "1";
19999
20793
  }
20000
- return surfaceFor(this.getAttribute("placement")).startsCollapsed && !this.hasAttribute("data-start-open");
20794
+ return this.#surface().startsCollapsed && !this.hasAttribute("data-start-open");
20001
20795
  }
20002
20796
  /**
20003
20797
  * Describe the panel to whoever is asking -- in practice the agent, through
@@ -20040,12 +20834,13 @@ var AgUiChat = class extends HTMLElement {
20040
20834
  * Flip the scheme word of the `theme` attribute between `light` and `dark`
20041
20835
  * and persist the choice per tab. Bound to the optional built-in header theme
20042
20836
  * toggle (`data-theme-toggle`). The theme name and any host words are kept:
20043
- * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. Any
20044
- * scheme but `dark` (including `auto`) flips to `dark` first.
20837
+ * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. With no
20838
+ * scheme word (`auto`, or a host rule setting `color-scheme` on the element),
20839
+ * it flips away from the scheme the panel is drawn in.
20045
20840
  */
20046
20841
  toggleTheme() {
20047
20842
  const theme = this.getAttribute("theme") ?? "";
20048
- const scheme = theme.split(/\s+/).includes("dark") ? "light" : "dark";
20843
+ const scheme = this.#drawnScheme() === "dark" ? "light" : "dark";
20049
20844
  this.setAttribute("theme", withScheme(theme, scheme));
20050
20845
  this.#storage.writePreference(THEME_KEY, scheme);
20051
20846
  this.#syncThemeGlyph();
@@ -20125,9 +20920,36 @@ var AgUiChat = class extends HTMLElement {
20125
20920
  this.conversationStore = store;
20126
20921
  this.#wireThreadStore();
20127
20922
  }
20923
+ /**
20924
+ * The scheme the panel is drawn in, which the toggle flips and its glyph
20925
+ * offers the way out of. A scheme word decides it, since the scheme rules
20926
+ * hold against a host's. Without one the panel follows whatever sets its
20927
+ * color-scheme: a host rule on the element, auto's "light dark", or the
20928
+ * light default. Both schemes allowed means the system picks. Read before
20929
+ * the shadow styles apply too (the glyph is drawn first), when the computed
20930
+ * value carries only a host rule, so auto is answered from its word.
20931
+ */
20932
+ #drawnScheme() {
20933
+ const words = (this.getAttribute("theme") ?? "").split(/\s+/);
20934
+ if (words.includes("dark")) {
20935
+ return "dark";
20936
+ }
20937
+ if (words.includes("light")) {
20938
+ return "light";
20939
+ }
20940
+ const declared = getComputedStyle(this).colorScheme.split(/\s+/);
20941
+ const dark = declared.includes("dark");
20942
+ if (dark !== declared.includes("light")) {
20943
+ return dark ? "dark" : "light";
20944
+ }
20945
+ if (dark || words.includes("auto")) {
20946
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
20947
+ }
20948
+ return "light";
20949
+ }
20128
20950
  /** Reflect the current theme on the toggle: show the destination's glyph. */
20129
20951
  #syncThemeGlyph() {
20130
- const dark = (this.getAttribute("theme") ?? "").split(/\s+/).includes("dark");
20952
+ const dark = this.#drawnScheme() === "dark";
20131
20953
  this.#themeToggle.replaceChildren(
20132
20954
  iconElement("theme", "theme-icon", dark ? ICON_SUN : ICON_MOON, null)
20133
20955
  );
@@ -20142,8 +20964,25 @@ var AgUiChat = class extends HTMLElement {
20142
20964
  * {@link toggleCollapsed} and {@link toggleTheme}.
20143
20965
  */
20144
20966
  openThreads() {
20967
+ if (this.collapsed && this.#surface().collapseTo === "bar") {
20968
+ this.#dockBar.append(this.#drawer.element);
20969
+ } else {
20970
+ this.#homeDrawer();
20971
+ }
20145
20972
  this.#history.openThreads();
20146
20973
  }
20974
+ /**
20975
+ * Put the drawer back in the panel, where every surface but the bar's
20976
+ * popover draws it. A popover still open is closed first: the list was
20977
+ * opened over the bar, and carried into the panel it would cover the
20978
+ * conversation the user asked to see.
20979
+ */
20980
+ #homeDrawer() {
20981
+ if (this.#drawer.element.parentNode === this.#dockBar) {
20982
+ this.#drawer.close();
20983
+ this.#chat.insertBefore(this.#drawer.element, this.#checkpoints.element);
20984
+ }
20985
+ }
20147
20986
  /**
20148
20987
  * Open the checkpoints panel (the `::part(checkpoints-button)` route).
20149
20988
  *
@@ -20167,16 +21006,19 @@ var AgUiChat = class extends HTMLElement {
20167
21006
  * for a host that lists conversations in its own chrome.
20168
21007
  *
20169
21008
  * Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
20170
- * fired. **A run in flight is cancelled**, as the list and {@link newChat} do;
21009
+ * fired; a second call for the thread already loading settles with that load.
21010
+ * If the store rejects the load, the element stays on the thread it was on,
21011
+ * fires nothing, and the promise rejects with the store's error.
21012
+ * **A run in flight is cancelled**, as the list and {@link newChat} do;
20171
21013
  * a host that would rather wait reads {@link running} first. The thread
20172
21014
  * already on screen is left alone, and so is everything before the element
20173
21015
  * connects, since until then it has no store to read the thread from.
20174
21016
  */
20175
- async switchThread(threadId) {
21017
+ switchThread(threadId) {
20176
21018
  if (!this.#connected) {
20177
- return;
21019
+ return Promise.resolve();
20178
21020
  }
20179
- await this.#history.switchThread(threadId);
21021
+ return this.#history.switchThread(threadId);
20180
21022
  }
20181
21023
  /**
20182
21024
  * Delete a stored thread, as the conversation list's delete does. Deleting the
@@ -20216,6 +21058,10 @@ var AgUiChat = class extends HTMLElement {
20216
21058
  * one. The page is not scrolled to bring the composer into view.
20217
21059
  */
20218
21060
  newChat() {
21061
+ if (!this.#connected) {
21062
+ this.#newChatOnConnect = true;
21063
+ return;
21064
+ }
20219
21065
  this.#cancelRun();
20220
21066
  this.#history.reapUnsent();
20221
21067
  this.#resetState();
@@ -20453,19 +21299,45 @@ var AgUiChat = class extends HTMLElement {
20453
21299
  launcher: this.#launcher,
20454
21300
  badge: this.#badge,
20455
21301
  railLabel: this.#railLabel,
21302
+ bar: this.#dockBar,
21303
+ barHistory: this.#barHistory,
20456
21304
  titleText,
20457
21305
  iconUrl: readLauncherIconUrl(this),
20458
21306
  strings: this.#strings,
20459
21307
  signal,
20460
- onOpen: () => this.setCollapsed(false)
21308
+ // The bar stays on screen with the panel open, as the way to minimise
21309
+ // it, so its strip toggles; everywhere else the launcher is only drawn
21310
+ // while collapsed and only ever opens.
21311
+ onOpen: () => this.setCollapsed(this.#surface().collapseTo === "bar" && !this.collapsed),
21312
+ // The bar's history button toggles: the popover has no backdrop, and a
21313
+ // press on the bar is not a press outside it, so this is how the button
21314
+ // that opened the list closes it again. openThreads() stays open-only.
21315
+ onHistory: () => {
21316
+ if (this.#drawer.isOpen()) {
21317
+ this.#drawer.close();
21318
+ } else {
21319
+ this.openThreads();
21320
+ }
21321
+ }
20461
21322
  });
21323
+ document.addEventListener(
21324
+ "pointerdown",
21325
+ (event) => {
21326
+ if (this.#drawer.element.parentNode === this.#dockBar && this.#drawer.isOpen()) {
21327
+ if (!event.composedPath().includes(this.#dockBar)) {
21328
+ this.#drawer.close();
21329
+ }
21330
+ }
21331
+ },
21332
+ { signal }
21333
+ );
20462
21334
  this.#placement.enableLauncherDrag(signal);
20463
21335
  this.#placement.mountResizeGrips(this.#chat);
20464
21336
  this.#builtInSheet = adoptStyles(this.#root, this.#extraStyles);
20465
21337
  const probe = this.#placement.probe;
20466
21338
  probe.className = "viewport-probe";
20467
21339
  probe.setAttribute("aria-hidden", "true");
20468
- this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#launcher);
21340
+ this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
20469
21341
  }
20470
21342
  /**
20471
21343
  * Reflect the collapsed state and the unread count on the launcher.
@@ -20479,7 +21351,7 @@ var AgUiChat = class extends HTMLElement {
20479
21351
  const unread = this.#unread;
20480
21352
  this.#badge.textContent = unread > 9 ? "9+" : String(unread);
20481
21353
  this.#badge.hidden = unread === 0 || !isUnreadBadgeEnabled(this);
20482
- const label = this.#badge.hidden ? this.#strings.expand : fillUiString(this.#strings.expandUnread, { count: unread });
21354
+ const label = !this.collapsed && this.#surface().collapseTo === "bar" ? this.#strings.collapse : this.#badge.hidden ? this.#strings.expand : fillUiString(this.#strings.expandUnread, { count: unread });
20483
21355
  this.#launcher.setAttribute("aria-label", label);
20484
21356
  this.#launcher.title = label;
20485
21357
  }
@@ -21112,11 +21984,12 @@ function setControlValue(el2, value) {
21112
21984
  }
21113
21985
 
21114
21986
  // src/version.ts
21115
- var VERSION = "0.47.0";
21987
+ var VERSION = "0.48.0";
21116
21988
  export {
21117
21989
  ATTACHMENT_EVENT,
21118
21990
  AgUiChat,
21119
21991
  AgUiClient,
21992
+ BAR_HEIGHT_EVENT,
21120
21993
  CHART_ACTIVITY_TYPE,
21121
21994
  CHART_TOOL_NAME,
21122
21995
  CHAT_CORNERS,
@@ -21149,6 +22022,7 @@ export {
21149
22022
  TOGGLE_EVENT,
21150
22023
  TOOL_CALL_STATUS,
21151
22024
  TOOL_DISPLAY,
22025
+ TOOL_GROUP_DISPLAY,
21152
22026
  TOOL_OUTCOME,
21153
22027
  ToolCallCard,
21154
22028
  UNREAD_EVENT,