@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
|
@@ -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,15 @@ 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, held to 3:1 against the
|
|
14
|
+
* surface behind the target.
|
|
15
|
+
*/
|
|
13
16
|
readonly color?: string;
|
|
17
|
+
/** The chat whose ring colour and accent to use. Default: the latest connected. */
|
|
18
|
+
readonly chat?: HTMLElement;
|
|
14
19
|
/** Gap between the target's box and the ring. Default 4. */
|
|
15
20
|
readonly padding?: number;
|
|
16
21
|
/** Corner radius of the cut-out and the ring. Default: the target's own. */
|
|
@@ -205,7 +210,7 @@ function safeColor(value: string | undefined): string | null {
|
|
|
205
210
|
* the padding box, leaving the frame.
|
|
206
211
|
*/
|
|
207
212
|
function ringPaint(target: HTMLElement, options: HighlightOverlayOptions): string {
|
|
208
|
-
const color = safeColor(options.color) ??
|
|
213
|
+
const color = safeColor(options.color) ?? ringColor(target, options.chat);
|
|
209
214
|
if (options.gradient !== true) {
|
|
210
215
|
return `border-color: ${color}`;
|
|
211
216
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { ChatRegistration } from "../core/chat_registry.js";
|
|
2
|
+
|
|
3
|
+
/** The public ring token, read from the target and then from the chat. */
|
|
4
|
+
const RING_TOKEN = "--ag-ui-highlight-ring-color";
|
|
5
|
+
|
|
6
|
+
/** The accent a host themes its page with, read from the target. */
|
|
7
|
+
const ACCENT_TOKEN = "--ag-ui-accent";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The accent the chat actually resolved, its theme's default included. Read
|
|
11
|
+
* through the private alias because the public token is unset on a page that
|
|
12
|
+
* themed nothing, which is exactly the page this exists for.
|
|
13
|
+
*/
|
|
14
|
+
const CHAT_ACCENT = "--_accent";
|
|
15
|
+
|
|
16
|
+
/** The package indigo, for a page with no chat and no theming at all. */
|
|
17
|
+
const PACKAGE_ACCENT = "#4f46e5";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The colour a highlight ring is drawn in when the caller passed none.
|
|
21
|
+
*
|
|
22
|
+
* In order: `--ag-ui-highlight-ring-color` on the target, the same token on
|
|
23
|
+
* the chat, the target's `--ag-ui-accent`, the chat's resolved accent, the
|
|
24
|
+
* package indigo. The target's accent stays above the chat's own default so a
|
|
25
|
+
* host that themes `--ag-ui-accent` on its page keeps the rings it has; the
|
|
26
|
+
* ring token sits above both so a host can keep the ring apart from the accent.
|
|
27
|
+
*
|
|
28
|
+
* Only the chat's own accent is adjusted, because it follows the widget's
|
|
29
|
+
* scheme rather than the page's: a dark widget's near-white accent on a light
|
|
30
|
+
* page all but disappears. It is held to 3:1 against the surface behind the
|
|
31
|
+
* target (see {@link surfaceBehind}): the accent as resolved if it holds, else
|
|
32
|
+
* the other half of the chat's light-dark pair, else the accent stepped toward
|
|
33
|
+
* black or white. Every value above it is a choice someone made, and is drawn
|
|
34
|
+
* as given.
|
|
35
|
+
*
|
|
36
|
+
* `chat` defaults to the most recently connected `<ag-ui-chat>`, the common
|
|
37
|
+
* page with one widget. A page with two passes the one the ring speaks for.
|
|
38
|
+
*/
|
|
39
|
+
export function ringColor(target: HTMLElement, chat?: HTMLElement): string {
|
|
40
|
+
const onTarget = readToken(target, RING_TOKEN);
|
|
41
|
+
if (onTarget !== "") {
|
|
42
|
+
return onTarget;
|
|
43
|
+
}
|
|
44
|
+
const owner = chat ?? ChatRegistration.latest();
|
|
45
|
+
const onChat = owner === null ? "" : resolveOn(owner, RING_TOKEN);
|
|
46
|
+
if (onChat !== "") {
|
|
47
|
+
return onChat;
|
|
48
|
+
}
|
|
49
|
+
const accent = readToken(target, ACCENT_TOKEN);
|
|
50
|
+
if (accent !== "") {
|
|
51
|
+
return accent;
|
|
52
|
+
}
|
|
53
|
+
if (owner === null) {
|
|
54
|
+
return PACKAGE_ACCENT;
|
|
55
|
+
}
|
|
56
|
+
const chatAccent = resolveOn(owner, CHAT_ACCENT);
|
|
57
|
+
return chatAccent === "" ? PACKAGE_ACCENT : legible(owner, chatAccent, target);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The contrast a ring needs against what it is drawn on (WCAG 2.4.13). */
|
|
61
|
+
const RING_CONTRAST = 3;
|
|
62
|
+
|
|
63
|
+
/** How far each step moves a failing accent toward black or white. */
|
|
64
|
+
const STEP = 0.1;
|
|
65
|
+
|
|
66
|
+
type Rgba = readonly [number, number, number, number];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `accent`, or the nearest variant of it that holds {@link RING_CONTRAST}
|
|
70
|
+
* against the surface behind `target`.
|
|
71
|
+
*
|
|
72
|
+
* Colours are compared as sRGB bytes painted onto a one-pixel canvas, the one
|
|
73
|
+
* reading that turns a resolved color-mix() or oklch() into numbers. Where
|
|
74
|
+
* there is no 2d context to paint on, nothing can be measured, and the accent
|
|
75
|
+
* is drawn as it was.
|
|
76
|
+
*/
|
|
77
|
+
function legible(chat: HTMLElement, accent: string, target: HTMLElement): string {
|
|
78
|
+
const paint = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
|
79
|
+
if (paint === null) {
|
|
80
|
+
return accent;
|
|
81
|
+
}
|
|
82
|
+
const bytes = (color: string): Rgba => {
|
|
83
|
+
paint.clearRect(0, 0, 1, 1);
|
|
84
|
+
paint.fillStyle = "rgb(0 0 0 / 0)";
|
|
85
|
+
paint.fillStyle = color;
|
|
86
|
+
paint.fillRect(0, 0, 1, 1);
|
|
87
|
+
const [r = 0, g = 0, b = 0, a = 0] = paint.getImageData(0, 0, 1, 1).data;
|
|
88
|
+
return [r, g, b, a / 255];
|
|
89
|
+
};
|
|
90
|
+
const surface = surfaceBehind(target, bytes);
|
|
91
|
+
const holds = (color: Rgba): boolean => contrast(over(color, surface), surface) >= RING_CONTRAST;
|
|
92
|
+
const own = [accent, resolveOn(chat, CHAT_ACCENT, "light"), resolveOn(chat, CHAT_ACCENT, "dark")];
|
|
93
|
+
const fit = own.find((candidate) => holds(bytes(candidate)));
|
|
94
|
+
if (fit !== undefined) {
|
|
95
|
+
return fit;
|
|
96
|
+
}
|
|
97
|
+
// Black or white, whichever the surface sets off better, holds at least
|
|
98
|
+
// 4.5:1 against any surface, so the walk always ends by the last step.
|
|
99
|
+
const from = over(bytes(accent), surface);
|
|
100
|
+
const black: Rgba = [0, 0, 0, 1];
|
|
101
|
+
const white: Rgba = [255, 255, 255, 1];
|
|
102
|
+
const toward = contrast(black, surface) > contrast(white, surface) ? black : white;
|
|
103
|
+
let step = 1;
|
|
104
|
+
while (!holds(mix(from, toward, step * STEP))) {
|
|
105
|
+
step += 1;
|
|
106
|
+
}
|
|
107
|
+
const [r, g, b] = mix(from, toward, step * STEP);
|
|
108
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The colour of the surface a ring around `target` is drawn on, as opaque
|
|
113
|
+
* sRGB bytes.
|
|
114
|
+
*
|
|
115
|
+
* The ring sits outside the target's border box (the flash outline is offset
|
|
116
|
+
* from it, the overlay leaves a gap), so the target's own fill is not under
|
|
117
|
+
* it: a white field on a dark page is ringed on the dark page. The surface is
|
|
118
|
+
* every background from the target's parent up, composited until one is
|
|
119
|
+
* opaque, over the page canvas, which follows the root's colour scheme. A
|
|
120
|
+
* background image is not read; its colour beneath is.
|
|
121
|
+
*/
|
|
122
|
+
function surfaceBehind(target: HTMLElement, bytes: (color: string) => Rgba): Rgba {
|
|
123
|
+
const layers: Rgba[] = [];
|
|
124
|
+
for (let el = parentOf(target); el !== null; el = parentOf(el)) {
|
|
125
|
+
const layer = bytes(window.getComputedStyle(el).backgroundColor);
|
|
126
|
+
layers.push(layer);
|
|
127
|
+
if (layer[3] === 1) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const probe = document.createElement("span");
|
|
132
|
+
probe.hidden = true;
|
|
133
|
+
probe.style.color = "Canvas";
|
|
134
|
+
document.documentElement.appendChild(probe);
|
|
135
|
+
let surface = bytes(window.getComputedStyle(probe).color);
|
|
136
|
+
probe.remove();
|
|
137
|
+
for (const layer of layers.reverse()) {
|
|
138
|
+
surface = over(layer, surface);
|
|
139
|
+
}
|
|
140
|
+
return surface;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The element above `el`, crossing out of a shadow root to its host. */
|
|
144
|
+
function parentOf(el: Element): Element | null {
|
|
145
|
+
if (el.parentElement !== null) {
|
|
146
|
+
return el.parentElement;
|
|
147
|
+
}
|
|
148
|
+
const root = el.getRootNode();
|
|
149
|
+
return root instanceof ShadowRoot ? root.host : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `top` painted over the opaque `under`. */
|
|
153
|
+
function over(top: Rgba, under: Rgba): Rgba {
|
|
154
|
+
return mix(under, top, top[3]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `from` moved `share` of the way to `to`, as opaque bytes. */
|
|
158
|
+
function mix(from: Rgba, to: Rgba, share: number): Rgba {
|
|
159
|
+
const channel = (index: 0 | 1 | 2): number =>
|
|
160
|
+
Math.round(from[index] + (to[index] - from[index]) * share);
|
|
161
|
+
return [channel(0), channel(1), channel(2), 1];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** WCAG relative luminance of opaque sRGB bytes. */
|
|
165
|
+
function luminance([r, g, b]: Rgba): number {
|
|
166
|
+
const linear = (channel: number): number => {
|
|
167
|
+
const c = channel / 255;
|
|
168
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
169
|
+
};
|
|
170
|
+
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** WCAG contrast ratio between two opaque colours. */
|
|
174
|
+
function contrast(a: Rgba, b: Rgba): number {
|
|
175
|
+
const [la, lb] = [luminance(a), luminance(b)];
|
|
176
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readToken(el: HTMLElement, name: string): string {
|
|
180
|
+
return window.getComputedStyle(el).getPropertyValue(name).trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The custom property `name` on `chat`, as the colour it computes to there, or
|
|
185
|
+
* `""` when it is unset or not a colour.
|
|
186
|
+
*
|
|
187
|
+
* Resolved inside the chat rather than handed to the target, because the
|
|
188
|
+
* chat's colours are `light-dark()` pairs and a custom property keeps them
|
|
189
|
+
* unresolved: written onto an element in the page, the pair would pick its
|
|
190
|
+
* half by the page's colour scheme, so a dark widget on a light page would ring
|
|
191
|
+
* in its light-mode accent. A probe inside the shadow tree inherits the chat's
|
|
192
|
+
* scheme, and its computed `color` is the resolved value.
|
|
193
|
+
*
|
|
194
|
+
* An element with no open shadow root (a host passing something other than a
|
|
195
|
+
* chat) is read as it is, unresolved; that is the best it offers. `scheme`
|
|
196
|
+
* forces one half of the pair, which is how a ring finds the chat's other
|
|
197
|
+
* accent when its own scheme's is too faint against the page.
|
|
198
|
+
*/
|
|
199
|
+
function resolveOn(chat: HTMLElement, name: string, scheme?: "light" | "dark"): string {
|
|
200
|
+
const value = readToken(chat, name);
|
|
201
|
+
const root = chat.shadowRoot;
|
|
202
|
+
if (value === "" || root === null) {
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
const probe = document.createElement("span");
|
|
206
|
+
probe.hidden = true;
|
|
207
|
+
// A scheme named here picks that half of a light-dark() pair instead of
|
|
208
|
+
// the one the chat's own scheme picks.
|
|
209
|
+
if (scheme !== undefined) {
|
|
210
|
+
probe.style.colorScheme = scheme;
|
|
211
|
+
}
|
|
212
|
+
probe.style.color = value;
|
|
213
|
+
// An unparseable value leaves the declaration empty, and the probe would
|
|
214
|
+
// then report the colour it inherits instead of saying it has none.
|
|
215
|
+
if (probe.style.color === "") {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
root.appendChild(probe);
|
|
219
|
+
const resolved = window.getComputedStyle(probe).color;
|
|
220
|
+
probe.remove();
|
|
221
|
+
return resolved;
|
|
222
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
4
|
ATTACHMENT_EVENT,
|
|
5
|
+
BAR_HEIGHT_EVENT,
|
|
5
6
|
CHART_ACTIVITY_TYPE,
|
|
6
7
|
COMPACTION_ACTIVITY_TYPE,
|
|
7
8
|
CUSTOM_AGENT_EVENT,
|
|
@@ -17,10 +18,13 @@ export {
|
|
|
17
18
|
STATE_EVENT,
|
|
18
19
|
SUBMIT_EVENT,
|
|
19
20
|
SUGGESTIONS_ACTIVITY_TYPE,
|
|
21
|
+
THREAD_EVENT,
|
|
20
22
|
TOGGLE_EVENT,
|
|
21
23
|
TOOL_CALL_STATUS,
|
|
22
24
|
TOOL_DISPLAY,
|
|
25
|
+
TOOL_GROUP_DISPLAY,
|
|
23
26
|
TOOL_OUTCOME,
|
|
27
|
+
type ToolGroupDisplayMode,
|
|
24
28
|
UNREAD_EVENT,
|
|
25
29
|
X_CONFIRM_KEY,
|
|
26
30
|
X_DESTRUCTIVE_KEY,
|
|
@@ -57,12 +61,14 @@ export {
|
|
|
57
61
|
} from "./core/create_http_agent.js";
|
|
58
62
|
export { defineAgUiChat } from "./core/define_ag_ui_chat.js";
|
|
59
63
|
export type { AttachmentsDetail } from "./core/events/attachments_detail.js";
|
|
64
|
+
export type { BarHeightDetail } from "./core/events/bar_height_detail.js";
|
|
60
65
|
export type { CustomAgentDetail } from "./core/events/custom_agent_detail.js";
|
|
61
66
|
export type { FeedbackDetail } from "./core/events/feedback_detail.js";
|
|
62
67
|
export type { InvalidateDetail } from "./core/events/invalidate_detail.js";
|
|
63
68
|
export type { RunFinishedDetail } from "./core/events/run_finished_detail.js";
|
|
64
69
|
export type { StateDetail } from "./core/events/state_detail.js";
|
|
65
70
|
export type { SubmitDetail } from "./core/events/submit_detail.js";
|
|
71
|
+
export type { ThreadDetail } from "./core/events/thread_detail.js";
|
|
66
72
|
export type { ToggleDetail } from "./core/events/toggle_detail.js";
|
|
67
73
|
export type { ToolRun } from "./core/events/tool_run.js";
|
|
68
74
|
export type { UnreadDetail } from "./core/events/unread_detail.js";
|
|
@@ -89,6 +95,7 @@ export {
|
|
|
89
95
|
type PressOptions,
|
|
90
96
|
prefersReducedMotion,
|
|
91
97
|
pressThenClick,
|
|
98
|
+
type RingOwnerOptions,
|
|
92
99
|
type ScrollOptions,
|
|
93
100
|
type SelectOptions,
|
|
94
101
|
scrollIntoCenterView,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { X_SUMMARY_KEY } from "../constants.js";
|
|
2
|
+
import type { CollapseTarget } from "../ui/placement/surface_for.js";
|
|
2
3
|
import type { ClientTool } from "./client_tool_registry.js";
|
|
3
4
|
|
|
4
5
|
/** Every corner the panel can be sent to, in the order the schema states them. */
|
|
@@ -11,7 +12,13 @@ export type ChatCorner = (typeof CHAT_CORNERS)[number];
|
|
|
11
12
|
export interface ChatSurfaceReport {
|
|
12
13
|
/** The placement in force, or `null` when the host set none. */
|
|
13
14
|
readonly placement: string | null;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* What the panel collapses to: `launcher`, `rail`, `header`, `bar` (the
|
|
17
|
+
* bottom bar it rises from), `none` (hidden, brought back by the host's own
|
|
18
|
+
* control), or `null` when it cannot collapse.
|
|
19
|
+
*/
|
|
20
|
+
readonly collapseTo: CollapseTarget;
|
|
21
|
+
/** Whether the panel is currently collapsed. */
|
|
15
22
|
readonly collapsed: boolean;
|
|
16
23
|
/** Whether this placement has a collapsed state at all. */
|
|
17
24
|
readonly collapsible: boolean;
|
|
@@ -154,9 +161,11 @@ export function createChatSurfaceTools(surface: ChatSurface): ClientTool[] {
|
|
|
154
161
|
moved: false,
|
|
155
162
|
reason: report.fullBleed
|
|
156
163
|
? "the panel fills the screen, so there is nowhere to move it to"
|
|
157
|
-
: report.
|
|
158
|
-
? "
|
|
159
|
-
:
|
|
164
|
+
: report.collapseTo === "bar" || report.collapseTo === "none"
|
|
165
|
+
? "the panel is docked: it rises from the page's bottom bar and stays there"
|
|
166
|
+
: report.draggable === false
|
|
167
|
+
? "this page has turned off moving the panel"
|
|
168
|
+
: `the "${report.placement ?? ""}" placement owns the panel's position`,
|
|
160
169
|
suggestion: report.collapsible ? "minimise_chat" : null,
|
|
161
170
|
};
|
|
162
171
|
}
|
|
@@ -166,9 +175,10 @@ export function createChatSurfaceTools(surface: ChatSurface): ClientTool[] {
|
|
|
166
175
|
{
|
|
167
176
|
name: "minimise_chat",
|
|
168
177
|
description:
|
|
169
|
-
"Collapse your own panel
|
|
170
|
-
"
|
|
171
|
-
"
|
|
178
|
+
"Collapse your own panel, so the user can see the whole page. The user reopens " +
|
|
179
|
+
"it from where it collapsed to (a launcher, a rail, a bottom bar or the page's " +
|
|
180
|
+
"own control). Answers `minimised: false` when the placement has no collapsed " +
|
|
181
|
+
"state, which is the case for a full-page chat.",
|
|
172
182
|
parameters: {
|
|
173
183
|
type: "object",
|
|
174
184
|
properties: {},
|
|
@@ -180,7 +190,7 @@ export function createChatSurfaceTools(surface: ChatSurface): ClientTool[] {
|
|
|
180
190
|
if (!report.collapsible) {
|
|
181
191
|
return {
|
|
182
192
|
minimised: false,
|
|
183
|
-
reason: "this placement has no collapsed state, so there is
|
|
193
|
+
reason: "this placement has no collapsed state, so there is nothing to minimise to",
|
|
184
194
|
};
|
|
185
195
|
}
|
|
186
196
|
surface.setCollapsed(true, { announce: true });
|