@artooi/ag-ui-web-component 0.33.1 → 0.35.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 +563 -1
- package/README.md +279 -16
- package/dist/ag-ui-web-component.bundle.js +676 -101
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +80 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +37 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +14 -0
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/highlight_overlay.d.ts +47 -0
- package/dist/dom/highlight_overlay.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2288 -388
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +96 -0
- package/dist/tools/chat_surface_tools.d.ts.map +1 -0
- package/dist/tools/page_action_tools.d.ts +2 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts +18 -0
- package/dist/ui/chart_block.d.ts.map +1 -1
- package/dist/ui/clamp_launcher.d.ts +10 -5
- package/dist/ui/clamp_launcher.d.ts.map +1 -1
- package/dist/ui/clamp_panel.d.ts +13 -0
- package/dist/ui/clamp_panel.d.ts.map +1 -0
- package/dist/ui/launcher_drag.d.ts +2 -2
- package/dist/ui/launcher_drag.d.ts.map +1 -1
- package/dist/ui/launcher_placement.d.ts +14 -1
- package/dist/ui/launcher_placement.d.ts.map +1 -1
- package/dist/ui/panel_drag.d.ts +40 -0
- package/dist/ui/panel_drag.d.ts.map +1 -0
- package/dist/ui/place_widget.d.ts +31 -0
- package/dist/ui/place_widget.d.ts.map +1 -0
- package/dist/ui/run_notice.d.ts +14 -3
- package/dist/ui/run_notice.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +21 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +16 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +87 -0
- package/src/core/ag_ui_chat.ts +1146 -38
- package/src/dom/animations.ts +30 -0
- package/src/dom/highlight_overlay.ts +256 -0
- package/src/index.ts +12 -0
- package/src/tools/chat_surface_tools.ts +207 -0
- package/src/tools/page_action_tools.ts +2 -0
- package/src/ui/chart_block.ts +222 -57
- package/src/ui/clamp_launcher.ts +25 -7
- package/src/ui/clamp_panel.ts +30 -0
- package/src/ui/launcher_drag.ts +11 -2
- package/src/ui/launcher_placement.ts +50 -60
- package/src/ui/panel_drag.ts +138 -0
- package/src/ui/place_widget.ts +64 -0
- package/src/ui/run_notice.ts +32 -3
- package/src/ui/styles.ts +622 -47
- package/src/ui/thread_drawer.ts +138 -8
- package/src/ui/ui_strings.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { PanelRect } from "./resize_handle.js";
|
|
2
|
+
|
|
3
|
+
/** What the drag needs from its host to do its job. */
|
|
4
|
+
export interface PanelDragOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the panel can be moved right now, read per interaction because
|
|
7
|
+
* both halves of the answer are live: a docked or full-bleed placement has
|
|
8
|
+
* nowhere to move the panel to, and a collapsed one has no panel on screen.
|
|
9
|
+
*/
|
|
10
|
+
readonly enabled: () => boolean;
|
|
11
|
+
/** The panel's current box, in viewport coordinates. */
|
|
12
|
+
readonly rect: () => PanelRect;
|
|
13
|
+
/**
|
|
14
|
+
* Put the panel at this box. Called per pointer move, with the box the press
|
|
15
|
+
* started on -- the whole gesture is one translation of that box, and a host
|
|
16
|
+
* with anything else to move alongside the panel needs the same distance
|
|
17
|
+
* rather than a distance measured from wherever the last move left things.
|
|
18
|
+
*/
|
|
19
|
+
readonly apply: (box: PanelRect, from: PanelRect) => void;
|
|
20
|
+
/** Called once per completed move, for persistence. Never per pointer move. */
|
|
21
|
+
readonly commit: (box: PanelRect, from: PanelRect) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How far the pointer must travel before this counts as a drag. The header is
|
|
26
|
+
* also where the title is selected and the controls are pressed, so a press
|
|
27
|
+
* that wanders by a pixel has to remain a press.
|
|
28
|
+
*/
|
|
29
|
+
const DRAG_THRESHOLD = 4;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Elements inside the header that own their own press. A drag started on one
|
|
33
|
+
* of these would move the panel out from under the control the user was
|
|
34
|
+
* aiming at, and every one of them is the only way to reach what it does.
|
|
35
|
+
*/
|
|
36
|
+
const CONTROLS = "button, a[href], input, select, textarea, [contenteditable]";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Let the user move the whole widget by dragging the panel's header.
|
|
40
|
+
*
|
|
41
|
+
* The launcher can already be dragged, and this is the same gesture on the
|
|
42
|
+
* half of the widget that is on screen when it is open -- a chat panel is a
|
|
43
|
+
* window, and a window moves by its title bar. The two stay one position
|
|
44
|
+
* rather than two: the host answers a moved panel by moving the launcher with
|
|
45
|
+
* it, so collapsing after a drag leaves the launcher where the panel was.
|
|
46
|
+
*
|
|
47
|
+
* **No keyboard path here, deliberately.** Every other drag in this component
|
|
48
|
+
* has arrow keys on the handle, because the handle is a control and the
|
|
49
|
+
* capability is reachable nowhere else. A header is not a control: making it
|
|
50
|
+
* focusable would put a tab stop with no role in front of every keyboard user,
|
|
51
|
+
* ahead of the controls they actually came for. The capability is not lost --
|
|
52
|
+
* arrow keys on the collapsed launcher move the widget, and the panel follows
|
|
53
|
+
* it -- so what is missing is a shortcut, not the ability.
|
|
54
|
+
*/
|
|
55
|
+
export function enablePanelDrag(handle: HTMLElement, options: PanelDragOptions): void {
|
|
56
|
+
handle.addEventListener("pointerdown", (event: PointerEvent) => {
|
|
57
|
+
// Secondary buttons open menus and paste on the platforms that have them;
|
|
58
|
+
// none of that is a drag.
|
|
59
|
+
//
|
|
60
|
+
// Three conditions in one arc, which coverage counts as one branch however
|
|
61
|
+
// many of them are deleted. Each is held by a named test in
|
|
62
|
+
// panel_drag.test.ts -- "ignores a secondary button", "does nothing where
|
|
63
|
+
// the placement has nowhere to move the panel", and "steps aside for a
|
|
64
|
+
// control in the header" -- verified by mutating each one out.
|
|
65
|
+
if (event.button !== 0 || !options.enabled() || onControl(event, handle)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const start = options.rect();
|
|
69
|
+
const originX = event.clientX;
|
|
70
|
+
const originY = event.clientY;
|
|
71
|
+
let dragging = false;
|
|
72
|
+
|
|
73
|
+
const boxAt = (x: number, y: number): PanelRect => {
|
|
74
|
+
// Measured from the box the press started on, never from the live one:
|
|
75
|
+
// reading it each move would chase the panel as it moves and the travel
|
|
76
|
+
// would compound.
|
|
77
|
+
const dx = x - originX;
|
|
78
|
+
const dy = y - originY;
|
|
79
|
+
return {
|
|
80
|
+
left: start.left + dx,
|
|
81
|
+
top: start.top + dy,
|
|
82
|
+
right: start.right + dx,
|
|
83
|
+
bottom: start.bottom + dy,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onMove = (move: PointerEvent): void => {
|
|
88
|
+
if (
|
|
89
|
+
!dragging &&
|
|
90
|
+
Math.hypot(move.clientX - originX, move.clientY - originY) < DRAG_THRESHOLD
|
|
91
|
+
) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
dragging = true;
|
|
95
|
+
handle.setAttribute("data-dragging", "true");
|
|
96
|
+
options.apply(boxAt(move.clientX, move.clientY), start);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onUp = (up: PointerEvent): void => {
|
|
100
|
+
window.removeEventListener("pointermove", onMove);
|
|
101
|
+
window.removeEventListener("pointerup", onUp);
|
|
102
|
+
window.removeEventListener("pointercancel", onUp);
|
|
103
|
+
if (!dragging) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
handle.removeAttribute("data-dragging");
|
|
107
|
+
options.commit(boxAt(up.clientX, up.clientY), start);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// The press is the panel's from here: without this the browser starts
|
|
111
|
+
// selecting the title text and the drag leaves a highlight behind it.
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
// Listeners on `window`, not the header: a fast drag outruns the pointer
|
|
114
|
+
// and would otherwise strand the panel mid-move with no pointerup.
|
|
115
|
+
window.addEventListener("pointermove", onMove);
|
|
116
|
+
window.addEventListener("pointerup", onUp);
|
|
117
|
+
// Routine on touch rather than exceptional: the browser takes the pointer
|
|
118
|
+
// back for a scroll or a system gesture and never sends pointerup.
|
|
119
|
+
window.addEventListener("pointercancel", onUp);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether the press landed on a control inside the header rather than on the
|
|
125
|
+
* header itself.
|
|
126
|
+
*
|
|
127
|
+
* The composed path rather than `target`, because a control a host slots into
|
|
128
|
+
* the header lives in the light DOM: retargeting reports the host element for
|
|
129
|
+
* it, which matches nothing. Everything below the handle is examined and
|
|
130
|
+
* nothing above it, so a control the header happens to sit inside is not one
|
|
131
|
+
* of ours.
|
|
132
|
+
*/
|
|
133
|
+
function onControl(event: PointerEvent, handle: HTMLElement): boolean {
|
|
134
|
+
const path = event.composedPath();
|
|
135
|
+
return path
|
|
136
|
+
.slice(0, path.indexOf(handle))
|
|
137
|
+
.some((node) => node instanceof Element && node.matches(CONTROLS));
|
|
138
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ExpandCorner, Extent, LauncherBox } from "./launcher_placement.js";
|
|
2
|
+
import type { PanelRect } from "./resize_handle.js";
|
|
3
|
+
|
|
4
|
+
/** Where to put the host box, and where the launcher sits inside it. */
|
|
5
|
+
export interface WidgetInsets {
|
|
6
|
+
/** An `inset` shorthand for the host box. */
|
|
7
|
+
readonly hostInset: string;
|
|
8
|
+
/** An `inset` shorthand for the launcher, relative to the host box. */
|
|
9
|
+
readonly launcherInset: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Express a panel and its launcher, both already positioned, as the two
|
|
14
|
+
* `inset` shorthands the element writes.
|
|
15
|
+
*
|
|
16
|
+
* Both are measured from the **same** corner, which is what stops a later
|
|
17
|
+
* resize from dragging the launcher: the pinned corner is the one edge a
|
|
18
|
+
* resize cannot move. Which corner that is says nothing about where either box
|
|
19
|
+
* ends up -- the positions are absolute and arrive decided -- so re-picking it
|
|
20
|
+
* moves nothing, and the launcher may sit outside its own host box. Nothing
|
|
21
|
+
* clips it there, and that is what lets a launcher be flush to a screen corner
|
|
22
|
+
* while the panel it opens keeps its margin.
|
|
23
|
+
*
|
|
24
|
+
* `screen` is the **whole** viewport, not the part a host has left free. These
|
|
25
|
+
* are CSS `inset` values on a fixed element, and the browser measures those
|
|
26
|
+
* from the real edges -- so a `bottom` expressed against a box inset from the
|
|
27
|
+
* top comes out short by exactly that inset. It only shows when the corner
|
|
28
|
+
* flips mid-drag, because that is when the same point stops being expressed
|
|
29
|
+
* from `top` and starts being expressed from `bottom`: the widget then leaps by
|
|
30
|
+
* the reserved edge, which is a jump the gesture cannot explain.
|
|
31
|
+
*/
|
|
32
|
+
export function placeWidget(
|
|
33
|
+
host: PanelRect,
|
|
34
|
+
launcher: LauncherBox,
|
|
35
|
+
corner: ExpandCorner,
|
|
36
|
+
screen: Extent,
|
|
37
|
+
): WidgetInsets {
|
|
38
|
+
return {
|
|
39
|
+
hostInset: inset({
|
|
40
|
+
top: corner.y === "top" ? host.top : null,
|
|
41
|
+
right: corner.x === "right" ? screen.width - host.right : null,
|
|
42
|
+
bottom: corner.y === "bottom" ? screen.height - host.bottom : null,
|
|
43
|
+
left: corner.x === "left" ? host.left : null,
|
|
44
|
+
}),
|
|
45
|
+
launcherInset: inset({
|
|
46
|
+
top: corner.y === "top" ? launcher.top - host.top : null,
|
|
47
|
+
right: corner.x === "right" ? host.right - (launcher.left + launcher.width) : null,
|
|
48
|
+
bottom: corner.y === "bottom" ? host.bottom - (launcher.top + launcher.height) : null,
|
|
49
|
+
left: corner.x === "left" ? launcher.left - host.left : null,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An `inset` shorthand; a null side is `auto`, so the opposite one pins it. */
|
|
55
|
+
function inset(sides: {
|
|
56
|
+
top: number | null;
|
|
57
|
+
right: number | null;
|
|
58
|
+
bottom: number | null;
|
|
59
|
+
left: number | null;
|
|
60
|
+
}): string {
|
|
61
|
+
const side = (value: number | null): string =>
|
|
62
|
+
value === null ? "auto" : `${Math.round(value)}px`;
|
|
63
|
+
return `${side(sides.top)} ${side(sides.right)} ${side(sides.bottom)} ${side(sides.left)}`;
|
|
64
|
+
}
|
package/src/ui/run_notice.ts
CHANGED
|
@@ -3,10 +3,23 @@
|
|
|
3
3
|
* skill loaded), rendered inline between turns.
|
|
4
4
|
*
|
|
5
5
|
* Distinct from a tool card, which reports work the agent asked for and
|
|
6
|
-
* settles, and from an error, which is a failure. A notice never settles
|
|
7
|
-
* no action
|
|
6
|
+
* settles, and from an error, which is a failure. A notice never settles and
|
|
7
|
+
* takes no action of its own.
|
|
8
|
+
*
|
|
9
|
+
* It may carry exactly one control, and only ever an undo. That is a narrower
|
|
10
|
+
* rule than "no controls", which is what this said until the agent could move
|
|
11
|
+
* the panel it speaks from: something that rearranges the user's window without
|
|
12
|
+
* being asked has to be both visible and reversible, and a notice is already
|
|
13
|
+
* the surface that says what the run did. Anything the user has to *decide* is
|
|
14
|
+
* a confirmation card instead -- the difference is that this reports something
|
|
15
|
+
* already done.
|
|
8
16
|
*/
|
|
9
|
-
export function renderRunNotice(
|
|
17
|
+
export function renderRunNotice(
|
|
18
|
+
icon: string,
|
|
19
|
+
text: string,
|
|
20
|
+
kind: string,
|
|
21
|
+
undo?: { readonly label: string; readonly onActivate: () => void },
|
|
22
|
+
): HTMLDivElement {
|
|
10
23
|
const notice = document.createElement("div");
|
|
11
24
|
notice.className = `run-notice run-notice--${kind}`;
|
|
12
25
|
notice.setAttribute("part", `run-notice run-notice-${kind}`);
|
|
@@ -28,5 +41,21 @@ export function renderRunNotice(icon: string, text: string, kind: string): HTMLD
|
|
|
28
41
|
label.textContent = text;
|
|
29
42
|
|
|
30
43
|
notice.append(glyph, label);
|
|
44
|
+
|
|
45
|
+
if (undo !== undefined) {
|
|
46
|
+
const button = document.createElement("button");
|
|
47
|
+
button.type = "button";
|
|
48
|
+
button.className = "run-notice-undo";
|
|
49
|
+
button.setAttribute("part", "run-notice-undo");
|
|
50
|
+
button.textContent = undo.label;
|
|
51
|
+
button.addEventListener("click", () => {
|
|
52
|
+
// One use. The state it restores is the state as it was when the notice
|
|
53
|
+
// was written, so offering it twice would put back something that has
|
|
54
|
+
// since moved again.
|
|
55
|
+
button.disabled = true;
|
|
56
|
+
undo.onActivate();
|
|
57
|
+
});
|
|
58
|
+
notice.append(button);
|
|
59
|
+
}
|
|
31
60
|
return notice;
|
|
32
61
|
}
|