@artooi/ag-ui-web-component 0.47.0 → 0.49.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 (60) hide show
  1. package/CHANGELOG.md +205 -1
  2. package/README.md +235 -16
  3. package/dist/ag-ui-web-component.bundle.js +647 -181
  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 +44 -5
  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 +1184 -69
  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 +16 -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 +32 -19
  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/quiet_styles.d.ts.map +1 -1
  38. package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/constants.ts +30 -2
  41. package/src/core/ag_ui_chat.ts +335 -28
  42. package/src/core/events/bar_height_detail.ts +5 -0
  43. package/src/dom/highlight_overlay.ts +2 -1
  44. package/src/dom/ring_color.ts +141 -4
  45. package/src/index.ts +4 -0
  46. package/src/tools/chat_surface_tools.ts +13 -5
  47. package/src/ui/history/conversation_history.ts +51 -7
  48. package/src/ui/placement/collapse_styles.ts +248 -2
  49. package/src/ui/placement/dock_styles.ts +95 -0
  50. package/src/ui/placement/panel_placement.ts +300 -16
  51. package/src/ui/placement/small_viewport_styles.ts +9 -0
  52. package/src/ui/placement/surface_for.ts +58 -22
  53. package/src/ui/progress/tool_group_styles.ts +52 -2
  54. package/src/ui/progress/tool_step_styles.ts +5 -3
  55. package/src/ui/shell/build_launcher.ts +32 -5
  56. package/src/ui/shell/token_styles.ts +58 -1
  57. package/src/ui/styles.ts +4 -0
  58. package/src/ui/themes/quiet_styles.ts +19 -3
  59. package/src/ui/themes/scheme_styles.ts +10 -1
  60. 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>`;
@@ -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) {
@@ -5682,12 +5810,14 @@ function boxAt(grip, axis, rect, x2, y2) {
5682
5810
  var CORNER = {
5683
5811
  collapseTo: "launcher",
5684
5812
  launcherDraggable: true,
5813
+ panelMovable: true,
5685
5814
  resizeAxis: "both",
5686
5815
  startsCollapsed: true
5687
5816
  };
5688
5817
  var DOCKED = {
5689
5818
  collapseTo: "launcher",
5690
5819
  launcherDraggable: false,
5820
+ panelMovable: false,
5691
5821
  resizeAxis: "width",
5692
5822
  startsCollapsed: false
5693
5823
  };
@@ -5703,34 +5833,58 @@ var SURFACES = /* @__PURE__ */ new Map([
5703
5833
  {
5704
5834
  collapseTo: "launcher",
5705
5835
  launcherDraggable: false,
5836
+ panelMovable: false,
5706
5837
  resizeAxis: "none",
5707
5838
  startsCollapsed: false
5708
5839
  }
5709
5840
  ],
5710
5841
  [
5711
5842
  "page",
5712
- { collapseTo: null, launcherDraggable: false, resizeAxis: "none", startsCollapsed: false }
5843
+ {
5844
+ collapseTo: null,
5845
+ launcherDraggable: false,
5846
+ panelMovable: false,
5847
+ resizeAxis: "none",
5848
+ startsCollapsed: false
5849
+ }
5713
5850
  ],
5714
5851
  [
5715
5852
  "embedded",
5716
- { collapseTo: "header", launcherDraggable: false, resizeAxis: "both", startsCollapsed: false }
5853
+ {
5854
+ collapseTo: "header",
5855
+ launcherDraggable: false,
5856
+ panelMovable: false,
5857
+ resizeAxis: "both",
5858
+ startsCollapsed: false
5859
+ }
5717
5860
  ]
5718
5861
  ]);
5719
5862
  var UNKNOWN = {
5720
5863
  collapseTo: "launcher",
5721
5864
  launcherDraggable: false,
5865
+ panelMovable: false,
5722
5866
  resizeAxis: "both",
5723
5867
  startsCollapsed: false
5724
5868
  };
5725
- function surfaceFor(placement) {
5726
- return SURFACES.get(placement) ?? UNKNOWN;
5869
+ function surfaceFor(placement, collapseTo = null) {
5870
+ const surface = SURFACES.get(placement) ?? UNKNOWN;
5871
+ if (collapseTo === "bar" && surface.collapseTo === "launcher") {
5872
+ return { ...surface, collapseTo: "bar", launcherDraggable: false };
5873
+ }
5874
+ if (collapseTo === "none" && surface.collapseTo !== null) {
5875
+ return { ...surface, collapseTo: "none", launcherDraggable: false };
5876
+ }
5877
+ return surface;
5727
5878
  }
5728
5879
 
5729
5880
  // src/ui/placement/panel_placement.ts
5730
5881
  var SIZE_KEY = "ag-ui-chat:size";
5731
5882
  var DRAG_WIDTH = "--_drag-width";
5732
5883
  var DRAG_HEIGHT = "--_drag-height";
5884
+ var DOCK_BOTTOM = "--_dock-bottom";
5885
+ var DOCK_RIGHT = "--_dock-right";
5733
5886
  var LAUNCHER_KEY = "ag-ui-chat:launcher";
5887
+ var DOCK_KEY = "ag-ui-chat:dock";
5734
5888
  function asPoint(value) {
5735
5889
  if (typeof value !== "object" || value === null) {
5736
5890
  return null;
@@ -5771,7 +5925,7 @@ var PanelPlacement = class {
5771
5925
  */
5772
5926
  enablePanelDrag(header) {
5773
5927
  enablePanelDrag(header, {
5774
- enabled: () => !this.#host.collapsed() && this.#launcherDraggable(),
5928
+ enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
5775
5929
  rect: () => this.#host.element.getBoundingClientRect(),
5776
5930
  apply: (box, from) => this.#movePanel(box, from),
5777
5931
  commit: (box, from) => this.#commitPanel(box, from)
@@ -5817,11 +5971,23 @@ var PanelPlacement = class {
5817
5971
  * Drop the dragged size and its stored copy, so the placement's own size and
5818
5972
  * the host's tokens apply again. For a host entering a sized state of its
5819
5973
  * own, which a user's drag would otherwise outrank.
5974
+ *
5975
+ * A docked panel the user dragged off its dock goes back to it as well, and
5976
+ * the stored spot with it: there the position is part of the same resting
5977
+ * state the host is asking for. A corner panel keeps a dragged position, as
5978
+ * it always has.
5820
5979
  */
5821
5980
  resetSize() {
5822
5981
  this.#host.element.style.removeProperty(DRAG_WIDTH);
5823
5982
  this.#host.element.style.removeProperty(DRAG_HEIGHT);
5824
5983
  this.#host.clearPreference(SIZE_KEY);
5984
+ if (this.#docked()) {
5985
+ if (this.#dockPos !== null) {
5986
+ this.#host.element.style.removeProperty("--ag-ui-inset");
5987
+ }
5988
+ this.#dockPos = null;
5989
+ this.#storeDockPosition(null);
5990
+ }
5825
5991
  this.syncResizeAnchor();
5826
5992
  }
5827
5993
  /**
@@ -5848,6 +6014,23 @@ var PanelPlacement = class {
5848
6014
  * from the launcher would move the panel the user just placed.
5849
6015
  */
5850
6016
  #panelPos = null;
6017
+ /**
6018
+ * Where the user dragged a docked panel, in viewport coordinates, or null
6019
+ * while it stands on the bar or the anchor.
6020
+ *
6021
+ * Apart from the two corner positions above because it moves nothing else:
6022
+ * a docked panel has no launcher to carry, and the corner it opens from is
6023
+ * the dock's. Held as the user stated it and never as clamped, so a bar that
6024
+ * grew, or a phone that took the panel full-bleed, gives the spot back
6025
+ * unchanged once there is room for it again.
6026
+ */
6027
+ #dockPos = null;
6028
+ /**
6029
+ * The line a docked panel stays above, in viewport coordinates: the bar's
6030
+ * top, or the anchor's. Infinite where neither is drawn. Measured by
6031
+ * syncDock, which runs on every change either can make.
6032
+ */
6033
+ #dockTop = Number.POSITIVE_INFINITY;
5851
6034
  /**
5852
6035
  * The corner the panel opens away from, once this element is placing itself.
5853
6036
  * Null means the host's layout still decides, and the anchor is measured.
@@ -5876,7 +6059,7 @@ var PanelPlacement = class {
5876
6059
  * rather than collapsed to nothing.
5877
6060
  */
5878
6061
  #withinViewport(box) {
5879
- const viewport = this.#viewport();
6062
+ const viewport = this.#docked() && this.#panelMovable() ? this.#dockRoom() : this.#viewport();
5880
6063
  const left = viewport.left + SCREEN_EDGE_MARGIN;
5881
6064
  const top = viewport.top + SCREEN_EDGE_MARGIN;
5882
6065
  const right = viewport.left + viewport.width - SCREEN_EDGE_MARGIN;
@@ -5905,10 +6088,15 @@ var PanelPlacement = class {
5905
6088
  const fullBleed = coversViewport(box, viewport);
5906
6089
  return {
5907
6090
  placement: this.#host.element.getAttribute("placement"),
6091
+ collapseTo: this.#surface().collapseTo,
5908
6092
  collapsed: this.#host.collapsed(),
5909
6093
  collapsible: this.#host.collapsible(),
5910
- movable: this.#launcherDraggable() && !fullBleed,
5911
- draggable: this.#launcherDraggable(),
6094
+ movable: this.#panelMovable() && !fullBleed,
6095
+ // Whether the page left moving on, which is not the same question as
6096
+ // whether this panel moves: a docked sidebar's position is owned by its
6097
+ // placement, and reporting that as the host having turned moving off is
6098
+ // the sentence the move refusal would then get wrong.
6099
+ draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false" && (this.#surface().launcherDraggable || this.#docked()),
5912
6100
  fullBleed,
5913
6101
  box: {
5914
6102
  left: Math.round(box.left),
@@ -5926,7 +6114,7 @@ var PanelPlacement = class {
5926
6114
  }
5927
6115
  /** The move behind `AgUiChat.moveTo`, whose doc is the contract. */
5928
6116
  moveTo(corner, options = {}) {
5929
- if (!this.#launcherDraggable()) {
6117
+ if (!this.#panelMovable()) {
5930
6118
  return false;
5931
6119
  }
5932
6120
  const restore = options.announce === true ? this.#captureGeometry() : null;
@@ -5936,11 +6124,22 @@ var PanelPlacement = class {
5936
6124
  return false;
5937
6125
  }
5938
6126
  const [edgeY, edgeX] = corner.split("-");
5939
- const nearX = viewport.left + EDGE_MARGIN;
5940
- const nearY = viewport.top + EDGE_MARGIN;
5941
- const left = edgeX === "left" ? nearX : Math.max(nearX, viewport.left + viewport.width - EDGE_MARGIN - box.width);
5942
- const top = edgeY === "top" ? nearY : Math.max(nearY, viewport.top + viewport.height - EDGE_MARGIN - box.height);
6127
+ const docked = this.#docked();
6128
+ const room = docked ? this.#dockRoom() : viewport;
6129
+ const nearX = room.left + EDGE_MARGIN;
6130
+ const nearY = room.top + EDGE_MARGIN;
6131
+ const left = edgeX === "left" ? nearX : Math.max(nearX, room.left + room.width - EDGE_MARGIN - box.width);
6132
+ const top = edgeY === "top" ? nearY : Math.max(nearY, room.top + room.height - EDGE_MARGIN - box.height);
5943
6133
  const host = { left, top, right: left + box.width, bottom: top + box.height };
6134
+ if (docked) {
6135
+ this.#dockPos = { left, top };
6136
+ this.#writeDockInset(host);
6137
+ this.#storeDockPosition(this.#dockPos);
6138
+ if (restore !== null) {
6139
+ this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
6140
+ }
6141
+ return true;
6142
+ }
5944
6143
  const launcherWidth = this.#host.launcher.offsetWidth;
5945
6144
  const launcherHeight = this.#host.launcher.offsetHeight;
5946
6145
  this.#placePanelAndLauncher(host, {
@@ -5971,6 +6170,7 @@ var PanelPlacement = class {
5971
6170
  const corner = this.#host.element.getAttribute("data-expand-corner");
5972
6171
  const launcherPos = this.#launcherPos;
5973
6172
  const panelPos = this.#panelPos;
6173
+ const dockPos = this.#dockPos;
5974
6174
  const expandCorner = this.#expandCorner;
5975
6175
  return () => {
5976
6176
  this.#restoreProperty("--ag-ui-inset", inset2);
@@ -5982,7 +6182,13 @@ var PanelPlacement = class {
5982
6182
  }
5983
6183
  this.#launcherPos = launcherPos;
5984
6184
  this.#panelPos = panelPos;
6185
+ this.#dockPos = dockPos;
5985
6186
  this.#expandCorner = expandCorner;
6187
+ if (this.#docked()) {
6188
+ this.#storeDockPosition(dockPos);
6189
+ this.syncResizeAnchor();
6190
+ return;
6191
+ }
5986
6192
  if (launcherPos === null) {
5987
6193
  this.#host.clearPreference(LAUNCHER_KEY);
5988
6194
  } else {
@@ -5999,7 +6205,7 @@ var PanelPlacement = class {
5999
6205
  * per interaction, because `placement` is a live attribute.
6000
6206
  */
6001
6207
  #resizeAxis() {
6002
- return surfaceFor(this.#host.element.getAttribute("placement")).resizeAxis;
6208
+ return this.#surface().resizeAxis;
6003
6209
  }
6004
6210
  /**
6005
6211
  * Which edges the layout is holding still, by measuring rather than guessing:
@@ -6099,13 +6305,145 @@ var PanelPlacement = class {
6099
6305
  this.#host.element.style.removeProperty(DRAG_WIDTH);
6100
6306
  }
6101
6307
  }
6308
+ /** The surface the live placement and collapse-to attributes describe. */
6309
+ #surface() {
6310
+ const element = this.#host.element;
6311
+ return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
6312
+ }
6313
+ /**
6314
+ * Whether the panel rises from the bottom bar or the host's anchor, until the
6315
+ * user drags it somewhere else.
6316
+ */
6317
+ #docked() {
6318
+ const target = this.#surface().collapseTo;
6319
+ return target === "bar" || target === "none";
6320
+ }
6321
+ /**
6322
+ * Write where a docked panel stands, and return the bar's height.
6323
+ *
6324
+ * Under the bar it is the strip's border box, which is 0 whenever the strip
6325
+ * is not drawn (a phone with the panel open, where the panel is the screen).
6326
+ * Under a host anchor it is the anchor's top and right, measured against the
6327
+ * layout viewport a fixed element is placed in; an anchor of null leaves the
6328
+ * panel in its placement's own corner. Every other surface carries neither
6329
+ * property, so nothing stale is left behind by a live switch.
6330
+ */
6331
+ syncDock(bar, anchor) {
6332
+ const element = this.#host.element;
6333
+ const target = this.#surface().collapseTo;
6334
+ const height = target === "bar" ? bar.offsetHeight : 0;
6335
+ if (target === "bar") {
6336
+ element.style.setProperty(DOCK_BOTTOM, `${height}px`);
6337
+ } else if (target === "none" && anchor !== null) {
6338
+ const rect = anchor.getBoundingClientRect();
6339
+ const screen = this.#screen();
6340
+ element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, screen.height - rect.top)}px`);
6341
+ element.style.setProperty(DOCK_RIGHT, `${Math.max(0, screen.width - rect.right)}px`);
6342
+ } else {
6343
+ element.style.removeProperty(DOCK_BOTTOM);
6344
+ }
6345
+ if (target !== "none" || anchor === null) {
6346
+ element.style.removeProperty(DOCK_RIGHT);
6347
+ }
6348
+ element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
6349
+ this.#dockTop = target === "bar" && height > 0 ? bar.getBoundingClientRect().top : target === "none" && anchor !== null ? anchor.getBoundingClientRect().top : Number.POSITIVE_INFINITY;
6350
+ this.#placeDocked();
6351
+ return height;
6352
+ }
6353
+ /**
6354
+ * The part of the usable viewport a docked panel may stand in: all of it
6355
+ * above the dock's line. The bar is what it minimises to, and the anchor is
6356
+ * the host's own way back, so a dragged panel never covers either.
6357
+ */
6358
+ #dockRoom() {
6359
+ const viewport = this.#viewport();
6360
+ const bottom = Math.min(viewport.top + viewport.height, this.#dockTop);
6361
+ return { ...viewport, height: Math.max(0, bottom - viewport.top) };
6362
+ }
6363
+ /**
6364
+ * Put a docked panel back where the user dragged it, held inside its room.
6365
+ *
6366
+ * The inline inset comes off first, which is both how the panel's resting
6367
+ * box is measured and what leaves a phone full-bleed: the small-viewport
6368
+ * block reads the host's inset before its own, so a dragged spot written
6369
+ * there would hold the panel off the screen's edges. The spot itself stays,
6370
+ * for the next time there is room for it.
6371
+ */
6372
+ #placeDocked() {
6373
+ if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
6374
+ return;
6375
+ }
6376
+ const at2 = this.#dockPos ?? this.#readDockPosition();
6377
+ if (at2 === null) {
6378
+ return;
6379
+ }
6380
+ this.#dockPos = at2;
6381
+ this.#host.element.style.removeProperty("--ag-ui-inset");
6382
+ const rest = this.#host.element.getBoundingClientRect();
6383
+ if (coversViewport(rest, this.#viewport())) {
6384
+ return;
6385
+ }
6386
+ this.syncResizeAnchor();
6387
+ this.#writeDockInset(
6388
+ clampPanel(
6389
+ { left: at2.left, top: at2.top, right: at2.left + rest.width, bottom: at2.top + rest.height },
6390
+ this.#dockRoom(),
6391
+ SCREEN_EDGE_MARGIN
6392
+ )
6393
+ );
6394
+ }
6395
+ /** Write the inset that stands a docked panel at `box`. */
6396
+ #writeDockInset(box) {
6397
+ this.#host.element.style.setProperty(
6398
+ "--ag-ui-inset",
6399
+ insetFrom(this.#anchor, box, this.#screen())
6400
+ );
6401
+ }
6402
+ /** Remember a docked panel's dragged spot, or forget it for null. */
6403
+ #storeDockPosition(at2) {
6404
+ if (at2 === null) {
6405
+ this.#host.clearPreference(DOCK_KEY);
6406
+ return;
6407
+ }
6408
+ this.#host.writePreference(DOCK_KEY, JSON.stringify({ left: at2.left, top: at2.top }));
6409
+ }
6410
+ /** The stored spot of a docked panel, or null for none or anything corrupt. */
6411
+ #readDockPosition() {
6412
+ const raw = this.#host.readPreference(DOCK_KEY);
6413
+ if (raw === null) {
6414
+ return null;
6415
+ }
6416
+ try {
6417
+ return asPoint(JSON.parse(raw));
6418
+ } catch {
6419
+ return null;
6420
+ }
6421
+ }
6422
+ /**
6423
+ * Whether the header drag and moveTo may move the open panel: the
6424
+ * placement's answer, unless the host turned moving off.
6425
+ */
6426
+ #panelMovable() {
6427
+ return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().panelMovable;
6428
+ }
6429
+ /**
6430
+ * Whether a header drag may start now. A docked panel on a phone is the
6431
+ * screen, and a drag there would pull it off the edges it fills; a corner
6432
+ * panel's header is left as it was.
6433
+ */
6434
+ #canDragPanel() {
6435
+ if (!this.#panelMovable()) {
6436
+ return false;
6437
+ }
6438
+ return !this.#docked() || !coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
6439
+ }
6102
6440
  /**
6103
6441
  * Whether this placement lets the launcher be dragged, read per interaction
6104
6442
  * because `placement` is a live attribute. `data-launcher-drag="false"` opts
6105
6443
  * a host out without giving up the launcher itself.
6106
6444
  */
