@artooi/ag-ui-web-component 0.46.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.
- package/CHANGELOG.md +221 -1
- package/README.md +298 -31
- package/dist/ag-ui-web-component.bundle.js +597 -177
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +37 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +73 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/chat_registry.d.ts +23 -0
- package/dist/core/chat_registry.d.ts.map +1 -0
- 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/core/events/run_finished_detail.d.ts +6 -0
- package/dist/core/events/run_finished_detail.d.ts.map +1 -1
- package/dist/core/events/thread_detail.d.ts +27 -0
- package/dist/core/events/thread_detail.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +6 -1
- package/dist/core/run_handlers.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +21 -6
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/highlight_overlay.d.ts +8 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +22 -0
- package/dist/dom/ring_color.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1164 -95
- 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 +24 -2
- 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 +17 -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 +21 -17
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_step_group.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/scheme_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +41 -2
- package/src/core/ag_ui_chat.ts +430 -27
- package/src/core/chat_registry.ts +53 -0
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/core/events/run_finished_detail.ts +6 -0
- package/src/core/events/thread_detail.ts +26 -0
- package/src/core/run_handlers.ts +9 -4
- package/src/dom/animations.ts +30 -31
- package/src/dom/highlight_overlay.ts +10 -5
- package/src/dom/ring_color.ts +222 -0
- package/src/index.ts +7 -0
- package/src/tools/chat_surface_tools.ts +18 -8
- package/src/ui/history/conversation_history.ts +75 -7
- package/src/ui/placement/collapse_styles.ts +248 -2
- package/src/ui/placement/dock_styles.ts +91 -0
- package/src/ui/placement/embedded_styles.ts +2 -2
- package/src/ui/placement/panel_placement.ts +105 -12
- package/src/ui/placement/preset_styles.ts +1 -1
- package/src/ui/placement/sidebar_styles.ts +1 -1
- package/src/ui/placement/small_viewport_styles.ts +14 -0
- package/src/ui/placement/surface_for.ts +29 -18
- package/src/ui/progress/tool_call_card.ts +18 -8
- package/src/ui/progress/tool_group_styles.ts +49 -0
- package/src/ui/progress/tool_step_group.ts +3 -0
- 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 +30 -3
- package/src/ui/styles.ts +4 -0
- 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,
|
|
@@ -10,8 +11,11 @@ import {
|
|
|
10
11
|
STATE_EVENT,
|
|
11
12
|
SUBMIT_EVENT,
|
|
12
13
|
SUGGESTIONS_ACTIVITY_TYPE,
|
|
14
|
+
THREAD_EVENT,
|
|
13
15
|
TOGGLE_EVENT,
|
|
14
16
|
TOOL_DISPLAY,
|
|
17
|
+
TOOL_GROUP_DISPLAY,
|
|
18
|
+
type ToolGroupDisplayMode,
|
|
15
19
|
UNREAD_EVENT,
|
|
16
20
|
} from "../constants.js";
|
|
17
21
|
import type { Skill } from "../skills/skill.js";
|
|
@@ -49,7 +53,7 @@ import { PendingDecision } from "../ui/interrupts/pending_decision.js";
|
|
|
49
53
|
// biome-ignore lint/style/useImportType: the emitted declaration file copies this form
|
|
50
54
|
import { type QuestionRenderer } from "../ui/interrupts/question_card.js";
|
|
51
55
|
import { PanelPlacement } from "../ui/placement/panel_placement.js";
|
|
52
|
-
import { surfaceFor } from "../ui/placement/surface_for.js";
|
|
56
|
+
import { type Surface, surfaceFor } from "../ui/placement/surface_for.js";
|
|
53
57
|
import { RunAnnouncer } from "../ui/progress/run_announcer.js";
|
|
54
58
|
import { renderRunNotice } from "../ui/progress/run_notice.js";
|
|
55
59
|
import { SubAgentProgress } from "../ui/progress/subagent_progress.js";
|
|
@@ -75,6 +79,7 @@ import type { ActivityRenderer } from "./activity_renderer.js";
|
|
|
75
79
|
import { AgUiClient } from "./agui_client.js";
|
|
76
80
|
// biome-ignore lint/style/useImportType: the emitted declaration file copies this form
|
|
77
81
|
import { type AttachmentRef } from "./attachment.js";
|
|
82
|
+
import { ChatRegistration } from "./chat_registry.js";
|
|
78
83
|
import type { ClientSeed } from "./client_seed.js";
|
|
79
84
|
import {
|
|
80
85
|
type ClientConversationStore,
|
|
@@ -83,8 +88,10 @@ import {
|
|
|
83
88
|
writeStoredItem,
|
|
84
89
|
} from "./conversation_store.js";
|
|
85
90
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
91
|
+
import type { BarHeightDetail } from "./events/bar_height_detail.js";
|
|
86
92
|
import type { StateDetail } from "./events/state_detail.js";
|
|
87
93
|
import type { SubmitDetail } from "./events/submit_detail.js";
|
|
94
|
+
import type { ThreadDetail } from "./events/thread_detail.js";
|
|
88
95
|
import type { ToggleDetail } from "./events/toggle_detail.js";
|
|
89
96
|
import type { UnreadDetail } from "./events/unread_detail.js";
|
|
90
97
|
import type { MessageRole } from "./message_role.js";
|
|
@@ -110,7 +117,7 @@ import { warnOnCrossOriginCredentials, withCredentials } from "./utils.js";
|
|
|
110
117
|
* Excludes the attributes that are re-read per use, where a late change works
|
|
111
118
|
* and a warning would be wrong: `data-runs-url`, `data-page-actions`,
|
|
112
119
|
* `data-text-animation`, `data-tool-display`, `endpoint`, and the CSS-reactive
|
|
113
|
-
* `theme` / `collapsed`.
|
|
120
|
+
* `theme` / `collapsed` / `data-tool-group-display`.
|
|
114
121
|
*/
|
|
115
122
|
const CONNECT_TIME_ATTRIBUTES = [
|
|
116
123
|
"data-attachments-url",
|
|
@@ -172,6 +179,20 @@ function withScheme(theme: string, scheme: string): string {
|
|
|
172
179
|
return [...names, scheme].join(" ");
|
|
173
180
|
}
|
|
174
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
|
+
|
|
175
196
|
/**
|
|
176
197
|
* `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
|
|
177
198
|
*
|
|
@@ -536,8 +557,21 @@ export class AgUiChat extends HTMLElement {
|
|
|
536
557
|
readonly #launcher: HTMLButtonElement;
|
|
537
558
|
/** The launcher's unread badge; hidden at zero, and when the host opts out. */
|
|
538
559
|
readonly #badge: HTMLSpanElement;
|
|
539
|
-
/** 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. */
|
|
540
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;
|
|
541
575
|
// Answers that finished while the widget was collapsed. Expanding clears it.
|
|
542
576
|
#unread = 0;
|
|
543
577
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
@@ -600,6 +634,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
600
634
|
*/
|
|
601
635
|
readonly #onViewportResize = (): void => {
|
|
602
636
|
this.#placement.publishVisualViewport();
|
|
637
|
+
this.#syncDock();
|
|
603
638
|
// Not while a gesture owns the position. Restoring re-applies the *stored*
|
|
604
639
|
// position, and mid-drag that value is the one from before the drag began
|
|
605
640
|
// -- so it puts the widget back where it was, the next pointer move puts it
|
|
@@ -624,6 +659,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
624
659
|
readonly #voice: ComposerVoice;
|
|
625
660
|
/** Whether the element is currently in the DOM; gates the connect-time warning. */
|
|
626
661
|
#connected = false;
|
|
662
|
+
/** A {@link newChat} asked for before the element connected, to start at connect. */
|
|
663
|
+
#newChatOnConnect = false;
|
|
627
664
|
/**
|
|
628
665
|
* Aborted when the element leaves the document, and replaced when it comes
|
|
629
666
|
* back.
|
|
@@ -750,6 +787,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
750
787
|
* then a host's {@link sendMessage} waits for it rather than refusing.
|
|
751
788
|
*/
|
|
752
789
|
#restoreHold: { readonly hold: object; readonly restored: Promise<void> } | null = null;
|
|
790
|
+
/**
|
|
791
|
+
* The thread the latest {@link THREAD_EVENT} reported, which the next one
|
|
792
|
+
* names as the thread it replaced; `null` before the first.
|
|
793
|
+
*/
|
|
794
|
+
#shownThread: string | null = null;
|
|
753
795
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
754
796
|
readonly #decision = new PendingDecision();
|
|
755
797
|
/**
|
|
@@ -781,6 +823,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
781
823
|
allowImages: () => this.allowImages,
|
|
782
824
|
follow: () => this.#transcript.follow(),
|
|
783
825
|
});
|
|
826
|
+
// This chat's place among the connected ones, which a free highlight ring reads.
|
|
827
|
+
readonly #chatRegistration = new ChatRegistration(this);
|
|
784
828
|
/**
|
|
785
829
|
* Which storage keys are this element's: the namespace it claims, the keys
|
|
786
830
|
* its layout preferences live under, and the built-in store scoped to them.
|
|
@@ -869,7 +913,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
869
913
|
root: this.#root,
|
|
870
914
|
connected: () => this.#connected,
|
|
871
915
|
collapsed: () => this.collapsed,
|
|
872
|
-
collapsible: () =>
|
|
916
|
+
collapsible: () => this.#surface().collapseTo !== null,
|
|
873
917
|
strings: () => this.#strings,
|
|
874
918
|
readPreference: (base) => this.#storage.readPreference(base),
|
|
875
919
|
writePreference: (base, value) => this.#storage.writePreference(base, value),
|
|
@@ -955,7 +999,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
955
999
|
});
|
|
956
1000
|
this.#drawer = new ThreadDrawer({
|
|
957
1001
|
onSelect: (threadId) => {
|
|
958
|
-
|
|
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);
|
|
959
1005
|
},
|
|
960
1006
|
onNew: () => {
|
|
961
1007
|
this.newChat();
|
|
@@ -1043,6 +1089,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1043
1089
|
cancelRun: () => this.#cancelRun(),
|
|
1044
1090
|
resetState: () => this.#resetState(),
|
|
1045
1091
|
setRunning: (running) => this.#setRunning(running),
|
|
1092
|
+
threadChanged: (threadId, reason) => this.#dispatchThread(threadId, reason),
|
|
1046
1093
|
});
|
|
1047
1094
|
}
|
|
1048
1095
|
|
|
@@ -1051,6 +1098,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
1051
1098
|
return [
|
|
1052
1099
|
"title-text",
|
|
1053
1100
|
"placement",
|
|
1101
|
+
"collapse-to",
|
|
1102
|
+
"anchor",
|
|
1054
1103
|
"credentials",
|
|
1055
1104
|
"user-key",
|
|
1056
1105
|
"user-name",
|
|
@@ -1073,7 +1122,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1073
1122
|
}
|
|
1074
1123
|
return;
|
|
1075
1124
|
}
|
|
1076
|
-
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
|
+
// cannot be moved, so the same handover applies -- and is live the same way.
|
|
1132
|
+
if (name === "placement" || name === "collapse-to") {
|
|
1077
1133
|
// A placement owns the axes it fixes, so hand those back before anything
|
|
1078
1134
|
// else: a size dragged under the previous placement would otherwise sit
|
|
1079
1135
|
// inline and outrank the new one.
|
|
@@ -1085,9 +1141,19 @@ export class AgUiChat extends HTMLElement {
|
|
|
1085
1141
|
// not just stop offering it: the control is gone from the header the
|
|
1086
1142
|
// moment the attribute changes, so a panel collapsed under the previous
|
|
1087
1143
|
// placement would have no way back.
|
|
1088
|
-
if (
|
|
1144
|
+
if (this.#surface().collapseTo === null && this.collapsed) {
|
|
1089
1145
|
this.setCollapsed(false);
|
|
1090
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();
|
|
1091
1157
|
// Placement also moves the panel, so the edges its layout holds still
|
|
1092
1158
|
// change with it. Deferred a frame so the new rules have applied.
|
|
1093
1159
|
requestAnimationFrame(() => this.#placement.syncResizeAnchor());
|
|
@@ -1407,6 +1473,32 @@ export class AgUiChat extends HTMLElement {
|
|
|
1407
1473
|
this.setAttribute("data-tool-display", value);
|
|
1408
1474
|
}
|
|
1409
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
|
+
|
|
1410
1502
|
/**
|
|
1411
1503
|
* The host's own constructed stylesheets, adopted after the built-in one.
|
|
1412
1504
|
*
|
|
@@ -1448,6 +1540,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1448
1540
|
// this instance doesn't share collapsed/theme/thread state with another on
|
|
1449
1541
|
// the same origin.
|
|
1450
1542
|
this.#storage.claim();
|
|
1543
|
+
this.#chatRegistration.join();
|
|
1451
1544
|
// Restore a dragged size before the panel paints, so it does not snap from
|
|
1452
1545
|
// the placement default to the user's width on the first frame.
|
|
1453
1546
|
this.#placement.restoreSize();
|
|
@@ -1481,10 +1574,32 @@ export class AgUiChat extends HTMLElement {
|
|
|
1481
1574
|
// namespaced per instance but not per placement, so a tab that collapsed a
|
|
1482
1575
|
// floating panel and later loaded the same instance as a page would restore
|
|
1483
1576
|
// a state that placement has no way out of.
|
|
1484
|
-
if (
|
|
1577
|
+
if (this.#surface().collapseTo !== null && this.#startsCollapsed()) {
|
|
1485
1578
|
this.setAttribute("collapsed", "");
|
|
1486
1579
|
}
|
|
1487
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
|
+
}
|
|
1488
1603
|
this.#skills.init();
|
|
1489
1604
|
// A `user-key` that changed while the element was out of the document gets
|
|
1490
1605
|
// the handover a live change gets, here, because this is the first moment
|
|
@@ -1530,7 +1645,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1530
1645
|
// The catalog requests go out a microtask later, so a host configuring
|
|
1531
1646
|
// through a framework ref still has a chance to be heard — see #startup.
|
|
1532
1647
|
queueMicrotask(() => this.#startup());
|
|
1533
|
-
|
|
1648
|
+
// A key changed while the element was away handed it over all the same,
|
|
1649
|
+
// and the restore is the arriving principal's: reported as the live change
|
|
1650
|
+
// is. An adoption is not a handover, and restores the same conversation.
|
|
1651
|
+
void this.#history.rehydrate(changed && held !== "" ? "rescope" : "restore");
|
|
1534
1652
|
if (changed) {
|
|
1535
1653
|
// The last of what a live change does: a drawer left open across the
|
|
1536
1654
|
// move still lists the threads of the principal who left, titles and
|
|
@@ -1541,6 +1659,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
1541
1659
|
// Last: everything above reads (and some of it sets) attributes, and none
|
|
1542
1660
|
// of that should trip the connect-time warning.
|
|
1543
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
|
+
}
|
|
1544
1668
|
}
|
|
1545
1669
|
|
|
1546
1670
|
/**
|
|
@@ -1594,7 +1718,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1594
1718
|
await Promise.all([
|
|
1595
1719
|
this.#tools.fetchCatalog(),
|
|
1596
1720
|
this.#skills.fetch(),
|
|
1597
|
-
this.#history.rehydrate(),
|
|
1721
|
+
this.#history.rehydrate("restore"),
|
|
1598
1722
|
]);
|
|
1599
1723
|
}
|
|
1600
1724
|
|
|
@@ -1611,6 +1735,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1611
1735
|
window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
|
|
1612
1736
|
window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
|
|
1613
1737
|
this.#storage.release();
|
|
1738
|
+
this.#chatRegistration.leave();
|
|
1614
1739
|
this.#cancelRun();
|
|
1615
1740
|
this.#excerpts.detachPageOffer();
|
|
1616
1741
|
this.#attachments.tray?.dispose();
|
|
@@ -1740,7 +1865,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1740
1865
|
* its own chrome.
|
|
1741
1866
|
*/
|
|
1742
1867
|
setCollapsed(collapsed: boolean, options: { readonly announce?: boolean } = {}): void {
|
|
1743
|
-
if (collapsed &&
|
|
1868
|
+
if (collapsed && this.#surface().collapseTo === null) {
|
|
1744
1869
|
return;
|
|
1745
1870
|
}
|
|
1746
1871
|
// Announced before the state changes, so the notice is written into a
|
|
@@ -1769,8 +1894,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1769
1894
|
if (collapsed) {
|
|
1770
1895
|
this.setAttribute("collapsed", "");
|
|
1771
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();
|
|
1772
1901
|
this.removeAttribute("collapsed");
|
|
1773
1902
|
}
|
|
1903
|
+
this.#syncDock();
|
|
1774
1904
|
writeStoredItem(this.#storage.key(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
1775
1905
|
// Expanding is what marks the waiting answers read; collapsing starts a
|
|
1776
1906
|
// fresh count. Either way the badge is cleared and the host told.
|
|
@@ -1794,6 +1924,126 @@ export class AgUiChat extends HTMLElement {
|
|
|
1794
1924
|
return this.#unread;
|
|
1795
1925
|
}
|
|
1796
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* The thread on screen; `null` before the element first connects.
|
|
1929
|
+
*
|
|
1930
|
+
* Only reads. `conversationStore.threadId()` mints and stores a thread when
|
|
1931
|
+
* the store points at none, so a host asking which conversation is showing
|
|
1932
|
+
* could start one; this never does. {@link THREAD_EVENT} says when it changes.
|
|
1933
|
+
*/
|
|
1934
|
+
get threadId(): string | null {
|
|
1935
|
+
const threadId = this.#history.threadId;
|
|
1936
|
+
return threadId === "" ? null : threadId;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Whether an interaction is in flight: the composer's own state, the one that
|
|
1941
|
+
* shows Stop. It spans every tool round, a retry and a checkpoint
|
|
1942
|
+
* continuation as well as a send, which {@link SUBMIT_EVENT} alone does not.
|
|
1943
|
+
* Reflected as `data-running` on the element, for a host's CSS.
|
|
1944
|
+
*/
|
|
1945
|
+
get running(): boolean {
|
|
1946
|
+
return this.#running;
|
|
1947
|
+
}
|
|
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
|
+
|
|
1797
2047
|
/**
|
|
1798
2048
|
* Whether to mount collapsed.
|
|
1799
2049
|
*
|
|
@@ -1816,10 +2066,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1816
2066
|
if (stored !== null) {
|
|
1817
2067
|
return stored === "1";
|
|
1818
2068
|
}
|
|
1819
|
-
return (
|
|
1820
|
-
surfaceFor(this.getAttribute("placement")).startsCollapsed &&
|
|
1821
|
-
!this.hasAttribute("data-start-open")
|
|
1822
|
-
);
|
|
2069
|
+
return this.#surface().startsCollapsed && !this.hasAttribute("data-start-open");
|
|
1823
2070
|
}
|
|
1824
2071
|
|
|
1825
2072
|
/**
|
|
@@ -1850,6 +2097,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1850
2097
|
return this.#placement.moveTo(corner, options);
|
|
1851
2098
|
}
|
|
1852
2099
|
|
|
2100
|
+
/**
|
|
2101
|
+
* Drop a size the user dragged, and its stored copy, so the placement's
|
|
2102
|
+
* default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
|
|
2103
|
+
*/
|
|
2104
|
+
resetSize(): void {
|
|
2105
|
+
this.#placement.resetSize();
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1853
2108
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
1854
2109
|
toggleCollapsed(): void {
|
|
1855
2110
|
this.setCollapsed(!this.collapsed);
|
|
@@ -1859,8 +2114,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1859
2114
|
* Flip the scheme word of the `theme` attribute between `light` and `dark`
|
|
1860
2115
|
* and persist the choice per tab. Bound to the optional built-in header theme
|
|
1861
2116
|
* toggle (`data-theme-toggle`). The theme name and any host words are kept:
|
|
1862
|
-
* `soft` becomes `soft dark`, and `soft dark` becomes `soft light`.
|
|
1863
|
-
* scheme
|
|
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.
|
|
1864
2120
|
*/
|
|
1865
2121
|
toggleTheme(): void {
|
|
1866
2122
|
// Only the scheme word flips, so a named theme survives the toggle:
|
|
@@ -1868,7 +2124,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1868
2124
|
// only the scheme word is saved: the name is the host's, and a name saved
|
|
1869
2125
|
// on one page would replace another page's theme on restore.
|
|
1870
2126
|
const theme = this.getAttribute("theme") ?? "";
|
|
1871
|
-
const scheme =
|
|
2127
|
+
const scheme = this.#drawnScheme() === "dark" ? "light" : "dark";
|
|
1872
2128
|
this.setAttribute("theme", withScheme(theme, scheme));
|
|
1873
2129
|
this.#storage.writePreference(THEME_KEY, scheme);
|
|
1874
2130
|
this.#syncThemeGlyph();
|
|
@@ -1960,7 +2216,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1960
2216
|
this.#voice.dispose();
|
|
1961
2217
|
this.#voice.wire();
|
|
1962
2218
|
this.#history.adoptActiveThread();
|
|
1963
|
-
void this.#history.rehydrate();
|
|
2219
|
+
void this.#history.rehydrate("rescope");
|
|
1964
2220
|
void this.#history.refreshDrawer();
|
|
1965
2221
|
}
|
|
1966
2222
|
|
|
@@ -1986,9 +2242,37 @@ export class AgUiChat extends HTMLElement {
|
|
|
1986
2242
|
this.#wireThreadStore();
|
|
1987
2243
|
}
|
|
1988
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
|
+
|
|
1989
2273
|
/** Reflect the current theme on the toggle: show the destination's glyph. */
|
|
1990
2274
|
#syncThemeGlyph(): void {
|
|
1991
|
-
const dark =
|
|
2275
|
+
const dark = this.#drawnScheme() === "dark";
|
|
1992
2276
|
// Replaced wholesale rather than toggling a class: the two marks are
|
|
1993
2277
|
// different paths, not one path in two states, and the slot has to keep
|
|
1994
2278
|
// working so a host can still supply its own.
|
|
@@ -2007,9 +2291,30 @@ export class AgUiChat extends HTMLElement {
|
|
|
2007
2291
|
* {@link toggleCollapsed} and {@link toggleTheme}.
|
|
2008
2292
|
*/
|
|
2009
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
|
+
}
|
|
2010
2302
|
this.#history.openThreads();
|
|
2011
2303
|
}
|
|
2012
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
|
+
|
|
2013
2318
|
/**
|
|
2014
2319
|
* Open the checkpoints panel (the `::part(checkpoints-button)` route).
|
|
2015
2320
|
*
|
|
@@ -2031,6 +2336,39 @@ export class AgUiChat extends HTMLElement {
|
|
|
2031
2336
|
this.#checkpoints.close();
|
|
2032
2337
|
}
|
|
2033
2338
|
|
|
2339
|
+
/**
|
|
2340
|
+
* Put a stored thread on screen, as picking it in the conversation list does,
|
|
2341
|
+
* for a host that lists conversations in its own chrome.
|
|
2342
|
+
*
|
|
2343
|
+
* Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
|
|
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;
|
|
2348
|
+
* a host that would rather wait reads {@link running} first. The thread
|
|
2349
|
+
* already on screen is left alone, and so is everything before the element
|
|
2350
|
+
* connects, since until then it has no store to read the thread from.
|
|
2351
|
+
*/
|
|
2352
|
+
switchThread(threadId: string): Promise<void> {
|
|
2353
|
+
if (!this.#connected) {
|
|
2354
|
+
return Promise.resolve();
|
|
2355
|
+
}
|
|
2356
|
+
return this.#history.switchThread(threadId);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* Delete a stored thread, as the conversation list's delete does. Deleting the
|
|
2361
|
+
* thread on screen cancels its run and puts a fresh thread in its place, and
|
|
2362
|
+
* {@link THREAD_EVENT} reports it; deleting another leaves the screen alone.
|
|
2363
|
+
* Does nothing before the element connects.
|
|
2364
|
+
*/
|
|
2365
|
+
deleteThread(threadId: string): void {
|
|
2366
|
+
if (!this.#connected) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
this.#history.deleteThread(threadId);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2034
2372
|
/**
|
|
2035
2373
|
* Open the checkpoints panel, or close it if it is already open — what the
|
|
2036
2374
|
* built-in ⭯ button does, because a control that opens a panel is read as the
|
|
@@ -2058,6 +2396,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2058
2396
|
* one. The page is not scrolled to bring the composer into view.
|
|
2059
2397
|
*/
|
|
2060
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
|
+
}
|
|
2061
2406
|
// Stop any in-flight run first — discarding the client mid-run would
|
|
2062
2407
|
// leave the old agent streaming into a cleared transcript.
|
|
2063
2408
|
this.#cancelRun();
|
|
@@ -2066,6 +2411,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2066
2411
|
this.#history.startThread();
|
|
2067
2412
|
this.#setRunning(false);
|
|
2068
2413
|
this.#setUnread(0);
|
|
2414
|
+
this.#dispatchThread(this.#history.threadId, "new");
|
|
2069
2415
|
// Whichever control started it: the header's, the history list's (which
|
|
2070
2416
|
// hands focus back to its opener as it closes, before this runs), or a
|
|
2071
2417
|
// host's own. A keyboard user otherwise had to find their way back to the
|
|
@@ -2340,6 +2686,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2340
2686
|
onFilesPicked: () => this.#attachments.onFilesPicked(),
|
|
2341
2687
|
});
|
|
2342
2688
|
|
|
2689
|
+
// Host content against the input, after everything of the turn being
|
|
2690
|
+
// written. A bare slot, whose own box is none, so an empty one takes no
|
|
2691
|
+
// space and the greeting layout carries a filled one with the composer.
|
|
2692
|
+
const aboveComposer = document.createElement("slot");
|
|
2693
|
+
aboveComposer.name = "above-composer";
|
|
2694
|
+
|
|
2343
2695
|
// A coarse footer slot below the composer.
|
|
2344
2696
|
const footer = document.createElement("slot");
|
|
2345
2697
|
footer.name = "footer";
|
|
@@ -2351,7 +2703,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
2351
2703
|
|
|
2352
2704
|
// Composer surfaces sit just above the input: the skills palette (opens on
|
|
2353
2705
|
// `/`), the chips, the hint saying what the composer still needs, the
|
|
2354
|
-
// queued messages and the pending-attachments tray
|
|
2706
|
+
// queued messages and the pending-attachments tray, then the host's own
|
|
2707
|
+
// content directly over the input.
|
|
2355
2708
|
this.#chat.replaceChildren(
|
|
2356
2709
|
header,
|
|
2357
2710
|
this.#messagesWrap,
|
|
@@ -2360,6 +2713,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2360
2713
|
this.#composerHint,
|
|
2361
2714
|
this.#queuedRow,
|
|
2362
2715
|
this.#attachSlot,
|
|
2716
|
+
aboveComposer,
|
|
2363
2717
|
inputRow,
|
|
2364
2718
|
footer,
|
|
2365
2719
|
this.#drawer.element,
|
|
@@ -2393,12 +2747,40 @@ export class AgUiChat extends HTMLElement {
|
|
|
2393
2747
|
launcher: this.#launcher,
|
|
2394
2748
|
badge: this.#badge,
|
|
2395
2749
|
railLabel: this.#railLabel,
|
|
2750
|
+
bar: this.#dockBar,
|
|
2751
|
+
barHistory: this.#barHistory,
|
|
2396
2752
|
titleText,
|
|
2397
2753
|
iconUrl: readLauncherIconUrl(this),
|
|
2398
2754
|
strings: this.#strings,
|
|
2399
2755
|
signal,
|
|
2400
|
-
|
|
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
|
+
},
|
|
2401
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
|
+
);
|
|
2402
2784
|
this.#placement.enableLauncherDrag(signal);
|
|
2403
2785
|
|
|
2404
2786
|
this.#placement.mountResizeGrips(this.#chat);
|
|
@@ -2406,7 +2788,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2406
2788
|
const probe = this.#placement.probe;
|
|
2407
2789
|
probe.className = "viewport-probe";
|
|
2408
2790
|
probe.setAttribute("aria-hidden", "true");
|
|
2409
|
-
this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#
|
|
2791
|
+
this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
|
|
2410
2792
|
}
|
|
2411
2793
|
|
|
2412
2794
|
/**
|
|
@@ -2423,9 +2805,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2423
2805
|
// layout problem — the badge is a circle, not a field.
|
|
2424
2806
|
this.#badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
2425
2807
|
this.#badge.hidden = unread === 0 || !isUnreadBadgeEnabled(this);
|
|
2426
|
-
const label =
|
|
2427
|
-
|
|
2428
|
-
|
|
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 });
|
|
2429
2814
|
this.#launcher.setAttribute("aria-label", label);
|
|
2430
2815
|
this.#launcher.title = label;
|
|
2431
2816
|
}
|
|
@@ -2594,6 +2979,22 @@ export class AgUiChat extends HTMLElement {
|
|
|
2594
2979
|
this.#history.stopContinuation();
|
|
2595
2980
|
}
|
|
2596
2981
|
|
|
2982
|
+
/**
|
|
2983
|
+
* Say the conversation on screen changed: {@link THREAD_EVENT}.
|
|
2984
|
+
*
|
|
2985
|
+
* The thread it replaced is the one last reported, rather than the one the
|
|
2986
|
+
* element last pointed at: a switch superseded before it replayed never drew
|
|
2987
|
+
* its thread, so naming it as the previous one left a host's own list
|
|
2988
|
+
* marking a thread that was never shown and still marking the one that was.
|
|
2989
|
+
*/
|
|
2990
|
+
#dispatchThread(threadId: string, reason: ThreadDetail["reason"]): void {
|
|
2991
|
+
const detail: ThreadDetail = { threadId, previousThreadId: this.#shownThread, reason };
|
|
2992
|
+
this.#shownThread = threadId;
|
|
2993
|
+
this.dispatchEvent(
|
|
2994
|
+
new CustomEvent<ThreadDetail>(THREAD_EVENT, { detail, bubbles: true, composed: true }),
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2597
2998
|
/**
|
|
2598
2999
|
* Swap the composer button between Send (idle) and Stop (running).
|
|
2599
3000
|
*
|
|
@@ -2615,6 +3016,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
2615
3016
|
this.#send.title = label;
|
|
2616
3017
|
this.#send.setAttribute("aria-label", label);
|
|
2617
3018
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
3019
|
+
// For a host's CSS, as `data-empty` is: the same state, on the element.
|
|
3020
|
+
this.toggleAttribute("data-running", running);
|
|
2618
3021
|
if (settled) {
|
|
2619
3022
|
this.#flushQueued();
|
|
2620
3023
|
}
|
|
@@ -3006,7 +3409,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
3006
3409
|
});
|
|
3007
3410
|
return new AgUiClient({
|
|
3008
3411
|
agent,
|
|
3009
|
-
handlers: this.#runHandlers.forClient(),
|
|
3412
|
+
handlers: this.#runHandlers.forClient(threadId),
|
|
3010
3413
|
getTools: () => this.#tools.advertise(),
|
|
3011
3414
|
getContext: () => this.#dispatch.buildContext(),
|
|
3012
3415
|
executeTool: (call) => this.#dispatch.execute(call),
|