@artooi/ag-ui-web-component 0.45.0 → 0.47.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 +146 -1
- package/README.md +191 -25
- package/dist/ag-ui-web-component.bundle.js +399 -157
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +14 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +47 -4
- 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/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 +7 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +14 -0
- package/dist/dom/ring_color.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +857 -86
- package/dist/index.js.map +4 -4
- package/dist/tools/tool_catalog.d.ts +5 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -1
- package/dist/tools/tool_dispatch.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +10 -1
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +6 -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 +12 -1
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_call_card.d.ts +8 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts +2 -0
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_group.d.ts +47 -0
- package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_styles.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/ledger_styles.d.ts.map +1 -1
- package/dist/ui/themes/soft_styles.d.ts.map +1 -1
- package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
- package/dist/ui/transcript/message_styles.d.ts.map +1 -1
- package/dist/ui/transcript/transcript.d.ts +16 -1
- package/dist/ui/transcript/transcript.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +18 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +15 -1
- package/src/core/ag_ui_chat.ts +122 -11
- package/src/core/chat_registry.ts +53 -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 +9 -5
- package/src/dom/ring_color.ts +85 -0
- package/src/index.ts +3 -0
- package/src/tools/tool_catalog.ts +23 -10
- package/src/tools/tool_dispatch.ts +26 -11
- package/src/ui/history/conversation_history.ts +28 -4
- package/src/ui/interrupts/confirmation_styles.ts +0 -2
- package/src/ui/placement/embedded_styles.ts +2 -2
- package/src/ui/placement/panel_placement.ts +34 -9
- 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 +5 -0
- package/src/ui/placement/surface_for.ts +12 -1
- package/src/ui/progress/thoughts_styles.ts +0 -2
- package/src/ui/progress/tool_call_card.ts +29 -8
- package/src/ui/progress/tool_group_styles.ts +232 -0
- package/src/ui/progress/tool_step_group.ts +272 -0
- package/src/ui/progress/tool_step_styles.ts +0 -2
- package/src/ui/shell/token_styles.ts +4 -2
- package/src/ui/styles.ts +5 -0
- package/src/ui/themes/ledger_styles.ts +15 -0
- package/src/ui/themes/soft_styles.ts +20 -0
- package/src/ui/transcript/groups_tool_steps.ts +16 -0
- package/src/ui/transcript/message_styles.ts +0 -2
- package/src/ui/transcript/transcript.ts +109 -3
- package/src/ui/ui_strings.ts +31 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
STATE_EVENT,
|
|
11
11
|
SUBMIT_EVENT,
|
|
12
12
|
SUGGESTIONS_ACTIVITY_TYPE,
|
|
13
|
+
THREAD_EVENT,
|
|
13
14
|
TOGGLE_EVENT,
|
|
14
15
|
TOOL_DISPLAY,
|
|
15
16
|
UNREAD_EVENT,
|
|
@@ -75,6 +76,7 @@ import type { ActivityRenderer } from "./activity_renderer.js";
|
|
|
75
76
|
import { AgUiClient } from "./agui_client.js";
|
|
76
77
|
// biome-ignore lint/style/useImportType: the emitted declaration file copies this form
|
|
77
78
|
import { type AttachmentRef } from "./attachment.js";
|
|
79
|
+
import { ChatRegistration } from "./chat_registry.js";
|
|
78
80
|
import type { ClientSeed } from "./client_seed.js";
|
|
79
81
|
import {
|
|
80
82
|
type ClientConversationStore,
|
|
@@ -85,6 +87,7 @@ import {
|
|
|
85
87
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
86
88
|
import type { StateDetail } from "./events/state_detail.js";
|
|
87
89
|
import type { SubmitDetail } from "./events/submit_detail.js";
|
|
90
|
+
import type { ThreadDetail } from "./events/thread_detail.js";
|
|
88
91
|
import type { ToggleDetail } from "./events/toggle_detail.js";
|
|
89
92
|
import type { UnreadDetail } from "./events/unread_detail.js";
|
|
90
93
|
import type { MessageRole } from "./message_role.js";
|
|
@@ -750,6 +753,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
750
753
|
* then a host's {@link sendMessage} waits for it rather than refusing.
|
|
751
754
|
*/
|
|
752
755
|
#restoreHold: { readonly hold: object; readonly restored: Promise<void> } | null = null;
|
|
756
|
+
/**
|
|
757
|
+
* The thread the latest {@link THREAD_EVENT} reported, which the next one
|
|
758
|
+
* names as the thread it replaced; `null` before the first.
|
|
759
|
+
*/
|
|
760
|
+
#shownThread: string | null = null;
|
|
753
761
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
754
762
|
readonly #decision = new PendingDecision();
|
|
755
763
|
/**
|
|
@@ -767,6 +775,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
767
775
|
askUser: () => this.askUser,
|
|
768
776
|
askUserRenderer: () => this.askUserRenderer,
|
|
769
777
|
decision: this.#decision,
|
|
778
|
+
setAsking: (callId, asking) => this.#transcript.setAsking(callId, asking),
|
|
770
779
|
ensureGroup: () => this.#transcript.ensureGroup(),
|
|
771
780
|
strings: () => this.#strings,
|
|
772
781
|
hidePending: () => this.#transcript.hidePending(),
|
|
@@ -780,6 +789,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
780
789
|
allowImages: () => this.allowImages,
|
|
781
790
|
follow: () => this.#transcript.follow(),
|
|
782
791
|
});
|
|
792
|
+
// This chat's place among the connected ones, which a free highlight ring reads.
|
|
793
|
+
readonly #chatRegistration = new ChatRegistration(this);
|
|
783
794
|
/**
|
|
784
795
|
* Which storage keys are this element's: the namespace it claims, the keys
|
|
785
796
|
* its layout preferences live under, and the built-in store scoped to them.
|
|
@@ -820,6 +831,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
820
831
|
resolveTool: (name) => this.#tools.resolve(name),
|
|
821
832
|
toolSummaries: () => this.toolSummaries,
|
|
822
833
|
serverSummary: (name) => this.#tools.summary(name),
|
|
834
|
+
toolDisplay: () => this.toolDisplay,
|
|
823
835
|
// A thunk over the live property, not the property itself: the card keeps
|
|
824
836
|
// this for the life of the call, and the result region is filled when the
|
|
825
837
|
// tool settles -- which can be long after a host set the hook.
|
|
@@ -1041,6 +1053,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1041
1053
|
cancelRun: () => this.#cancelRun(),
|
|
1042
1054
|
resetState: () => this.#resetState(),
|
|
1043
1055
|
setRunning: (running) => this.#setRunning(running),
|
|
1056
|
+
threadChanged: (threadId, reason) => this.#dispatchThread(threadId, reason),
|
|
1044
1057
|
});
|
|
1045
1058
|
}
|
|
1046
1059
|
|
|
@@ -1377,22 +1390,28 @@ export class AgUiChat extends HTMLElement {
|
|
|
1377
1390
|
|
|
1378
1391
|
/**
|
|
1379
1392
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
1380
|
-
* attribute (`minimal` / `inline` / `compact` / `full`).
|
|
1393
|
+
* attribute (`grouped` / `minimal` / `inline` / `compact` / `full`).
|
|
1394
|
+
* Defaults to `grouped`, for an absent attribute and for a value the
|
|
1395
|
+
* element does not know; `full` is the per-call card default before
|
|
1396
|
+
* `grouped` existed.
|
|
1381
1397
|
*
|
|
1382
|
-
*
|
|
1383
|
-
* restyles every card already in the
|
|
1384
|
-
*
|
|
1398
|
+
* The four card modes are applied by the shadow CSS from the attribute
|
|
1399
|
+
* itself, so changing between them restyles every card already in the
|
|
1400
|
+
* transcript. Whether calls fold into a step group is decided as each card
|
|
1401
|
+
* is built: switching to `grouped` folds only the calls drawn afterwards,
|
|
1402
|
+
* and switching away flattens the existing groups, again by CSS.
|
|
1385
1403
|
*/
|
|
1386
1404
|
get toolDisplay(): ToolDisplayMode {
|
|
1387
1405
|
const attr = this.getAttribute("data-tool-display");
|
|
1388
1406
|
if (
|
|
1389
1407
|
attr === TOOL_DISPLAY.INLINE ||
|
|
1390
1408
|
attr === TOOL_DISPLAY.MINIMAL ||
|
|
1391
|
-
attr === TOOL_DISPLAY.COMPACT
|
|
1409
|
+
attr === TOOL_DISPLAY.COMPACT ||
|
|
1410
|
+
attr === TOOL_DISPLAY.FULL
|
|
1392
1411
|
) {
|
|
1393
1412
|
return attr;
|
|
1394
1413
|
}
|
|
1395
|
-
return TOOL_DISPLAY.
|
|
1414
|
+
return TOOL_DISPLAY.GROUPED;
|
|
1396
1415
|
}
|
|
1397
1416
|
|
|
1398
1417
|
set toolDisplay(value: ToolDisplayMode) {
|
|
@@ -1440,6 +1459,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1440
1459
|
// this instance doesn't share collapsed/theme/thread state with another on
|
|
1441
1460
|
// the same origin.
|
|
1442
1461
|
this.#storage.claim();
|
|
1462
|
+
this.#chatRegistration.join();
|
|
1443
1463
|
// Restore a dragged size before the panel paints, so it does not snap from
|
|
1444
1464
|
// the placement default to the user's width on the first frame.
|
|
1445
1465
|
this.#placement.restoreSize();
|
|
@@ -1522,7 +1542,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1522
1542
|
// The catalog requests go out a microtask later, so a host configuring
|
|
1523
1543
|
// through a framework ref still has a chance to be heard — see #startup.
|
|
1524
1544
|
queueMicrotask(() => this.#startup());
|
|
1525
|
-
|
|
1545
|
+
// A key changed while the element was away handed it over all the same,
|
|
1546
|
+
// and the restore is the arriving principal's: reported as the live change
|
|
1547
|
+
// is. An adoption is not a handover, and restores the same conversation.
|
|
1548
|
+
void this.#history.rehydrate(changed && held !== "" ? "rescope" : "restore");
|
|
1526
1549
|
if (changed) {
|
|
1527
1550
|
// The last of what a live change does: a drawer left open across the
|
|
1528
1551
|
// move still lists the threads of the principal who left, titles and
|
|
@@ -1586,7 +1609,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1586
1609
|
await Promise.all([
|
|
1587
1610
|
this.#tools.fetchCatalog(),
|
|
1588
1611
|
this.#skills.fetch(),
|
|
1589
|
-
this.#history.rehydrate(),
|
|
1612
|
+
this.#history.rehydrate("restore"),
|
|
1590
1613
|
]);
|
|
1591
1614
|
}
|
|
1592
1615
|
|
|
@@ -1603,6 +1626,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1603
1626
|
window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
|
|
1604
1627
|
window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
|
|
1605
1628
|
this.#storage.release();
|
|
1629
|
+
this.#chatRegistration.leave();
|
|
1606
1630
|
this.#cancelRun();
|
|
1607
1631
|
this.#excerpts.detachPageOffer();
|
|
1608
1632
|
this.#attachments.tray?.dispose();
|
|
@@ -1786,6 +1810,28 @@ export class AgUiChat extends HTMLElement {
|
|
|
1786
1810
|
return this.#unread;
|
|
1787
1811
|
}
|
|
1788
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* The thread on screen; `null` before the element first connects.
|
|
1815
|
+
*
|
|
1816
|
+
* Only reads. `conversationStore.threadId()` mints and stores a thread when
|
|
1817
|
+
* the store points at none, so a host asking which conversation is showing
|
|
1818
|
+
* could start one; this never does. {@link THREAD_EVENT} says when it changes.
|
|
1819
|
+
*/
|
|
1820
|
+
get threadId(): string | null {
|
|
1821
|
+
const threadId = this.#history.threadId;
|
|
1822
|
+
return threadId === "" ? null : threadId;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Whether an interaction is in flight: the composer's own state, the one that
|
|
1827
|
+
* shows Stop. It spans every tool round, a retry and a checkpoint
|
|
1828
|
+
* continuation as well as a send, which {@link SUBMIT_EVENT} alone does not.
|
|
1829
|
+
* Reflected as `data-running` on the element, for a host's CSS.
|
|
1830
|
+
*/
|
|
1831
|
+
get running(): boolean {
|
|
1832
|
+
return this.#running;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1789
1835
|
/**
|
|
1790
1836
|
* Whether to mount collapsed.
|
|
1791
1837
|
*
|
|
@@ -1842,6 +1888,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1842
1888
|
return this.#placement.moveTo(corner, options);
|
|
1843
1889
|
}
|
|
1844
1890
|
|
|
1891
|
+
/**
|
|
1892
|
+
* Drop a size the user dragged, and its stored copy, so the placement's
|
|
1893
|
+
* default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
|
|
1894
|
+
*/
|
|
1895
|
+
resetSize(): void {
|
|
1896
|
+
this.#placement.resetSize();
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1845
1899
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
1846
1900
|
toggleCollapsed(): void {
|
|
1847
1901
|
this.setCollapsed(!this.collapsed);
|
|
@@ -1952,7 +2006,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1952
2006
|
this.#voice.dispose();
|
|
1953
2007
|
this.#voice.wire();
|
|
1954
2008
|
this.#history.adoptActiveThread();
|
|
1955
|
-
void this.#history.rehydrate();
|
|
2009
|
+
void this.#history.rehydrate("rescope");
|
|
1956
2010
|
void this.#history.refreshDrawer();
|
|
1957
2011
|
}
|
|
1958
2012
|
|
|
@@ -2023,6 +2077,36 @@ export class AgUiChat extends HTMLElement {
|
|
|
2023
2077
|
this.#checkpoints.close();
|
|
2024
2078
|
}
|
|
2025
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* Put a stored thread on screen, as picking it in the conversation list does,
|
|
2082
|
+
* for a host that lists conversations in its own chrome.
|
|
2083
|
+
*
|
|
2084
|
+
* 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;
|
|
2086
|
+
* a host that would rather wait reads {@link running} first. The thread
|
|
2087
|
+
* already on screen is left alone, and so is everything before the element
|
|
2088
|
+
* connects, since until then it has no store to read the thread from.
|
|
2089
|
+
*/
|
|
2090
|
+
async switchThread(threadId: string): Promise<void> {
|
|
2091
|
+
if (!this.#connected) {
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
await this.#history.switchThread(threadId);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Delete a stored thread, as the conversation list's delete does. Deleting the
|
|
2099
|
+
* thread on screen cancels its run and puts a fresh thread in its place, and
|
|
2100
|
+
* {@link THREAD_EVENT} reports it; deleting another leaves the screen alone.
|
|
2101
|
+
* Does nothing before the element connects.
|
|
2102
|
+
*/
|
|
2103
|
+
deleteThread(threadId: string): void {
|
|
2104
|
+
if (!this.#connected) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
this.#history.deleteThread(threadId);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2026
2110
|
/**
|
|
2027
2111
|
* Open the checkpoints panel, or close it if it is already open — what the
|
|
2028
2112
|
* built-in ⭯ button does, because a control that opens a panel is read as the
|
|
@@ -2058,6 +2142,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2058
2142
|
this.#history.startThread();
|
|
2059
2143
|
this.#setRunning(false);
|
|
2060
2144
|
this.#setUnread(0);
|
|
2145
|
+
this.#dispatchThread(this.#history.threadId, "new");
|
|
2061
2146
|
// Whichever control started it: the header's, the history list's (which
|
|
2062
2147
|
// hands focus back to its opener as it closes, before this runs), or a
|
|
2063
2148
|
// host's own. A keyboard user otherwise had to find their way back to the
|
|
@@ -2332,6 +2417,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2332
2417
|
onFilesPicked: () => this.#attachments.onFilesPicked(),
|
|
2333
2418
|
});
|
|
2334
2419
|
|
|
2420
|
+
// Host content against the input, after everything of the turn being
|
|
2421
|
+
// written. A bare slot, whose own box is none, so an empty one takes no
|
|
2422
|
+
// space and the greeting layout carries a filled one with the composer.
|
|
2423
|
+
const aboveComposer = document.createElement("slot");
|
|
2424
|
+
aboveComposer.name = "above-composer";
|
|
2425
|
+
|
|
2335
2426
|
// A coarse footer slot below the composer.
|
|
2336
2427
|
const footer = document.createElement("slot");
|
|
2337
2428
|
footer.name = "footer";
|
|
@@ -2343,7 +2434,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
2343
2434
|
|
|
2344
2435
|
// Composer surfaces sit just above the input: the skills palette (opens on
|
|
2345
2436
|
// `/`), the chips, the hint saying what the composer still needs, the
|
|
2346
|
-
// queued messages and the pending-attachments tray
|
|
2437
|
+
// queued messages and the pending-attachments tray, then the host's own
|
|
2438
|
+
// content directly over the input.
|
|
2347
2439
|
this.#chat.replaceChildren(
|
|
2348
2440
|
header,
|
|
2349
2441
|
this.#messagesWrap,
|
|
@@ -2352,6 +2444,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2352
2444
|
this.#composerHint,
|
|
2353
2445
|
this.#queuedRow,
|
|
2354
2446
|
this.#attachSlot,
|
|
2447
|
+
aboveComposer,
|
|
2355
2448
|
inputRow,
|
|
2356
2449
|
footer,
|
|
2357
2450
|
this.#drawer.element,
|
|
@@ -2586,6 +2679,22 @@ export class AgUiChat extends HTMLElement {
|
|
|
2586
2679
|
this.#history.stopContinuation();
|
|
2587
2680
|
}
|
|
2588
2681
|
|
|
2682
|
+
/**
|
|
2683
|
+
* Say the conversation on screen changed: {@link THREAD_EVENT}.
|
|
2684
|
+
*
|
|
2685
|
+
* The thread it replaced is the one last reported, rather than the one the
|
|
2686
|
+
* element last pointed at: a switch superseded before it replayed never drew
|
|
2687
|
+
* its thread, so naming it as the previous one left a host's own list
|
|
2688
|
+
* marking a thread that was never shown and still marking the one that was.
|
|
2689
|
+
*/
|
|
2690
|
+
#dispatchThread(threadId: string, reason: ThreadDetail["reason"]): void {
|
|
2691
|
+
const detail: ThreadDetail = { threadId, previousThreadId: this.#shownThread, reason };
|
|
2692
|
+
this.#shownThread = threadId;
|
|
2693
|
+
this.dispatchEvent(
|
|
2694
|
+
new CustomEvent<ThreadDetail>(THREAD_EVENT, { detail, bubbles: true, composed: true }),
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2589
2698
|
/**
|
|
2590
2699
|
* Swap the composer button between Send (idle) and Stop (running).
|
|
2591
2700
|
*
|
|
@@ -2607,6 +2716,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
2607
2716
|
this.#send.title = label;
|
|
2608
2717
|
this.#send.setAttribute("aria-label", label);
|
|
2609
2718
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
2719
|
+
// For a host's CSS, as `data-empty` is: the same state, on the element.
|
|
2720
|
+
this.toggleAttribute("data-running", running);
|
|
2610
2721
|
if (settled) {
|
|
2611
2722
|
this.#flushQueued();
|
|
2612
2723
|
}
|
|
@@ -2998,7 +3109,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2998
3109
|
});
|
|
2999
3110
|
return new AgUiClient({
|
|
3000
3111
|
agent,
|
|
3001
|
-
handlers: this.#runHandlers.forClient(),
|
|
3112
|
+
handlers: this.#runHandlers.forClient(threadId),
|
|
3002
3113
|
getTools: () => this.#tools.advertise(),
|
|
3003
3114
|
getContext: () => this.#dispatch.buildContext(),
|
|
3004
3115
|
executeTool: (call) => this.#dispatch.execute(call),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<ag-ui-chat>` elements connected to this document, oldest first.
|
|
3
|
+
*
|
|
4
|
+
* The highlight ring is drawn in the host's page, outside every chat, so a
|
|
5
|
+
* token set on a chat never reaches it through the cascade. A free `flash()`
|
|
6
|
+
* still has to answer "which widget is this ring speaking for", and that is a
|
|
7
|
+
* question about the live elements rather than about any one of them.
|
|
8
|
+
*
|
|
9
|
+
* Module-level on purpose, and the second piece of shared mutable state the
|
|
10
|
+
* repository's `CLAUDE.md` records, beside the claimed storage namespaces. See
|
|
11
|
+
* the conditions it states, and how this one meets them, before adding a
|
|
12
|
+
* third.
|
|
13
|
+
*/
|
|
14
|
+
const CONNECTED_CHATS = new Set<HTMLElement>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One chat's place in the document's list of connected chats.
|
|
18
|
+
*
|
|
19
|
+
* Owned one-to-one by an `<ag-ui-chat>`, which joins on connect and leaves on
|
|
20
|
+
* disconnect, so the list holds exactly the elements that are in a document
|
|
21
|
+
* now. Typed as `HTMLElement` rather than the element's class, because the DOM
|
|
22
|
+
* driver reads it and must not import the element.
|
|
23
|
+
*/
|
|
24
|
+
export class ChatRegistration {
|
|
25
|
+
readonly #chat: HTMLElement;
|
|
26
|
+
|
|
27
|
+
constructor(chat: HTMLElement) {
|
|
28
|
+
this.#chat = chat;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Join, as the most recently connected chat. An element moved in the DOM
|
|
33
|
+
* left on disconnect before it reconnected, so it rejoins at the end and no
|
|
34
|
+
* entry needs removing first.
|
|
35
|
+
*/
|
|
36
|
+
join(): void {
|
|
37
|
+
CONNECTED_CHATS.add(this.#chat);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Leave. The previous chat, if one is still connected, becomes the latest. */
|
|
41
|
+
leave(): void {
|
|
42
|
+
CONNECTED_CHATS.delete(this.#chat);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The most recently connected chat still in the document, or `null`. */
|
|
46
|
+
static latest(): HTMLElement | null {
|
|
47
|
+
let latest: HTMLElement | null = null;
|
|
48
|
+
for (const chat of CONNECTED_CHATS) {
|
|
49
|
+
latest = chat;
|
|
50
|
+
}
|
|
51
|
+
return latest;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -2,6 +2,12 @@ import type { ToolRun } from "./tool_run.js";
|
|
|
2
2
|
|
|
3
3
|
/** `detail` shape of the {@link RUN_FINISHED_EVENT} CustomEvent. */
|
|
4
4
|
export interface RunFinishedDetail {
|
|
5
|
+
/**
|
|
6
|
+
* The thread the run belonged to, fixed when its client was built. Not always
|
|
7
|
+
* the thread on screen: a run stopped by a new chat or a thread switch still
|
|
8
|
+
* reports once its request closes, and by then another thread is.
|
|
9
|
+
*/
|
|
10
|
+
readonly threadId: string;
|
|
5
11
|
/** In settle order. Empty when the interaction called no tools. */
|
|
6
12
|
readonly tools: readonly ToolRun[];
|
|
7
13
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** `detail` shape of the {@link THREAD_EVENT} CustomEvent. */
|
|
2
|
+
export interface ThreadDetail {
|
|
3
|
+
/** The thread now on screen. */
|
|
4
|
+
readonly threadId: string;
|
|
5
|
+
/**
|
|
6
|
+
* The thread on screen before the change: the one the previous event
|
|
7
|
+
* reported, and `null` before the first, as at the restore when the element
|
|
8
|
+
* first connects. Equal to `threadId` when the same thread was restored
|
|
9
|
+
* again (`reload()`, the element connecting again).
|
|
10
|
+
*/
|
|
11
|
+
readonly previousThreadId: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* What changed it:
|
|
14
|
+
*
|
|
15
|
+
* - `new`: `newChat()` or the built-in New chat button.
|
|
16
|
+
* - `switch`: `switchThread()` or a row picked in the conversation list.
|
|
17
|
+
* - `delete`: the active thread was deleted, and a fresh one took its place.
|
|
18
|
+
* - `restore`: the stored thread was replayed, on connect or by `reload()`.
|
|
19
|
+
* - `rescope`: a `user-key` change handed the element to another principal,
|
|
20
|
+
* live or while it was out of the document.
|
|
21
|
+
*
|
|
22
|
+
* A closed set today; a host should ignore a reason it does not know, so a
|
|
23
|
+
* later one can be added without breaking it.
|
|
24
|
+
*/
|
|
25
|
+
readonly reason: "new" | "switch" | "delete" | "restore" | "rescope";
|
|
26
|
+
}
|
package/src/core/run_handlers.ts
CHANGED
|
@@ -176,8 +176,13 @@ export class RunHandlers {
|
|
|
176
176
|
* A handler table for one AG-UI client. Every table built for the current
|
|
177
177
|
* conversation draws into the same transcript and shares its bookkeeping, so
|
|
178
178
|
* a continuation run reports into the interaction the user is looking at.
|
|
179
|
+
*
|
|
180
|
+
* `threadId` is the thread the client was built for, named on the
|
|
181
|
+
* run-finished detail. Taken here rather than read when the run settles,
|
|
182
|
+
* because a stopped run settles after the reset that stopped it, when the
|
|
183
|
+
* thread on screen is already the next one.
|
|
179
184
|
*/
|
|
180
|
-
forClient(): AgUiClientHandlers {
|
|
185
|
+
forClient(threadId: string): AgUiClientHandlers {
|
|
181
186
|
const ledger = this.#ledger;
|
|
182
187
|
const attached = (): boolean => ledger === this.#ledger;
|
|
183
188
|
const { onSettled, ...drawing } = this.#tableFor(ledger);
|
|
@@ -190,7 +195,7 @@ export class RunHandlers {
|
|
|
190
195
|
// Last, so a listener that refetches sees a transcript that has already
|
|
191
196
|
// stopped changing -- and outside the check, because what a run did is
|
|
192
197
|
// true whichever conversation is on screen when it ends.
|
|
193
|
-
this.#dispatchRunFinished(ledger);
|
|
198
|
+
this.#dispatchRunFinished(ledger, threadId);
|
|
194
199
|
},
|
|
195
200
|
};
|
|
196
201
|
}
|
|
@@ -444,7 +449,7 @@ export class RunHandlers {
|
|
|
444
449
|
* server is a fact about the run, and a name can appear on both sides across a
|
|
445
450
|
* conversation.
|
|
446
451
|
*/
|
|
447
|
-
#dispatchRunFinished(ledger: RunLedger): void {
|
|
452
|
+
#dispatchRunFinished(ledger: RunLedger, threadId: string): void {
|
|
448
453
|
const tools: ToolRun[] = ledger.tools.map(({ id, name }) => ({
|
|
449
454
|
name,
|
|
450
455
|
side: ledger.serverSettled.has(id) ? "server" : "client",
|
|
@@ -455,7 +460,7 @@ export class RunHandlers {
|
|
|
455
460
|
ledger.invalidated.clear();
|
|
456
461
|
this.#host.element.dispatchEvent(
|
|
457
462
|
new CustomEvent<RunFinishedDetail>(RUN_FINISHED_EVENT, {
|
|
458
|
-
detail: { tools, invalidated },
|
|
463
|
+
detail: { threadId, tools, invalidated },
|
|
459
464
|
bubbles: true,
|
|
460
465
|
composed: true,
|
|
461
466
|
}),
|
package/src/dom/animations.ts
CHANGED
|
@@ -8,17 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { showHighlightOverlay } from "./highlight_overlay.js";
|
|
10
10
|
import { setNativeChecked, setNativeValue } from "./native_setter.js";
|
|
11
|
-
|
|
12
|
-
/** Ring colour when the host page has not themed `--ag-ui-accent`. */
|
|
13
|
-
const ACCENT = "#4f46e5";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Fallback for the box-shadow rings, which read as a soft halo rather than a
|
|
17
|
-
* line and so carry their own alpha.
|
|
18
|
-
*/
|
|
19
|
-
const ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
|
|
20
|
-
|
|
21
|
-
const ACCENT_PROPERTY = "--ag-ui-accent";
|
|
11
|
+
import { ringColor } from "./ring_color.js";
|
|
22
12
|
|
|
23
13
|
function delay(ms: number): Promise<void> {
|
|
24
14
|
return new Promise<void>((resolve) => {
|
|
@@ -48,19 +38,23 @@ function motionDelay(ms: number): Promise<void> {
|
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
/**
|
|
51
|
-
*
|
|
41
|
+
* Which `<ag-ui-chat>` a ring speaks for, so it is drawn in that chat's colour.
|
|
52
42
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
43
|
+
* The ring is drawn in the host's page, which a token set on the chat never
|
|
44
|
+
* reaches, so the chat is read directly. Unset, it is the most recently
|
|
45
|
+
* connected chat; a page with two widgets names the one acting.
|
|
56
46
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
export interface RingOwnerOptions {
|
|
48
|
+
/** The chat whose ring colour and accent to use. Default: the latest connected. */
|
|
49
|
+
chat?: HTMLElement;
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The soft halo the press and toggle rings draw: the ring colour at 40%.
|
|
54
|
+
* Mixed rather than given its own alpha, so it follows whichever colour won.
|
|
55
|
+
*/
|
|
56
|
+
function ringShadow(el: HTMLElement, chat: HTMLElement | undefined): string {
|
|
57
|
+
return `0 0 0 3px color-mix(in srgb, ${ringColor(el, chat)} 40%, transparent)`;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
/** An element whose `value` can be typed into. */
|
|
@@ -93,7 +87,7 @@ export async function typeInto(
|
|
|
93
87
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
94
88
|
}
|
|
95
89
|
|
|
96
|
-
export interface HighlightClickOptions {
|
|
90
|
+
export interface HighlightClickOptions extends RingOwnerOptions {
|
|
97
91
|
/** Milliseconds to hold the highlight before clicking. Default 280. */
|
|
98
92
|
highlightMs?: number;
|
|
99
93
|
}
|
|
@@ -106,7 +100,7 @@ export async function highlightThenClick(
|
|
|
106
100
|
const highlightMs = options.highlightMs ?? 280;
|
|
107
101
|
const previousOutline = el.style.outline;
|
|
108
102
|
const previousOffset = el.style.outlineOffset;
|
|
109
|
-
el.style.outline = `2px solid ${
|
|
103
|
+
el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
|
|
110
104
|
el.style.outlineOffset = "2px";
|
|
111
105
|
await delay(highlightMs);
|
|
112
106
|
el.style.outline = previousOutline;
|
|
@@ -176,10 +170,14 @@ const FLASH_MS = 1200;
|
|
|
176
170
|
/** Share of {@link FLASH_MS} spent fading out rather than holding. */
|
|
177
171
|
const FLASH_FADE_RATIO = 1 / 3;
|
|
178
172
|
|
|
179
|
-
export interface FlashOptions {
|
|
173
|
+
export interface FlashOptions extends RingOwnerOptions {
|
|
180
174
|
/** Total milliseconds the ring is on screen, hold plus fade. Default 1200. */
|
|
181
175
|
flashMs?: number;
|
|
182
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Ring colour. Defaults to `--ag-ui-highlight-ring-color` (on the target,
|
|
178
|
+
* then on the chat), then the target's `--ag-ui-accent`, then the chat's
|
|
179
|
+
* accent.
|
|
180
|
+
*/
|
|
183
181
|
color?: string;
|
|
184
182
|
/**
|
|
185
183
|
* Dim everything except the target.
|
|
@@ -226,6 +224,7 @@ async function flashRing(
|
|
|
226
224
|
gradient: options.gradient === true,
|
|
227
225
|
...(options.color === undefined ? {} : { color: options.color }),
|
|
228
226
|
...(options.ringPadding === undefined ? {} : { padding: options.ringPadding }),
|
|
227
|
+
...(options.chat === undefined ? {} : { chat: options.chat }),
|
|
229
228
|
});
|
|
230
229
|
await delay(flashMs);
|
|
231
230
|
dismiss();
|
|
@@ -234,7 +233,7 @@ async function flashRing(
|
|
|
234
233
|
const previousOutline = el.style.outline;
|
|
235
234
|
const previousOffset = el.style.outlineOffset;
|
|
236
235
|
const previousTransition = el.style.transition;
|
|
237
|
-
const color = options.color ??
|
|
236
|
+
const color = options.color ?? ringColor(el, options.chat);
|
|
238
237
|
// outline, not box-shadow: a shadow paints outside the border box, so any
|
|
239
238
|
// overflow:hidden ancestor sharing the target's box (a card, a table cell)
|
|
240
239
|
// clips the whole ring away while the helper still reports success.
|
|
@@ -273,7 +272,7 @@ export function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Pro
|
|
|
273
272
|
return flashRing(el, options, true);
|
|
274
273
|
}
|
|
275
274
|
|
|
276
|
-
export interface PressOptions {
|
|
275
|
+
export interface PressOptions extends RingOwnerOptions {
|
|
277
276
|
/** Milliseconds to hold the pressed state before clicking. Default 140. */
|
|
278
277
|
pressMs?: number;
|
|
279
278
|
}
|
|
@@ -290,7 +289,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
|
|
|
290
289
|
const previousShadow = el.style.boxShadow;
|
|
291
290
|
el.style.transition = "transform 80ms ease";
|
|
292
291
|
el.style.transform = "scale(0.96)";
|
|
293
|
-
el.style.boxShadow = ringShadow(el);
|
|
292
|
+
el.style.boxShadow = ringShadow(el, options.chat);
|
|
294
293
|
await motionDelay(pressMs);
|
|
295
294
|
el.style.transform = previousTransform;
|
|
296
295
|
el.style.transition = previousTransition;
|
|
@@ -298,7 +297,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
|
|
|
298
297
|
el.click();
|
|
299
298
|
}
|
|
300
299
|
|
|
301
|
-
export interface SelectOptions {
|
|
300
|
+
export interface SelectOptions extends RingOwnerOptions {
|
|
302
301
|
/** Milliseconds to hold the highlight before committing. Default 220. */
|
|
303
302
|
highlightMs?: number;
|
|
304
303
|
}
|
|
@@ -330,7 +329,7 @@ export async function selectOption(
|
|
|
330
329
|
const highlightMs = options.highlightMs ?? 220;
|
|
331
330
|
const previousOutline = el.style.outline;
|
|
332
331
|
const previousOffset = el.style.outlineOffset;
|
|
333
|
-
el.style.outline = `2px solid ${
|
|
332
|
+
el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
|
|
334
333
|
el.style.outlineOffset = "2px";
|
|
335
334
|
await motionDelay(highlightMs);
|
|
336
335
|
setNativeValue(el, option.value);
|
|
@@ -340,7 +339,7 @@ export async function selectOption(
|
|
|
340
339
|
el.style.outlineOffset = previousOffset;
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
export interface ToggleOptions {
|
|
342
|
+
export interface ToggleOptions extends RingOwnerOptions {
|
|
344
343
|
/** Milliseconds to hold the flash. Default 200. */
|
|
345
344
|
flashMs?: number;
|
|
346
345
|
}
|
|
@@ -356,7 +355,7 @@ export async function toggleControl(
|
|
|
356
355
|
): Promise<void> {
|
|
357
356
|
const flashMs = options.flashMs ?? 200;
|
|
358
357
|
const previousShadow = el.style.boxShadow;
|
|
359
|
-
el.style.boxShadow = ringShadow(el);
|
|
358
|
+
el.style.boxShadow = ringShadow(el, options.chat);
|
|
360
359
|
await motionDelay(flashMs);
|
|
361
360
|
setNativeChecked(el, checked);
|
|
362
361
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { HIGHLIGHT_OVERLAY_Z_INDEX } from "../constants.js";
|
|
2
|
-
|
|
3
|
-
/** The package accent, used when the host has themed nothing. */
|
|
4
|
-
const ACCENT = "#4f46e5";
|
|
2
|
+
import { ringColor } from "./ring_color.js";
|
|
5
3
|
|
|
6
4
|
/** How the overlay is drawn around the element being pointed at. */
|
|
7
5
|
export interface HighlightOverlayOptions {
|
|
@@ -9,8 +7,14 @@ export interface HighlightOverlayOptions {
|
|
|
9
7
|
readonly scrim?: boolean;
|
|
10
8
|
/** Draw the ring as a moving gradient rather than a flat colour. Default false. */
|
|
11
9
|
readonly gradient?: boolean;
|
|
12
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Ring colour, and the flat fallback for the gradient. Default:
|
|
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.
|
|
14
|
+
*/
|
|
13
15
|
readonly color?: string;
|
|
16
|
+
/** The chat whose ring colour and accent to use. Default: the latest connected. */
|
|
17
|
+
readonly chat?: HTMLElement;
|
|
14
18
|
/** Gap between the target's box and the ring. Default 4. */
|
|
15
19
|
readonly padding?: number;
|
|
16
20
|
/** Corner radius of the cut-out and the ring. Default: the target's own. */
|
|
@@ -205,7 +209,7 @@ function safeColor(value: string | undefined): string | null {
|
|
|
205
209
|
* the padding box, leaving the frame.
|
|
206
210
|
*/
|
|
207
211
|
function ringPaint(target: HTMLElement, options: HighlightOverlayOptions): string {
|
|
208
|
-
const color = safeColor(options.color) ??
|
|
212
|
+
const color = safeColor(options.color) ?? ringColor(target, options.chat);
|
|
209
213
|
if (options.gradient !== true) {
|
|
210
214
|
return `border-color: ${color}`;
|
|
211
215
|
}
|