@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
|
@@ -22,11 +22,31 @@ import {
|
|
|
22
22
|
type ResizeGrip,
|
|
23
23
|
type ResizeSize,
|
|
24
24
|
} from "./resize_handle.js";
|
|
25
|
-
import { surfaceFor } from "./surface_for.js";
|
|
25
|
+
import { type Surface, surfaceFor } from "./surface_for.js";
|
|
26
26
|
|
|
27
27
|
/** Per-tab persistence key for a dragged panel size. */
|
|
28
28
|
const SIZE_KEY = "ag-ui-chat:size";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The private properties a dragged size lives in. They are not the public
|
|
32
|
+
* `--ag-ui-width` / `--ag-ui-height` tokens, which belong to the host: an
|
|
33
|
+
* inline write there outranked every host rule and was read back by the
|
|
34
|
+
* small-viewport block, so a size dragged in a wide window leaked into the
|
|
35
|
+
* phone shape. The placement rules read these before the host token; the
|
|
36
|
+
* small-viewport block does not read them at all.
|
|
37
|
+
*/
|
|
38
|
+
const DRAG_WIDTH = "--_drag-width";
|
|
39
|
+
const DRAG_HEIGHT = "--_drag-height";
|
|
40
|
+
/**
|
|
41
|
+
* How far above the bottom of the layout viewport a docked panel stands, and
|
|
42
|
+
* how far in from its right edge: the bar's height, or the host anchor's top
|
|
43
|
+
* and right. Private and written inline, like the dragged size, and read only
|
|
44
|
+
* by the dock rules; never an inline --_inset, which would outrank the phone
|
|
45
|
+
* block and keep a docked panel off full-bleed on a phone.
|
|
46
|
+
*/
|
|
47
|
+
const DOCK_BOTTOM = "--_dock-bottom";
|
|
48
|
+
const DOCK_RIGHT = "--_dock-right";
|
|
49
|
+
|
|
30
50
|
/** Per-tab persistence key for a dragged launcher position. */
|
|
31
51
|
const LAUNCHER_KEY = "ag-ui-chat:launcher";
|
|
32
52
|
|
|
@@ -189,6 +209,20 @@ export class PanelPlacement {
|
|
|
189
209
|
this.#applySize(this.#readSize());
|
|
190
210
|
}
|
|
191
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Drop the dragged size and its stored copy, so the placement's own size and
|
|
214
|
+
* the host's tokens apply again. For a host entering a sized state of its
|
|
215
|
+
* own, which a user's drag would otherwise outrank.
|
|
216
|
+
*/
|
|
217
|
+
resetSize(): void {
|
|
218
|
+
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
219
|
+
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
220
|
+
this.#host.clearPreference(SIZE_KEY);
|
|
221
|
+
// The anchor was measured against the dragged box; the resting one may
|
|
222
|
+
// pin different edges.
|
|
223
|
+
this.syncResizeAnchor();
|
|
224
|
+
}
|
|
225
|
+
|
|
192
226
|
/**
|
|
193
227
|
* A zero-sized box carrying the host's viewport insets as padding, so they
|
|
194
228
|
* can be read back as used pixel lengths. See the `.viewport-probe` rule for
|
|
@@ -284,6 +318,7 @@ export class PanelPlacement {
|
|
|
284
318
|
const fullBleed = coversViewport(box, viewport);
|
|
285
319
|
return {
|
|
286
320
|
placement: this.#host.element.getAttribute("placement"),
|
|
321
|
+
collapseTo: this.#surface().collapseTo,
|
|
287
322
|
collapsed: this.#host.collapsed(),
|
|
288
323
|
collapsible: this.#host.collapsible(),
|
|
289
324
|
movable: this.#launcherDraggable() && !fullBleed,
|
|
@@ -400,7 +435,7 @@ export class PanelPlacement {
|
|
|
400
435
|
* per interaction, because `placement` is a live attribute.
|
|
401
436
|
*/
|
|
402
437
|
#resizeAxis(): ResizeAxis {
|
|
403
|
-
return
|
|
438
|
+
return this.#surface().resizeAxis;
|
|
404
439
|
}
|
|
405
440
|
|
|
406
441
|
/**
|
|
@@ -414,8 +449,8 @@ export class PanelPlacement {
|
|
|
414
449
|
*/
|
|
415
450
|
#measureAnchor(): ResizeAnchor {
|
|
416
451
|
const before = this.#host.element.getBoundingClientRect();
|
|
417
|
-
const width = this.#host.element.style.getPropertyValue(
|
|
418
|
-
const height = this.#host.element.style.getPropertyValue(
|
|
452
|
+
const width = this.#host.element.style.getPropertyValue(DRAG_WIDTH);
|
|
453
|
+
const height = this.#host.element.style.getPropertyValue(DRAG_HEIGHT);
|
|
419
454
|
// Shrink first. Growing is the obvious probe and cannot answer the question
|
|
420
455
|
// at a size already resting against max-width or max-height: the box does
|
|
421
456
|
// not change, no edge moves, and every clamped axis then reads as pinned on
|
|
@@ -431,8 +466,8 @@ export class PanelPlacement {
|
|
|
431
466
|
const grown = shrunk.x === null || shrunk.y === null ? this.#probeAnchor(before, 1) : shrunk;
|
|
432
467
|
// Restore exactly what was there, including "nothing" — leaving a probe
|
|
433
468
|
// value behind would pin a panel that had been sizing itself.
|
|
434
|
-
this.#restoreProperty(
|
|
435
|
-
this.#restoreProperty(
|
|
469
|
+
this.#restoreProperty(DRAG_WIDTH, width);
|
|
470
|
+
this.#restoreProperty(DRAG_HEIGHT, height);
|
|
436
471
|
return {
|
|
437
472
|
// Neither direction moved it: the axis cannot be resized at all, so the
|
|
438
473
|
// floating default is the best answer available and is the one the
|
|
@@ -497,7 +532,7 @@ export class PanelPlacement {
|
|
|
497
532
|
/**
|
|
498
533
|
* Write a dragged size onto the host, on the axes this placement leaves free.
|
|
499
534
|
*
|
|
500
|
-
* Writing
|
|
535
|
+
* Writing a custom property rather than inline `width` / `height` does not
|
|
501
536
|
* by itself leave placement in charge: an inline custom property still
|
|
502
537
|
* outranks a `:host([placement=…])` rule setting the same property, so a
|
|
503
538
|
* height dragged while floating would cap a docked sidebar asking for
|
|
@@ -518,10 +553,10 @@ export class PanelPlacement {
|
|
|
518
553
|
// symmetry nobody has asked for. The limit that matters, staying inside
|
|
519
554
|
// what the host left free, is enforced above.
|
|
520
555
|
if (size.width !== undefined) {
|
|
521
|
-
this.#host.element.style.setProperty(
|
|
556
|
+
this.#host.element.style.setProperty(DRAG_WIDTH, `${size.width}px`);
|
|
522
557
|
}
|
|
523
558
|
if (size.height !== undefined && axis === "both") {
|
|
524
|
-
this.#host.element.style.setProperty(
|
|
559
|
+
this.#host.element.style.setProperty(DRAG_HEIGHT, `${size.height}px`);
|
|
525
560
|
}
|
|
526
561
|
}
|
|
527
562
|
|
|
@@ -535,11 +570,54 @@ export class PanelPlacement {
|
|
|
535
570
|
releaseOwnedAxes(): void {
|
|
536
571
|
const axis = this.#resizeAxis();
|
|
537
572
|
if (axis !== "both") {
|
|
538
|
-
this.#host.element.style.removeProperty(
|
|
573
|
+
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
539
574
|
}
|
|
540
575
|
if (axis === "none") {
|
|
541
|
-
this.#host.element.style.removeProperty(
|
|
576
|
+
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** The surface the live placement and collapse-to attributes describe. */
|
|
581
|
+
#surface(): Surface {
|
|
582
|
+
const element = this.#host.element;
|
|
583
|
+
return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Whether the panel rises from the bottom bar or the host's anchor. */
|
|
587
|
+
#docked(): boolean {
|
|
588
|
+
const target = this.#surface().collapseTo;
|
|
589
|
+
return target === "bar" || target === "none";
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Write where a docked panel stands, and return the bar's height.
|
|
594
|
+
*
|
|
595
|
+
* Under the bar it is the strip's border box, which is 0 whenever the strip
|
|
596
|
+
* is not drawn (a phone with the panel open, where the panel is the screen).
|
|
597
|
+
* Under a host anchor it is the anchor's top and right, measured against the
|
|
598
|
+
* layout viewport a fixed element is placed in; an anchor of null leaves the
|
|
599
|
+
* panel in its placement's own corner. Every other surface carries neither
|
|
600
|
+
* property, so nothing stale is left behind by a live switch.
|
|
601
|
+
*/
|
|
602
|
+
syncDock(bar: HTMLElement, anchor: Element | null): number {
|
|
603
|
+
const element = this.#host.element;
|
|
604
|
+
const target = this.#surface().collapseTo;
|
|
605
|
+
const height = target === "bar" ? bar.offsetHeight : 0;
|
|
606
|
+
if (target === "bar") {
|
|
607
|
+
element.style.setProperty(DOCK_BOTTOM, `${height}px`);
|
|
608
|
+
} else if (target === "none" && anchor !== null) {
|
|
609
|
+
const rect = anchor.getBoundingClientRect();
|
|
610
|
+
const screen = this.#screen();
|
|
611
|
+
element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, screen.height - rect.top)}px`);
|
|
612
|
+
element.style.setProperty(DOCK_RIGHT, `${Math.max(0, screen.width - rect.right)}px`);
|
|
613
|
+
} else {
|
|
614
|
+
element.style.removeProperty(DOCK_BOTTOM);
|
|
615
|
+
}
|
|
616
|
+
if (target !== "none" || anchor === null) {
|
|
617
|
+
element.style.removeProperty(DOCK_RIGHT);
|
|
542
618
|
}
|
|
619
|
+
element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
|
|
620
|
+
return height;
|
|
543
621
|
}
|
|
544
622
|
|
|
545
623
|
/**
|
|
@@ -550,7 +628,7 @@ export class PanelPlacement {
|
|
|
550
628
|
#launcherDraggable(): boolean {
|
|
551
629
|
return (
|
|
552
630
|
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
553
|
-
|
|
631
|
+
this.#surface().launcherDraggable
|
|
554
632
|
);
|
|
555
633
|
}
|
|
556
634
|
|
|
@@ -1032,6 +1110,21 @@ export class PanelPlacement {
|
|
|
1032
1110
|
*/
|
|
1033
1111
|
#applyResize(grip: ResizeGrip, box: PanelRect): PanelRect {
|
|
1034
1112
|
box = this.#withinViewport(box);
|
|
1113
|
+
if (this.#docked()) {
|
|
1114
|
+
// A docked panel keeps its resize but never its position: the edges it
|
|
1115
|
+
// stands on are the bar's or the anchor's, and a grip on one of them
|
|
1116
|
+
// would write the position below and take the panel off its dock. So
|
|
1117
|
+
// the pinned edges stay where they are and only the free ones follow.
|
|
1118
|
+
const now = this.#host.element.getBoundingClientRect();
|
|
1119
|
+
box = {
|
|
1120
|
+
left: grip.x === this.#anchor.x ? now.left : box.left,
|
|
1121
|
+
right: grip.x === this.#anchor.x ? now.right : box.right,
|
|
1122
|
+
top: grip.y === this.#anchor.y ? now.top : box.top,
|
|
1123
|
+
bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom,
|
|
1124
|
+
};
|
|
1125
|
+
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1126
|
+
return box;
|
|
1127
|
+
}
|
|
1035
1128
|
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1036
1129
|
if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
|
|
1037
1130
|
return box;
|
|
@@ -12,7 +12,7 @@ export const PRESET_STYLES: string = `
|
|
|
12
12
|
|
|
13
13
|
:host([placement="side"]) {
|
|
14
14
|
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
15
|
-
--_width: var(--ag-ui-width, 420px);
|
|
15
|
+
--_width: var(--_drag-width, var(--ag-ui-width, 420px));
|
|
16
16
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
17
17
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
18
18
|
--_panel-radius: var(--ag-ui-panel-radius, 0);
|
|
@@ -7,7 +7,7 @@ export const SIDEBAR_STYLES: string = `
|
|
|
7
7
|
host-managed push instead. */
|
|
8
8
|
:host([placement="sidebar"]) {
|
|
9
9
|
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
10
|
-
--_width: var(--ag-ui-width, 420px);
|
|
10
|
+
--_width: var(--_drag-width, var(--ag-ui-width, 420px));
|
|
11
11
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
12
12
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
13
13
|
--_panel-radius: var(--ag-ui-panel-radius, 0);
|
|
@@ -17,6 +17,11 @@ export const SMALL_VIEWPORT_STYLES: string = `
|
|
|
17
17
|
fine-pointered and small, and conflating the two gets both wrong. What the
|
|
18
18
|
pointer decides is which controls make sense, further down.
|
|
19
19
|
|
|
20
|
+
The size the user dragged is deliberately not read here. The desktop
|
|
21
|
+
placements read it before the host token, and this block restates the sizes
|
|
22
|
+
without it, so a persisted or freshly dragged size cannot change the shape
|
|
23
|
+
of a phone, and a window narrowed after a drag goes full-bleed.
|
|
24
|
+
|
|
20
25
|
Two placements are left alone. "page" is already this shape. "embedded" sits
|
|
21
26
|
in a box the host sized and placed, and taking that over would break the app
|
|
22
27
|
shell it was embedded into -- the host is the only party that knows whether
|
|
@@ -52,5 +57,14 @@ export const SMALL_VIEWPORT_STYLES: string = `
|
|
|
52
57
|
:host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
|
|
53
58
|
display: none;
|
|
54
59
|
}
|
|
60
|
+
|
|
61
|
+
/* The bottom bar shows only while collapsed. With the panel open the panel
|
|
62
|
+
is the screen, its header's minimise control is the way back, and a
|
|
63
|
+
second strip fixed to the bottom edge would sit under the keyboard while
|
|
64
|
+
the composer has focus. Hidden by display, so the bar measures 0 and the
|
|
65
|
+
host's padding goes with it. */
|
|
66
|
+
:host([data-collapsed-to="bar"]:not([collapsed]):not([data-small-viewport="off"])) .dock-bar {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
55
69
|
}
|
|
56
70
|
`;
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import type { ResizeAxis } from "./resize_handle.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* What a collapsed panel shrinks to
|
|
5
|
-
*
|
|
6
|
-
* Only `null` versus not-`null` is read today: every caller asks whether the
|
|
7
|
-
* surface can collapse at all, never which target it collapses to. The three
|
|
8
|
-
* names below therefore describe the surface rather than drive it, and nothing
|
|
9
|
-
* relies on them (changing one fails only the test of this record). The
|
|
10
|
-
* bottom-bar collapse is expected to read or replace them, and a reader that
|
|
11
|
-
* does should come with a test outside this record's own.
|
|
4
|
+
* What a collapsed panel shrinks to, stamped on the element as
|
|
5
|
+
* data-collapsed-to so the stylesheet draws it.
|
|
12
6
|
*
|
|
13
7
|
* - `launcher` -- the round button in a corner.
|
|
14
8
|
* - `rail` -- the slim full-height edge the sidebar docks against; the same
|
|
15
9
|
* launcher element, shaped by the stylesheet.
|
|
16
10
|
* - `header` -- the in-flow collapse: the body hides and the header bar stays
|
|
17
11
|
* in the host's layout, so there is no launcher at all.
|
|
12
|
+
* - `bar` -- `collapse-to="bar"` over a placement that would otherwise
|
|
13
|
+
* collapse to `launcher`: a strip along the bottom edge, the panel rising
|
|
14
|
+
* from its right end.
|
|
15
|
+
* - `none` -- `collapse-to="none"` over any collapsible placement: collapsed
|
|
16
|
+
* is hidden, and the host's own control brings the panel back.
|
|
18
17
|
* - `null` -- no collapsed state. `page` is a dedicated route rather than a
|
|
19
18
|
* panel on someone else's page, so there is no "away" for it to go to:
|
|
20
19
|
* collapsing it left a strip of application chrome fixed over a route with
|
|
@@ -25,7 +24,7 @@ import type { ResizeAxis } from "./resize_handle.js";
|
|
|
25
24
|
* those paths asks this record; the stylesheet covers the one path that
|
|
26
25
|
* never does, an attribute written straight onto the element.
|
|
27
26
|
*/
|
|
28
|
-
export type CollapseTarget = "launcher" | "rail" | "header" | null;
|
|
27
|
+
export type CollapseTarget = "launcher" | "rail" | "header" | "bar" | "none" | null;
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Everything the script needs to know about one display surface, in one
|
|
@@ -36,11 +35,7 @@ export type CollapseTarget = "launcher" | "rail" | "header" | null;
|
|
|
36
35
|
* under this directory); this is the part of a surface that code decides.
|
|
37
36
|
*/
|
|
38
37
|
export interface Surface {
|
|
39
|
-
/**
|
|
40
|
-
* What collapsing shrinks the panel to; `null` when it cannot collapse. Only
|
|
41
|
-
* the `null` test is authoritative: the non-null names are descriptive (see
|
|
42
|
-
* {@link CollapseTarget}), and nothing branches on which one a surface has.
|
|
43
|
-
*/
|
|
38
|
+
/** What collapsing shrinks the panel to; `null` when it cannot collapse. */
|
|
44
39
|
readonly collapseTo: CollapseTarget;
|
|
45
40
|
/**
|
|
46
41
|
* Whether a gesture may move the launcher. The rest have nowhere to put it:
|
|
@@ -127,9 +122,25 @@ const UNKNOWN: Surface = {
|
|
|
127
122
|
};
|
|
128
123
|
|
|
129
124
|
/**
|
|
130
|
-
* The surface `placement`
|
|
131
|
-
* because
|
|
125
|
+
* The surface `placement` and `collapse-to` describe together. Read per
|
|
126
|
+
* interaction by every caller, because both are live attributes.
|
|
127
|
+
*
|
|
128
|
+
* `collapse-to` only ever replaces the round launcher or the whole collapsed
|
|
129
|
+
* state, never a placement's own shape: `bar` applies where the placement
|
|
130
|
+
* collapses to `launcher`, so the sidebar keeps its rail and an embedded chat
|
|
131
|
+
* its header bar, and `none` applies to every collapsible placement. Absent,
|
|
132
|
+
* `auto` and any other value leave the placement as it was. A docked surface
|
|
133
|
+
* cannot be moved -- the panel rises from the bar or the host's anchor, and a
|
|
134
|
+
* position the user dragged would take it away from either -- so both turn
|
|
135
|
+
* the launcher drag off, and with it the header drag and `moveTo`.
|
|
132
136
|
*/
|
|
133
|
-
export function surfaceFor(placement: string | null): Surface {
|
|
134
|
-
|
|
137
|
+
export function surfaceFor(placement: string | null, collapseTo: string | null = null): Surface {
|
|
138
|
+
const surface = SURFACES.get(placement) ?? UNKNOWN;
|
|
139
|
+
if (collapseTo === "bar" && surface.collapseTo === "launcher") {
|
|
140
|
+
return { ...surface, collapseTo: "bar", launcherDraggable: false };
|
|
141
|
+
}
|
|
142
|
+
if (collapseTo === "none" && surface.collapseTo !== null) {
|
|
143
|
+
return { ...surface, collapseTo: "none", launcherDraggable: false };
|
|
144
|
+
}
|
|
145
|
+
return surface;
|
|
135
146
|
}
|
|
@@ -197,9 +197,7 @@ export class ToolCallCard {
|
|
|
197
197
|
|
|
198
198
|
this.element = document.createElement("div");
|
|
199
199
|
this.element.className = "tool-call";
|
|
200
|
-
this.element.setAttribute("part", "tool-card");
|
|
201
200
|
this.element.setAttribute("data-tool-name", name);
|
|
202
|
-
this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
|
|
203
201
|
this.element.setAttribute("data-expanded", "false");
|
|
204
202
|
|
|
205
203
|
const head = document.createElement("div");
|
|
@@ -221,8 +219,7 @@ export class ToolCallCard {
|
|
|
221
219
|
|
|
222
220
|
this.#status = document.createElement("span");
|
|
223
221
|
this.#status.className = "tool-call-status";
|
|
224
|
-
this.#
|
|
225
|
-
this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
|
|
222
|
+
this.#setStatus(TOOL_CALL_STATUS.PENDING);
|
|
226
223
|
|
|
227
224
|
this.#decision = document.createElement("span");
|
|
228
225
|
this.#decision.className = "tool-call-decision";
|
|
@@ -283,8 +280,7 @@ export class ToolCallCard {
|
|
|
283
280
|
if (this.#settled) {
|
|
284
281
|
return;
|
|
285
282
|
}
|
|
286
|
-
this
|
|
287
|
-
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
283
|
+
this.#setStatus(status);
|
|
288
284
|
}
|
|
289
285
|
|
|
290
286
|
/**
|
|
@@ -326,8 +322,7 @@ export class ToolCallCard {
|
|
|
326
322
|
return;
|
|
327
323
|
}
|
|
328
324
|
this.#settled = true;
|
|
329
|
-
this
|
|
330
|
-
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
325
|
+
this.#setStatus(status);
|
|
331
326
|
this.#resultLabel.textContent = resultLabels(this.#strings)[status];
|
|
332
327
|
this.#renderPayload(
|
|
333
328
|
this.#resultBody,
|
|
@@ -337,6 +332,21 @@ export class ToolCallCard {
|
|
|
337
332
|
this.#resultSection.hidden = false;
|
|
338
333
|
}
|
|
339
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Write the status everywhere it is read from, in one place so they cannot
|
|
337
|
+
* disagree: `data-status` for the shadow CSS, the pill's text, and the part
|
|
338
|
+
* lists. `::part()` takes no attribute selector, so the card and its pill also
|
|
339
|
+
* name the status as a part (`tool-card-error`, `tool-card-status-error`), the
|
|
340
|
+
* modifier convention a run notice uses, and a host styles failures alone.
|
|
341
|
+
* Setting the whole list replaces the previous modifier.
|
|
342
|
+
*/
|
|
343
|
+
#setStatus(status: ToolCallStatus): void {
|
|
344
|
+
this.element.setAttribute("data-status", status);
|
|
345
|
+
this.element.setAttribute("part", `tool-card tool-card-${status}`);
|
|
346
|
+
this.#status.setAttribute("part", `tool-card-status tool-card-status-${status}`);
|
|
347
|
+
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
348
|
+
}
|
|
349
|
+
|
|
340
350
|
/**
|
|
341
351
|
* Fill one body region, offering it to the host formatter first.
|
|
342
352
|
*
|
|
@@ -21,6 +21,18 @@ const GROUPED = `:host(:where(:not(${CARD_MODES})))`;
|
|
|
21
21
|
/** The host in one of the four card modes, where a group drawn earlier is flattened. */
|
|
22
22
|
const CARDS = `:host(:is(${CARD_MODES}))`;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A card inside an opened group, with the host in grouped mode and the given
|
|
26
|
+
* data-tool-group-display. The inner attribute sits inside the same :host()
|
|
27
|
+
* as the grouped test, since the host is featureless outside it, and being
|
|
28
|
+
* grouped-only is what lets an outer card mode flatten the group into its own
|
|
29
|
+
* cards: none of these rules match there. Full is no rule at all, so an absent
|
|
30
|
+
* or unknown inner value draws the full card a group always drew.
|
|
31
|
+
*/
|
|
32
|
+
function inner(mode: string): string {
|
|
33
|
+
return `:host(:where(:not(${CARD_MODES}))[data-tool-group-display="${mode}"]) .tool-group-list .tool-call`;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export const TOOL_GROUP_STYLES: string = `
|
|
25
37
|
/* ── The folded tool-step group ──────────────────────────────────
|
|
26
38
|
One quiet row per run of consecutive calls, the cards one click down as
|
|
@@ -205,6 +217,43 @@ ${GROUPED} .tool-group .tool-call-subagent {
|
|
|
205
217
|
flex-basis: 100%;
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
/* The inner display mode: how an opened group draws its calls. The same
|
|
221
|
+
visibility rules the card modes apply to a flat card, so a call inside the
|
|
222
|
+
group shows the regions the flat card of that mode shows. The flat inline
|
|
223
|
+
mode's chrome rule is left out on purpose: a card in the group has already
|
|
224
|
+
lost its box and is laid out as a row of the list, and that rule's padding
|
|
225
|
+
and gap would undo the row. Selected from the host attribute, so changing it
|
|
226
|
+
restyles groups already on screen. */
|
|
227
|
+
${inner("compact")}[data-expanded="false"] .tool-call-section {
|
|
228
|
+
display: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
${inner("inline")} .tool-call-section--args {
|
|
232
|
+
display: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
${inner("minimal")} .tool-call-toggle,
|
|
236
|
+
${inner("minimal")} .tool-call-body {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* A call waiting on an approval shows its arguments in every inner mode, as it
|
|
241
|
+
does in every card mode, and for the same reason: the arguments are what
|
|
242
|
+
tell three gated calls of one tool apart. These come after the hides above,
|
|
243
|
+
which they tie with on weight. The arguments region only, never every
|
|
244
|
+
section, since the result region carries the hidden attribute until a
|
|
245
|
+
result exists and a display value here would override it. The arguments
|
|
246
|
+
region carries it too when the call has none, so it is excluded while
|
|
247
|
+
hidden rather than framed empty. */
|
|
248
|
+
${inner("minimal")}[data-status="deferred"] .tool-call-body {
|
|
249
|
+
display: flex;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
${inner("compact")}[data-status="deferred"] .tool-call-section--args:not([hidden]),
|
|
253
|
+
${inner("inline")}[data-status="deferred"] .tool-call-section--args:not([hidden]) {
|
|
254
|
+
display: flex;
|
|
255
|
+
}
|
|
256
|
+
|
|
208
257
|
/* Grouping is decided as each card is built, so a card mode set afterwards
|
|
209
258
|
finds groups already drawn. It flattens them: the group and its list stop
|
|
210
259
|
generating boxes, the row and the repeat ordinals go, and every card lays
|
|
@@ -225,6 +225,9 @@ export class ToolStepGroup {
|
|
|
225
225
|
state = "error";
|
|
226
226
|
}
|
|
227
227
|
this.element.setAttribute("data-state", state);
|
|
228
|
+
// The state is also a part, because `::part()` takes no attribute selector;
|
|
229
|
+
// the whole list is rewritten so the old modifier goes.
|
|
230
|
+
this.element.setAttribute("part", `tool-group tool-group-${state}`);
|
|
228
231
|
this.element.setAttribute("data-count", String(total));
|
|
229
232
|
|
|
230
233
|
const only = total === 1 ? steps[0] : undefined;
|
|
@@ -277,9 +277,11 @@ export const TOOL_STEP_STYLES: string = `
|
|
|
277
277
|
|
|
278
278
|
/* The arguments region only, never every section: the result region carries the
|
|
279
279
|
hidden attribute until a result exists, and a display value here overrides it,
|
|
280
|
-
framing an empty RESULT heading under the question.
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
framing an empty RESULT heading under the question. The arguments region is
|
|
281
|
+
hidden too when the call has none, and is left alone then, or it would frame
|
|
282
|
+
an empty ARGUMENTS heading. */
|
|
283
|
+
:host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]),
|
|
284
|
+
:host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]) {
|
|
283
285
|
display: flex;
|
|
284
286
|
}
|
|
285
287
|
|
|
@@ -7,21 +7,32 @@ export interface LauncherOptions {
|
|
|
7
7
|
readonly launcher: HTMLButtonElement;
|
|
8
8
|
readonly badge: HTMLElement;
|
|
9
9
|
readonly railLabel: HTMLElement;
|
|
10
|
+
/** The bottom bar's wrapper, which holds the launcher and the history button. */
|
|
11
|
+
readonly bar: HTMLElement;
|
|
12
|
+
/** The bottom bar's history button, a sibling of the launcher in the bar. */
|
|
13
|
+
readonly barHistory: HTMLButtonElement;
|
|
10
14
|
readonly titleText: string;
|
|
11
15
|
readonly iconUrl: string | null;
|
|
12
16
|
readonly strings: UiStrings;
|
|
13
17
|
readonly signal: AbortSignal;
|
|
14
18
|
readonly onOpen: () => void;
|
|
19
|
+
readonly onHistory: () => void;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
|
-
* Build what a collapsed widget shrinks to: a round floating button,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the panel
|
|
23
|
+
* Build what a collapsed widget shrinks to: a round floating button, the slim
|
|
24
|
+
* edge rail under `placement="sidebar"`, or the strip along the bottom edge
|
|
25
|
+
* under `collapse-to="bar"` -- one element, shaped by the stylesheet. The
|
|
26
|
+
* element mounts the bar wrapper as a sibling of the panel, so it survives the
|
|
27
|
+
* panel being hidden.
|
|
28
|
+
*
|
|
29
|
+
* The wrapper is there for the bar's history button: a button cannot nest in
|
|
30
|
+
* a button, so the strip is the launcher and the history button side by side.
|
|
31
|
+
* Outside the bar the wrapper draws no box and the history button is not
|
|
32
|
+
* drawn, so the launcher lays out against the host exactly as it did alone.
|
|
22
33
|
*/
|
|
23
34
|
export function buildLauncher(options: LauncherOptions): void {
|
|
24
|
-
const { launcher, badge, railLabel } = options;
|
|
35
|
+
const { launcher, badge, railLabel, bar, barHistory } = options;
|
|
25
36
|
launcher.className = "launcher";
|
|
26
37
|
launcher.type = "button";
|
|
27
38
|
launcher.setAttribute("part", "launcher");
|
|
@@ -47,4 +58,20 @@ export function buildLauncher(options: LauncherOptions): void {
|
|
|
47
58
|
badge,
|
|
48
59
|
);
|
|
49
60
|
launcher.addEventListener("click", options.onOpen, { signal: options.signal });
|
|
61
|
+
bar.className = "dock-bar";
|
|
62
|
+
bar.setAttribute("part", "bar");
|
|
63
|
+
barHistory.className = "bar-history";
|
|
64
|
+
barHistory.type = "button";
|
|
65
|
+
barHistory.setAttribute("part", "bar-history");
|
|
66
|
+
barHistory.title = options.strings.chatHistory;
|
|
67
|
+
barHistory.setAttribute("aria-label", options.strings.chatHistory);
|
|
68
|
+
// A glyph rather than the header's icon slot: a slot name may appear once
|
|
69
|
+
// per shadow root, and the header's history button already holds it.
|
|
70
|
+
const glyph = document.createElement("span");
|
|
71
|
+
glyph.className = "bar-history-glyph";
|
|
72
|
+
glyph.setAttribute("aria-hidden", "true");
|
|
73
|
+
glyph.textContent = "\u2630";
|
|
74
|
+
barHistory.replaceChildren(glyph);
|
|
75
|
+
barHistory.addEventListener("click", options.onHistory, { signal: options.signal });
|
|
76
|
+
bar.replaceChildren(launcher, barHistory);
|
|
50
77
|
}
|
|
@@ -37,7 +37,15 @@ export const TOKEN_STYLES: string = `
|
|
|
37
37
|
--_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
|
|
38
38
|
--_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
|
|
39
39
|
--_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
|
|
40
|
-
|
|
40
|
+
/* The focus ring is drawn on the panel, so it has to hold 3:1 against
|
|
41
|
+
--_bg whatever accent a host brings (WCAG 2.4.13). Two parts accent to
|
|
42
|
+
three parts ink does that at the worst case, an accent at the far end
|
|
43
|
+
from the ink (white on a light panel, black on a dark one): the ink
|
|
44
|
+
alone sits far above 3:1, and three fifths of it keeps the mix there
|
|
45
|
+
while the hue still reads as the accent. Half and half fell to 2.3:1
|
|
46
|
+
for black on the code theme. The accent over transparent, as before,
|
|
47
|
+
measured only the accent, and a light host accent fell to 1.6:1. */
|
|
48
|
+
--_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 40%, var(--_fg)));
|
|
41
49
|
/* The user's turn is the accent at a whisper. With Quiet's graphite accent
|
|
42
50
|
that is a neutral wash; once a host sets a brand accent it becomes a tint
|
|
43
51
|
of the brand, which is the one thing Soft does that Quiet keeps. */
|
|
@@ -300,8 +308,27 @@ export const TOKEN_STYLES: string = `
|
|
|
300
308
|
grid/flex layout) to embed it in the page flow instead of floating. */
|
|
301
309
|
--_position: var(--ag-ui-position, fixed);
|
|
302
310
|
--_z-index: var(--ag-ui-z-index, 2147483000);
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
/* A size the user dragged, written inline on the host by the placement
|
|
312
|
+
code, and read ahead of the host's tokens so the drag wins. There is no
|
|
313
|
+
public token behind it; it is declared initial here only so a chat that
|
|
314
|
+
was never dragged does not inherit one, from a dragged chat it is
|
|
315
|
+
slotted into or from any page rule naming it. The inline value still
|
|
316
|
+
beats this default. */
|
|
317
|
+
--_drag-width: initial;
|
|
318
|
+
--_drag-height: initial;
|
|
319
|
+
/* Where a docked panel stands, written inline the same way by the placement
|
|
320
|
+
code: the bottom bar's height, or the host anchor's distance from the
|
|
321
|
+
bottom and right edges. Read only by the dock rules, each with a 0px
|
|
322
|
+
fallback; declared initial so it never inherits either. */
|
|
323
|
+
--_dock-bottom: initial;
|
|
324
|
+
--_dock-right: initial;
|
|
325
|
+
/* The gap between a panel and the host's own button it rises from under
|
|
326
|
+
collapse-to="none". The button usually sits inside a taller toolbar, so a
|
|
327
|
+
host sets this to the button's distance from the toolbar's top plus the
|
|
328
|
+
clearance it wants, and the panel stands clear of the toolbar. */
|
|
329
|
+
--_anchor-gap: var(--ag-ui-anchor-gap, 8px);
|
|
330
|
+
--_width: var(--_drag-width, var(--ag-ui-width, 380px));
|
|
331
|
+
--_height: var(--_drag-height, var(--ag-ui-height, 560px));
|
|
305
332
|
/* The gutter a resting floating panel keeps between itself and the edge of
|
|
306
333
|
the box the host left free. One number, because the inset below spends it
|
|
307
334
|
and the cap beneath has to know it was spent. */
|
package/src/ui/styles.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { APPROVAL_STYLES } from "./interrupts/approval_styles.js";
|
|
|
23
23
|
import { CONFIRMATION_STYLES } from "./interrupts/confirmation_styles.js";
|
|
24
24
|
import { QUESTION_STYLES } from "./interrupts/question_styles.js";
|
|
25
25
|
import { COLLAPSE_STYLES } from "./placement/collapse_styles.js";
|
|
26
|
+
import { DOCK_STYLES } from "./placement/dock_styles.js";
|
|
26
27
|
import { EMBEDDED_STYLES } from "./placement/embedded_styles.js";
|
|
27
28
|
import { LAUNCHER_STYLES } from "./placement/launcher_styles.js";
|
|
28
29
|
import { PAGE_STYLES } from "./placement/page_styles.js";
|
|
@@ -65,6 +66,9 @@ export const STYLES: string = [
|
|
|
65
66
|
THEME_STYLES,
|
|
66
67
|
PRESET_STYLES,
|
|
67
68
|
PAGE_STYLES,
|
|
69
|
+
// Ahead of the phone block and no heavier than it: a docked panel is still
|
|
70
|
+
// full-bleed on a phone.
|
|
71
|
+
DOCK_STYLES,
|
|
68
72
|
SMALL_VIEWPORT_STYLES,
|
|
69
73
|
SIDEBAR_STYLES,
|
|
70
74
|
LAUNCHER_STYLES,
|
|
@@ -6,9 +6,18 @@ export const SCHEME_STYLES: string = `
|
|
|
6
6
|
dark) and beside a theme name (soft dark). auto follows the operating
|
|
7
7
|
system through color-scheme itself, with no media query to keep in step.
|
|
8
8
|
code is a dark-only theme and has no light half. */
|
|
9
|
+
/* Important, so a scheme word holds against a host rule that sets the
|
|
10
|
+
element's color-scheme (a page that follows its own dark mode). A rule
|
|
11
|
+
from outside the shadow tree beats every :host rule at normal weight, and
|
|
12
|
+
the theme toggle writes the word; without this its press changed nothing.
|
|
13
|
+
code stays last, so code light is still dark. */
|
|
14
|
+
:host([theme~="light"]) {
|
|
15
|
+
color-scheme: light !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
:host([theme~="dark"]),
|
|
10
19
|
:host([theme~="code"]) {
|
|
11
|
-
color-scheme: dark;
|
|
20
|
+
color-scheme: dark !important;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
:host([theme~="auto"]) {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.48.0";
|