6107
6445
  #launcherDraggable() {
6108
- return this.#host.element.getAttribute("data-launcher-drag") !== "false" && surfaceFor(this.#host.element.getAttribute("placement")).launcherDraggable;
6446
+ return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().launcherDraggable;
6109
6447
  }
6110
6448
  /**
6111
6449
  * The viewport the launcher and the panel both have to fit inside.
@@ -6275,6 +6613,12 @@ var PanelPlacement = class {
6275
6613
  * other still names the old one would move the launcher for no reason.
6276
6614
  */
6277
6615
  #movePanel(box, from) {
6616
+ if (this.#docked()) {
6617
+ const held2 = clampPanel(box, this.#dockRoom(), SCREEN_EDGE_MARGIN);
6618
+ this.#dockPos = { left: held2.left, top: held2.top };
6619
+ this.#writeDockInset(held2);
6620
+ return { held: held2, launcher: null };
6621
+ }
6278
6622
  if (!this.#launcherDraggable()) {
6279
6623
  return { held: box, launcher: null };
6280
6624
  }
@@ -6314,6 +6658,10 @@ var PanelPlacement = class {
6314
6658
  */
6315
6659
  #commitPanel(box, from) {
6316
6660
  const { held, launcher } = this.#movePanel(box, from);
6661
+ if (this.#docked()) {
6662
+ this.#storeDockPosition({ left: held.left, top: held.top });
6663
+ return;
6664
+ }
6317
6665
  if (launcher === null) {
6318
6666
  return;
6319
6667
  }
@@ -6394,6 +6742,10 @@ var PanelPlacement = class {
6394
6742
  * unreachable -- it is the only way back to a collapsed conversation.
6395
6743
  */
6396
6744
  restoreLauncherPosition() {
6745
+ if (this.#docked()) {
6746
+ this.#placeDocked();
6747
+ return;
6748
+ }
6397
6749
  const stored = this.#readLauncherPosition();
6398
6750
  const launcher = this.#launcherPos ?? stored;
6399
6751
  if (launcher === null) {
@@ -6437,6 +6789,10 @@ var PanelPlacement = class {
6437
6789
  * the switch inline and pins a sidebar to wherever the floating launcher was.
6438
6790
  */
6439
6791
  releaseLauncherPosition() {
6792
+ if (this.#dockPos !== null && !(this.#docked() && this.#panelMovable())) {
6793
+ this.#dockPos = null;
6794
+ this.#host.element.style.removeProperty("--ag-ui-inset");
6795
+ }
6440
6796
  if (this.#launcherDraggable()) {
6441
6797
  return;
6442
6798
  }
@@ -6463,7 +6819,21 @@ var PanelPlacement = class {
6463
6819
  */
6464
6820
  #applyResize(grip, box) {
6465
6821
  box = this.#withinViewport(box);
6822
+ if (this.#docked() && this.#dockPos === null) {
6823
+ const now = this.#host.element.getBoundingClientRect();
6824
+ box = {
6825
+ left: grip.x === this.#anchor.x ? now.left : box.left,
6826
+ right: grip.x === this.#anchor.x ? now.right : box.right,
6827
+ top: grip.y === this.#anchor.y ? now.top : box.top,
6828
+ bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom
6829
+ };
6830
+ this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
6831
+ return box;
6832
+ }
6466
6833
  this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
6834
+ if (this.#docked()) {
6835
+ this.#dockPos = { left: box.left, top: box.top };
6836
+ }
6467
6837
  if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
6468
6838
  return box;
6469
6839
  }
@@ -6486,6 +6856,9 @@ var PanelPlacement = class {
6486
6856
  const held = this.#applyResize(grip, box);
6487
6857
  this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
6488
6858
  this.#storeLauncherPosition();
6859
+ if (this.#dockPos !== null) {
6860
+ this.#storeDockPosition(this.#dockPos);
6861
+ }
6489
6862
  this.syncResizeAnchor();
6490
6863
  }
6491
6864
  /**
@@ -8430,12 +8803,18 @@ var COLLAPSE_STYLES = `
8430
8803
  :host([collapsed][placement="embedded"]) .messages-wrap,
8431
8804
  :host([collapsed][placement="embedded"]) .messages,
8432
8805
  :host([collapsed][placement="embedded"]) .input-row,
8806
+ :host([collapsed][placement="embedded"]) slot[name="above-composer"],
8433
8807
  :host([collapsed][placement="embedded"]) .skill-chips,
8434
8808
  :host([collapsed][placement="embedded"]) .skill-palette,
8435
8809
  :host([collapsed][placement="embedded"]) .skill-hint {
8436
8810
  display: none;
8437
8811
  }
8438
8812
 
8813
+ /* The above-composer slot is in the list above because it is the host's content
8814
+ against the composer: a bare slot has no box of its own, so hiding the input
8815
+ row alone left the slotted row showing under the header bar. Its sibling slot
8816
+ below the composer is a footer and is left to the host. */
8817
+
8439
8818
  /* The page placement has no collapsed state, so it offers no control for one.
8440
8819
  A dedicated route has no "away" to go to: shrinking it left a strip of
8441
8820
  application chrome fixed over a route that no longer had an owner, and this
@@ -8463,6 +8842,338 @@ var COLLAPSE_STYLES = `
8463
8842
  transform: none;
8464
8843
  visibility: visible;
8465
8844
  }
8845
+
8846
+ /* \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
8847
+ The round launcher drawn as a strip along the bottom edge: the title, the
8848
+ unread badge and a running mark, with a history button beside it. The
8849
+ wrapper draws no box anywhere else, so the launcher lays out against the
8850
+ host as it always has; under the bar it is fixed to the bottom of the
8851
+ viewport, outside the host's box, which sits where the panel opens.
8852
+
8853
+ Here rather than with the launcher because it restyles the launcher's
8854
+ collapsed and pressed states, which this module follows in the cascade.
8855
+ The strip stays drawn while the panel is open on a wide viewport, as the
8856
+ way to minimise it; the phone block hides it then. */
8857
+ .dock-bar {
8858
+ display: contents;
8859
+ }
8860
+
8861
+ .bar-history {
8862
+ display: none;
8863
+ }
8864
+
8865
+ :host([data-collapsed-to="bar"]) .dock-bar {
8866
+ position: fixed;
8867
+ inset: auto var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left);
8868
+ display: flex;
8869
+ align-items: center;
8870
+ justify-content: flex-end;
8871
+ gap: 4px;
8872
+ box-sizing: border-box;
8873
+ min-height: 40px;
8874
+ padding: 4px 8px;
8875
+ border-top: 1px solid var(--_border);
8876
+ background: var(--_bg);
8877
+ color: var(--_fg);
8878
+ pointer-events: auto;
8879
+ }
8880
+
8881
+ /* A left-hand chat keeps its side: its controls and its panel at the left end. */
8882
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar {
8883
+ justify-content: flex-start;
8884
+ }
8885
+
8886
+ /* The launcher as the strip's title button. Its resting state hides it behind
8887
+ an open panel; the bar keeps it on screen, so every part of that state is
8888
+ restated, and the hover and press scales are cancelled at the weight of the
8889
+ rules that set them. The title truncates rather than taking a fixed width,
8890
+ leaving room beside it for whatever a later release adds to the row. */
8891
+ :host([data-collapsed-to="bar"]) .launcher {
8892
+ position: static;
8893
+ inset: auto;
8894
+ flex: 0 1 auto;
8895
+ justify-content: flex-start;
8896
+ gap: 8px;
8897
+ width: auto;
8898
+ min-width: 0;
8899
+ max-width: 100%;
8900
+ height: auto;
8901
+ min-height: 32px;
8902
+ padding: 4px 10px;
8903
+ border-radius: 8px;
8904
+ background: transparent;
8905
+ color: var(--_fg);
8906
+ box-shadow: none;
8907
+ touch-action: manipulation;
8908
+ opacity: 1;
8909
+ transform: none;
8910
+ visibility: visible;
8911
+ }
8912
+
8913
+ :host([data-collapsed-to="bar"]) .launcher:hover {
8914
+ background: var(--_wash);
8915
+ }
8916
+
8917
+ /* Bare at rest, like the history button beside it. The themes frame the
8918
+ round launcher with a border or a shadow at the weight of the rule above
8919
+ and later in the cascade, so the child combinator on the bar is there for
8920
+ weight, not scope: the launcher is always the bar's direct child. */
8921
+ :host([data-collapsed-to="bar"]) .dock-bar > .launcher {
8922
+ border: 0;
8923
+ box-shadow: none;
8924
+ }
8925
+
8926
+ :host([data-collapsed-to="bar"][collapsed]) .launcher:hover,
8927
+ :host([data-collapsed-to="bar"][collapsed]) .launcher:active {
8928
+ transform: none;
8929
+ }
8930
+
8931
+ :host([data-collapsed-to="bar"]) .launcher .icon-holder {
8932
+ flex: none;
8933
+ width: 18px;
8934
+ height: 18px;
8935
+ }
8936
+
8937
+ :host([data-collapsed-to="bar"]) .rail-label {
8938
+ display: block;
8939
+ min-width: 0;
8940
+ overflow: hidden;
8941
+ font-size: 0.9em;
8942
+ font-weight: 600;
8943
+ white-space: nowrap;
8944
+ text-overflow: ellipsis;
8945
+ }
8946
+
8947
+ :host([data-collapsed-to="bar"]) .launcher-badge {
8948
+ position: static;
8949
+ flex: none;
8950
+ box-shadow: none;
8951
+ }
8952
+
8953
+ /* The running mark: the element already reflects a run as data-running, so
8954
+ the bar shows one without a state of its own. */
8955
+ :host([data-collapsed-to="bar"][data-running]) .launcher::after {
8956
+ content: "";
8957
+ flex: none;
8958
+ width: 8px;
8959
+ height: 8px;
8960
+ border-radius: 50%;
8961
+ background: var(--_accent);
8962
+ animation: ag-ui-dock-running 1.2s ease-in-out infinite;
8963
+ }
8964
+
8965
+ @keyframes ag-ui-dock-running {
8966
+ 50% {
8967
+ opacity: 0.35;
8968
+ }
8969
+ }
8970
+
8971
+ @media (prefers-reduced-motion: reduce) {
8972
+ :host([data-collapsed-to="bar"][data-running]) .launcher::after {
8973
+ animation: none;
8974
+ }
8975
+ }
8976
+
8977
+ :host([data-collapsed-to="bar"]) .bar-history {
8978
+ display: inline-flex;
8979
+ flex: none;
8980
+ align-items: center;
8981
+ justify-content: center;
8982
+ width: 32px;
8983
+ height: 32px;
8984
+ padding: 0;
8985
+ border: 0;
8986
+ border-radius: 8px;
8987
+ background: transparent;
8988
+ color: var(--_muted);
8989
+ font: inherit;
8990
+ cursor: pointer;
8991
+ }
8992
+
8993
+ :host([data-collapsed-to="bar"]) .bar-history:hover {
8994
+ background: var(--_wash);
8995
+ color: var(--_fg);
8996
+ }
8997
+
8998
+ /* Both declare a display of their own, so each needs its own hidden rule. */
8999
+ :host([data-collapsed-to]) .dock-bar[hidden],
9000
+ :host([data-collapsed-to]) .bar-history[hidden] {
9001
+ display: none;
9002
+ }
9003
+
9004
+ /* History from the bar: the panel's own drawer, moved beside the bar while
9005
+ the panel is away, drawn as a popover standing on the bar's end. There is
9006
+ no backdrop; a press outside the bar closes it, as Escape does.
9007
+
9008
+ Its outer edge keeps the panel's gutter, so the popover and the panel line
9009
+ up on the side they share. The bar is already inset by the host's reserved
9010
+ edges, so the gutter alone is the whole offset from it, and the width cap
9011
+ leaves the gutter free on both sides. A phone keeps a narrow margin
9012
+ instead, in the block below: the panel there is the whole screen, so there
9013
+ is no edge to line up with. */
9014
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer {
9015
+ inset: auto var(--_edge-gutter) calc(100% + 8px) auto;
9016
+ width: min(320px, calc(var(--_viewport-width) - 2 * var(--_edge-gutter)));
9017
+ height: min(440px, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - 16px));
9018
+ border-radius: var(--_panel-radius);
9019
+ box-shadow: var(--_shadow);
9020
+ overflow: hidden;
9021
+ }
9022
+
9023
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar > .drawer {
9024
+ inset: auto auto calc(100% + 8px) var(--_edge-gutter);
9025
+ }
9026
+
9027
+ @media (max-width: 600px) {
9028
+ :host([data-collapsed-to="bar"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
9029
+ inset: auto 8px calc(100% + 8px) auto;
9030
+ width: min(320px, calc(100vw - 16px));
9031
+ }
9032
+
9033
+ :host([data-collapsed-to="bar"][placement="bottom-left"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
9034
+ inset: auto auto calc(100% + 8px) 8px;
9035
+ }
9036
+ }
9037
+
9038
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-backdrop {
9039
+ display: none;
9040
+ }
9041
+
9042
+ /* The popover closes in one step. The in-panel drawer slides its panel out
9043
+ and holds the overlay's visibility until the slide ends; here the overlay
9044
+ is the popover's own box, rounded and clipped, so the slide moved the list
9045
+ inside a box that stayed put (read as a shrink) and the box went only once
9046
+ the slide had finished. This outweighs the drawer's own hidden rule, so the
9047
+ box hides at once, and whatever the list inside does is out of sight. The
9048
+ way in needs no rule: the drawer is moved into the bar as it opens, and a
9049
+ moved element starts no transition. Nothing is left for reduced motion. */
9050
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer {
9051
+ transition: none;
9052
+ }
9053
+
9054
+ /* Border-box, so the list's own border sits inside the popover's clipped box
9055
+ rather than 2px past its bottom edge. */
9056
+ :host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-panel {
9057
+ box-sizing: border-box;
9058
+ width: 100%;
9059
+ border: 1px solid var(--_border);
9060
+ border-radius: inherit;
9061
+ box-shadow: none;
9062
+ }
9063
+
9064
+ /* \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
9065
+ Collapsed is hidden: no launcher, no rail, no header bar. The host's control
9066
+ calls the public API, and the panel rises from its anchor (the dock module)
9067
+ or from the placement's corner. Each placement's own collapsed rule is
9068
+ overridden at its weight or above, later in the cascade. */
9069
+ :host([data-collapsed-to="none"][collapsed]) {
9070
+ pointer-events: none;
9071
+ }
9072
+
9073
+ :host([data-collapsed-to="none"][collapsed]) .launcher {
9074
+ display: none;
9075
+ }
9076
+
9077
+ :host([data-collapsed-to="none"][collapsed][placement="sidebar"]) {
9078
+ width: 0;
9079
+ }
9080
+
9081
+ :host([data-collapsed-to="none"][collapsed][placement="embedded"]) {
9082
+ display: none;
9083
+ }
9084
+ `;
9085
+
9086
+ // src/ui/placement/dock_styles.ts
9087
+ var DOCK_STYLES = `
9088
+ /* \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
9089
+ A docked panel rises from the bar along the bottom edge, or from the host's
9090
+ own control, instead of resting in a corner of its own. The element stamps
9091
+ what it collapses to as data-collapsed-to, and writes where the dock is as
9092
+ two private properties: --_dock-bottom, the bar's height or the distance
9093
+ from the layout viewport's bottom to the anchor's top, and --_dock-right,
9094
+ the anchor's distance from the right edge. Both are read with a 0px
9095
+ fallback, so a panel measured before the bar has laid out stands where a
9096
+ bar of no height would put it.
9097
+
9098
+ The cap follows the token block's rule: what the host left free less the
9099
+ one gutter the anchored edge spends, so a panel grown to the cap runs from
9100
+ just above the bar to the far edge. A host therefore needs no inset to keep
9101
+ the panel off its bar; --ag-ui-inset still wins when set. A panel the user
9102
+ dragged off its dock is written there inline, which is how the dragged spot
9103
+ outranks every rule below, the anchor gap included; the element holds that
9104
+ spot above the bar or the anchor itself, and drops it on a phone, where the
9105
+ small-viewport block reads the same property first.
9106
+
9107
+ These blocks restate tokens and nothing else, and sit ahead of the phone
9108
+ block at no more weight than it has, so on a phone the panel is the screen
9109
+ as every other placement is there. The bar measures 0 while it is not drawn
9110
+ (a phone with the panel open), which is what lets the side and full shapes
9111
+ below fall back to the whole height without a phone rule of their own.
9112
+
9113
+ Each placement keeps its own side and shape; only the bottom edge moves. */
9114
+ :host([data-collapsed-to="bar"]) {
9115
+ --_inset: var(
9116
+ --ag-ui-inset,
9117
+ auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
9118
+ calc(
9119
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
9120
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
9121
+ )
9122
+ auto
9123
+ );
9124
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - var(--_edge-gutter)));
9125
+ }
9126
+
9127
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) {
9128
+ --_inset: var(
9129
+ --ag-ui-inset,
9130
+ auto auto
9131
+ calc(
9132
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
9133
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
9134
+ )
9135
+ calc(var(--_edge-gutter) + var(--_viewport-inset-left))
9136
+ );
9137
+ }
9138
+
9139
+ /* The column and the full-screen panel stop at the bar rather than covering
9140
+ it, so the bar stays where the user minimises from. */
9141
+ :host([data-collapsed-to="bar"][placement="side"]) {
9142
+ --_inset: var(
9143
+ --ag-ui-inset,
9144
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
9145
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) auto
9146
+ );
9147
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
9148
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
9149
+ }
9150
+
9151
+ :host([data-collapsed-to="bar"][placement="full"]) {
9152
+ --_inset: var(
9153
+ --ag-ui-inset,
9154
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
9155
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) var(--_viewport-inset-left)
9156
+ );
9157
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
9158
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
9159
+ }
9160
+
9161
+ /* The host's own bar: the panel stands the anchor gap (8px unless the host
9162
+ says otherwise) above the anchor and lines up with its right edge. Only
9163
+ once the element found the anchor; until then, and when it never does, the
9164
+ placement's own corner stands.
9165
+
9166
+ The cap spends the larger of the gap and the gutter. The gutter alone is
9167
+ what this always spent, so the default is unchanged; but a host that opens
9168
+ the gap past the gutter to clear a tall toolbar would otherwise push a
9169
+ panel grown to the cap off the top of the window by the difference. */
9170
+ :host([data-collapsed-to="none"][data-dock-anchored]) {
9171
+ --_inset: var(--ag-ui-inset, auto var(--_dock-right, 0px) calc(var(--_dock-bottom, 0px) + var(--_anchor-gap)) auto);
9172
+ --_max-height: var(
9173
+ --ag-ui-max-height,
9174
+ calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - max(var(--_edge-gutter), var(--_anchor-gap)))
9175
+ );
9176
+ }
8466
9177
  `;
8467
9178
 
8468
9179
  // src/ui/placement/embedded_styles.ts
@@ -9085,6 +9796,15 @@ var SMALL_VIEWPORT_STYLES = `
9085
9796
  :host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
9086
9797
  display: none;
9087
9798
  }
