@artooi/ag-ui-web-component 0.23.0 → 0.24.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 +149 -36
- package/README.md +140 -9
- package/dist/ag-ui-web-component.bundle.js +113 -122
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +71 -85
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +105 -127
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +24 -30
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +9 -14
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +38 -27
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -15
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +10 -9
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +11 -20
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +8 -8
- package/dist/core/transcribe_audio.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +15 -18
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/core/utils.d.ts +4 -6
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +22 -30
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/dom_driver.d.ts +7 -7
- package/dist/dom/native_setter.d.ts +2 -2
- package/dist/dom/native_setter.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +357 -383
- package/dist/index.js.map +2 -2
- package/dist/skills/fill_template.d.ts +4 -5
- package/dist/skills/fill_template.d.ts.map +1 -1
- package/dist/skills/parse_skills.d.ts.map +1 -1
- package/dist/skills/skill.d.ts +7 -8
- package/dist/skills/skill.d.ts.map +1 -1
- package/dist/tools/client_tool_registry.d.ts +2 -2
- package/dist/tools/page_action_tools.d.ts +7 -10
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/tools/page_state.d.ts +5 -8
- package/dist/tools/page_state.d.ts.map +1 -1
- package/dist/tools/route_map.d.ts +7 -10
- package/dist/tools/route_map.d.ts.map +1 -1
- package/dist/ui/approval_card.d.ts +15 -20
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/attach_copy_buttons.d.ts +4 -10
- package/dist/ui/attach_copy_buttons.d.ts.map +1 -1
- package/dist/ui/attachment_chips.d.ts +7 -10
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +6 -6
- package/dist/ui/checkpoint_menu.d.ts +7 -8
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +10 -15
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/question_card.d.ts +12 -15
- package/dist/ui/question_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -7
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +8 -8
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +21 -34
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/run_notice.d.ts +5 -7
- package/dist/ui/run_notice.d.ts.map +1 -1
- package/dist/ui/skills_menu.d.ts +4 -5
- package/dist/ui/skills_menu.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/thoughts_block.d.ts +9 -11
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +6 -5
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +17 -25
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +6 -12
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +10 -11
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +74 -87
- package/src/core/ag_ui_chat.ts +313 -269
- package/src/core/agui_client.ts +60 -71
- package/src/core/attachment.ts +9 -14
- package/src/core/conversation_store.ts +55 -33
- package/src/core/create_http_agent.ts +18 -22
- package/src/core/remote_conversation_store.ts +28 -15
- package/src/core/run_index.ts +14 -23
- package/src/core/transcribe_audio.ts +9 -10
- package/src/core/upload_attachment.ts +18 -21
- package/src/core/utils.ts +4 -6
- package/src/dom/animations.ts +33 -43
- package/src/dom/dom_driver.ts +7 -7
- package/src/dom/native_setter.ts +11 -12
- package/src/index.ts +3 -0
- package/src/skills/fill_template.ts +4 -5
- package/src/skills/parse_skills.ts +3 -4
- package/src/skills/skill.ts +7 -8
- package/src/tools/client_tool_registry.ts +2 -2
- package/src/tools/page_action_tools.ts +12 -15
- package/src/tools/page_state.ts +5 -8
- package/src/tools/route_map.ts +15 -19
- package/src/ui/approval_card.ts +15 -20
- package/src/ui/attach_copy_buttons.ts +9 -18
- package/src/ui/attachment_chips.ts +7 -10
- package/src/ui/attachment_tray.ts +6 -6
- package/src/ui/checkpoint_menu.ts +7 -8
- package/src/ui/confirmation_card.ts +10 -15
- package/src/ui/question_card.ts +12 -15
- package/src/ui/relative_time.ts +5 -7
- package/src/ui/render_markdown.ts +25 -51
- package/src/ui/resize_handle.ts +25 -38
- package/src/ui/run_notice.ts +9 -12
- package/src/ui/skills_menu.ts +4 -5
- package/src/ui/styles.ts +92 -101
- package/src/ui/thoughts_block.ts +11 -13
- package/src/ui/thread_drawer.ts +6 -5
- package/src/ui/tool_call_card.ts +22 -32
- package/src/ui/ui_strings.ts +6 -12
- package/src/ui/voice_input.ts +10 -11
- package/src/version.ts +1 -1
package/src/tools/route_map.ts
CHANGED
|
@@ -7,9 +7,8 @@ export interface Route {
|
|
|
7
7
|
readonly id: string;
|
|
8
8
|
/**
|
|
9
9
|
* The URL path to navigate to. May contain `:name` placeholders for dynamic
|
|
10
|
-
* segments (e.g. `/admin/shop/book/:pk/change/`)
|
|
11
|
-
*
|
|
12
|
-
* parameterised — few pages have a truly static path.
|
|
10
|
+
* segments (e.g. `/admin/shop/book/:pk/change/`), which the agent fills via
|
|
11
|
+
* `navigate_to_route`'s `params` argument.
|
|
13
12
|
*/
|
|
14
13
|
readonly path: string;
|
|
15
14
|
/** Human label shown to the agent. */
|
|
@@ -33,20 +32,19 @@ const PATH_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
|
33
32
|
|
|
34
33
|
/** The `:name` path-parameter names declared in a path template, in order. */
|
|
35
34
|
function pathParamNames(path: string): string[] {
|
|
36
|
-
//
|
|
37
|
-
// the group
|
|
38
|
-
//
|
|
39
|
-
// unreachable branch no test can ever cover.
|
|
35
|
+
// Slice the ":" off the whole match rather than read the capture group: under
|
|
36
|
+
// `noUncheckedIndexedAccess`, indexing the group forces an `undefined` guard
|
|
37
|
+
// on a case the regex cannot produce, which no test can cover.
|
|
40
38
|
return [...path.matchAll(PATH_PARAM_RE)].map((match) => match[0].slice(1));
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
|
-
* Substitute `:name` placeholders in
|
|
42
|
+
* Substitute `:name` placeholders in `path` from `params`.
|
|
45
43
|
*
|
|
46
|
-
* Returns the concrete path plus the params
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* Returns the concrete path plus the params no placeholder consumed, for the
|
|
45
|
+
* caller to append as a query string. Throws (naming `routeId`) when a declared
|
|
46
|
+
* path param is missing or empty — a half-filled path must never be navigated
|
|
47
|
+
* to.
|
|
50
48
|
*/
|
|
51
49
|
function fillPath(
|
|
52
50
|
routeId: string,
|
|
@@ -79,13 +77,11 @@ function withQuery(path: string, params: Record<string, unknown>): string {
|
|
|
79
77
|
* The built-in `route.*` tools, bound to live getters so a host can set
|
|
80
78
|
* `routeMap` / `navigate` before or after mount.
|
|
81
79
|
*
|
|
82
|
-
* `list_routes` is read-only and advertises each route's
|
|
83
|
-
*
|
|
84
|
-
* `x-navigates` so an MPA reload
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* routes client-side instead and the run loop simply continues — see
|
|
88
|
-
* `AgUiChat`'s execute path.
|
|
80
|
+
* `list_routes` is read-only and advertises each route's `pathParams` so the
|
|
81
|
+
* model knows what to supply. `navigate_to_route` fills those params, appends
|
|
82
|
+
* the rest as a query string, and is stamped `x-navigates` so an MPA reload
|
|
83
|
+
* checkpoints and resumes; with a host `navigate(path)` callback (an SPA) it
|
|
84
|
+
* routes client-side and the run loop continues instead.
|
|
89
85
|
*/
|
|
90
86
|
export function createRouteTools(
|
|
91
87
|
getRouteMap: () => RouteMap,
|
package/src/ui/approval_card.ts
CHANGED
|
@@ -25,9 +25,8 @@ function actionButton(modifier: string, label: string): HTMLButtonElement {
|
|
|
25
25
|
/** Options for {@link requestApproval}. */
|
|
26
26
|
export interface ApprovalOptions {
|
|
27
27
|
/**
|
|
28
|
-
* Aborting this signal resolves the card as
|
|
29
|
-
* `data-resolved="denied"`
|
|
30
|
-
* pending approval when the user cancels the whole run.
|
|
28
|
+
* Aborting this signal resolves the card as denied, with buttons disabled and
|
|
29
|
+
* `data-resolved="denied"` — how a Stop control dismisses a pending approval.
|
|
31
30
|
*/
|
|
32
31
|
signal?: AbortSignal;
|
|
33
32
|
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
@@ -36,13 +35,11 @@ export interface ApprovalOptions {
|
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* A fully custom renderer for a server-side-tool approval, set via
|
|
39
|
-
* `AgUiChat.approvalRenderer`. Receives the {@link ApprovalRequest}
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* anything else. See the `strings` / `::part()` seams for styling the built-in
|
|
45
|
-
* card instead of replacing it.
|
|
38
|
+
* `AgUiChat.approvalRenderer`. Receives the {@link ApprovalRequest} and an
|
|
39
|
+
* `AbortSignal` that fires when the run is stopped, and resolves `true` to
|
|
40
|
+
* approve or `false` to deny. Replaces the built-in {@link requestApproval}
|
|
41
|
+
* card entirely, the host owning the DOM; to restyle the built-in card instead,
|
|
42
|
+
* use the `strings` and `::part()` seams.
|
|
46
43
|
*/
|
|
47
44
|
export type ApprovalRenderer = (
|
|
48
45
|
request: ApprovalRequest,
|
|
@@ -50,17 +47,15 @@ export type ApprovalRenderer = (
|
|
|
50
47
|
) => Promise<boolean>;
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
|
-
* Append an inline
|
|
54
|
-
*
|
|
50
|
+
* Append an inline approval card to `host` and resolve `true` to run a gated
|
|
51
|
+
* server-side tool or `false` to deny it.
|
|
55
52
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* transcript as a resolved record (buttons disabled, `data-resolved` set)
|
|
63
|
-
* rather than vanishing.
|
|
53
|
+
* Distinct from the client-tool confirmation card
|
|
54
|
+
* ({@link requestConfirmation}): a gated server tool defers instead of
|
|
55
|
+
* executing, so the run finishes on an AG-UI interrupt the client answers with
|
|
56
|
+
* `resume[]`, and this is the browser half of that loop. Unlike the
|
|
57
|
+
* confirmation card, this one stays in the transcript as a resolved record,
|
|
58
|
+
* buttons disabled and `data-resolved` set.
|
|
64
59
|
*/
|
|
65
60
|
export function requestApproval(
|
|
66
61
|
host: Node & ParentNode,
|
|
@@ -6,17 +6,11 @@ const CONFIRM_MS = 1500;
|
|
|
6
6
|
/**
|
|
7
7
|
* Give every fenced code block in `root` a copy button.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* made harder than the page around it.
|
|
9
|
+
* Call only on finished bubbles: the streaming bubble reassigns its `innerHTML`
|
|
10
|
+
* on every delta, so a button attached mid-stream is discarded and rebuilt for
|
|
11
|
+
* each one.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
* `innerHTML` on every delta, so a button attached mid-stream would be
|
|
16
|
-
* discarded and rebuilt for each one; waiting until the turn ends costs
|
|
17
|
-
* nothing and does that work once.
|
|
18
|
-
*
|
|
19
|
-
* Idempotent: a bubble already processed is skipped, so a re-render or a second
|
|
13
|
+
* Idempotent — a bubble already processed is skipped, so a re-render or a second
|
|
20
14
|
* call cannot stack buttons.
|
|
21
15
|
*/
|
|
22
16
|
export function attachCopyButtons(root: ParentNode, strings: UiStrings): void {
|
|
@@ -43,9 +37,8 @@ function button(code: Element, strings: UiStrings): HTMLButtonElement {
|
|
|
43
37
|
copy.setAttribute("aria-label", strings.copyCode);
|
|
44
38
|
copy.addEventListener("click", () => {
|
|
45
39
|
void writeText(text).then((ok) => {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// finds out somewhere else entirely.
|
|
40
|
+
// Report the real outcome: a button that always claims success leaves the
|
|
41
|
+
// reader pasting stale clipboard content.
|
|
49
42
|
confirm(copy, ok ? strings.copied : strings.copyFailed, strings);
|
|
50
43
|
});
|
|
51
44
|
});
|
|
@@ -53,11 +46,9 @@ function button(code: Element, strings: UiStrings): HTMLButtonElement {
|
|
|
53
46
|
}
|
|
54
47
|
|
|
55
48
|
/**
|
|
56
|
-
* Copy `text`, reporting whether it landed.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* absent in some embeddings, so its absence is an ordinary outcome rather than
|
|
60
|
-
* an error worth throwing at the host page.
|
|
49
|
+
* Copy `text`, reporting whether it landed. The Clipboard API needs a secure
|
|
50
|
+
* context and a user gesture and is absent in some embeddings, so failure is an
|
|
51
|
+
* ordinary outcome rather than an error to throw at the host page.
|
|
61
52
|
*/
|
|
62
53
|
async function writeText(text: string): Promise<boolean> {
|
|
63
54
|
const clipboard = navigator.clipboard;
|
|
@@ -2,10 +2,10 @@ import { ICON_FILE, ICON_FILE_IMAGE, ICON_FILE_PDF, ICON_FILE_TEXT } from "../co
|
|
|
2
2
|
import type { AttachmentRef } from "../core/attachment.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Render the read-only attachment chips
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Render the read-only attachment chips on a sent user message bubble and on
|
|
6
|
+
* restored history — one chip per ref with a type icon, filename, and human
|
|
7
|
+
* size. Static: no progress and no remove, both of which live in the composer
|
|
8
|
+
* tray.
|
|
9
9
|
*/
|
|
10
10
|
export function renderAttachmentChips(refs: readonly AttachmentRef[]): HTMLDivElement {
|
|
11
11
|
const list = document.createElement("div");
|
|
@@ -45,12 +45,9 @@ function renderChip(ref: AttachmentRef): HTMLDivElement {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* A coarse type mark for a chip — image, PDF, text document, or generic file —
|
|
48
|
-
* as
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* composer's SVG send, attach and mic buttons: emoji render at a different
|
|
52
|
-
* optical weight, vary by platform, and take neither the glyph size nor
|
|
53
|
-
* `currentColor`, so the two never matched.
|
|
48
|
+
* as inline SVG markup rather than a character, so it matches the composer's
|
|
49
|
+
* SVG buttons in weight and takes the glyph size and `currentColor`, which
|
|
50
|
+
* emoji do not.
|
|
54
51
|
*
|
|
55
52
|
* The MIME string only selects among author-written constants and is never
|
|
56
53
|
* interpolated into one, so the result is safe to assign as markup.
|
|
@@ -36,13 +36,13 @@ interface TrayItem {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* The composer's pending-attachments tray: a chip per picked file with a
|
|
39
|
-
* progress bar while it uploads, settling to a ready chip
|
|
40
|
-
* ref
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* progress bar while it uploads, settling to a ready chip holding the durable
|
|
40
|
+
* ref or an error chip with retry. A stateful view like {@link ThreadDrawer}:
|
|
41
|
+
* the host appends {@link element}, calls {@link add} on pick or drop, reads
|
|
42
|
+
* {@link readyRefs} on send, and clears it.
|
|
43
43
|
*
|
|
44
|
-
* Client-side size
|
|
45
|
-
* uploading
|
|
44
|
+
* Client-side size and type guards reject a bad file into an error chip without
|
|
45
|
+
* uploading, for instant feedback, but the server stays the authority.
|
|
46
46
|
*/
|
|
47
47
|
export class AttachmentTray {
|
|
48
48
|
/** The tray root; append above the input row. Hidden while empty. */
|
|
@@ -8,16 +8,15 @@ export type CheckpointVerb = "resume" | "fork";
|
|
|
8
8
|
/**
|
|
9
9
|
* The checkpoint panel: continuable runs, each offering **resume** or **fork**.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* would make "resume" look like "open", which it isn't: resuming starts a new
|
|
11
|
+
* Deliberately separate from the thread drawer: a thread is a conversation you
|
|
12
|
+
* switch to, a checkpoint is a run you continue from, and one thread holds
|
|
13
|
+
* many. One list would make "resume" read as "open", when resuming starts a new
|
|
15
14
|
* run seeded from a snapshot.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The host feeds only rows the server marked `continuable`, a run without a
|
|
17
|
+
* snapshot having nothing to resume from. Pure DOM, like {@link SkillsMenu}:
|
|
18
|
+
* append {@link element}, toggle it, feed {@link setRuns}, act on
|
|
19
|
+
* {@link onPick}.
|
|
21
20
|
*/
|
|
22
21
|
export class CheckpointMenu {
|
|
23
22
|
/** The panel root. Append to the chat shell; hidden until opened. */
|
|
@@ -24,9 +24,9 @@ function actionButton(modifier: string, label: string): HTMLButtonElement {
|
|
|
24
24
|
/** Options for {@link requestConfirmation}. */
|
|
25
25
|
export interface ConfirmationOptions {
|
|
26
26
|
/**
|
|
27
|
-
* Aborting this signal resolves the card as declined
|
|
28
|
-
* `data-resolved="declined"`
|
|
29
|
-
*
|
|
27
|
+
* Aborting this signal resolves the card as declined, with buttons disabled
|
|
28
|
+
* and `data-resolved="declined"` — how a Stop control dismisses a pending
|
|
29
|
+
* confirmation.
|
|
30
30
|
*/
|
|
31
31
|
signal?: AbortSignal;
|
|
32
32
|
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
@@ -34,20 +34,15 @@ export interface ConfirmationOptions {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Append an inline confirmation card to
|
|
38
|
-
* resolve
|
|
37
|
+
* Append an inline confirmation card to `host` (the chat message list) and
|
|
38
|
+
* resolve `true` on confirm, `false` on cancel.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* steals focus from the page. Resolves ``true`` on confirm, ``false`` on
|
|
43
|
-
* cancel.
|
|
40
|
+
* The card lives in the transcript rather than in a modal overlay, so it reads
|
|
41
|
+
* after the assistant's explanation and never steals focus from the page.
|
|
44
42
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* everything else. The tool card this gates is that record — it settles to
|
|
49
|
-
* `done` or `declined` and carries the decision. Leaving the spent form in
|
|
50
|
-
* place made an answered question read as outstanding.
|
|
43
|
+
* Answering it removes it: the record of the decision belongs to the tool card
|
|
44
|
+
* this gates, which settles to `done` or `declined` and carries it. A spent
|
|
45
|
+
* form left in place reads as still outstanding.
|
|
51
46
|
*/
|
|
52
47
|
export function requestConfirmation(
|
|
53
48
|
host: Node & ParentNode,
|
package/src/ui/question_card.ts
CHANGED
|
@@ -19,9 +19,8 @@ export interface QuestionRequest {
|
|
|
19
19
|
/** Options for {@link requestQuestion}. */
|
|
20
20
|
export interface QuestionOptions {
|
|
21
21
|
/**
|
|
22
|
-
* Aborting this signal resolves the card with an empty answer
|
|
23
|
-
*
|
|
24
|
-
* the user cancels the whole run.
|
|
22
|
+
* Aborting this signal resolves the card with an empty answer and disabled
|
|
23
|
+
* fields — how a Stop control dismisses an open question.
|
|
25
24
|
*/
|
|
26
25
|
signal?: AbortSignal;
|
|
27
26
|
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
@@ -32,11 +31,9 @@ export interface QuestionOptions {
|
|
|
32
31
|
* A fully custom renderer for the `ask_user` question, set via
|
|
33
32
|
* `AgUiChat.askUserRenderer`. Receives the parsed {@link QuestionRequest} and an
|
|
34
33
|
* `AbortSignal` that fires when the run is stopped, and resolves with the user's
|
|
35
|
-
* answer
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* else. See the `strings` / `::part()` seams for styling the built-in card
|
|
39
|
-
* instead of replacing it.
|
|
34
|
+
* answer; an empty string means no answer. Replaces the built-in
|
|
35
|
+
* {@link requestQuestion} card entirely — see {@link ApprovalRenderer} for the
|
|
36
|
+
* same seam on approvals.
|
|
40
37
|
*/
|
|
41
38
|
export type QuestionRenderer = (
|
|
42
39
|
request: QuestionRequest,
|
|
@@ -54,14 +51,14 @@ function answerInput(placeholder: string): HTMLInputElement {
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
/**
|
|
57
|
-
* Append an inline
|
|
58
|
-
*
|
|
54
|
+
* Append an inline question card to `host` and resolve with the user's answer —
|
|
55
|
+
* the browser half of the built-in `ask_user` frontend tool.
|
|
59
56
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
57
|
+
* Collects a typed answer rather than the yes/no the confirmation and approval
|
|
58
|
+
* cards return: a radio pick from `options`, or free text when `allowCustom` is
|
|
59
|
+
* set or no `options` are given. Stays in the transcript as a resolved record,
|
|
60
|
+
* controls disabled and `data-resolved` set; a Stop while it is open resolves
|
|
61
|
+
* it with an empty string.
|
|
65
62
|
*/
|
|
66
63
|
export function requestQuestion(
|
|
67
64
|
host: Node & ParentNode,
|
package/src/ui/relative_time.ts
CHANGED
|
@@ -4,13 +4,11 @@ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
|
4
4
|
* A compact relative timestamp for a thread row — e.g. `"just now"`, `"5m ago"`,
|
|
5
5
|
* `"3h ago"`, `"2d ago"`, `"4w ago"`.
|
|
6
6
|
*
|
|
7
|
-
* `now` is injectable so callers
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* {@link UiStrings} (the `{n}` token is filled in here) so a localized host
|
|
13
|
-
* translates them; the bucketing stays integer-rounded and locale-neutral.
|
|
7
|
+
* `now` is injectable so callers and tests can pin the reference point. A
|
|
8
|
+
* future timestamp (clock skew) and a non-finite one (an unparseable or missing
|
|
9
|
+
* `updated_at`, arriving as `NaN`) both read as `justNow`, rather than
|
|
10
|
+
* rendering a nonsense age. Unit words come from {@link UiStrings} with `{n}`
|
|
11
|
+
* filled in here; the bucketing stays integer-rounded and locale-neutral.
|
|
14
12
|
*/
|
|
15
13
|
export function relativeTime(
|
|
16
14
|
timestamp: number,
|
|
@@ -1,52 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
// vitest.config.ts,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// **Root cause, upstream, and both are refusing to move.** DOMPurify 3.4.8
|
|
8
|
-
// changed one line, reading the tag name through
|
|
9
|
-
// `lookupGetter(Node.prototype, "nodeName")` instead of `currentNode.nodeName`
|
|
10
|
-
// (cure53/DOMPurify 3.4.7...3.4.8). happy-dom defines an *own* `nodeName`
|
|
11
|
-
// getter on **both** `Node.prototype` (returning `""`) and `Element.prototype`,
|
|
12
|
-
// so grabbing the base one defeats the dispatch and every element resolves to
|
|
13
|
-
// `tagName === ""`. Not in ALLOWED_TAGS, so the wrapper is stripped and its
|
|
14
|
-
// children are re-inserted as clones the NodeIterator never revisits — which is
|
|
15
|
-
// why nested payloads come back *entirely* unsanitised. Real browsers and jsdom
|
|
16
|
-
// define `nodeName` only on `Node.prototype` and are unaffected.
|
|
17
|
-
//
|
|
18
|
-
// happy-dom bug: capricorn86/happy-dom#2182 (open)
|
|
19
|
-
// working fix: capricorn86/happy-dom#2183 (closed, never merged)
|
|
20
|
-
// DOMPurify's answer: cure53/DOMPurify#1457, #1496 (closed, wontfix —
|
|
21
|
-
// "happy-dom is not supported")
|
|
22
|
-
//
|
|
23
|
-
// ⚠ DOMPurify's README names happy-dom as **not safe**: combining them "will
|
|
24
|
-
// likely lead to XSS". jsdom is the only non-browser DOM it supports. So the
|
|
25
|
-
// old exact pin at 3.4.7 was never the safety it looked like — cure53 notes it
|
|
26
|
-
// "doesn't really work, it just appears so". Running these assertions in
|
|
27
|
-
// Chromium is the fix; the pin was a placebo.
|
|
28
|
-
//
|
|
29
|
-
// ⚠ If you ever move these assertions back under happy-dom to make them faster,
|
|
30
|
-
// you remove the only check that this module does anything at all.
|
|
1
|
+
// This module's sanitisation is verified only by the `chromium` project in
|
|
2
|
+
// vitest.config.ts, and must stay there. DOMPurify 3.4.8+ silently sanitises
|
|
3
|
+
// nothing under happy-dom (every element resolves to an empty tag name), so a
|
|
4
|
+
// happy-dom-only suite goes green while this module strips nothing at all.
|
|
5
|
+
// Moving those assertions back under happy-dom for speed removes the only check
|
|
6
|
+
// that this module does anything. CLAUDE.md records the upstream root cause.
|
|
31
7
|
import DOMPurify from "dompurify";
|
|
32
8
|
import { Marked } from "marked";
|
|
33
9
|
|
|
34
|
-
//
|
|
35
|
-
// singleton
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// construction would re-pay setup on every streaming re-render.
|
|
10
|
+
// A local parser instance, so configuration never leaks into the shared
|
|
11
|
+
// `marked` singleton a host app's deduped copy would share. Built once at module
|
|
12
|
+
// scope and never mutated; per-call construction would re-pay setup on every
|
|
13
|
+
// streaming re-render.
|
|
39
14
|
const parser = new Marked({ gfm: true, breaks: true });
|
|
40
15
|
|
|
41
|
-
// Conservative allowlist for assistant chat content
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
16
|
+
// Conservative allowlist for assistant chat content. Rendering untrusted
|
|
17
|
+
// model/tool output as HTML is an XSS surface and this sanitiser is the only
|
|
18
|
+
// thing standing in front of it, so `iframe`, `style` and all scripting stay
|
|
19
|
+
// out.
|
|
45
20
|
//
|
|
46
|
-
// `img` is excluded by default: a model-controlled
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// that trust their content can opt back in via `allowImages`.
|
|
21
|
+
// `img` is excluded by default: a model-controlled image URL is fetched with no
|
|
22
|
+
// user interaction, turning prompt-injected page data into a zero-click
|
|
23
|
+
// exfiltration channel. Hosts that trust their content opt in via `allowImages`.
|
|
50
24
|
const ALLOWED_TAGS = [
|
|
51
25
|
"a",
|
|
52
26
|
"p",
|
|
@@ -89,9 +63,9 @@ const ALLOWED_ATTR_WITH_IMAGES = [...ALLOWED_ATTR, "src", "alt", "width", "heigh
|
|
|
89
63
|
/** Options for {@link renderMarkdown}. */
|
|
90
64
|
export interface RenderMarkdownOptions {
|
|
91
65
|
/**
|
|
92
|
-
* Permit `<img>` tags (and their `src`/`alt`/`width`/`height` attributes)
|
|
93
|
-
*
|
|
94
|
-
*
|
|
66
|
+
* Permit `<img>` tags (and their `src`/`alt`/`width`/`height` attributes) in
|
|
67
|
+
* the sanitised output. Off by default; see the exfiltration note on the
|
|
68
|
+
* allowlist above. Only enable for trusted content sources.
|
|
95
69
|
*/
|
|
96
70
|
readonly allowImages?: boolean;
|
|
97
71
|
}
|
|
@@ -99,13 +73,13 @@ export interface RenderMarkdownOptions {
|
|
|
99
73
|
/**
|
|
100
74
|
* Render markdown (and any embedded raw HTML) to a sanitised HTML string.
|
|
101
75
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* {@link ALLOWED_ATTR}
|
|
105
|
-
*
|
|
76
|
+
* Markdown syntax and literal HTML share one path: `marked` emits HTML, then
|
|
77
|
+
* DOMPurify strips everything outside {@link ALLOWED_TAGS} /
|
|
78
|
+
* {@link ALLOWED_ATTR} — scripts, event handlers, `javascript:` URLs. Links are
|
|
79
|
+
* hardened with `target="_blank"` and `rel="noopener noreferrer"`.
|
|
106
80
|
*
|
|
107
81
|
* The result is trimmed so a single-paragraph message round-trips to clean
|
|
108
|
-
* `textContent
|
|
82
|
+
* `textContent`, without the wrapping paragraph's trailing newline.
|
|
109
83
|
*/
|
|
110
84
|
export function renderMarkdown(text: string, options?: RenderMarkdownOptions): string {
|
|
111
85
|
const allowImages = options?.allowImages === true;
|
package/src/ui/resize_handle.ts
CHANGED
|
@@ -7,10 +7,8 @@ export interface ResizeAnchor {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* What the current placement allows: both axes, width only, or nothing.
|
|
11
|
-
*
|
|
12
|
-
* Which *corner* the grip sits on is not part of this — that follows the host's
|
|
13
|
-
* layout, which the component measures rather than assumes.
|
|
10
|
+
* What the current placement allows: both axes, width only, or nothing. Which
|
|
11
|
+
* corner the grip sits on is separate, and is measured rather than assumed.
|
|
14
12
|
*/
|
|
15
13
|
export type ResizeAxis = "none" | "width" | "both";
|
|
16
14
|
|
|
@@ -31,22 +29,16 @@ export interface PanelRect {
|
|
|
31
29
|
/** What the handle needs from its host to do its job. */
|
|
32
30
|
export interface ResizeOptions {
|
|
33
31
|
/**
|
|
34
|
-
* Which axes the current placement allows, read
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* once at construction, a handle built while floating kept its axes after the
|
|
38
|
-
* host switched to a docked or full-bleed layout.
|
|
32
|
+
* Which axes the current placement allows, read per interaction. A getter
|
|
33
|
+
* because `placement` is a live attribute, and a value read at construction
|
|
34
|
+
* would survive the host switching to a docked or full-bleed layout.
|
|
39
35
|
*/
|
|
40
36
|
readonly axis: () => ResizeAxis;
|
|
41
37
|
/**
|
|
42
|
-
* Which edges the layout is holding still, measured at the moment of the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* bottom-right and an embedded one goes wherever the host's own CSS puts it —
|
|
47
|
-
* the demo playground drops it in a right-aligned flex slot, so "embedded"
|
|
48
|
-
* alone says nothing. Guessing produced a panel that shrank when dragged
|
|
49
|
-
* outward and travelled by its opposite corner.
|
|
38
|
+
* Which edges the layout is holding still, measured at the moment of the drag
|
|
39
|
+
* rather than derived from `placement`: a floating panel is pinned
|
|
40
|
+
* bottom-right, while an embedded one goes wherever the host's CSS puts it,
|
|
41
|
+
* so `placement` alone cannot answer the question.
|
|
50
42
|
*/
|
|
51
43
|
readonly anchor: () => ResizeAnchor;
|
|
52
44
|
/** The panel's current bounding box. */
|
|
@@ -66,25 +58,20 @@ const MIN_HEIGHT = 240;
|
|
|
66
58
|
/**
|
|
67
59
|
* A drag handle that resizes the chat panel.
|
|
68
60
|
*
|
|
69
|
-
*
|
|
70
|
-
* `--ag-ui-height` to: themeable by the page, immovable by the person reading a
|
|
71
|
-
* long answer in a 380px column.
|
|
72
|
-
*
|
|
73
|
-
* **The new size is measured from the edge that is not moving, never from a
|
|
74
|
-
* delta**, and which edge that is is **measured rather than assumed**. A
|
|
75
|
-
* floating panel is pinned bottom-right; an embedded one goes wherever the
|
|
76
|
-
* host's CSS puts it, so `placement` does not answer the question. Getting it
|
|
77
|
-
* wrong is very visible: the panel shrinks when dragged outward and travels by
|
|
78
|
-
* its opposite corner.
|
|
61
|
+
* Two rules keep it correct, and both are easy to break invisibly:
|
|
79
62
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
63
|
+
* - The new size is measured from the edge that is *not* moving, never from a
|
|
64
|
+
* delta, and that edge is measured rather than assumed. Getting it wrong is
|
|
65
|
+
* very visible: the panel shrinks when dragged outward and travels by its
|
|
66
|
+
* opposite corner.
|
|
67
|
+
* - It writes the `--ag-ui-width` / `--ag-ui-height` custom properties, not
|
|
68
|
+
* inline `width` / `height`. The placement rules set those same properties,
|
|
69
|
+
* so an inline dimension would outrank and fight them — a sidebar would keep
|
|
70
|
+
* its dragged width after switching to fullscreen. Writing the property
|
|
71
|
+
* leaves placement the final say.
|
|
84
72
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* mount with.
|
|
73
|
+
* Axes and anchor are both read per interaction, so a runtime `placement`
|
|
74
|
+
* change takes effect at once.
|
|
88
75
|
*/
|
|
89
76
|
export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
90
77
|
const handle = document.createElement("div");
|
|
@@ -116,7 +103,7 @@ export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
|
116
103
|
if (axis === "none") {
|
|
117
104
|
return;
|
|
118
105
|
}
|
|
119
|
-
// Captured once: the
|
|
106
|
+
// Captured once: the pinned edges cannot move during the drag, and reading
|
|
120
107
|
// them live would chase the panel as it resizes.
|
|
121
108
|
const anchor = options.anchor();
|
|
122
109
|
const rect = options.rect();
|
|
@@ -140,7 +127,7 @@ export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
|
140
127
|
event.preventDefault();
|
|
141
128
|
});
|
|
142
129
|
|
|
143
|
-
// Keyboard parity
|
|
130
|
+
// Keyboard parity: a pointer-only resize is unreachable without a mouse, and
|
|
144
131
|
// this control has no equivalent elsewhere in the UI.
|
|
145
132
|
handle.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
146
133
|
const axis = options.axis();
|
|
@@ -150,8 +137,8 @@ export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
|
150
137
|
const anchor = options.anchor();
|
|
151
138
|
const rect = options.rect();
|
|
152
139
|
const step = event.shiftKey ? 64 : 16;
|
|
153
|
-
// An arrow moves the grip,
|
|
154
|
-
//
|
|
140
|
+
// An arrow moves the grip, so whether it grows or shrinks depends on which
|
|
141
|
+
// side the grip is on — the asymmetry the pointer path also handles.
|
|
155
142
|
const outward = anchor.x === "right" ? -1 : 1;
|
|
156
143
|
const width = rect.right - rect.left;
|
|
157
144
|
const height = rect.bottom - rect.top;
|
package/src/ui/run_notice.ts
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A muted one-line notice about something the
|
|
3
|
-
*
|
|
2
|
+
* A muted one-line notice about something the run did (history condensed, a
|
|
3
|
+
* skill loaded), rendered inline between turns.
|
|
4
4
|
*
|
|
5
|
-
* Distinct from a tool card
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* takes an action, and carries no controls — so it stays visually quiet and out
|
|
9
|
-
* of the way of the conversation it annotates.
|
|
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, takes
|
|
7
|
+
* no action, and carries no controls.
|
|
10
8
|
*/
|
|
11
9
|
export function renderRunNotice(icon: string, text: string, kind: string): HTMLDivElement {
|
|
12
10
|
const notice = document.createElement("div");
|
|
13
11
|
notice.className = `run-notice run-notice--${kind}`;
|
|
14
12
|
notice.setAttribute("part", `run-notice run-notice-${kind}`);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// utterance, which is right for an annotation about turns already spoken.
|
|
13
|
+
// status, not alert: informational, so it must not interrupt a screen reader
|
|
14
|
+
// mid-sentence. Polite announcements land after the current utterance.
|
|
18
15
|
notice.setAttribute("role", "status");
|
|
19
16
|
|
|
20
17
|
const glyph = document.createElement("span");
|
|
21
18
|
glyph.className = "run-notice-icon";
|
|
22
19
|
glyph.setAttribute("part", "run-notice-icon");
|
|
23
20
|
glyph.textContent = icon;
|
|
24
|
-
// Decorative: the adjacent text already says what happened,
|
|
25
|
-
//
|
|
21
|
+
// Decorative: the adjacent text already says what happened, so announcing the
|
|
22
|
+
// glyph's name would only add noise.
|
|
26
23
|
glyph.setAttribute("aria-hidden", "true");
|
|
27
24
|
|
|
28
25
|
const label = document.createElement("span");
|