@artooi/ag-ui-web-component 0.28.0 → 0.30.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 +615 -1
- package/README.md +564 -35
- package/dist/ag-ui-web-component.bundle.js +491 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +129 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +232 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +56 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2081 -98
- package/dist/index.js.map +4 -4
- package/dist/ui/approval_card.d.ts +18 -0
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts +10 -0
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +16 -0
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/message_actions.d.ts +56 -0
- package/dist/ui/message_actions.d.ts.map +1 -0
- package/dist/ui/page_quote_offer.d.ts +33 -0
- package/dist/ui/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/quote_selection.d.ts +66 -0
- package/dist/ui/quote_selection.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +10 -0
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/stick_to_bottom.d.ts +55 -0
- package/dist/ui/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/subagent_panel.d.ts +92 -0
- package/dist/ui/subagent_panel.d.ts.map +1 -0
- package/dist/ui/subagent_update.d.ts +19 -0
- package/dist/ui/subagent_update.d.ts.map +1 -0
- package/dist/ui/suggestion_chips.d.ts +29 -0
- package/dist/ui/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/thread_drawer.d.ts +10 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +81 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +50 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +138 -1
- package/src/core/ag_ui_chat.ts +1081 -73
- package/src/core/agui_client.ts +89 -2
- package/src/index.ts +43 -0
- package/src/ui/approval_card.ts +90 -2
- package/src/ui/checkpoint_menu.ts +22 -5
- package/src/ui/confirmation_card.ts +29 -1
- package/src/ui/message_actions.ts +170 -0
- package/src/ui/page_quote_offer.ts +215 -0
- package/src/ui/quote_selection.ts +345 -0
- package/src/ui/relative_time.ts +11 -0
- package/src/ui/stick_to_bottom.ts +126 -0
- package/src/ui/styles.ts +410 -0
- package/src/ui/subagent_panel.ts +213 -0
- package/src/ui/subagent_update.ts +80 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +138 -3
- package/src/ui/ui_strings.ts +75 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow one `ag_ui.subagent` payload into a {@link SubAgentUpdate}.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the panel for the reason `chartSpecFrom` is kept out of the
|
|
5
|
+
* renderer: the panel's job is drawing, and a value that reaches it has already
|
|
6
|
+
* been vouched for.
|
|
7
|
+
*
|
|
8
|
+
* Defensive about the payload, not about the name. A `CUSTOM` event's `value` is
|
|
9
|
+
* `unknown` by the protocol, so a server can put anything there, and a malformed
|
|
10
|
+
* announcement must not take a run down with it — the same rule the invalidation
|
|
11
|
+
* channel applies to the same field. What is refused here is only what cannot be
|
|
12
|
+
* rendered at all: without a `delegationId` there is no card to attach to, and
|
|
13
|
+
* without a known `phase` there is no state to be in. Everything else degrades to
|
|
14
|
+
* `null`, which the panel reads as "said nothing about this".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SUBAGENT_PHASE } from "../constants.js";
|
|
18
|
+
import type { SubAgentPhase, SubAgentTool, SubAgentUpdate } from "./subagent_panel.js";
|
|
19
|
+
|
|
20
|
+
const PHASES: readonly string[] = Object.values(SUBAGENT_PHASE);
|
|
21
|
+
|
|
22
|
+
/** A record view of `value`, or `null` for anything that is not an object. */
|
|
23
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
24
|
+
// `typeof null` is "object", and an array is one too — neither carries the
|
|
25
|
+
// keys below, and both arrive from a JSON decoder without any warning.
|
|
26
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return value as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A non-empty string, or `null`. */
|
|
33
|
+
function asText(value: unknown): string | null {
|
|
34
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `tool` record the two tool phases carry.
|
|
39
|
+
*
|
|
40
|
+
* All-or-nothing: the contract states all three keys on every tool phase, so a
|
|
41
|
+
* partial record is a payload this client does not understand rather than a step
|
|
42
|
+
* to draw half of. A step row keyed by an empty id would also collide with the
|
|
43
|
+
* next one, silently merging two of the child's calls into one row.
|
|
44
|
+
*/
|
|
45
|
+
function asTool(value: unknown): SubAgentTool | null {
|
|
46
|
+
const record = asRecord(value);
|
|
47
|
+
if (record === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const toolCallId = asText(record["toolCallId"]);
|
|
51
|
+
const name = asText(record["name"]);
|
|
52
|
+
const ok = record["ok"];
|
|
53
|
+
if (toolCallId === null || name === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (ok !== null && typeof ok !== "boolean") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { toolCallId, name, ok };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Narrow an `ag_ui.subagent` `CUSTOM` value, or `null` if it cannot be drawn. */
|
|
63
|
+
export function subAgentUpdate(value: unknown): SubAgentUpdate | null {
|
|
64
|
+
const record = asRecord(value);
|
|
65
|
+
if (record === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const delegationId = asText(record["delegationId"]);
|
|
69
|
+
const phase = record["phase"];
|
|
70
|
+
if (delegationId === null || typeof phase !== "string" || !PHASES.includes(phase)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
delegationId,
|
|
75
|
+
phase: phase as SubAgentPhase,
|
|
76
|
+
agent: asText(record["agent"]),
|
|
77
|
+
status: asText(record["status"]),
|
|
78
|
+
tool: asTool(record["tool"]),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { UiStrings } from "./ui_strings.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Most prompts one push may draw.
|
|
5
|
+
*
|
|
6
|
+
* **Keep in step with `MAX_SUGGESTIONS` in django-ag-ui's
|
|
7
|
+
* `agent/suggestions_activity.py`**, which raises past it. Mirroring is the
|
|
8
|
+
* whole point: this side silently draws no more than its limit and has no
|
|
9
|
+
* channel to report the difference, so a producer that does not know the same
|
|
10
|
+
* number ships suggestions that never appear. That is the hole the chart bounds
|
|
11
|
+
* exist to close, and it was found there by shipping it.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_SUGGESTIONS = 4;
|
|
14
|
+
|
|
15
|
+
/** Longest one prompt may be. Mirrored for the same reason as the count. */
|
|
16
|
+
export const MAX_SUGGESTION_CHARS = 120;
|
|
17
|
+
|
|
18
|
+
/** The `prompts` a `suggestions` activity carries, or `null` when it carries none. */
|
|
19
|
+
export function suggestionPrompts(content: unknown): string[] | null {
|
|
20
|
+
if (typeof content !== "object" || content === null) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const raw = (content as { prompts?: unknown }).prompts;
|
|
24
|
+
if (!Array.isArray(raw)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const prompts = raw
|
|
28
|
+
.filter((prompt): prompt is string => typeof prompt === "string")
|
|
29
|
+
.map((prompt) => prompt.trim())
|
|
30
|
+
.filter((prompt) => prompt !== "" && prompt.length <= MAX_SUGGESTION_CHARS)
|
|
31
|
+
.slice(0, MAX_SUGGESTIONS);
|
|
32
|
+
return prompts.length === 0 ? null : prompts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Draw follow-up prompts as chips that send themselves when clicked.
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` when nothing survives, which is the registry's signal to draw
|
|
39
|
+
* nothing rather than an empty row -- the same contract the chart renderer uses
|
|
40
|
+
* for a spec it cannot draw.
|
|
41
|
+
*
|
|
42
|
+
* Buttons rather than links or list items: each one performs an action in the
|
|
43
|
+
* page, and the thing it sends is the label, so the accessible name is the
|
|
44
|
+
* prompt itself and needs no `aria-label` restating it.
|
|
45
|
+
*/
|
|
46
|
+
export function renderSuggestionChips(
|
|
47
|
+
content: unknown,
|
|
48
|
+
strings: UiStrings,
|
|
49
|
+
onPick: (prompt: string) => void,
|
|
50
|
+
): HTMLElement | null {
|
|
51
|
+
const prompts = suggestionPrompts(content);
|
|
52
|
+
if (prompts === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const row = document.createElement("div");
|
|
56
|
+
row.className = "suggestions";
|
|
57
|
+
row.setAttribute("part", "suggestions");
|
|
58
|
+
// A group, labelled: without it a screen reader meets a row of unrelated
|
|
59
|
+
// buttons with no hint that they are the assistant's offer rather than the
|
|
60
|
+
// page's own controls.
|
|
61
|
+
row.setAttribute("role", "group");
|
|
62
|
+
row.setAttribute("aria-label", strings.suggestions);
|
|
63
|
+
for (const prompt of prompts) {
|
|
64
|
+
const chip = document.createElement("button");
|
|
65
|
+
chip.type = "button";
|
|
66
|
+
chip.className = "suggestion-chip";
|
|
67
|
+
chip.setAttribute("part", "suggestion-chip");
|
|
68
|
+
chip.textContent = prompt;
|
|
69
|
+
chip.addEventListener("click", () => onPick(prompt));
|
|
70
|
+
row.appendChild(chip);
|
|
71
|
+
}
|
|
72
|
+
return row;
|
|
73
|
+
}
|
package/src/ui/thread_drawer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThreadMeta } from "../core/conversation_store.js";
|
|
2
|
-
import { relativeTime } from "./relative_time.js";
|
|
2
|
+
import { type RelativeTimeFormatter, relativeTime } from "./relative_time.js";
|
|
3
3
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
4
4
|
|
|
5
5
|
/** Actions the host ({@link AgUiChat}) wires to the drawer's rows. */
|
|
@@ -35,6 +35,7 @@ export class ThreadDrawer {
|
|
|
35
35
|
readonly #heading: HTMLSpanElement;
|
|
36
36
|
readonly #newButton: HTMLButtonElement;
|
|
37
37
|
readonly #list: HTMLDivElement;
|
|
38
|
+
#formatRelativeTime: RelativeTimeFormatter | null = null;
|
|
38
39
|
#strings: UiStrings;
|
|
39
40
|
#threads: readonly ThreadMeta[] = [];
|
|
40
41
|
#activeId = "";
|
|
@@ -91,6 +92,25 @@ export class ThreadDrawer {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/** Re-localize the drawer's chrome and rows (the host calls this on connect). */
|
|
95
|
+
/**
|
|
96
|
+
* Replace the timestamp formatter, or restore the built-in with `null`.
|
|
97
|
+
*
|
|
98
|
+
* The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
|
|
99
|
+
* in this component, so it never disagrees with a host's own formatting by
|
|
100
|
+
* guessing a locale. That is a defensible default and a poor requirement, so
|
|
101
|
+
* this is the way out.
|
|
102
|
+
*/
|
|
103
|
+
setRelativeTimeFormatter(format: RelativeTimeFormatter | null): void {
|
|
104
|
+
this.#formatRelativeTime = format;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** This row's timestamp, through the host's formatter when it set one. */
|
|
108
|
+
#formatTime(timestamp: number): string {
|
|
109
|
+
return this.#formatRelativeTime !== null
|
|
110
|
+
? this.#formatRelativeTime(timestamp)
|
|
111
|
+
: relativeTime(timestamp, undefined, this.#strings);
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
setStrings(strings: UiStrings): void {
|
|
95
115
|
this.#strings = strings;
|
|
96
116
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
@@ -202,7 +222,7 @@ export class ThreadDrawer {
|
|
|
202
222
|
const time = document.createElement("span");
|
|
203
223
|
time.className = "drawer-row-time";
|
|
204
224
|
time.setAttribute("part", "drawer-row-time");
|
|
205
|
-
time.textContent =
|
|
225
|
+
time.textContent = this.#formatTime(meta.updatedAt);
|
|
206
226
|
const preview = document.createElement("span");
|
|
207
227
|
preview.className = "drawer-row-preview";
|
|
208
228
|
preview.setAttribute("part", "drawer-row-preview");
|
package/src/ui/tool_call_card.ts
CHANGED
|
@@ -42,6 +42,61 @@ function formatPayload(text: string): string {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* One region of a card's body, handed to a host {@link ToolPayloadFormatter}.
|
|
47
|
+
*
|
|
48
|
+
* A discriminated union rather than three positional parameters because the two
|
|
49
|
+
* halves do not carry the same thing: arguments are the parsed record the call
|
|
50
|
+
* was made with, and a result is the raw string the tool returned, which may not
|
|
51
|
+
* be JSON at all. Flattening both into one `payload` parameter would force every
|
|
52
|
+
* formatter to re-derive which it had before it could read it, and the
|
|
53
|
+
* arguments would arrive re-serialised for no reason.
|
|
54
|
+
*
|
|
55
|
+
* `toolName` is the raw tool name, not the card's `x-summary` label -- a
|
|
56
|
+
* formatter dispatches on identity, and the label is a display string a server
|
|
57
|
+
* may change.
|
|
58
|
+
*/
|
|
59
|
+
export type ToolPayload =
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: "arguments";
|
|
62
|
+
readonly toolName: string;
|
|
63
|
+
readonly args: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
readonly kind: "result";
|
|
67
|
+
readonly toolName: string;
|
|
68
|
+
readonly status: SettledStatus;
|
|
69
|
+
readonly text: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Renders one region of a tool card's body, for a host that would rather show a
|
|
74
|
+
* table or a summary line than a wall of pretty-printed JSON.
|
|
75
|
+
*
|
|
76
|
+
* Return a `Node` to take the region over, a `string` to replace its text, or
|
|
77
|
+
* `null` to fall through to the built-in pretty-print -- so a formatter that
|
|
78
|
+
* only cares about one tool, or only about results, declines the rest rather
|
|
79
|
+
* than reimplementing them.
|
|
80
|
+
*
|
|
81
|
+
* **Presentation only.** The model reads the tool result from its own copy of
|
|
82
|
+
* the message, which this never touches, so anything said here is said to the
|
|
83
|
+
* person and not to the agent. Translating a value -- an enum constant into a
|
|
84
|
+
* friendly label, an epoch into a date -- belongs on the server, where it also
|
|
85
|
+
* reaches the model's prose; doing it here would make the card and the answer
|
|
86
|
+
* beside it disagree about what happened.
|
|
87
|
+
*
|
|
88
|
+
* A returned string is set as text, never parsed as markup: this is not a
|
|
89
|
+
* second HTML channel into the transcript, and a host that wants elements
|
|
90
|
+
* builds them itself and returns the node.
|
|
91
|
+
*/
|
|
92
|
+
export type ToolPayloadFormatter = (payload: ToolPayload) => Node | string | null;
|
|
93
|
+
|
|
94
|
+
/** Optional per-card wiring beyond the name, arguments, label and strings. */
|
|
95
|
+
export interface ToolCallCardOptions {
|
|
96
|
+
/** Host presentation hook for both body regions. See {@link ToolPayloadFormatter}. */
|
|
97
|
+
readonly formatPayload?: ToolPayloadFormatter;
|
|
98
|
+
}
|
|
99
|
+
|
|
45
100
|
/**
|
|
46
101
|
* A live tool-call card for the chat transcript.
|
|
47
102
|
*
|
|
@@ -61,6 +116,12 @@ function formatPayload(text: string): string {
|
|
|
61
116
|
* `--ag-ui-tool-icon-*` custom properties or the `tool-card-icon` part without
|
|
62
117
|
* the card reaching into the host stylesheet.
|
|
63
118
|
*
|
|
119
|
+
* Either region may be drawn by the host instead: `options.formatPayload` is
|
|
120
|
+
* asked about each one and pretty-prints as before whenever it declines. The
|
|
121
|
+
* arguments are offered from the constructor and the result from {@link settle},
|
|
122
|
+
* because that is when each exists -- so a formatter is asked twice per card,
|
|
123
|
+
* potentially long apart.
|
|
124
|
+
*
|
|
64
125
|
* Pure DOM. The host appends {@link element} into its shadow root; all visible
|
|
65
126
|
* text comes from {@link UiStrings}.
|
|
66
127
|
*/
|
|
@@ -83,6 +144,23 @@ export class ToolCallCard {
|
|
|
83
144
|
*/
|
|
84
145
|
readonly approvalSlot: HTMLDivElement;
|
|
85
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Where a *nested* run's progress renders — the sub-agent this call delegated
|
|
149
|
+
* to, narrating itself while the card waits.
|
|
150
|
+
*
|
|
151
|
+
* A slot rather than a rendered thing, on the same reasoning as
|
|
152
|
+
* {@link approvalSlot}: the card owns the position and something else owns the
|
|
153
|
+
* content. What makes the position right is that the wire keys a delegation on
|
|
154
|
+
* this card's own `toolCallId`, so the run being narrated is the one this card
|
|
155
|
+
* already stands for.
|
|
156
|
+
*
|
|
157
|
+
* Placed above the Details toggle rather than inside the body, because the
|
|
158
|
+
* body is what the display modes hide — and a progress line that only appears
|
|
159
|
+
* in `full` mode would leave exactly the stall it exists to end. Empty on every
|
|
160
|
+
* card that delegated nothing, and hidden while empty by the shadow CSS.
|
|
161
|
+
*/
|
|
162
|
+
readonly subagentSlot: HTMLDivElement;
|
|
163
|
+
|
|
86
164
|
readonly #status: HTMLSpanElement;
|
|
87
165
|
readonly #decision: HTMLSpanElement;
|
|
88
166
|
readonly #toggle: HTMLButtonElement;
|
|
@@ -90,6 +168,17 @@ export class ToolCallCard {
|
|
|
90
168
|
readonly #resultLabel: HTMLSpanElement;
|
|
91
169
|
readonly #resultBody: HTMLPreElement;
|
|
92
170
|
readonly #strings: UiStrings;
|
|
171
|
+
/**
|
|
172
|
+
* The arguments this call was made with.
|
|
173
|
+
*
|
|
174
|
+
* Retained rather than only rendered, because an approval interrupt names a
|
|
175
|
+
* `toolCallId` and nothing else -- so this card is the only place the args
|
|
176
|
+
* still exist when the user is asked to approve, edit or deny the call.
|
|
177
|
+
*/
|
|
178
|
+
readonly args: Record<string, unknown>;
|
|
179
|
+
/** The raw tool name, kept for the payloads handed to {@link ToolPayloadFormatter}. */
|
|
180
|
+
readonly #name: string;
|
|
181
|
+
readonly #formatPayload: ToolPayloadFormatter | null;
|
|
93
182
|
#settled = false;
|
|
94
183
|
|
|
95
184
|
constructor(
|
|
@@ -97,8 +186,12 @@ export class ToolCallCard {
|
|
|
97
186
|
args: Record<string, unknown>,
|
|
98
187
|
summary?: string,
|
|
99
188
|
strings: UiStrings = DEFAULT_UI_STRINGS,
|
|
189
|
+
options: ToolCallCardOptions = {},
|
|
100
190
|
) {
|
|
101
191
|
this.#strings = strings;
|
|
192
|
+
this.args = args;
|
|
193
|
+
this.#name = name;
|
|
194
|
+
this.#formatPayload = options.formatPayload ?? null;
|
|
102
195
|
|
|
103
196
|
this.element = document.createElement("div");
|
|
104
197
|
this.element.className = "tool-call";
|
|
@@ -137,7 +230,11 @@ export class ToolCallCard {
|
|
|
137
230
|
head.append(icon, label, this.#status, this.#decision);
|
|
138
231
|
|
|
139
232
|
const argsSection = this.#section("args", strings.argumentsLabel);
|
|
140
|
-
|
|
233
|
+
this.#renderPayload(
|
|
234
|
+
argsSection.body,
|
|
235
|
+
{ kind: "arguments", toolName: name, args },
|
|
236
|
+
JSON.stringify(args, null, 2),
|
|
237
|
+
);
|
|
141
238
|
// Drop the region rather than frame an empty object.
|
|
142
239
|
argsSection.root.hidden = Object.keys(args).length === 0;
|
|
143
240
|
|
|
@@ -166,7 +263,11 @@ export class ToolCallCard {
|
|
|
166
263
|
this.approvalSlot.className = "tool-call-approval";
|
|
167
264
|
this.approvalSlot.setAttribute("part", "tool-card-approval");
|
|
168
265
|
|
|
169
|
-
this.
|
|
266
|
+
this.subagentSlot = document.createElement("div");
|
|
267
|
+
this.subagentSlot.className = "tool-call-subagent";
|
|
268
|
+
this.subagentSlot.setAttribute("part", "tool-card-subagent");
|
|
269
|
+
|
|
270
|
+
this.element.append(head, this.subagentSlot, this.#toggle, body, this.approvalSlot);
|
|
170
271
|
}
|
|
171
272
|
|
|
172
273
|
/**
|
|
@@ -215,10 +316,44 @@ export class ToolCallCard {
|
|
|
215
316
|
this.element.setAttribute("data-status", status);
|
|
216
317
|
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
217
318
|
this.#resultLabel.textContent = resultLabels(this.#strings)[status];
|
|
218
|
-
this.#
|
|
319
|
+
this.#renderPayload(
|
|
320
|
+
this.#resultBody,
|
|
321
|
+
{ kind: "result", toolName: this.#name, status, text },
|
|
322
|
+
formatPayload(text),
|
|
323
|
+
);
|
|
219
324
|
this.#resultSection.hidden = false;
|
|
220
325
|
}
|
|
221
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Fill one body region, offering it to the host formatter first.
|
|
329
|
+
*
|
|
330
|
+
* `fallback` is computed by the caller rather than here because the two
|
|
331
|
+
* regions build it differently -- arguments are already parsed, a result is a
|
|
332
|
+
* string that may or may not be JSON -- and because a formatter that takes
|
|
333
|
+
* the region over should not have paid for a pretty-print nobody sees. It is
|
|
334
|
+
* cheap either way; the point is that the built-in rendering stays written
|
|
335
|
+
* once, beside the payload it belongs to.
|
|
336
|
+
*
|
|
337
|
+
* The `data-formatted` marker is what the shadow CSS reads to relax the
|
|
338
|
+
* preformatted whitespace on a region a host owns -- a table inherits it as
|
|
339
|
+
* mangled cell spacing, a sentence as line breaks nobody typed. Marked for a
|
|
340
|
+
* returned string too: one rule, and a summary line wants ordinary wrapping
|
|
341
|
+
* as much as a table does.
|
|
342
|
+
*/
|
|
343
|
+
#renderPayload(body: HTMLPreElement, payload: ToolPayload, fallback: string): void {
|
|
344
|
+
const rendered = this.#formatPayload === null ? null : this.#formatPayload(payload);
|
|
345
|
+
if (rendered === null) {
|
|
346
|
+
body.textContent = fallback;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
body.setAttribute("data-formatted", "true");
|
|
350
|
+
if (typeof rendered === "string") {
|
|
351
|
+
body.textContent = rendered;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
body.replaceChildren(rendered);
|
|
355
|
+
}
|
|
356
|
+
|
|
222
357
|
/** Build one labelled region of the body: a heading plus a payload block. */
|
|
223
358
|
#section(
|
|
224
359
|
kind: string,
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -33,6 +33,18 @@ export interface UiStrings {
|
|
|
33
33
|
// ── Messages region ─────────────────────────────────────────────────────────
|
|
34
34
|
/** `aria-label` of the scrolling message log. */
|
|
35
35
|
conversation: string;
|
|
36
|
+
/** The button offering to return to the foot of the transcript. */
|
|
37
|
+
jumpToLatest: string;
|
|
38
|
+
/** Announced when a turn starts. Screen-reader only; never rendered. */
|
|
39
|
+
announceResponding: string;
|
|
40
|
+
/** Announced when the answer has finished arriving. Screen-reader only. */
|
|
41
|
+
announceAnswerReady: string;
|
|
42
|
+
/** Announced when a card is waiting for the user's decision. Token: `{count}`. */
|
|
43
|
+
announceAwaitingDecision: string;
|
|
44
|
+
/** Announced when the user stopped the run. Screen-reader only. */
|
|
45
|
+
announceStopped: string;
|
|
46
|
+
/** Announced when the run failed. Screen-reader only. */
|
|
47
|
+
announceFailed: string;
|
|
36
48
|
/** `aria-label` of the "thinking" pending indicator, and the thoughts region's
|
|
37
49
|
* header while the model is still reasoning. */
|
|
38
50
|
thinking: string;
|
|
@@ -48,6 +60,10 @@ export interface UiStrings {
|
|
|
48
60
|
declinedAction: string;
|
|
49
61
|
/** A navigating tool's card text while the page reloads. */
|
|
50
62
|
navigating: string;
|
|
63
|
+
/** Notice shown when the server replaced the conversation wholesale. */
|
|
64
|
+
historyReplaced: string;
|
|
65
|
+
/** Notice shown when a pushed chart could not be drawn and was removed. */
|
|
66
|
+
chartUndrawable: string;
|
|
51
67
|
/** Missing-placeholder skill hint. Tokens: `{title}`, `{fields}`. */
|
|
52
68
|
skillNeeds: string;
|
|
53
69
|
/** Notice shown when the agent condensed earlier turns. Token: `{count}`. */
|
|
@@ -115,9 +131,45 @@ export interface UiStrings {
|
|
|
115
131
|
/** Label on the toggle that expands a tool card's body. */
|
|
116
132
|
details: string;
|
|
117
133
|
|
|
134
|
+
// ── Delegated sub-agent ─────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* The delegation row's text before the server's own status line lands.
|
|
137
|
+
*
|
|
138
|
+
* A fallback, not a state: every announcement carries a pre-rendered `status`,
|
|
139
|
+
* and this only shows if one arrives unusable. The row is the expander, so it
|
|
140
|
+
* must never be blank.
|
|
141
|
+
*/
|
|
142
|
+
subAgentWorking: string;
|
|
143
|
+
/** `aria-label` of the region holding the sub-agent's own tool calls. */
|
|
144
|
+
subAgentSteps: string;
|
|
145
|
+
|
|
118
146
|
// ── Confirmation card ───────────────────────────────────────────────────────
|
|
147
|
+
/** `aria-label` of the editable arguments field on an approval card. */
|
|
148
|
+
approvalEditArgs: string;
|
|
149
|
+
/** Shown when the edited arguments are not valid JSON. */
|
|
150
|
+
approvalArgsInvalid: string;
|
|
151
|
+
/** Shown when the edited arguments parse but are not a JSON object. */
|
|
152
|
+
approvalArgsNotAnObject: string;
|
|
153
|
+
/** `aria-label` of the follow-up suggestion chips row. */
|
|
154
|
+
suggestions: string;
|
|
155
|
+
/** `aria-label` of a message's action row. */
|
|
156
|
+
messageActions: string;
|
|
157
|
+
/** The offer that floats beside a selection in the transcript. */
|
|
158
|
+
quoteSelection: string;
|
|
159
|
+
/** Copy this message (button `title` / `aria-label`). Its confirmation and
|
|
160
|
+
* failure text are the code block's `copied` / `copyFailed`, which say the
|
|
161
|
+
* same thing about the same clipboard. */
|
|
162
|
+
copyMessage: string;
|
|
163
|
+
/** Ask for a different answer to the same question. */
|
|
164
|
+
retryMessage: string;
|
|
165
|
+
/** Rate this answer as good. */
|
|
166
|
+
feedbackUp: string;
|
|
167
|
+
/** Rate this answer as poor. */
|
|
168
|
+
feedbackDown: string;
|
|
119
169
|
/** `aria-label` of the inline confirmation card. */
|
|
120
170
|
confirmAction: string;
|
|
171
|
+
/** Waive confirmation for this tool for the rest of the session. Token: `{tool}`. */
|
|
172
|
+
confirmAlways: string;
|
|
121
173
|
/** Generic confirmation prompt when a tool has no `x-confirm`. Token: `{tool}`. */
|
|
122
174
|
confirmRun: string;
|
|
123
175
|
/** Confirm button. */
|
|
@@ -229,6 +281,12 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
229
281
|
forkedRun: "branched",
|
|
230
282
|
|
|
231
283
|
conversation: "Conversation",
|
|
284
|
+
jumpToLatest: "Jump to latest",
|
|
285
|
+
announceResponding: "Assistant is responding",
|
|
286
|
+
announceAnswerReady: "Assistant answered",
|
|
287
|
+
announceAwaitingDecision: "{count} action is waiting for your approval",
|
|
288
|
+
announceStopped: "Response stopped",
|
|
289
|
+
announceFailed: "The response failed",
|
|
232
290
|
thinking: "Assistant is thinking…",
|
|
233
291
|
thoughts: "Thoughts",
|
|
234
292
|
stopped: "⏹ Stopped",
|
|
@@ -236,6 +294,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
236
294
|
noResult: "No result returned.",
|
|
237
295
|
declinedAction: "User declined the action.",
|
|
238
296
|
navigating: "Navigating…",
|
|
297
|
+
historyReplaced:
|
|
298
|
+
"The server replaced this conversation's history. Reload to see the updated transcript.",
|
|
299
|
+
chartUndrawable: "A chart could not be drawn from the data sent, so it was removed.",
|
|
239
300
|
historyCompacted: "Earlier turns condensed to fit the context window ({count} removed)",
|
|
240
301
|
usingSkill: "Using skill {name}",
|
|
241
302
|
runInterrupted: "The previous response didn’t finish — the page changed before it arrived.",
|
|
@@ -270,7 +331,21 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
270
331
|
declinedLabel: "Declined",
|
|
271
332
|
details: "Details",
|
|
272
333
|
|
|
334
|
+
subAgentWorking: "Working…",
|
|
335
|
+
subAgentSteps: "Steps the sub-agent took",
|
|
336
|
+
|
|
337
|
+
approvalEditArgs: "Edit the arguments before approving",
|
|
338
|
+
approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
|
|
339
|
+
approvalArgsNotAnObject: "Arguments have to be a JSON object.",
|
|
340
|
+
suggestions: "Suggested follow-ups",
|
|
341
|
+
messageActions: "Message actions",
|
|
342
|
+
quoteSelection: "Quote",
|
|
343
|
+
copyMessage: "Copy message",
|
|
344
|
+
retryMessage: "Try again",
|
|
345
|
+
feedbackUp: "Good answer",
|
|
346
|
+
feedbackDown: "Poor answer",
|
|
273
347
|
confirmAction: "Confirm action",
|
|
348
|
+
confirmAlways: "Always allow",
|
|
274
349
|
confirmRun: "Run “{tool}”?",
|
|
275
350
|
confirm: "Confirm",
|
|
276
351
|
cancel: "Cancel",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.30.0";
|