9799
+
9800
+ /* The bottom bar shows only while collapsed. With the panel open the panel
9801
+ is the screen, its header's minimise control is the way back, and a
9802
+ second strip fixed to the bottom edge would sit under the keyboard while
9803
+ the composer has focus. Hidden by display, so the bar measures 0 and the
9804
+ host's padding goes with it. */
9805
+ :host([data-collapsed-to="bar"]:not([collapsed]):not([data-small-viewport="off"])) .dock-bar {
9806
+ display: none;
9807
+ }
9088
9808
  }
9089
9809
  `;
9090
9810
 
@@ -9216,6 +9936,9 @@ var CARD_MODES = [
9216
9936
  ].join(", ");
9217
9937
  var GROUPED = `:host(:where(:not(${CARD_MODES})))`;
9218
9938
  var CARDS = `:host(:is(${CARD_MODES}))`;
9939
+ function inner(mode) {
9940
+ return `:host(:where(:not(${CARD_MODES}))[data-tool-group-display="${mode}"]) .tool-group-list .tool-call`;
9941
+ }
9219
9942
  var TOOL_GROUP_STYLES = `
9220
9943
  /* \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
9944
  One quiet row per run of consecutive calls, the cards one click down as
@@ -9356,7 +10079,8 @@ ${GROUPED} .tool-group[data-open="false"] .tool-group-list {
9356
10079
 
9357
10080
  /* A card inside the group is one line of the list: icon, name, and its
9358
10081
  Details control at the end, with the arguments and result opening under
9359
- the whole row. */
10082
+ the whole row. Its ink is the second ink in every theme, unless the host
10083
+ set the tool token, which then reaches the grouped calls too. */
9360
10084
  ${GROUPED} .tool-group .tool-call {
9361
10085
  max-width: 100%;
9362
10086
  align-self: stretch;
@@ -9367,7 +10091,7 @@ ${GROUPED} .tool-group .tool-call {
9367
10091
  border: 0;
9368
10092
  border-radius: 0;
9369
10093
  background: transparent;
9370
- color: var(--_ink-2);
10094
+ color: var(--_tool-fg-set, var(--_ink-2));
9371
10095
  font-family: inherit;
9372
10096
  font-size: var(--_text-sm);
9373
10097
  }
@@ -9400,6 +10124,43 @@ ${GROUPED} .tool-group .tool-call-subagent {
9400
10124
  flex-basis: 100%;
9401
10125
  }
9402
10126
 
10127
+ /* The inner display mode: how an opened group draws its calls. The same
10128
+ visibility rules the card modes apply to a flat card, so a call inside the
10129
+ group shows the regions the flat card of that mode shows. The flat inline
10130
+ mode's chrome rule is left out on purpose: a card in the group has already
10131
+ lost its box and is laid out as a row of the list, and that rule's padding
10132
+ and gap would undo the row. Selected from the host attribute, so changing it
10133
+ restyles groups already on screen. */
10134
+ ${inner("compact")}[data-expanded="false"] .tool-call-section {
10135
+ display: none;
10136
+ }
10137
+
10138
+ ${inner("inline")} .tool-call-section--args {
10139
+ display: none;
10140
+ }
10141
+
10142
+ ${inner("minimal")} .tool-call-toggle,
10143
+ ${inner("minimal")} .tool-call-body {
10144
+ display: none;
10145
+ }
10146
+
10147
+ /* A call waiting on an approval shows its arguments in every inner mode, as it
10148
+ does in every card mode, and for the same reason: the arguments are what
10149
+ tell three gated calls of one tool apart. These come after the hides above,
10150
+ which they tie with on weight. The arguments region only, never every
10151
+ section, since the result region carries the hidden attribute until a
10152
+ result exists and a display value here would override it. The arguments
10153
+ region carries it too when the call has none, so it is excluded while
10154
+ hidden rather than framed empty. */
10155
+ ${inner("minimal")}[data-status="deferred"] .tool-call-body {
10156
+ display: flex;
10157
+ }
10158
+
10159
+ ${inner("compact")}[data-status="deferred"] .tool-call-section--args:not([hidden]),
10160
+ ${inner("inline")}[data-status="deferred"] .tool-call-section--args:not([hidden]) {
10161
+ display: flex;
10162
+ }
10163
+
9403
10164
  /* Grouping is decided as each card is built, so a card mode set afterwards
9404
10165
  finds groups already drawn. It flattens them: the group and its list stop
9405
10166
  generating boxes, the row and the repeat ordinals go, and every card lays
@@ -9703,9 +10464,11 @@ var TOOL_STEP_STYLES = `
9703
10464
 
9704
10465
  /* The arguments region only, never every section: the result region carries the
9705
10466
  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 {
10467
+ framing an empty RESULT heading under the question. The arguments region is
10468
+ hidden too when the call has none, and is left alone then, or it would frame
10469
+ an empty ARGUMENTS heading. */
10470
+ :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]),
10471
+ :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]) {
9709
10472
  display: flex;
9710
10473
  }
