@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
@@ -1,6 +1,7 @@
1
1
  import type { Context, Tool } from "@ag-ui/core";
2
2
  import {
3
3
  ATTACHMENT_EVENT,
4
+ BAR_HEIGHT_EVENT,
4
5
  CHART_ACTIVITY_TYPE,
5
6
  COMPACTION_ACTIVITY_TYPE,
6
7
  CUSTOM_AGENT_EVENT,
@@ -13,6 +14,8 @@ import {
13
14
  THREAD_EVENT,
14
15
  TOGGLE_EVENT,
15
16
  TOOL_DISPLAY,
17
+ TOOL_GROUP_DISPLAY,
18
+ type ToolGroupDisplayMode,
16
19
  UNREAD_EVENT,
17
20
  } from "../constants.js";
18
21
  import type { Skill } from "../skills/skill.js";
@@ -50,7 +53,7 @@ import { PendingDecision } from "../ui/interrupts/pending_decision.js";
50
53
  // biome-ignore lint/style/useImportType: the emitted declaration file copies this form
51
54
  import { type QuestionRenderer } from "../ui/interrupts/question_card.js";
52
55
  import { PanelPlacement } from "../ui/placement/panel_placement.js";
53
- import { surfaceFor } from "../ui/placement/surface_for.js";
56
+ import { type Surface, surfaceFor } from "../ui/placement/surface_for.js";
54
57
  import { RunAnnouncer } from "../ui/progress/run_announcer.js";
55
58
  import { renderRunNotice } from "../ui/progress/run_notice.js";
56
59
  import { SubAgentProgress } from "../ui/progress/subagent_progress.js";
@@ -85,6 +88,7 @@ import {
85
88
  writeStoredItem,
86
89
  } from "./conversation_store.js";
87
90
  import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
91
+ import type { BarHeightDetail } from "./events/bar_height_detail.js";
88
92
  import type { StateDetail } from "./events/state_detail.js";
89
93
  import type { SubmitDetail } from "./events/submit_detail.js";
90
94
  import type { ThreadDetail } from "./events/thread_detail.js";
@@ -113,7 +117,7 @@ import { warnOnCrossOriginCredentials, withCredentials } from "./utils.js";
113
117
  * Excludes the attributes that are re-read per use, where a late change works
114
118
  * and a warning would be wrong: `data-runs-url`, `data-page-actions`,
115
119
  * `data-text-animation`, `data-tool-display`, `endpoint`, and the CSS-reactive
116
- * `theme` / `collapsed`.
120
+ * `theme` / `collapsed` / `data-tool-group-display`.
117
121
  */
118
122
  const CONNECT_TIME_ATTRIBUTES = [
119
123
  "data-attachments-url",
@@ -175,6 +179,20 @@ function withScheme(theme: string, scheme: string): string {
175
179
  return [...names, scheme].join(" ");
176
180
  }
177
181
 
182
+ /**
183
+ * The element a selector names, in the given root first and then the
184
+ * document; null for no match, and for a selector that does not parse, which
185
+ * is the same mistake from the host's side.
186
+ */
187
+ function findAnchor(root: Node, selector: string): Element | null {
188
+ try {
189
+ const scope = root instanceof ShadowRoot ? root : document;
190
+ return scope.querySelector(selector) ?? document.querySelector(selector);
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
178
196
  /**
179
197
  * `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
180
198
  *
@@ -539,8 +557,21 @@ export class AgUiChat extends HTMLElement {
539
557
  readonly #launcher: HTMLButtonElement;
540
558
  /** The launcher's unread badge; hidden at zero, and when the host opts out. */
541
559
  readonly #badge: HTMLSpanElement;
542
- /** The rail's vertical caption. Rendered only by the sidebar's edge rail. */
560
+ /** The rail's vertical caption. Rendered only by the sidebar's edge rail and the bottom bar. */
543
561
  readonly #railLabel: HTMLSpanElement = document.createElement("span");
562
+ /**
563
+ * The bottom bar under collapse-to="bar": the launcher drawn as a strip, and
564
+ * the history button beside it. Outside the bar it draws no box of its own.
565
+ */
566
+ readonly #dockBar: HTMLDivElement = document.createElement("div");
567
+ /** The bottom bar's history button, which opens the drawer as a popover. */
568
+ readonly #barHistory: HTMLButtonElement = document.createElement("button");
569
+ /** The bar height last reported, so the event fires on a change only. */
570
+ #barHeight = 0;
571
+ /** The element the panel rises from under collapse-to="none", set as a property. */
572
+ #anchor: Element | null = null;
573
+ /** Whether a missing anchor has been reported; once per anchor given. */
574
+ #anchorWarned = false;
544
575
  // Answers that finished while the widget was collapsed. Expanding clears it.
545
576
  #unread = 0;
546
577
  /** Empty-state region at the top of the message list; hidden once anything renders. */
@@ -603,6 +634,7 @@ export class AgUiChat extends HTMLElement {
603
634
  */
604
635
  readonly #onViewportResize = (): void => {
605
636
  this.#placement.publishVisualViewport();
637
+ this.#syncDock();
606
638
  // Not while a gesture owns the position. Restoring re-applies the *stored*
607
639
  // position, and mid-drag that value is the one from before the drag began
608
640
  // -- so it puts the widget back where it was, the next pointer move puts it
@@ -627,6 +659,8 @@ export class AgUiChat extends HTMLElement {
627
659
  readonly #voice: ComposerVoice;
628
660
  /** Whether the element is currently in the DOM; gates the connect-time warning. */
629
661
  #connected = false;
662
+ /** A {@link newChat} asked for before the element connected, to start at connect. */
663
+ #newChatOnConnect = false;
630
664
  /**
631
665
  * Aborted when the element leaves the document, and replaced when it comes
632
666
  * back.
@@ -879,7 +913,7 @@ export class AgUiChat extends HTMLElement {
879
913
  root: this.#root,
880
914
  connected: () => this.#connected,
881
915
  collapsed: () => this.collapsed,
882
- collapsible: () => surfaceFor(this.getAttribute("placement")).collapseTo !== null,
916
+ collapsible: () => this.#surface().collapseTo !== null,
883
917
  strings: () => this.#strings,
884
918
  readPreference: (base) => this.#storage.readPreference(base),
885
919
  writePreference: (base, value) => this.#storage.writePreference(base, value),
@@ -965,7 +999,9 @@ export class AgUiChat extends HTMLElement {
965
999
  });
966
1000
  this.#drawer = new ThreadDrawer({
967
1001
  onSelect: (threadId) => {
968
- void this.#history.switchThread(threadId);
1002
+ // switchThread rejects when the store cannot load the thread, and keeps
1003
+ // the old one on screen; a pick has nobody to tell, so it ends here.
1004
+ this.#history.switchThread(threadId).catch(() => undefined);
969
1005
  },
970
1006
  onNew: () => {
971
1007
  this.newChat();
@@ -1062,6 +1098,8 @@ export class AgUiChat extends HTMLElement {
1062
1098
  return [
1063
1099
  "title-text",
1064
1100
  "placement",
1101
+ "collapse-to",
1102
+ "anchor",
1065
1103
  "credentials",
1066
1104
  "user-key",
1067
1105
  "user-name",
@@ -1084,7 +1122,14 @@ export class AgUiChat extends HTMLElement {
1084
1122
  }
1085
1123
  return;
1086
1124
  }
1087
- if (name === "placement") {
1125
+ if (name === "anchor") {
1126
+ this.#anchorWarned = false;
1127
+ this.#syncDock();
1128
+ return;
1129
+ }
1130
+ // collapse-to changes the surface the way placement does -- a docked panel
1131
+ // cannot be moved, so the same handover applies -- and is live the same way.
1132
+ if (name === "placement" || name === "collapse-to") {
1088
1133
  // A placement owns the axes it fixes, so hand those back before anything
1089
1134
  // else: a size dragged under the previous placement would otherwise sit
1090
1135
  // inline and outrank the new one.
@@ -1096,9 +1141,19 @@ export class AgUiChat extends HTMLElement {
1096
1141
  // not just stop offering it: the control is gone from the header the
1097
1142
  // moment the attribute changes, so a panel collapsed under the previous
1098
1143
  // placement would have no way back.
1099
- if (surfaceFor(this.getAttribute("placement")).collapseTo === null && this.collapsed) {
1144
+ if (this.#surface().collapseTo === null && this.collapsed) {
1100
1145
  this.setCollapsed(false);
1101
1146
  }
1147
+ this.#stampCollapseTarget();
1148
+ if (name === "collapse-to") {
1149
+ // The strip's label and the popover's home both depend on the target,
1150
+ // and neither is re-derived by the dock sync.
1151
+ this.#syncLauncher();
1152
+ if (this.#surface().collapseTo !== "bar") {
1153
+ this.#homeDrawer();
1154
+ }
1155
+ }
1156
+ this.#syncDock();
1102
1157
  // Placement also moves the panel, so the edges its layout holds still
1103
1158
  // change with it. Deferred a frame so the new rules have applied.
1104
1159
  requestAnimationFrame(() => this.#placement.syncResizeAnchor());
@@ -1418,6 +1473,32 @@ export class AgUiChat extends HTMLElement {
1418
1473
  this.setAttribute("data-tool-display", value);
1419
1474
  }
1420
1475
 
1476
+ /**
1477
+ * How an opened step group draws its calls, from the
1478
+ * `data-tool-group-display` attribute (`full` / `compact` / `minimal` /
1479
+ * `inline`). Defaults to `full`, for an absent attribute and for a value the
1480
+ * element does not know, which is what a group drew before the attribute.
1481
+ *
1482
+ * Read only while `toolDisplay` is `grouped`: under a card mode the groups
1483
+ * are flattened into that mode's cards. Applied by the shadow CSS from the
1484
+ * attribute itself, so changing it restyles groups already on screen.
1485
+ */
1486
+ get toolGroupDisplay(): ToolGroupDisplayMode {
1487
+ const attr = this.getAttribute("data-tool-group-display");
1488
+ if (
1489
+ attr === TOOL_GROUP_DISPLAY.INLINE ||
1490
+ attr === TOOL_GROUP_DISPLAY.MINIMAL ||
1491
+ attr === TOOL_GROUP_DISPLAY.COMPACT
1492
+ ) {
1493
+ return attr;
1494
+ }
1495
+ return TOOL_GROUP_DISPLAY.FULL;
1496
+ }
1497
+
1498
+ set toolGroupDisplay(value: ToolGroupDisplayMode) {
1499
+ this.setAttribute("data-tool-group-display", value);
1500
+ }
1501
+
1421
1502
  /**
1422
1503
  * The host's own constructed stylesheets, adopted after the built-in one.
1423
1504
  *
@@ -1493,10 +1574,32 @@ export class AgUiChat extends HTMLElement {
1493
1574
  // namespaced per instance but not per placement, so a tab that collapsed a
1494
1575
  // floating panel and later loaded the same instance as a page would restore
1495
1576
  // a state that placement has no way out of.
1496
- if (surfaceFor(this.getAttribute("placement")).collapseTo !== null && this.#startsCollapsed()) {
1577
+ if (this.#surface().collapseTo !== null && this.#startsCollapsed()) {
1497
1578
  this.setAttribute("collapsed", "");
1498
1579
  }
1499
1580
  this.#syncLauncher();
1581
+ this.#stampCollapseTarget();
1582
+ // The bar's height follows its content (the font, a long title), so it is
1583
+ // measured rather than known; the observer reports a change of size, and
1584
+ // the strip going from drawn to not drawn is one.
1585
+ const dockObserver = new ResizeObserver(() => this.#syncDock());
1586
+ dockObserver.observe(this.#dockBar);
1587
+ this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
1588
+ this.#syncDock();
1589
+ // Under collapse-to="none" the host draws the collapsed state itself, so
1590
+ // it has to learn the state the element mounted in -- restored from
1591
+ // storage or the placement's default -- without waiting for a change.
1592
+ // Dispatched directly rather than through setCollapsed, which would write
1593
+ // storage and clear the unread count for a state nobody changed.
1594
+ if (this.#surface().collapseTo === "none") {
1595
+ this.dispatchEvent(
1596
+ new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
1597
+ detail: { collapsed: this.collapsed },
1598
+ bubbles: true,
1599
+ composed: true,
1600
+ }),
1601
+ );
1602
+ }
1500
1603
  this.#skills.init();
1501
1604
  // A `user-key` that changed while the element was out of the document gets
1502
1605
  // the handover a live change gets, here, because this is the first moment
@@ -1556,6 +1659,12 @@ export class AgUiChat extends HTMLElement {
1556
1659
  // Last: everything above reads (and some of it sets) attributes, and none
1557
1660
  // of that should trip the connect-time warning.
1558
1661
  this.#connected = true;
1662
+ if (this.#newChatOnConnect) {
1663
+ this.#newChatOnConnect = false;
1664
+ // After the restore above has started: starting the chat stops it, so
1665
+ // the one event is the new chat's and the conversation left stays kept.
1666
+ this.newChat();
1667
+ }
1559
1668
  }
1560
1669
 
1561
1670
  /**
@@ -1756,7 +1865,7 @@ export class AgUiChat extends HTMLElement {
1756
1865
  * its own chrome.
1757
1866
  */
1758
1867
  setCollapsed(collapsed: boolean, options: { readonly announce?: boolean } = {}): void {
1759
- if (collapsed && surfaceFor(this.getAttribute("placement")).collapseTo === null) {
1868
+ if (collapsed && this.#surface().collapseTo === null) {
1760
1869
  return;
1761
1870
  }
1762
1871
  // Announced before the state changes, so the notice is written into a
@@ -1785,8 +1894,13 @@ export class AgUiChat extends HTMLElement {
1785
1894
  if (collapsed) {
1786
1895
  this.setAttribute("collapsed", "");
1787
1896
  } else {
1897
+ // The history popover belongs to the bar only while the panel is away.
1898
+ // Opening the panel closes it and takes the drawer back, so the panel
1899
+ // lands on the conversation rather than on a list over it.
1900
+ this.#homeDrawer();
1788
1901
  this.removeAttribute("collapsed");
1789
1902
  }
1903
+ this.#syncDock();
1790
1904
  writeStoredItem(this.#storage.key(COLLAPSED_KEY), collapsed ? "1" : "0");
1791
1905
  // Expanding is what marks the waiting answers read; collapsing starts a
1792
1906
  // fresh count. Either way the badge is cleared and the host told.
@@ -1832,6 +1946,104 @@ export class AgUiChat extends HTMLElement {
1832
1946
  return this.#running;
1833
1947
  }
1834
1948
 
1949
+ /**
1950
+ * The height of the bottom bar in CSS px, and 0 when none is drawn: under
1951
+ * any collapse-to but "bar", and on a phone while the panel is open, where
1952
+ * the panel is the screen. The bar is fixed over the page, so a host pads
1953
+ * its own page by this much; {@link BAR_HEIGHT_EVENT} says when it changes.
1954
+ */
1955
+ get barHeight(): number {
1956
+ return this.#barHeight;
1957
+ }
1958
+
1959
+ /**
1960
+ * The element the panel rises from under collapse-to="none", right-aligned
1961
+ * to it: typically the host's own button in its own bar. Takes precedence
1962
+ * over the anchor attribute, which names one by selector. One that resolves
1963
+ * to nothing leaves the panel in its placement's own corner, with a console
1964
+ * warning.
1965
+ */
1966
+ get anchor(): Element | null {
1967
+ return this.#anchor;
1968
+ }
1969
+
1970
+ set anchor(value: Element | null) {
1971
+ this.#anchor = value;
1972
+ this.#anchorWarned = false;
1973
+ this.#syncDock();
1974
+ }
1975
+
1976
+ /** The surface the live placement and collapse-to attributes describe. */
1977
+ #surface(): Surface {
1978
+ return surfaceFor(this.getAttribute("placement"), this.getAttribute("collapse-to"));
1979
+ }
1980
+
1981
+ /**
1982
+ * Stamp what the panel collapses to as data-collapsed-to, which is what the
1983
+ * stylesheet draws the bar and the hidden state from. Resolved here rather
1984
+ * than in CSS, because "bar" over a sidebar is its rail and "none" over a
1985
+ * page is no collapse at all, and the stylesheet would have to restate that
1986
+ * table.
1987
+ */
1988
+ #stampCollapseTarget(): void {
1989
+ const target = this.#surface().collapseTo;
1990
+ if (target === null) {
1991
+ this.removeAttribute("data-collapsed-to");
1992
+ } else {
1993
+ this.setAttribute("data-collapsed-to", target);
1994
+ }
1995
+ }
1996
+
1997
+ /** Re-measure the bar or the anchor, and tell the host when the bar's height changed. */
1998
+ #syncDock(): void {
1999
+ // isConnected rather than the connected flag, which connectedCallback
2000
+ // sets only at its end, after the first measurement here.
2001
+ if (!this.isConnected) {
2002
+ return;
2003
+ }
2004
+ const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
2005
+ if (height === this.#barHeight) {
2006
+ return;
2007
+ }
2008
+ this.#barHeight = height;
2009
+ this.dispatchEvent(
2010
+ new CustomEvent<BarHeightDetail>(BAR_HEIGHT_EVENT, {
2011
+ detail: { height },
2012
+ bubbles: true,
2013
+ composed: true,
2014
+ }),
2015
+ );
2016
+ }
2017
+
2018
+ /**
2019
+ * The anchor in force under collapse-to="none": the property, else the
2020
+ * attribute's selector, looked up in the root this element lives in and
2021
+ * then the document, so a chat inside a framework's shadow root can name a
2022
+ * button beside it. Null under any other collapse-to, with nothing given,
2023
+ * or when what was given is not in the document -- reported once per anchor.
2024
+ */
2025
+ #resolveAnchor(): Element | null {
2026
+ if (this.#surface().collapseTo !== "none") {
2027
+ return null;
2028
+ }
2029
+ const selector = this.getAttribute("anchor") ?? "";
2030
+ if (this.#anchor === null && selector === "") {
2031
+ return null;
2032
+ }
2033
+ const found = this.#anchor ?? findAnchor(this.getRootNode(), selector);
2034
+ if (found?.isConnected === true) {
2035
+ return found;
2036
+ }
2037
+ if (!this.#anchorWarned) {
2038
+ this.#anchorWarned = true;
2039
+ console.warn(
2040
+ `<ag-ui-chat>: the anchor ${this.#anchor === null ? `"${selector}"` : "element"} ` +
2041
+ "is not in the document, so the panel opens from its placement's own corner.",
2042
+ );
2043
+ }
2044
+ return null;
2045
+ }
2046
+
1835
2047
  /**
1836
2048
  * Whether to mount collapsed.
1837
2049
  *
@@ -1854,10 +2066,7 @@ export class AgUiChat extends HTMLElement {
1854
2066
  if (stored !== null) {
1855
2067
  return stored === "1";
1856
2068
  }
1857
- return (
1858
- surfaceFor(this.getAttribute("placement")).startsCollapsed &&
1859
- !this.hasAttribute("data-start-open")
1860
- );
2069
+ return this.#surface().startsCollapsed && !this.hasAttribute("data-start-open");
1861
2070
  }
1862
2071
 
1863
2072
  /**
@@ -1905,8 +2114,9 @@ export class AgUiChat extends HTMLElement {
1905
2114
  * Flip the scheme word of the `theme` attribute between `light` and `dark`
1906
2115
  * and persist the choice per tab. Bound to the optional built-in header theme
1907
2116
  * toggle (`data-theme-toggle`). The theme name and any host words are kept:
1908
- * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. Any
1909
- * scheme but `dark` (including `auto`) flips to `dark` first.
2117
+ * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. With no
2118
+ * scheme word (`auto`, or a host rule setting `color-scheme` on the element),
2119
+ * it flips away from the scheme the panel is drawn in.
1910
2120
  */
1911
2121
  toggleTheme(): void {
1912
2122
  // Only the scheme word flips, so a named theme survives the toggle:
@@ -1914,7 +2124,7 @@ export class AgUiChat extends HTMLElement {
1914
2124
  // only the scheme word is saved: the name is the host's, and a name saved
1915
2125
  // on one page would replace another page's theme on restore.
1916
2126
  const theme = this.getAttribute("theme") ?? "";
1917
- const scheme = theme.split(/\s+/).includes("dark") ? "light" : "dark";
2127
+ const scheme = this.#drawnScheme() === "dark" ? "light" : "dark";
1918
2128
  this.setAttribute("theme", withScheme(theme, scheme));
1919
2129
  this.#storage.writePreference(THEME_KEY, scheme);
1920
2130
  this.#syncThemeGlyph();
@@ -2032,9 +2242,37 @@ export class AgUiChat extends HTMLElement {
2032
2242
  this.#wireThreadStore();
2033
2243
  }
2034
2244
 
2245
+ /**
2246
+ * The scheme the panel is drawn in, which the toggle flips and its glyph
2247
+ * offers the way out of. A scheme word decides it, since the scheme rules
2248
+ * hold against a host's. Without one the panel follows whatever sets its
2249
+ * color-scheme: a host rule on the element, auto's "light dark", or the
2250
+ * light default. Both schemes allowed means the system picks. Read before
2251
+ * the shadow styles apply too (the glyph is drawn first), when the computed
2252
+ * value carries only a host rule, so auto is answered from its word.
2253
+ */
2254
+ #drawnScheme(): "light" | "dark" {
2255
+ const words = (this.getAttribute("theme") ?? "").split(/\s+/);
2256
+ if (words.includes("dark")) {
2257
+ return "dark";
2258
+ }
2259
+ if (words.includes("light")) {
2260
+ return "light";
2261
+ }
2262
+ const declared = getComputedStyle(this).colorScheme.split(/\s+/);
2263
+ const dark = declared.includes("dark");
2264
+ if (dark !== declared.includes("light")) {
2265
+ return dark ? "dark" : "light";
2266
+ }
2267
+ if (dark || words.includes("auto")) {
2268
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
2269
+ }
2270
+ return "light";
2271
+ }
2272
+
2035
2273
  /** Reflect the current theme on the toggle: show the destination's glyph. */
2036
2274
  #syncThemeGlyph(): void {
2037
- const dark = (this.getAttribute("theme") ?? "").split(/\s+/).includes("dark");
2275
+ const dark = this.#drawnScheme() === "dark";
2038
2276
  // Replaced wholesale rather than toggling a class: the two marks are
2039
2277
  // different paths, not one path in two states, and the slot has to keep
2040
2278
  // working so a host can still supply its own.
@@ -2053,9 +2291,30 @@ export class AgUiChat extends HTMLElement {
2053
2291
  * {@link toggleCollapsed} and {@link toggleTheme}.
2054
2292
  */
2055
2293
  openThreads(): void {
2294
+ // Collapsed into the bar, the list opens as a popover over the bar rather
2295
+ // than expanding the panel: the same drawer, moved beside the bar, since
2296
+ // nothing inside the hidden panel can be drawn.
2297
+ if (this.collapsed && this.#surface().collapseTo === "bar") {
2298
+ this.#dockBar.append(this.#drawer.element);
2299
+ } else {
2300
+ this.#homeDrawer();
2301
+ }
2056
2302
  this.#history.openThreads();
2057
2303
  }
2058
2304
 
2305
+ /**
2306
+ * Put the drawer back in the panel, where every surface but the bar's
2307
+ * popover draws it. A popover still open is closed first: the list was
2308
+ * opened over the bar, and carried into the panel it would cover the
2309
+ * conversation the user asked to see.
2310
+ */
2311
+ #homeDrawer(): void {
2312
+ if (this.#drawer.element.parentNode === this.#dockBar) {
2313
+ this.#drawer.close();
2314
+ this.#chat.insertBefore(this.#drawer.element, this.#checkpoints.element);
2315
+ }
2316
+ }
2317
+
2059
2318
  /**
2060
2319
  * Open the checkpoints panel (the `::part(checkpoints-button)` route).
2061
2320
  *
@@ -2082,16 +2341,19 @@ export class AgUiChat extends HTMLElement {
2082
2341
  * for a host that lists conversations in its own chrome.
2083
2342
  *
2084
2343
  * Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
2085
- * fired. **A run in flight is cancelled**, as the list and {@link newChat} do;
2344
+ * fired; a second call for the thread already loading settles with that load.
2345
+ * If the store rejects the load, the element stays on the thread it was on,
2346
+ * fires nothing, and the promise rejects with the store's error.
2347
+ * **A run in flight is cancelled**, as the list and {@link newChat} do;
2086
2348
  * a host that would rather wait reads {@link running} first. The thread
2087
2349
  * already on screen is left alone, and so is everything before the element
2088
2350
  * connects, since until then it has no store to read the thread from.
2089
2351
  */
2090
- async switchThread(threadId: string): Promise<void> {
2352
+ switchThread(threadId: string): Promise<void> {
2091
2353
  if (!this.#connected) {
2092
- return;
2354
+ return Promise.resolve();
2093
2355
  }
2094
- await this.#history.switchThread(threadId);
2356
+ return this.#history.switchThread(threadId);
2095
2357
  }
2096
2358
 
2097
2359
  /**
@@ -2134,6 +2396,13 @@ export class AgUiChat extends HTMLElement {
2134
2396
  * one. The page is not scrolled to bring the composer into view.
2135
2397
  */
2136
2398
  newChat(): void {
2399
+ // Before the element connects it has no store scoped to its namespace and
2400
+ // no thread to leave, and minting one would write to the unscoped store and
2401
+ // fire an event for a thread it does not yet show. Connecting starts it.
2402
+ if (!this.#connected) {
2403
+ this.#newChatOnConnect = true;
2404
+ return;
2405
+ }
2137
2406
  // Stop any in-flight run first — discarding the client mid-run would
2138
2407
  // leave the old agent streaming into a cleared transcript.
2139
2408
  this.#cancelRun();
@@ -2478,12 +2747,40 @@ export class AgUiChat extends HTMLElement {
2478
2747
  launcher: this.#launcher,
2479
2748
  badge: this.#badge,
2480
2749
  railLabel: this.#railLabel,
2750
+ bar: this.#dockBar,
2751
+ barHistory: this.#barHistory,
2481
2752
  titleText,
2482
2753
  iconUrl: readLauncherIconUrl(this),
2483
2754
  strings: this.#strings,
2484
2755
  signal,
2485
- onOpen: () => this.setCollapsed(false),
2756
+ // The bar stays on screen with the panel open, as the way to minimise
2757
+ // it, so its strip toggles; everywhere else the launcher is only drawn
2758
+ // while collapsed and only ever opens.
2759
+ onOpen: () => this.setCollapsed(this.#surface().collapseTo === "bar" && !this.collapsed),
2760
+ // The bar's history button toggles: the popover has no backdrop, and a
2761
+ // press on the bar is not a press outside it, so this is how the button
2762
+ // that opened the list closes it again. openThreads() stays open-only.
2763
+ onHistory: () => {
2764
+ if (this.#drawer.isOpen()) {
2765
+ this.#drawer.close();
2766
+ } else {
2767
+ this.openThreads();
2768
+ }
2769
+ },
2486
2770
  });
2771
+ // The popover has no backdrop to catch a click away, so a press outside
2772
+ // the bar closes it, as Escape already does.
2773
+ document.addEventListener(
2774
+ "pointerdown",
2775
+ (event) => {
2776
+ if (this.#drawer.element.parentNode === this.#dockBar && this.#drawer.isOpen()) {
2777
+ if (!event.composedPath().includes(this.#dockBar)) {
2778
+ this.#drawer.close();
2779
+ }
2780
+ }
2781
+ },
2782
+ { signal },
2783
+ );
2487
2784
  this.#placement.enableLauncherDrag(signal);
2488
2785
 
2489
2786
  this.#placement.mountResizeGrips(this.#chat);
@@ -2491,7 +2788,7 @@ export class AgUiChat extends HTMLElement {
2491
2788
  const probe = this.#placement.probe;
2492
2789
  probe.className = "viewport-probe";
2493
2790
  probe.setAttribute("aria-hidden", "true");
2494
- this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#launcher);
2791
+ this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
2495
2792
  }
2496
2793
 
2497
2794
  /**
@@ -2508,9 +2805,12 @@ export class AgUiChat extends HTMLElement {
2508
2805
  // layout problem — the badge is a circle, not a field.
2509
2806
  this.#badge.textContent = unread > 9 ? "9+" : String(unread);
2510
2807
  this.#badge.hidden = unread === 0 || !isUnreadBadgeEnabled(this);
2511
- const label = this.#badge.hidden
2512
- ? this.#strings.expand
2513
- : fillUiString(this.#strings.expandUnread, { count: unread });
2808
+ const label =
2809
+ !this.collapsed && this.#surface().collapseTo === "bar"
2810
+ ? this.#strings.collapse
2811
+ : this.#badge.hidden
2812
+ ? this.#strings.expand
2813
+ : fillUiString(this.#strings.expandUnread, { count: unread });
2514
2814
  this.#launcher.setAttribute("aria-label", label);
2515
2815
  this.#launcher.title = label;
2516
2816
  }
@@ -0,0 +1,5 @@
1
+ /** `detail` shape of the {@link BAR_HEIGHT_EVENT} CustomEvent. */
2
+ export interface BarHeightDetail {
3
+ /** The bottom bar's height in CSS px; 0 when no bar is drawn. */
4
+ readonly height: number;
5
+ }
@@ -10,7 +10,8 @@ export interface HighlightOverlayOptions {
10
10
  /**
11
11
  * Ring colour, and the flat fallback for the gradient. Default:
12
12
  * `--ag-ui-highlight-ring-color` on the target, then on the chat, then the
13
- * target's `--ag-ui-accent`, then the chat's accent.
13
+ * target's `--ag-ui-accent`, then the chat's accent, held to 3:1 against the
14
+ * surface behind the target.
14
15
  */
15
16
  readonly color?: string;
16
17
  /** The chat whose ring colour and accent to use. Default: the latest connected. */