@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.
- package/CHANGELOG.md +205 -1
- package/README.md +235 -16
- package/dist/ag-ui-web-component.bundle.js +647 -181
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +27 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +44 -5
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/events/bar_height_detail.d.ts +6 -0
- package/dist/core/events/bar_height_detail.d.ts.map +1 -0
- package/dist/dom/highlight_overlay.d.ts +2 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +8 -0
- package/dist/dom/ring_color.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1184 -69
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +8 -1
- package/dist/tools/chat_surface_tools.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +16 -3
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
- package/dist/ui/placement/dock_styles.d.ts +2 -0
- package/dist/ui/placement/dock_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts +16 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
- package/dist/ui/placement/surface_for.d.ts +32 -19
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
- package/dist/ui/shell/build_launcher.d.ts +15 -4
- package/dist/ui/shell/build_launcher.d.ts.map +1 -1
- package/dist/ui/shell/token_styles.d.ts.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +30 -2
- package/src/core/ag_ui_chat.ts +335 -28
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/dom/highlight_overlay.ts +2 -1
- package/src/dom/ring_color.ts +141 -4
- package/src/index.ts +4 -0
- package/src/tools/chat_surface_tools.ts +13 -5
- package/src/ui/history/conversation_history.ts +51 -7
- package/src/ui/placement/collapse_styles.ts +248 -2
- package/src/ui/placement/dock_styles.ts +95 -0
- package/src/ui/placement/panel_placement.ts +300 -16
- package/src/ui/placement/small_viewport_styles.ts +9 -0
- package/src/ui/placement/surface_for.ts +58 -22
- package/src/ui/progress/tool_group_styles.ts +52 -2
- package/src/ui/progress/tool_step_styles.ts +5 -3
- package/src/ui/shell/build_launcher.ts +32 -5
- package/src/ui/shell/token_styles.ts +58 -1
- package/src/ui/styles.ts +4 -0
- package/src/ui/themes/quiet_styles.ts +19 -3
- package/src/ui/themes/scheme_styles.ts +10 -1
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -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: () =>
|
|
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
|
-
|
|
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,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
1084
1122
|
}
|
|
1085
1123
|
return;
|
|
1086
1124
|
}
|
|
1087
|
-
if (name === "
|
|
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
|
+
// has no launcher to drag, and keeps its own dragged spot apart from the
|
|
1132
|
+
// corner's, so the same handover applies -- and is live the same way.
|
|
1133
|
+
if (name === "placement" || name === "collapse-to") {
|
|
1088
1134
|
// A placement owns the axes it fixes, so hand those back before anything
|
|
1089
1135
|
// else: a size dragged under the previous placement would otherwise sit
|
|
1090
1136
|
// inline and outrank the new one.
|
|
@@ -1096,9 +1142,19 @@ export class AgUiChat extends HTMLElement {
|
|
|
1096
1142
|
// not just stop offering it: the control is gone from the header the
|
|
1097
1143
|
// moment the attribute changes, so a panel collapsed under the previous
|
|
1098
1144
|
// placement would have no way back.
|
|
1099
|
-
if (
|
|
1145
|
+
if (this.#surface().collapseTo === null && this.collapsed) {
|
|
1100
1146
|
this.setCollapsed(false);
|
|
1101
1147
|
}
|
|
1148
|
+
this.#stampCollapseTarget();
|
|
1149
|
+
if (name === "collapse-to") {
|
|
1150
|
+
// The strip's label and the popover's home both depend on the target,
|
|
1151
|
+
// and neither is re-derived by the dock sync.
|
|
1152
|
+
this.#syncLauncher();
|
|
1153
|
+
if (this.#surface().collapseTo !== "bar") {
|
|
1154
|
+
this.#homeDrawer();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
this.#syncDock();
|
|
1102
1158
|
// Placement also moves the panel, so the edges its layout holds still
|
|
1103
1159
|
// change with it. Deferred a frame so the new rules have applied.
|
|
1104
1160
|
requestAnimationFrame(() => this.#placement.syncResizeAnchor());
|
|
@@ -1418,6 +1474,32 @@ export class AgUiChat extends HTMLElement {
|
|
|
1418
1474
|
this.setAttribute("data-tool-display", value);
|
|
1419
1475
|
}
|
|
1420
1476
|
|
|
1477
|
+
/**
|
|
1478
|
+
* How an opened step group draws its calls, from the
|
|
1479
|
+
* `data-tool-group-display` attribute (`full` / `compact` / `minimal` /
|
|
1480
|
+
* `inline`). Defaults to `full`, for an absent attribute and for a value the
|
|
1481
|
+
* element does not know, which is what a group drew before the attribute.
|
|
1482
|
+
*
|
|
1483
|
+
* Read only while `toolDisplay` is `grouped`: under a card mode the groups
|
|
1484
|
+
* are flattened into that mode's cards. Applied by the shadow CSS from the
|
|
1485
|
+
* attribute itself, so changing it restyles groups already on screen.
|
|
1486
|
+
*/
|
|
1487
|
+
get toolGroupDisplay(): ToolGroupDisplayMode {
|
|
1488
|
+
const attr = this.getAttribute("data-tool-group-display");
|
|
1489
|
+
if (
|
|
1490
|
+
attr === TOOL_GROUP_DISPLAY.INLINE ||
|
|
1491
|
+
attr === TOOL_GROUP_DISPLAY.MINIMAL ||
|
|
1492
|
+
attr === TOOL_GROUP_DISPLAY.COMPACT
|
|
1493
|
+
) {
|
|
1494
|
+
return attr;
|
|
1495
|
+
}
|
|
1496
|
+
return TOOL_GROUP_DISPLAY.FULL;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
set toolGroupDisplay(value: ToolGroupDisplayMode) {
|
|
1500
|
+
this.setAttribute("data-tool-group-display", value);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1421
1503
|
/**
|
|
1422
1504
|
* The host's own constructed stylesheets, adopted after the built-in one.
|
|
1423
1505
|
*
|
|
@@ -1493,10 +1575,32 @@ export class AgUiChat extends HTMLElement {
|
|
|
1493
1575
|
// namespaced per instance but not per placement, so a tab that collapsed a
|
|
1494
1576
|
// floating panel and later loaded the same instance as a page would restore
|
|
1495
1577
|
// a state that placement has no way out of.
|
|
1496
|
-
if (
|
|
1578
|
+
if (this.#surface().collapseTo !== null && this.#startsCollapsed()) {
|
|
1497
1579
|
this.setAttribute("collapsed", "");
|
|
1498
1580
|
}
|
|
1499
1581
|
this.#syncLauncher();
|
|
1582
|
+
this.#stampCollapseTarget();
|
|
1583
|
+
// The bar's height follows its content (the font, a long title), so it is
|
|
1584
|
+
// measured rather than known; the observer reports a change of size, and
|
|
1585
|
+
// the strip going from drawn to not drawn is one.
|
|
1586
|
+
const dockObserver = new ResizeObserver(() => this.#syncDock());
|
|
1587
|
+
dockObserver.observe(this.#dockBar);
|
|
1588
|
+
this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
|
|
1589
|
+
this.#syncDock();
|
|
1590
|
+
// Under collapse-to="none" the host draws the collapsed state itself, so
|
|
1591
|
+
// it has to learn the state the element mounted in -- restored from
|
|
1592
|
+
// storage or the placement's default -- without waiting for a change.
|
|
1593
|
+
// Dispatched directly rather than through setCollapsed, which would write
|
|
1594
|
+
// storage and clear the unread count for a state nobody changed.
|
|
1595
|
+
if (this.#surface().collapseTo === "none") {
|
|
1596
|
+
this.dispatchEvent(
|
|
1597
|
+
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
1598
|
+
detail: { collapsed: this.collapsed },
|
|
1599
|
+
bubbles: true,
|
|
1600
|
+
composed: true,
|
|
1601
|
+
}),
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1500
1604
|
this.#skills.init();
|
|
1501
1605
|
// A `user-key` that changed while the element was out of the document gets
|
|
1502
1606
|
// the handover a live change gets, here, because this is the first moment
|
|
@@ -1556,6 +1660,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
1556
1660
|
// Last: everything above reads (and some of it sets) attributes, and none
|
|
1557
1661
|
// of that should trip the connect-time warning.
|
|
1558
1662
|
this.#connected = true;
|
|
1663
|
+
if (this.#newChatOnConnect) {
|
|
1664
|
+
this.#newChatOnConnect = false;
|
|
1665
|
+
// After the restore above has started: starting the chat stops it, so
|
|
1666
|
+
// the one event is the new chat's and the conversation left stays kept.
|
|
1667
|
+
this.newChat();
|
|
1668
|
+
}
|
|
1559
1669
|
}
|
|
1560
1670
|
|
|
1561
1671
|
/**
|
|
@@ -1756,7 +1866,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1756
1866
|
* its own chrome.
|
|
1757
1867
|
*/
|
|
1758
1868
|
setCollapsed(collapsed: boolean, options: { readonly announce?: boolean } = {}): void {
|
|
1759
|
-
if (collapsed &&
|
|
1869
|
+
if (collapsed && this.#surface().collapseTo === null) {
|
|
1760
1870
|
return;
|
|
1761
1871
|
}
|
|
1762
1872
|
// Announced before the state changes, so the notice is written into a
|
|
@@ -1785,8 +1895,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1785
1895
|
if (collapsed) {
|
|
1786
1896
|
this.setAttribute("collapsed", "");
|
|
1787
1897
|
} else {
|
|
1898
|
+
// The history popover belongs to the bar only while the panel is away.
|
|
1899
|
+
// Opening the panel closes it and takes the drawer back, so the panel
|
|
1900
|
+
// lands on the conversation rather than on a list over it.
|
|
1901
|
+
this.#homeDrawer();
|
|
1788
1902
|
this.removeAttribute("collapsed");
|
|
1789
1903
|
}
|
|
1904
|
+
this.#syncDock();
|
|
1790
1905
|
writeStoredItem(this.#storage.key(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
1791
1906
|
// Expanding is what marks the waiting answers read; collapsing starts a
|
|
1792
1907
|
// fresh count. Either way the badge is cleared and the host told.
|
|
@@ -1832,6 +1947,104 @@ export class AgUiChat extends HTMLElement {
|
|
|
1832
1947
|
return this.#running;
|
|
1833
1948
|
}
|
|
1834
1949
|
|
|
1950
|
+
/**
|
|
1951
|
+
* The height of the bottom bar in CSS px, and 0 when none is drawn: under
|
|
1952
|
+
* any collapse-to but "bar", and on a phone while the panel is open, where
|
|
1953
|
+
* the panel is the screen. The bar is fixed over the page, so a host pads
|
|
1954
|
+
* its own page by this much; {@link BAR_HEIGHT_EVENT} says when it changes.
|
|
1955
|
+
*/
|
|
1956
|
+
get barHeight(): number {
|
|
1957
|
+
return this.#barHeight;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* The element the panel rises from under collapse-to="none", right-aligned
|
|
1962
|
+
* to it: typically the host's own button in its own bar. Takes precedence
|
|
1963
|
+
* over the anchor attribute, which names one by selector. One that resolves
|
|
1964
|
+
* to nothing leaves the panel in its placement's own corner, with a console
|
|
1965
|
+
* warning.
|
|
1966
|
+
*/
|
|
1967
|
+
get anchor(): Element | null {
|
|
1968
|
+
return this.#anchor;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
set anchor(value: Element | null) {
|
|
1972
|
+
this.#anchor = value;
|
|
1973
|
+
this.#anchorWarned = false;
|
|
1974
|
+
this.#syncDock();
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/** The surface the live placement and collapse-to attributes describe. */
|
|
1978
|
+
#surface(): Surface {
|
|
1979
|
+
return surfaceFor(this.getAttribute("placement"), this.getAttribute("collapse-to"));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* Stamp what the panel collapses to as data-collapsed-to, which is what the
|
|
1984
|
+
* stylesheet draws the bar and the hidden state from. Resolved here rather
|
|
1985
|
+
* than in CSS, because "bar" over a sidebar is its rail and "none" over a
|
|
1986
|
+
* page is no collapse at all, and the stylesheet would have to restate that
|
|
1987
|
+
* table.
|
|
1988
|
+
*/
|
|
1989
|
+
#stampCollapseTarget(): void {
|
|
1990
|
+
const target = this.#surface().collapseTo;
|
|
1991
|
+
if (target === null) {
|
|
1992
|
+
this.removeAttribute("data-collapsed-to");
|
|
1993
|
+
} else {
|
|
1994
|
+
this.setAttribute("data-collapsed-to", target);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
/** Re-measure the bar or the anchor, and tell the host when the bar's height changed. */
|
|
1999
|
+
#syncDock(): void {
|
|
2000
|
+
// isConnected rather than the connected flag, which connectedCallback
|
|
2001
|
+
// sets only at its end, after the first measurement here.
|
|
2002
|
+
if (!this.isConnected) {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
|
|
2006
|
+
if (height === this.#barHeight) {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.#barHeight = height;
|
|
2010
|
+
this.dispatchEvent(
|
|
2011
|
+
new CustomEvent<BarHeightDetail>(BAR_HEIGHT_EVENT, {
|
|
2012
|
+
detail: { height },
|
|
2013
|
+
bubbles: true,
|
|
2014
|
+
composed: true,
|
|
2015
|
+
}),
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* The anchor in force under collapse-to="none": the property, else the
|
|
2021
|
+
* attribute's selector, looked up in the root this element lives in and
|
|
2022
|
+
* then the document, so a chat inside a framework's shadow root can name a
|
|
2023
|
+
* button beside it. Null under any other collapse-to, with nothing given,
|
|
2024
|
+
* or when what was given is not in the document -- reported once per anchor.
|
|
2025
|
+
*/
|
|
2026
|
+
#resolveAnchor(): Element | null {
|
|
2027
|
+
if (this.#surface().collapseTo !== "none") {
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
const selector = this.getAttribute("anchor") ?? "";
|
|
2031
|
+
if (this.#anchor === null && selector === "") {
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
const found = this.#anchor ?? findAnchor(this.getRootNode(), selector);
|
|
2035
|
+
if (found?.isConnected === true) {
|
|
2036
|
+
return found;
|
|
2037
|
+
}
|
|
2038
|
+
if (!this.#anchorWarned) {
|
|
2039
|
+
this.#anchorWarned = true;
|
|
2040
|
+
console.warn(
|
|
2041
|
+
`<ag-ui-chat>: the anchor ${this.#anchor === null ? `"${selector}"` : "element"} ` +
|
|
2042
|
+
"is not in the document, so the panel opens from its placement's own corner.",
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
1835
2048
|
/**
|
|
1836
2049
|
* Whether to mount collapsed.
|
|
1837
2050
|
*
|
|
@@ -1854,10 +2067,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1854
2067
|
if (stored !== null) {
|
|
1855
2068
|
return stored === "1";
|
|
1856
2069
|
}
|
|
1857
|
-
return (
|
|
1858
|
-
surfaceFor(this.getAttribute("placement")).startsCollapsed &&
|
|
1859
|
-
!this.hasAttribute("data-start-open")
|
|
1860
|
-
);
|
|
2070
|
+
return this.#surface().startsCollapsed && !this.hasAttribute("data-start-open");
|
|
1861
2071
|
}
|
|
1862
2072
|
|
|
1863
2073
|
/**
|
|
@@ -1867,7 +2077,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1867
2077
|
* `movable` folds two separate reasons into the one answer a caller needs:
|
|
1868
2078
|
* a placement that places itself owns its position, and a panel that fills
|
|
1869
2079
|
* the screen has nowhere to go. Reporting them apart would make every caller
|
|
1870
|
-
* re-derive the same conjunction.
|
|
2080
|
+
* re-derive the same conjunction. A corner panel docked under
|
|
2081
|
+
* `collapse-to="bar"` or `"none"` is movable on a wide viewport, like the
|
|
2082
|
+
* corner panel it is.
|
|
1871
2083
|
*/
|
|
1872
2084
|
describeSurface(): ChatSurfaceReport {
|
|
1873
2085
|
return this.#placement.describeSurface();
|
|
@@ -1882,7 +2094,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
1882
2094
|
* the corner it opens from is re-picked, and switching placement hands
|
|
1883
2095
|
* everything back. What it must not do is claim a move it did not make --
|
|
1884
2096
|
* hence the boolean, and hence {@link describeSurface} existing so a caller
|
|
1885
|
-
* can ask first.
|
|
2097
|
+
* can ask first. A docked panel moves too, within the room above its bar or
|
|
2098
|
+
* anchor, and remembers the spot as a header drag would.
|
|
1886
2099
|
*/
|
|
1887
2100
|
moveTo(corner: ChatCorner, options: { readonly announce?: boolean } = {}): boolean {
|
|
1888
2101
|
return this.#placement.moveTo(corner, options);
|
|
@@ -1891,6 +2104,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1891
2104
|
/**
|
|
1892
2105
|
* Drop a size the user dragged, and its stored copy, so the placement's
|
|
1893
2106
|
* default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
|
|
2107
|
+
* Under `collapse-to="bar"` or `"none"` it also returns a panel the user
|
|
2108
|
+
* dragged off its dock to the dock, and forgets the stored spot; a corner
|
|
2109
|
+
* panel keeps a dragged position.
|
|
1894
2110
|
*/
|
|
1895
2111
|
resetSize(): void {
|
|
1896
2112
|
this.#placement.resetSize();
|
|
@@ -1905,8 +2121,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1905
2121
|
* Flip the scheme word of the `theme` attribute between `light` and `dark`
|
|
1906
2122
|
* and persist the choice per tab. Bound to the optional built-in header theme
|
|
1907
2123
|
* toggle (`data-theme-toggle`). The theme name and any host words are kept:
|
|
1908
|
-
* `soft` becomes `soft dark`, and `soft dark` becomes `soft light`.
|
|
1909
|
-
* scheme
|
|
2124
|
+
* `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. With no
|
|
2125
|
+
* scheme word (`auto`, or a host rule setting `color-scheme` on the element),
|
|
2126
|
+
* it flips away from the scheme the panel is drawn in.
|
|
1910
2127
|
*/
|
|
1911
2128
|
toggleTheme(): void {
|
|
1912
2129
|
// Only the scheme word flips, so a named theme survives the toggle:
|
|
@@ -1914,7 +2131,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1914
2131
|
// only the scheme word is saved: the name is the host's, and a name saved
|
|
1915
2132
|
// on one page would replace another page's theme on restore.
|
|
1916
2133
|
const theme = this.getAttribute("theme") ?? "";
|
|
1917
|
-
const scheme =
|
|
2134
|
+
const scheme = this.#drawnScheme() === "dark" ? "light" : "dark";
|
|
1918
2135
|
this.setAttribute("theme", withScheme(theme, scheme));
|
|
1919
2136
|
this.#storage.writePreference(THEME_KEY, scheme);
|
|
1920
2137
|
this.#syncThemeGlyph();
|
|
@@ -2032,9 +2249,37 @@ export class AgUiChat extends HTMLElement {
|
|
|
2032
2249
|
this.#wireThreadStore();
|
|
2033
2250
|
}
|
|
2034
2251
|
|
|
2252
|
+
/**
|
|
2253
|
+
* The scheme the panel is drawn in, which the toggle flips and its glyph
|
|
2254
|
+
* offers the way out of. A scheme word decides it, since the scheme rules
|
|
2255
|
+
* hold against a host's. Without one the panel follows whatever sets its
|
|
2256
|
+
* color-scheme: a host rule on the element, auto's "light dark", or the
|
|
2257
|
+
* light default. Both schemes allowed means the system picks. Read before
|
|
2258
|
+
* the shadow styles apply too (the glyph is drawn first), when the computed
|
|
2259
|
+
* value carries only a host rule, so auto is answered from its word.
|
|
2260
|
+
*/
|
|
2261
|
+
#drawnScheme(): "light" | "dark" {
|
|
2262
|
+
const words = (this.getAttribute("theme") ?? "").split(/\s+/);
|
|
2263
|
+
if (words.includes("dark")) {
|
|
2264
|
+
return "dark";
|
|
2265
|
+
}
|
|
2266
|
+
if (words.includes("light")) {
|
|
2267
|
+
return "light";
|
|
2268
|
+
}
|
|
2269
|
+
const declared = getComputedStyle(this).colorScheme.split(/\s+/);
|
|
2270
|
+
const dark = declared.includes("dark");
|
|
2271
|
+
if (dark !== declared.includes("light")) {
|
|
2272
|
+
return dark ? "dark" : "light";
|
|
2273
|
+
}
|
|
2274
|
+
if (dark || words.includes("auto")) {
|
|
2275
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
2276
|
+
}
|
|
2277
|
+
return "light";
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2035
2280
|
/** Reflect the current theme on the toggle: show the destination's glyph. */
|
|
2036
2281
|
#syncThemeGlyph(): void {
|
|
2037
|
-
const dark =
|
|
2282
|
+
const dark = this.#drawnScheme() === "dark";
|
|
2038
2283
|
// Replaced wholesale rather than toggling a class: the two marks are
|
|
2039
2284
|
// different paths, not one path in two states, and the slot has to keep
|
|
2040
2285
|
// working so a host can still supply its own.
|
|
@@ -2053,9 +2298,30 @@ export class AgUiChat extends HTMLElement {
|
|
|
2053
2298
|
* {@link toggleCollapsed} and {@link toggleTheme}.
|
|
2054
2299
|
*/
|
|
2055
2300
|
openThreads(): void {
|
|
2301
|
+
// Collapsed into the bar, the list opens as a popover over the bar rather
|
|
2302
|
+
// than expanding the panel: the same drawer, moved beside the bar, since
|
|
2303
|
+
// nothing inside the hidden panel can be drawn.
|
|
2304
|
+
if (this.collapsed && this.#surface().collapseTo === "bar") {
|
|
2305
|
+
this.#dockBar.append(this.#drawer.element);
|
|
2306
|
+
} else {
|
|
2307
|
+
this.#homeDrawer();
|
|
2308
|
+
}
|
|
2056
2309
|
this.#history.openThreads();
|
|
2057
2310
|
}
|
|
2058
2311
|
|
|
2312
|
+
/**
|
|
2313
|
+
* Put the drawer back in the panel, where every surface but the bar's
|
|
2314
|
+
* popover draws it. A popover still open is closed first: the list was
|
|
2315
|
+
* opened over the bar, and carried into the panel it would cover the
|
|
2316
|
+
* conversation the user asked to see.
|
|
2317
|
+
*/
|
|
2318
|
+
#homeDrawer(): void {
|
|
2319
|
+
if (this.#drawer.element.parentNode === this.#dockBar) {
|
|
2320
|
+
this.#drawer.close();
|
|
2321
|
+
this.#chat.insertBefore(this.#drawer.element, this.#checkpoints.element);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2059
2325
|
/**
|
|
2060
2326
|
* Open the checkpoints panel (the `::part(checkpoints-button)` route).
|
|
2061
2327
|
*
|
|
@@ -2082,16 +2348,19 @@ export class AgUiChat extends HTMLElement {
|
|
|
2082
2348
|
* for a host that lists conversations in its own chrome.
|
|
2083
2349
|
*
|
|
2084
2350
|
* Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
|
|
2085
|
-
* fired
|
|
2351
|
+
* fired; a second call for the thread already loading settles with that load.
|
|
2352
|
+
* If the store rejects the load, the element stays on the thread it was on,
|
|
2353
|
+
* fires nothing, and the promise rejects with the store's error.
|
|
2354
|
+
* **A run in flight is cancelled**, as the list and {@link newChat} do;
|
|
2086
2355
|
* a host that would rather wait reads {@link running} first. The thread
|
|
2087
2356
|
* already on screen is left alone, and so is everything before the element
|
|
2088
2357
|
* connects, since until then it has no store to read the thread from.
|
|
2089
2358
|
*/
|
|
2090
|
-
|
|
2359
|
+
switchThread(threadId: string): Promise<void> {
|
|
2091
2360
|
if (!this.#connected) {
|
|
2092
|
-
return;
|
|
2361
|
+
return Promise.resolve();
|
|
2093
2362
|
}
|
|
2094
|
-
|
|
2363
|
+
return this.#history.switchThread(threadId);
|
|
2095
2364
|
}
|
|
2096
2365
|
|
|
2097
2366
|
/**
|
|
@@ -2134,6 +2403,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2134
2403
|
* one. The page is not scrolled to bring the composer into view.
|
|
2135
2404
|
*/
|
|
2136
2405
|
newChat(): void {
|
|
2406
|
+
// Before the element connects it has no store scoped to its namespace and
|
|
2407
|
+
// no thread to leave, and minting one would write to the unscoped store and
|
|
2408
|
+
// fire an event for a thread it does not yet show. Connecting starts it.
|
|
2409
|
+
if (!this.#connected) {
|
|
2410
|
+
this.#newChatOnConnect = true;
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2137
2413
|
// Stop any in-flight run first — discarding the client mid-run would
|
|
2138
2414
|
// leave the old agent streaming into a cleared transcript.
|
|
2139
2415
|
this.#cancelRun();
|
|
@@ -2478,12 +2754,40 @@ export class AgUiChat extends HTMLElement {
|
|
|
2478
2754
|
launcher: this.#launcher,
|
|
2479
2755
|
badge: this.#badge,
|
|
2480
2756
|
railLabel: this.#railLabel,
|
|
2757
|
+
bar: this.#dockBar,
|
|
2758
|
+
barHistory: this.#barHistory,
|
|
2481
2759
|
titleText,
|
|
2482
2760
|
iconUrl: readLauncherIconUrl(this),
|
|
2483
2761
|
strings: this.#strings,
|
|
2484
2762
|
signal,
|
|
2485
|
-
|
|
2763
|
+
// The bar stays on screen with the panel open, as the way to minimise
|
|
2764
|
+
// it, so its strip toggles; everywhere else the launcher is only drawn
|
|
2765
|
+
// while collapsed and only ever opens.
|
|
2766
|
+
onOpen: () => this.setCollapsed(this.#surface().collapseTo === "bar" && !this.collapsed),
|
|
2767
|
+
// The bar's history button toggles: the popover has no backdrop, and a
|
|
2768
|
+
// press on the bar is not a press outside it, so this is how the button
|
|
2769
|
+
// that opened the list closes it again. openThreads() stays open-only.
|
|
2770
|
+
onHistory: () => {
|
|
2771
|
+
if (this.#drawer.isOpen()) {
|
|
2772
|
+
this.#drawer.close();
|
|
2773
|
+
} else {
|
|
2774
|
+
this.openThreads();
|
|
2775
|
+
}
|
|
2776
|
+
},
|
|
2486
2777
|
});
|
|
2778
|
+
// The popover has no backdrop to catch a click away, so a press outside
|
|
2779
|
+
// the bar closes it, as Escape already does.
|
|
2780
|
+
document.addEventListener(
|
|
2781
|
+
"pointerdown",
|
|
2782
|
+
(event) => {
|
|
2783
|
+
if (this.#drawer.element.parentNode === this.#dockBar && this.#drawer.isOpen()) {
|
|
2784
|
+
if (!event.composedPath().includes(this.#dockBar)) {
|
|
2785
|
+
this.#drawer.close();
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
},
|
|
2789
|
+
{ signal },
|
|
2790
|
+
);
|
|
2487
2791
|
this.#placement.enableLauncherDrag(signal);
|
|
2488
2792
|
|
|
2489
2793
|
this.#placement.mountResizeGrips(this.#chat);
|
|
@@ -2491,7 +2795,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2491
2795
|
const probe = this.#placement.probe;
|
|
2492
2796
|
probe.className = "viewport-probe";
|
|
2493
2797
|
probe.setAttribute("aria-hidden", "true");
|
|
2494
|
-
this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#
|
|
2798
|
+
this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
|
|
2495
2799
|
}
|
|
2496
2800
|
|
|
2497
2801
|
/**
|
|
@@ -2508,9 +2812,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2508
2812
|
// layout problem — the badge is a circle, not a field.
|
|
2509
2813
|
this.#badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
2510
2814
|
this.#badge.hidden = unread === 0 || !isUnreadBadgeEnabled(this);
|
|
2511
|
-
const label =
|
|
2512
|
-
|
|
2513
|
-
|
|
2815
|
+
const label =
|
|
2816
|
+
!this.collapsed && this.#surface().collapseTo === "bar"
|
|
2817
|
+
? this.#strings.collapse
|
|
2818
|
+
: this.#badge.hidden
|
|
2819
|
+
? this.#strings.expand
|
|
2820
|
+
: fillUiString(this.#strings.expandUnread, { count: unread });
|
|
2514
2821
|
this.#launcher.setAttribute("aria-label", label);
|
|
2515
2822
|
this.#launcher.title = label;
|
|
2516
2823
|
}
|
|
@@ -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. */
|