9711
10474
 
@@ -10067,7 +10830,15 @@ var TOKEN_STYLES = `
10067
10830
  --_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
10068
10831
  --_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
10069
10832
  --_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));
10833
+ /* The focus ring is drawn on the panel, so it has to hold 3:1 against
10834
+ --_bg whatever accent a host brings (WCAG 2.4.13). Two parts accent to
10835
+ three parts ink does that at the worst case, an accent at the far end
10836
+ from the ink (white on a light panel, black on a dark one): the ink
10837
+ alone sits far above 3:1, and three fifths of it keeps the mix there
10838
+ while the hue still reads as the accent. Half and half fell to 2.3:1
10839
+ for black on the code theme. The accent over transparent, as before,
10840
+ measured only the accent, and a light host accent fell to 1.6:1. */
10841
+ --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 40%, var(--_fg)));
10071
10842
  /* The user's turn is the accent at a whisper. With Quiet's graphite accent
10072
10843
  that is a neutral wash; once a host sets a brand accent it becomes a tint
10073
10844
  of the brand, which is the one thing Soft does that Quiet keeps. */
@@ -10080,6 +10851,23 @@ var TOKEN_STYLES = `
10080
10851
  --_tool-fg: var(--ag-ui-tool-fg, var(--_ink-2));
10081
10852
  --_header-bg: var(--ag-ui-header-bg, var(--_bg));
10082
10853
  --_header-fg: var(--ag-ui-header-fg, var(--_fg));
10854
+ /* Switches: a public token read with no fallback, named for it with -set.
10855
+ With the token unset the switch computes to the guaranteed-invalid value,
10856
+ so a rule that reads it with a fallback gets the fallback; that is how a
10857
+ rule tells a token the host set from the theme's default for it. The
10858
+ default theme's header buttons follow the header text only once a host
10859
+ sets it, and a step group's calls follow the tool token only once a host
10860
+ sets it (classic defaults --_tool-fg to the accent, which a group never
10861
+ drew). */
10862
+ --_header-fg-set: var(--ag-ui-header-fg);
10863
+ --_tool-fg-set: var(--ag-ui-tool-fg);
10864
+ /* The header buttons' hover square, by default, once the header text is
10865
+ set. Here it is the text at about a fifth, which every browser can mix;
10866
+ where relative colour syntax is supported (see the block at the end) it
10867
+ is the text's opposite instead. It reads the switch, so it is unset with
10868
+ it, and the square below falls to the panel's hover shade. */
10869
+ --_header-btn-hover-auto: color-mix(in srgb, var(--_header-fg-set) 18%, transparent);
10870
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
10083
10871
  --_border: var(--ag-ui-border, var(--_line));
10084
10872
  --_radius: var(--ag-ui-radius, 12px);
10085
10873
  /* The panel's own frame, which is the one corner a placement can put against
@@ -10330,6 +11118,25 @@ var TOKEN_STYLES = `
10330
11118
  grid/flex layout) to embed it in the page flow instead of floating. */
10331
11119
  --_position: var(--ag-ui-position, fixed);
10332
11120
  --_z-index: var(--ag-ui-z-index, 2147483000);
11121
+ /* A size the user dragged, written inline on the host by the placement
11122
+ code, and read ahead of the host's tokens so the drag wins. There is no
11123
+ public token behind it; it is declared initial here only so a chat that
11124
+ was never dragged does not inherit one, from a dragged chat it is
11125
+ slotted into or from any page rule naming it. The inline value still
11126
+ beats this default. */
11127
+ --_drag-width: initial;
11128
+ --_drag-height: initial;
11129
+ /* Where a docked panel stands, written inline the same way by the placement
11130
+ code: the bottom bar's height, or the host anchor's distance from the
11131
+ bottom and right edges. Read only by the dock rules, each with a 0px
11132
+ fallback; declared initial so it never inherits either. */
11133
+ --_dock-bottom: initial;
11134
+ --_dock-right: initial;
11135
+ /* The gap between a panel and the host's own button it rises from under
11136
+ collapse-to="none". The button usually sits inside a taller toolbar, so a
11137
+ host sets this to the button's distance from the toolbar's top plus the
11138
+ clearance it wants, and the panel stands clear of the toolbar. */
11139
+ --_anchor-gap: var(--ag-ui-anchor-gap, 8px);
10333
11140
  --_width: var(--_drag-width, var(--ag-ui-width, 380px));
10334
11141
  --_height: var(--_drag-height, var(--ag-ui-height, 560px));
10335
11142
  /* The gutter a resting floating panel keeps between itself and the edge of
@@ -10433,6 +11240,19 @@ var TOKEN_STYLES = `
10433
11240
  --_on-accent: var(--ag-ui-on-accent, #ffffff);
10434
11241
  }
10435
11242
  }
11243
+
11244
+ /* The header buttons' default hover square as the header text's opposite,
11245
+ so white text gets a darker square and dark text a lighter one. A square
11246
+ mixed from the text itself moves the ground toward the glyph: white on a
11247
+ mid-blue header fell from 5.1:1 to 3.75:1 under the pointer. Moving away
11248
+ from it can only raise what the host's own pair reads. Gated on this
11249
+ exact expression, so a browser that cannot compute it keeps the square
11250
+ mixed from the text rather than losing the square altogether. */
11251
+ @supports (color: rgb(from red calc(255 - r) calc(255 - g) calc(255 - b))) {
11252
+ :host {
11253
+ --_header-btn-hover-auto: color-mix(in srgb, rgb(from var(--_header-fg-set) calc(255 - r) calc(255 - g) calc(255 - b)) 18%, transparent);
11254
+ }
11255
+ }
10436
11256
  `;
10437
11257
 
10438
11258
  // src/ui/themes/classic_styles.ts
@@ -10847,14 +11667,30 @@ ${THEME_HOST.quiet} .header-title {
10847
11667
  letter-spacing: -0.01em;
10848
11668
  }
10849
11669
 
11670
+ /* The buttons sit back in the third ink on the panel. Once the host sets the
11671
+ header text they draw in it instead, since the header behind them is then
11672
+ the host's colour (see the switches in the token defaults). So does their
11673
+ rest opacity: the frame's 0.85 lets the header through the glyph, which
11674
+ took white on a mid-blue header from 5.1:1 to 4.2:1. With the header text
11675
+ set, the opacity below reads a colour, which no opacity can be, so it is
11676
+ invalid at computed-value time and falls to the initial 1; unset, it reads
11677
+ the fallback, the frame's own 0.85. */
10850
11678
  ${THEME_HOST.quiet} .header-btn {
10851
- color: var(--_ink-3);
11679
+ color: var(--_header-fg-set, var(--_ink-3));
11680
+ opacity: var(--_header-fg-set, 0.85);
10852
11681
  border-radius: 8px;
10853
11682
  }
10854
11683
 
11684
+ /* The hover square is the host's own when it set one; otherwise it is mixed
11685
+ from the header text when the host set that, so it reads on any header the
11686
+ host painted; otherwise it is the panel shade.
11687
+ The glyph takes the header text, which is the full ink when that is unset.
11688
+ The opacity restates the frame's hover value, which the rest rule above
11689
+ outweighs. */
10855
11690
  ${THEME_HOST.quiet} .header-btn:hover {
10856
- color: var(--_fg);
10857
- background: var(--_hover);
11691
+ color: var(--_header-fg);
11692
+ opacity: 1;
11693
+ background: var(--_header-btn-hover);
10858
11694
  }
10859
11695
 
10860
11696
  ${THEME_HOST.quiet} .message--user {
@@ -10945,9 +11781,18 @@ var SCHEME_STYLES = `
10945
11781
  dark) and beside a theme name (soft dark). auto follows the operating
10946
11782
  system through color-scheme itself, with no media query to keep in step.
10947
11783
  code is a dark-only theme and has no light half. */
11784
+ /* Important, so a scheme word holds against a host rule that sets the
11785
+ element's color-scheme (a page that follows its own dark mode). A rule
11786
+ from outside the shadow tree beats every :host rule at normal weight, and
11787
+ the theme toggle writes the word; without this its press changed nothing.
11788
+ code stays last, so code light is still dark. */
11789
+ :host([theme~="light"]) {
11790
+ color-scheme: light !important;
11791
+ }
11792
+
10948
11793
  :host([theme~="dark"]),
10949
11794
  :host([theme~="code"]) {
10950
- color-scheme: dark;
11795
+ color-scheme: dark !important;
10951
11796
  }
10952
11797
 
10953
11798
  :host([theme~="auto"]) {
@@ -11988,6 +12833,9 @@ var STYLES = [
11988
12833
  THEME_STYLES,
11989
12834
  PRESET_STYLES,
11990
12835
  PAGE_STYLES,
12836
+ // Ahead of the phone block and no heavier than it: a docked panel is still
12837
+ // full-bleed on a phone.
12838
+ DOCK_STYLES,
11991
12839
  SMALL_VIEWPORT_STYLES,
11992
12840
  SIDEBAR_STYLES,
11993
12841
  LAUNCHER_STYLES,
@@ -12117,7 +12965,7 @@ function buildHeader(options) {
12117
12965
 
12118
12966
  // src/ui/shell/build_launcher.ts
12119
12967
  function buildLauncher(options) {
12120
- const { launcher, badge, railLabel } = options;
12968
+ const { launcher, badge, railLabel, bar, barHistory } = options;
12121
12969
  launcher.className = "launcher";
12122
12970
  launcher.type = "button";
12123
12971
  launcher.setAttribute("part", "launcher");
@@ -12136,6 +12984,20 @@ function buildLauncher(options) {
12136
12984
  badge
12137
12985
  );
12138
12986
  launcher.addEventListener("click", options.onOpen, { signal: options.signal });
12987
+ bar.className = "dock-bar";
12988
+ bar.setAttribute("part", "bar");
12989
+ barHistory.className = "bar-history";
12990
+ barHistory.type = "button";
12991
+ barHistory.setAttribute("part", "bar-history");
12992
+ barHistory.title = options.strings.chatHistory;
12993
+ barHistory.setAttribute("aria-label", options.strings.chatHistory);
12994
+ const glyph = document.createElement("span");
12995
+ glyph.className = "bar-history-glyph";
12996
+ glyph.setAttribute("aria-hidden", "true");
12997
+ glyph.textContent = "\u2630";
12998
+ barHistory.replaceChildren(glyph);
12999
+ barHistory.addEventListener("click", options.onHistory, { signal: options.signal });
13000
+ bar.replaceChildren(launcher, barHistory);
12139
13001
  }
12140
13002
 
12141
13003
  // src/ui/shell/is_unread_badge_enabled.ts
@@ -12216,12 +13078,12 @@ ${listText(element, tag === "OL")}
12216
13078
 
12217
13079
  `;
12218
13080
  }
12219
- const inner = children(element);
13081
+ const inner2 = children(element);
12220
13082
  return BLOCK_TAGS.has(tag) ? `
12221
13083
 
12222
- ${inner}
13084
+ ${inner2}
12223
13085
 
12224
- ` : inner;
13086
+ ` : inner2;
12225
13087
  }
12226
13088
  function children(element) {
12227
13089
  let joined = "";
@@ -18582,6 +19444,14 @@ function withScheme(theme, scheme) {
18582
19444
  const names = theme.split(/\s+/).filter((word) => word !== "" && !SCHEME_WORDS.includes(word));
18583
19445
  return [...names, scheme].join(" ");
18584
19446
  }
19447
+ function findAnchor(root, selector) {
19448
+ try {
19449
+ const scope = root instanceof ShadowRoot ? root : document;
19450
+ return scope.querySelector(selector) ?? document.querySelector(selector);
19451
+ } catch {
19452
+ return null;
19453
+ }
19454
+ }
18585
19455
  var AgUiChat = class extends HTMLElement {
18586
19456
  /** Agent factory; override to inject a custom or fake agent (tests). */
18587
19457
  agentFactory = createHttpAgent;
@@ -18903,8 +19773,21 @@ var AgUiChat = class extends HTMLElement {
18903
19773
  #launcher;
18904
19774
  /** The launcher's unread badge; hidden at zero, and when the host opts out. */
18905
19775
  #badge;
18906
- /** The rail's vertical caption. Rendered only by the sidebar's edge rail. */
19776
+ /** The rail's vertical caption. Rendered only by the sidebar's edge rail and the bottom bar. */
18907
19777
  #railLabel = document.createElement("span");
19778
+ /**
19779
+ * The bottom bar under collapse-to="bar": the launcher drawn as a strip, and
19780
+ * the history button beside it. Outside the bar it draws no box of its own.
19781
+ */
19782
+ #dockBar = document.createElement("div");
19783
+ /** The bottom bar's history button, which opens the drawer as a popover. */
19784
+ #barHistory = document.createElement("button");
19785
+ /** The bar height last reported, so the event fires on a change only. */
19786
+ #barHeight = 0;
19787
+ /** The element the panel rises from under collapse-to="none", set as a property. */
19788
+ #anchor = null;
19789
+ /** Whether a missing anchor has been reported; once per anchor given. */
19790
+ #anchorWarned = false;
18908
19791
  // Answers that finished while the widget was collapsed. Expanding clears it.
18909
19792
  #unread = 0;
18910
19793
  /** Empty-state region at the top of the message list; hidden once anything renders. */
@@ -18965,6 +19848,7 @@ var AgUiChat = class extends HTMLElement {
18965
19848
  */
18966
19849
  #onViewportResize = () => {
18967
19850
  this.#placement.publishVisualViewport();
19851
+ this.#syncDock();
18968
19852
  if (this.#placement.dragging()) {
18969
19853
  return;
18970
19854
  }
@@ -18976,6 +19860,8 @@ var AgUiChat = class extends HTMLElement {
18976
19860
  #voice;
18977
19861
  /** Whether the element is currently in the DOM; gates the connect-time warning. */
18978
19862
  #connected = false;
19863
+ /** A {@link newChat} asked for before the element connected, to start at connect. */
19864
+ #newChatOnConnect = false;
18979
19865
  /**
18980
19866
  * Aborted when the element leaves the document, and replaced when it comes
18981
19867
  * back.
@@ -19226,7 +20112,7 @@ var AgUiChat = class extends HTMLElement {
19226
20112
  root: this.#root,
19227
20113
  connected: () => this.#connected,
19228
20114
  collapsed: () => this.collapsed,
19229
- collapsible: () => surfaceFor(this.getAttribute("placement")).collapseTo !== null,
20115
+ collapsible: () => this.#surface().collapseTo !== null,
19230
20116
  strings: () => this.#strings,
19231
20117
  readPreference: (base) => this.#storage.readPreference(base),
19232
20118
  writePreference: (base, value) => this.#storage.writePreference(base, value),
@@ -19302,7 +20188,7 @@ var AgUiChat = class extends HTMLElement {
19302
20188
  });
19303
20189
  this.#drawer = new ThreadDrawer({
19304
20190
  onSelect: (threadId) => {
19305
- void this.#history.switchThread(threadId);
20191
+ this.#history.switchThread(threadId).catch(() => void 0);
19306
20192
  },
19307
20193
  onNew: () => {
19308
20194
  this.newChat();
@@ -19395,6 +20281,8 @@ var AgUiChat = class extends HTMLElement {
19395
20281
  return [
19396
20282
  "title-text",
19397
20283
  "placement",
20284
+ "collapse-to",
20285
+ "anchor",
19398
20286
  "credentials",
19399
20287
  "user-key",
19400
20288
  "user-name",
@@ -19410,12 +20298,25 @@ var AgUiChat = class extends HTMLElement {
19410
20298
  }
19411
20299
  return;
19412
20300
  }
19413
- if (name === "placement") {
20301
+ if (name === "anchor") {
20302
+ this.#anchorWarned = false;
20303
+ this.#syncDock();
20304
+ return;
20305
+ }
20306
+ if (name === "placement" || name === "collapse-to") {
19414
20307
  this.#placement.releaseOwnedAxes();
19415
20308
  this.#placement.releaseLauncherPosition();
19416
- if (surfaceFor(this.getAttribute("placement")).collapseTo === null && this.collapsed) {
20309
+ if (this.#surface().collapseTo === null && this.collapsed) {
19417
20310
  this.setCollapsed(false);
19418
20311
  }
20312
+ this.#stampCollapseTarget();
20313
+ if (name === "collapse-to") {
20314
+ this.#syncLauncher();
20315
+ if (this.#surface().collapseTo !== "bar") {
20316
+ this.#homeDrawer();
20317
+ }
20318
+ }
20319
+ this.#syncDock();
19419
20320
  requestAnimationFrame(() => this.#placement.syncResizeAnchor());
19420
20321
  return;
19421
20322
  }
@@ -19672,6 +20573,26 @@ var AgUiChat = class extends HTMLElement {
19672
20573
  set toolDisplay(value) {
19673
20574
  this.setAttribute("data-tool-display", value);
19674
20575
  }
20576
+ /**
20577
+ * How an opened step group draws its calls, from the
20578
+ * `data-tool-group-display` attribute (`full` / `compact` / `minimal` /
20579
+ * `inline`). Defaults to `full`, for an absent attribute and for a value the
20580
+ * element does not know, which is what a group drew before the attribute.
20581
+ *
20582
+ * Read only while `toolDisplay` is `grouped`: under a card mode the groups
20583
+ * are flattened into that mode's cards. Applied by the shadow CSS from the
20584
+ * attribute itself, so changing it restyles groups already on screen.
20585
+ */
20586
+ get toolGroupDisplay() {
20587
+ const attr = this.getAttribute("data-tool-group-display");
20588
+ if (attr === TOOL_GROUP_DISPLAY.INLINE || attr === TOOL_GROUP_DISPLAY.MINIMAL || attr === TOOL_GROUP_DISPLAY.COMPACT) {
20589
+ return attr;
20590
+ }
20591
+ return TOOL_GROUP_DISPLAY.FULL;
20592
+ }
20593
+ set toolGroupDisplay(value) {
20594
+ this.setAttribute("data-tool-group-display", value);
20595
+ }
19675
20596
  /**
19676
20597
  * The host's own constructed stylesheets, adopted after the built-in one.
19677
20598
  *
@@ -19717,10 +20638,24 @@ var AgUiChat = class extends HTMLElement {
19717
20638
  this.#render();
19718
20639
  this.#drawer.setStrings(this.#strings);
19719
20640
  this.#checkpoints.setStrings(this.#strings);
19720
- if (surfaceFor(this.getAttribute("placement")).collapseTo !== null && this.#startsCollapsed()) {
20641
+ if (this.#surface().collapseTo !== null && this.#startsCollapsed()) {
19721
20642
  this.setAttribute("collapsed", "");
19722
20643
  }
19723
20644
  this.#syncLauncher();
20645
+ this.#stampCollapseTarget();
20646
+ const dockObserver = new ResizeObserver(() => this.#syncDock());
20647
+ dockObserver.observe(this.#dockBar);
20648
+ this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
20649
+ this.#syncDock();
20650
+ if (this.#surface().collapseTo === "none") {
20651
+ this.dispatchEvent(
20652
+ new CustomEvent(TOGGLE_EVENT, {
20653
+ detail: { collapsed: this.collapsed },
20654
+ bubbles: true,
20655
+ composed: true
20656
+ })
20657
+ );
20658
+ }
19724
20659
  this.#skills.init();
19725
20660
  const held = this.#principalKey;
19726
20661
  const changed = held !== null && held !== this.userKey;
@@ -19743,6 +20678,10 @@ var AgUiChat = class extends HTMLElement {
19743
20678
  void this.#history.refreshDrawer();
19744
20679
  }
19745
20680
  this.#connected = true;
20681
+ if (this.#newChatOnConnect) {
20682
+ this.#newChatOnConnect = false;
20683
+ this.newChat();
20684
+ }
19746
20685
  }
19747
20686
  /**
19748
20687
  * The catalog requests the element issues on startup: the tool labels
@@ -19875,15 +20814,15 @@ var AgUiChat = class extends HTMLElement {
19875
20814
  this.#threadStore = null;
19876
20815
  const url = this.getAttribute("data-threads-url");
19877
20816
  if (url !== null) {
19878
- const inner = this.conversationStore;
20817
+ const inner2 = this.conversationStore;
19879
20818
  const remote = new RemoteConversationStore(
19880
20819
  url,
19881
20820
  () => this.#headersFor(url),
19882
- inner,
20821
+ inner2,
19883
20822
  () => this.#requestCredentials(),
19884
20823
  this.getAttribute("data-threads-cache") !== "false"
19885
20824
  );
19886
- this.#threadStore = { remote, inner };
20825
+ this.#threadStore = { remote, inner: inner2 };
19887
20826
  this.conversationStore = remote;
19888
20827
  }
19889
20828
  }
@@ -19922,7 +20861,7 @@ var AgUiChat = class extends HTMLElement {
19922
20861
  * its own chrome.
19923
20862
  */
19924
20863
  setCollapsed(collapsed, options = {}) {
19925
- if (collapsed && surfaceFor(this.getAttribute("placement")).collapseTo === null) {
20864
+ if (collapsed && this.#surface().collapseTo === null) {
19926
20865
  return;
19927
20866
  }
19928
20867
  if (options.announce === true && collapsed && !this.collapsed) {
@@ -19934,8 +20873,10 @@ var AgUiChat = class extends HTMLElement {
19934
20873
  if (collapsed) {
19935
20874
  this.setAttribute("collapsed", "");
19936
20875
  } else {
20876
+ this.#homeDrawer();
19937
20877
  this.removeAttribute("collapsed");
19938
20878
  }
20879
+ this.#syncDock();
19939
20880
  writeStoredItem(this.#storage.key(COLLAPSED_KEY), collapsed ? "1" : "0");
19940
20881
  this.#setUnread(0);
19941
20882
  this.dispatchEvent(
@@ -19975,6 +20916,94 @@ var AgUiChat = class extends HTMLElement {
19975
20916
  get running() {
19976
20917
  return this.#running;
19977
20918
  }
20919
+ /**
20920
+ * The height of the bottom bar in CSS px, and 0 when none is drawn: under
20921
+ * any collapse-to but "bar", and on a phone while the panel is open, where
20922
+ * the panel is the screen. The bar is fixed over the page, so a host pads
20923
+ * its own page by this much; {@link BAR_HEIGHT_EVENT} says when it changes.
20924
+ */
20925
+ get barHeight() {
20926
+ return this.#barHeight;
20927
+ }
20928
+ /**
20929
+ * The element the panel rises from under collapse-to="none", right-aligned
20930
+ * to it: typically the host's own button in its own bar. Takes precedence
20931
+ * over the anchor attribute, which names one by selector. One that resolves
20932
+ * to nothing leaves the panel in its placement's own corner, with a console
20933
+ * warning.
20934
+ */
20935
+ get anchor() {
20936
+ return this.#anchor;
20937
+ }
20938
+ set anchor(value) {
20939
+ this.#anchor = value;
20940
+ this.#anchorWarned = false;
20941
+ this.#syncDock();
20942
+ }
20943
+ /** The surface the live placement and collapse-to attributes describe. */
20944
+ #surface() {
20945
+ return surfaceFor(this.getAttribute("placement"), this.getAttribute("collapse-to"));
20946
+ }
20947
+ /**
20948
+ * Stamp what the panel collapses to as data-collapsed-to, which is what the
20949
+ * stylesheet draws the bar and the hidden state from. Resolved here rather
20950
+ * than in CSS, because "bar" over a sidebar is its rail and "none" over a
20951
+ * page is no collapse at all, and the stylesheet would have to restate that
20952
+ * table.
20953
+ */
20954
+ #stampCollapseTarget() {
20955
+ const target = this.#surface().collapseTo;
20956
+ if (target === null) {
20957
+ this.removeAttribute("data-collapsed-to");
20958
+ } else {
20959
+ this.setAttribute("data-collapsed-to", target);
20960
+ }
20961
+ }
20962
+ /** Re-measure the bar or the anchor, and tell the host when the bar's height changed. */
20963
+ #syncDock() {
20964
+ if (!this.isConnected) {
20965
+ return;
20966
+ }
20967
+ const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
20968
+ if (height === this.#barHeight) {
20969
+ return;
20970
+ }
20971
+ this.#barHeight = height;
20972
+ this.dispatchEvent(
20973
+ new CustomEvent(BAR_HEIGHT_EVENT, {
20974
+ detail: { height },
20975
+ bubbles: true,
20976
+ composed: true
20977
+ })
20978
+ );
20979
+ }
20980
+ /**
20981
+ * The anchor in force under collapse-to="none": the property, else the
20982
+ * attribute's selector, looked up in the root this element lives in and
20983
+ * then the document, so a chat inside a framework's shadow root can name a
20984
+ * button beside it. Null under any other collapse-to, with nothing given,
20985
+ * or when what was given is not in the document -- reported once per anchor.
20986
+ */
20987
+ #resolveAnchor() {
20988
+ if (this.#surface().collapseTo !== "none") {
20989
+ return null;
20990
+ }
20991
+ const selector = this.getAttribute("anchor") ?? "";
20992
+ if (this.#anchor === null && selector === "") {
20993
+ return null;
20994
+ }
20995
+ const found = this.#anchor ?? findAnchor(this.getRootNode(), selector);
20996
+ if (found?.isConnected === true) {
20997
+ return found;
20998
+ }
20999
+ if (!this.#anchorWarned) {
21000
+ this.#anchorWarned = true;
21001
+ console.warn(
21002
+ `<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.`
21003
+ );
21004
+ }
21005
+ return null;
21006
+ }
19978
21007
  /**
19979
21008
  * Whether to mount collapsed.
19980
21009
  *
@@ -19997,7 +21026,7 @@ var AgUiChat = class extends HTMLElement {
19997
21026
  if (stored !== null) {
19998
21027
  return stored === "1";
19999
21028
  }
20000
- return surfaceFor(this.getAttribute("placement")).startsCollapsed && !this.hasAttribute("data-start-open");
21029
+ return this.#surface().startsCollapsed && !this.hasAttribute("data-start-open");
20001
21030
  }
20002
21031
  /**
20003
21032
  * Describe the panel to whoever is asking -- in practice the agent, through
@@ -20006,7 +21035,9 @@ var AgUiChat = class extends HTMLElement {
20006
21035
  * `movable` folds two separate reasons into the one answer a caller needs:
20007
21036
  * a placement that places itself owns its position, and a panel that fills
20008
21037
  * the screen has nowhere to go. Reporting them apart would make every caller
20009
- * re-derive the same conjunction.
21038
+ * re-derive the same conjunction. A corner panel docked under
21039
+ * `collapse-to="bar"` or `"none"` is movable on a wide viewport, like the
21040
+ * corner panel it is.
20010
21041
  */
20011
21042
  describeSurface() {
20012
21043
  return this.#placement.describeSurface();
@@ -20020,7 +21051,8 @@ var AgUiChat = class extends HTMLElement {
20020
21051
  * the corner it opens from is re-picked, and switching placement hands
20021
21052
  * everything back. What it must not do is claim a move it did not make --
20022
21053
  * hence the boolean, and hence {@link describeSurface} existing so a caller
20023
- * can ask first.
21054
+ * can ask first. A docked panel moves too, within the room above its bar or
21055
+ * anchor, and remembers the spot as a header drag would.
20024
21056
  */
20025
21057
  moveTo(corner, options = {}) {
20026
21058
  return this.#placement.moveTo(corner, options);
@@ -20028,6 +21060,9 @@ var AgUiChat = class extends HTMLElement {
20028
21060
  /**
20029
21061
  * Drop a size the user dragged, and its stored copy, so the placement's
20030
21062
  * default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
21063
+ * Under `collapse-to="bar"` or `"none"` it also returns a panel the user
21064
+ * dragged off its dock to the dock, and forgets the stored spot; a corner
21065
+ * panel keeps a dragged position.
20031
21066
  */
20032
21067
  resetSize() {
20033
21068
  this.#placement.resetSize();
@@ -20040,12 +21075,13 @@ var AgUiChat = class extends HTMLElement {
20040
21075
  * Flip the scheme word of the `theme` attribute between `light` and `dark`
20041
21076
  * and persist the choice per tab. Bound to the optional built-in header theme
20042
21077
  * 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.
21078
+ * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. With no
21079
+ * scheme word (`auto`, or a host rule setting `color-scheme` on the element),
21080
+ * it flips away from the scheme the panel is drawn in.
20045
21081
  */
20046
21082
  toggleTheme() {
20047
21083
  const theme = this.getAttribute("theme") ?? "";
20048
- const scheme = theme.split(/\s+/).includes("dark") ? "light" : "dark";
21084
+ const scheme = this.#drawnScheme() === "dark" ? "light" : "dark";
20049
21085
  this.setAttribute("theme", withScheme(theme, scheme));
20050
21086
  this.#storage.writePreference(THEME_KEY, scheme);
20051
21087
  this.#syncThemeGlyph();
@@ -20125,9 +21161,36 @@ var AgUiChat = class extends HTMLElement {
20125
21161
  this.conversationStore = store;
20126
21162
  this.#wireThreadStore();
20127
21163
  }
21164
+ /**
21165
+ * The scheme the panel is drawn in, which the toggle flips and its glyph
21166
+ * offers the way out of. A scheme word decides it, since the scheme rules
21167
+ * hold against a host's. Without one the panel follows whatever sets its
21168
+ * color-scheme: a host rule on the element, auto's "light dark", or the
21169
+ * light default. Both schemes allowed means the system picks. Read before
21170
+ * the shadow styles apply too (the glyph is drawn first), when the computed
21171
+ * value carries only a host rule, so auto is answered from its word.
21172
+ */
21173
+ #drawnScheme() {
21174
+ const words = (this.getAttribute("theme") ?? "").split(/\s+/);
21175
+ if (words.includes("dark")) {
21176
+ return "dark";
21177
+ }
21178
+ if (words.includes("light")) {
21179
+ return "light";
21180
+ }
21181
+ const declared = getComputedStyle(this).colorScheme.split(/\s+/);
21182
+ const dark = declared.includes("dark");
21183
+ if (dark !== declared.includes("light")) {
21184
+ return dark ? "dark" : "light";
21185
+ }
21186
+ if (dark || words.includes("auto")) {
21187
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
21188
+ }
21189
+ return "light";
21190
+ }
20128
21191
  /** Reflect the current theme on the toggle: show the destination's glyph. */
20129
21192
  #syncThemeGlyph() {
20130
- const dark = (this.getAttribute("theme") ?? "").split(/\s+/).includes("dark");
21193
+ const dark = this.#drawnScheme() === "dark";
20131
21194
  this.#themeToggle.replaceChildren(
20132
21195
  iconElement("theme", "theme-icon", dark ? ICON_SUN : ICON_MOON, null)
20133
21196
  );
@@ -20142,8 +21205,25 @@ var AgUiChat = class extends HTMLElement {
20142
21205
  * {@link toggleCollapsed} and {@link toggleTheme}.
20143
21206
  */
20144
21207
  openThreads() {
21208
+ if (this.collapsed && this.#surface().collapseTo === "bar") {
21209
+ this.#dockBar.append(this.#drawer.element);
21210
+ } else {
21211
+ this.#homeDrawer();
21212
+ }
20145
21213
  this.#history.openThreads();
20146
21214
  }
21215
+ /**
21216
+ * Put the drawer back in the panel, where every surface but the bar's
21217
+ * popover draws it. A popover still open is closed first: the list was
21218
+ * opened over the bar, and carried into the panel it would cover the
21219
+ * conversation the user asked to see.
21220
+ */
21221
+ #homeDrawer() {
21222
+ if (this.#drawer.element.parentNode === this.#dockBar) {
21223
+ this.#drawer.close();
21224
+ this.#chat.insertBefore(this.#drawer.element, this.#checkpoints.element);
21225
+ }
21226
+ }
20147
21227
  /**
20148
21228
  * Open the checkpoints panel (the `::part(checkpoints-button)` route).
20149
21229
  *
@@ -20167,16 +21247,19 @@ var AgUiChat = class extends HTMLElement {
20167
21247
  * for a host that lists conversations in its own chrome.
20168
21248
  *
20169
21249
  * 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;
21250
+ * fired; a second call for the thread already loading settles with that load.
21251
+ * If the store rejects the load, the element stays on the thread it was on,
21252
+ * fires nothing, and the promise rejects with the store's error.
21253
+ * **A run in flight is cancelled**, as the list and {@link newChat} do;
20171
21254
  * a host that would rather wait reads {@link running} first. The thread
20172
21255
  * already on screen is left alone, and so is everything before the element
20173
21256
  * connects, since until then it has no store to read the thread from.
20174
21257
  */
20175
- async switchThread(threadId) {
21258
+ switchThread(threadId) {
20176
21259
  if (!this.#connected) {
20177
- return;
21260
+ return Promise.resolve();
20178
21261
  }
20179
- await this.#history.switchThread(threadId);
21262
+ return this.#history.switchThread(threadId);
20180
21263
  }
20181
21264
  /**
20182
21265
  * Delete a stored thread, as the conversation list's delete does. Deleting the
@@ -20216,6 +21299,10 @@ var AgUiChat = class extends HTMLElement {
20216
21299
  * one. The page is not scrolled to bring the composer into view.
20217
21300
  */
20218
21301
  newChat() {
21302
+ if (!this.#connected) {
21303
+ this.#newChatOnConnect = true;
21304
+ return;
21305
+ }
20219
21306
  this.#cancelRun();
20220
21307
  this.#history.reapUnsent();
20221
21308
  this.#resetState();
@@ -20453,19 +21540,45 @@ var AgUiChat = class extends HTMLElement {
20453
21540
  launcher: this.#launcher,
20454
21541
  badge: this.#badge,
20455
21542
  railLabel: this.#railLabel,
21543
+ bar: this.#dockBar,
21544
+ barHistory: this.#barHistory,
20456
21545
  titleText,
20457
21546
  iconUrl: readLauncherIconUrl(this),
20458
21547
  strings: this.#strings,
20459
21548
  signal,
20460
- onOpen: () => this.setCollapsed(false)
21549
+ // The bar stays on screen with the panel open, as the way to minimise
21550
+ // it, so its strip toggles; everywhere else the launcher is only drawn
21551
+ // while collapsed and only ever opens.
21552
+ onOpen: () => this.setCollapsed(this.#surface().collapseTo === "bar" && !this.collapsed),
21553
+ // The bar's history button toggles: the popover has no backdrop, and a
21554
+ // press on the bar is not a press outside it, so this is how the button
21555
+ // that opened the list closes it again. openThreads() stays open-only.
21556
+ onHistory: () => {
21557
+ if (this.#drawer.isOpen()) {
21558
+ this.#drawer.close();
21559
+ } else {
21560
+ this.openThreads();
21561
+ }
21562
+ }
20461
21563
  });
21564
+ document.addEventListener(
21565
+ "pointerdown",
21566
+ (event) => {
21567
+ if (this.#drawer.element.parentNode === this.#dockBar && this.#drawer.isOpen()) {
21568
+ if (!event.composedPath().includes(this.#dockBar)) {
21569
+ this.#drawer.close();
21570
+ }
21571
+ }
21572
+ },
21573
+ { signal }
21574
+ );
20462
21575
  this.#placement.enableLauncherDrag(signal);
20463
21576
  this.#placement.mountResizeGrips(this.#chat);
20464
21577
  this.#builtInSheet = adoptStyles(this.#root, this.#extraStyles);
20465
21578
  const probe = this.#placement.probe;
20466
21579
  probe.className = "viewport-probe";
20467
21580
  probe.setAttribute("aria-hidden", "true");
20468
- this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#launcher);
21581
+ this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
20469
21582
  }
20470
21583
  /**
20471
21584
  * Reflect the collapsed state and the unread count on the launcher.
@@ -20479,7 +21592,7 @@ var AgUiChat = class extends HTMLElement {
20479
21592
  const unread = this.#unread;
20480
21593
  this.#badge.textContent = unread > 9 ? "9+" : String(unread);
20481
21594
  this.#badge.hidden = unread === 0 || !isUnreadBadgeEnabled(this);
20482
- const label = this.#badge.hidden ? this.#strings.expand : fillUiString(this.#strings.expandUnread, { count: unread });
21595
+ const label = !this.collapsed && this.#surface().collapseTo === "bar" ? this.#strings.collapse : this.#badge.hidden ? this.#strings.expand : fillUiString(this.#strings.expandUnread, { count: unread });
20483
21596
  this.#launcher.setAttribute("aria-label", label);
20484
21597
  this.#launcher.title = label;
20485
21598
  }
@@ -21112,11 +22225,12 @@ function setControlValue(el2, value) {
21112
22225
  }
21113
22226
 
21114
22227
  // src/version.ts
21115
- var VERSION = "0.47.0";
22228
+ var VERSION = "0.49.0";
21116
22229
  export {
21117
22230
  ATTACHMENT_EVENT,
21118
22231
  AgUiChat,
21119
22232
  AgUiClient,
22233
+ BAR_HEIGHT_EVENT,
21120
22234
  CHART_ACTIVITY_TYPE,
21121
22235
  CHART_TOOL_NAME,
21122
22236
  CHAT_CORNERS,
@@ -21149,6 +22263,7 @@ export {
21149
22263
  TOGGLE_EVENT,
21150
22264
  TOOL_CALL_STATUS,
21151
22265
  TOOL_DISPLAY,
22266
+ TOOL_GROUP_DISPLAY,
21152
22267
  TOOL_OUTCOME,
21153
22268
  ToolCallCard,
21154
22269
  UNREAD_EVENT,