@artooi/ag-ui-web-component 0.38.0 → 0.40.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 +515 -1
- package/README.md +232 -56
- package/dist/ag-ui-web-component.bundle.js +370 -139
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +17 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/activity_registry.d.ts +46 -0
- package/dist/core/activity_registry.d.ts.map +1 -0
- package/dist/core/ag_ui_chat.d.ts +23 -10
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +28 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/client_seed.d.ts +27 -0
- package/dist/core/client_seed.d.ts.map +1 -0
- package/dist/core/read_max_tool_rounds.d.ts +9 -0
- package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +80 -0
- package/dist/core/run_handlers.d.ts.map +1 -0
- package/dist/core/storage_scope.d.ts +110 -0
- package/dist/core/storage_scope.d.ts.map +1 -0
- package/dist/core/tool_outcome.d.ts +2 -3
- package/dist/core/tool_outcome.d.ts.map +1 -1
- package/dist/core/utils.d.ts +46 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +16268 -14747
- package/dist/index.js.map +4 -4
- package/dist/skills/skill_catalog.d.ts +67 -0
- package/dist/skills/skill_catalog.d.ts.map +1 -0
- package/dist/skills/skill_name_from.d.ts +12 -0
- package/dist/skills/skill_name_from.d.ts.map +1 -0
- package/dist/tools/tool_catalog.d.ts +79 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -0
- package/dist/tools/tool_dispatch.d.ts +104 -0
- package/dist/tools/tool_dispatch.d.ts.map +1 -0
- package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
- package/dist/ui/composer/auto_grow.d.ts +10 -0
- package/dist/ui/composer/auto_grow.d.ts.map +1 -0
- package/dist/ui/composer/composer_attachments.d.ts +57 -0
- package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
- package/dist/ui/composer/composer_voice.d.ts +48 -0
- package/dist/ui/composer/composer_voice.d.ts.map +1 -0
- package/dist/ui/composer/voice_input.d.ts.map +1 -1
- package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
- package/dist/ui/fill_ui_string.d.ts +32 -0
- package/dist/ui/fill_ui_string.d.ts.map +1 -0
- package/dist/ui/history/conversation_history.d.ts +203 -0
- package/dist/ui/history/conversation_history.d.ts.map +1 -0
- package/dist/ui/history/relative_time.d.ts.map +1 -1
- package/dist/ui/history/thread_drawer.d.ts +0 -14
- package/dist/ui/history/thread_drawer.d.ts.map +1 -1
- package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
- package/dist/ui/interrupts/pending_decision.d.ts +23 -0
- package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
- package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
- package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
- package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
- package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
- package/dist/ui/placement/launcher_drag.d.ts +6 -0
- package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +119 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -0
- package/dist/ui/progress/run_announcer.d.ts +41 -0
- package/dist/ui/progress/run_announcer.d.ts.map +1 -0
- package/dist/ui/progress/subagent_progress.d.ts +58 -0
- package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/shell/adopt_styles.d.ts +23 -0
- package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
- package/dist/ui/shell/glyph_slot.d.ts +7 -0
- package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
- package/dist/ui/shell/header_button.d.ts +10 -0
- package/dist/ui/shell/header_button.d.ts.map +1 -0
- package/dist/ui/shell/icon_element.d.ts +12 -0
- package/dist/ui/shell/icon_element.d.ts.map +1 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
- package/dist/ui/shell/read_launcher_icon_url.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/transcript/answer_actions.d.ts +39 -0
- package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
- package/dist/ui/transcript/answer_stream.d.ts +56 -0
- package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
- package/dist/ui/transcript/render_or_warn.d.ts +16 -0
- package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
- package/dist/ui/transcript/starter_chips.d.ts +21 -0
- package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
- package/dist/ui/transcript/transcript.d.ts +171 -0
- package/dist/ui/transcript/transcript.d.ts.map +1 -0
- package/dist/ui/ui_strings.d.ts +29 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +17 -15
- package/src/core/activity_registry.ts +133 -0
- package/src/core/ag_ui_chat.ts +945 -4419
- package/src/core/agui_client.ts +172 -16
- package/src/core/client_seed.ts +27 -0
- package/src/core/read_max_tool_rounds.ts +13 -0
- package/src/core/remote_conversation_store.ts +1 -1
- package/src/core/run_handlers.ts +501 -0
- package/src/core/storage_scope.ts +279 -0
- package/src/core/tool_outcome.ts +5 -3
- package/src/core/utils.ts +109 -0
- package/src/skills/skill_catalog.ts +166 -0
- package/src/skills/skill_name_from.ts +19 -0
- package/src/tools/tool_catalog.ts +354 -0
- package/src/tools/tool_dispatch.ts +521 -0
- package/src/ui/composer/attachment_tray.ts +6 -5
- package/src/ui/composer/auto_grow.ts +12 -0
- package/src/ui/composer/composer_attachments.ts +324 -0
- package/src/ui/composer/composer_voice.ts +93 -0
- package/src/ui/composer/voice_input.ts +4 -4
- package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
- package/src/ui/fill_ui_string.ts +41 -0
- package/src/ui/history/conversation_history.ts +699 -0
- package/src/ui/history/relative_time.ts +5 -4
- package/src/ui/history/thread_drawer.ts +3 -50
- package/src/ui/interrupts/confirmation_card.ts +4 -2
- package/src/ui/interrupts/pending_decision.ts +34 -0
- package/src/ui/placement/is_collapsible_placement.ts +19 -0
- package/src/ui/placement/is_draggable_placement.ts +23 -0
- package/src/ui/placement/launcher_drag.ts +104 -89
- package/src/ui/placement/panel_placement.ts +1130 -0
- package/src/ui/progress/run_announcer.ts +67 -0
- package/src/ui/progress/subagent_progress.ts +201 -0
- package/src/ui/progress/tool_call_card.ts +2 -0
- package/src/ui/shell/adopt_styles.ts +28 -0
- package/src/ui/shell/glyph_slot.ts +12 -0
- package/src/ui/shell/header_button.ts +21 -0
- package/src/ui/shell/icon_element.ts +33 -0
- package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
- package/src/ui/shell/read_launcher_icon_url.ts +9 -0
- package/src/ui/styles.ts +301 -70
- package/src/ui/transcript/answer_actions.ts +139 -0
- package/src/ui/transcript/answer_stream.ts +123 -0
- package/src/ui/transcript/render_or_warn.ts +22 -0
- package/src/ui/transcript/starter_chips.ts +42 -0
- package/src/ui/transcript/transcript.ts +429 -0
- package/src/ui/ui_strings.ts +38 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { MESSAGE_ROLE, X_SUMMARY_KEY } from "../../constants.js";
|
|
2
|
+
import type { AgUiToolCall } from "../../core/agui_client.js";
|
|
3
|
+
import type { MessageRole } from "../../core/message_role.js";
|
|
4
|
+
import { skillNameFrom } from "../../skills/skill_name_from.js";
|
|
5
|
+
import type { ChartRenderer, ClientTool } from "../../tools/client_tool_registry.js";
|
|
6
|
+
import { attachCopyButtons } from "../excerpts/attach_copy_buttons.js";
|
|
7
|
+
import { fillUiString } from "../fill_ui_string.js";
|
|
8
|
+
import { prettifyToolName } from "../progress/prettify_tool_name.js";
|
|
9
|
+
import { renderRunNotice } from "../progress/run_notice.js";
|
|
10
|
+
import { ThoughtsBlock } from "../progress/thoughts_block.js";
|
|
11
|
+
import { ToolCallCard, type ToolPayload } from "../progress/tool_call_card.js";
|
|
12
|
+
import type { UiStrings } from "../ui_strings.js";
|
|
13
|
+
import { renderMarkdown } from "./render_markdown.js";
|
|
14
|
+
import { renderOrWarn } from "./render_or_warn.js";
|
|
15
|
+
import { wrapWords } from "./reveal_words.js";
|
|
16
|
+
import { createStickToBottom, type StickToBottom } from "./stick_to_bottom.js";
|
|
17
|
+
|
|
18
|
+
/** What the transcript needs from the element that owns it. */
|
|
19
|
+
export interface TranscriptHost {
|
|
20
|
+
/** The custom element: its text-animation attribute, and the `data-empty` stamp. */
|
|
21
|
+
readonly element: HTMLElement;
|
|
22
|
+
/** The scrolling message list. */
|
|
23
|
+
readonly messages: HTMLDivElement;
|
|
24
|
+
/** The empty-state region at the top of the list. */
|
|
25
|
+
readonly emptyWrap: HTMLDivElement;
|
|
26
|
+
/** The resolved string table. */
|
|
27
|
+
readonly strings: () => UiStrings;
|
|
28
|
+
/** The element's `allowImages`, read per render. */
|
|
29
|
+
readonly allowImages: () => boolean;
|
|
30
|
+
/** Resolve a frontend tool by name, for its `x-summary`. */
|
|
31
|
+
readonly resolveTool: (name: string) => ClientTool | null;
|
|
32
|
+
/** The element's `toolSummaries`, read per card. */
|
|
33
|
+
readonly toolSummaries: () => Record<string, string>;
|
|
34
|
+
/** The server catalog's label for a tool, if it sent one. */
|
|
35
|
+
readonly serverSummary: (name: string) => string | undefined;
|
|
36
|
+
/** The element's `formatToolPayload`, asked per region of a card. */
|
|
37
|
+
readonly formatToolPayload: (payload: ToolPayload) => Node | string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The transcript: the bubbles, the open answer group, the pending dots, the
|
|
42
|
+
* reasoning region, the tool cards by call id, and the scroller that follows
|
|
43
|
+
* the foot.
|
|
44
|
+
*
|
|
45
|
+
* The hub most other parts of the element draw into, so it holds only the
|
|
46
|
+
* transcript's own state and asks the element for nothing but configuration.
|
|
47
|
+
*
|
|
48
|
+
* Owned one-to-one by an `<ag-ui-chat>`, and holding no state outside the
|
|
49
|
+
* instance.
|
|
50
|
+
*/
|
|
51
|
+
export class Transcript {
|
|
52
|
+
readonly #host: TranscriptHost;
|
|
53
|
+
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
54
|
+
readonly #toolCards = new Map<string, ToolCallCard>();
|
|
55
|
+
/** Card elements by call id, so a rendering handler can find its own card. */
|
|
56
|
+
readonly #cardElements = new Map<string, HTMLElement>();
|
|
57
|
+
/**
|
|
58
|
+
* Call ids whose card was already settled from a streamed server-side result
|
|
59
|
+
* (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
|
|
60
|
+
* the real output with the generic "executed on the server" fallback.
|
|
61
|
+
*/
|
|
62
|
+
readonly #serverSettled = new Set<string>();
|
|
63
|
+
/** Follows the foot of the transcript, and stops when the reader scrolls away. */
|
|
64
|
+
#scroller!: StickToBottom;
|
|
65
|
+
#pending: HTMLDivElement | null = null;
|
|
66
|
+
// The current assistant turn's grouping container. One `.answer`
|
|
67
|
+
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
68
|
+
// pending indicator — so it can be boxed as one "well" by CSS. Opened on the
|
|
69
|
+
// turn's first run start, closed at settle, so it spans the whole multi-round
|
|
70
|
+
// frontend-tool loop (which is several AG-UI runs), not one run. `null`
|
|
71
|
+
// between turns; user bubbles never enter it.
|
|
72
|
+
#currentGroup: HTMLDivElement | null = null;
|
|
73
|
+
// The current turn's streamed-reasoning region, shown at the top of
|
|
74
|
+
// the answer group while a reasoning model thinks and collapsed once the
|
|
75
|
+
// answer's first text token arrives. `null` outside a reasoning turn.
|
|
76
|
+
#thoughts: ThoughtsBlock | null = null;
|
|
77
|
+
|
|
78
|
+
constructor(host: TranscriptHost) {
|
|
79
|
+
this.#host = host;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start following the foot of the list, and wire the jump button to it.
|
|
84
|
+
* Called while rendering rather than at construction: the viewport has to
|
|
85
|
+
* exist and the observer has to have something to observe. The button
|
|
86
|
+
* outlives a connection, so its listener goes under `signal`.
|
|
87
|
+
*/
|
|
88
|
+
mountScroller(jumpButton: HTMLButtonElement, signal: AbortSignal): void {
|
|
89
|
+
jumpButton.addEventListener(
|
|
90
|
+
"click",
|
|
91
|
+
() => {
|
|
92
|
+
this.jump();
|
|
93
|
+
},
|
|
94
|
+
{ signal },
|
|
95
|
+
);
|
|
96
|
+
this.#scroller = createStickToBottom({
|
|
97
|
+
viewport: this.#host.messages,
|
|
98
|
+
onMissedContent: (missed) => {
|
|
99
|
+
jumpButton.dataset["missed"] = String(missed);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Stop following, when the element leaves the document. */
|
|
105
|
+
disposeScroller(): void {
|
|
106
|
+
this.#scroller.dispose();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Keep the list at its foot, if the reader is there. */
|
|
110
|
+
follow(): void {
|
|
111
|
+
this.#scroller.follow();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Go to the foot, whether or not the reader had scrolled away. */
|
|
115
|
+
jump(): void {
|
|
116
|
+
this.#scroller.jump();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether the empty-state region is showing, which is when nothing else is. */
|
|
120
|
+
isEmpty(): boolean {
|
|
121
|
+
return !this.#host.emptyWrap.hidden;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The card drawn for a call id, while it is still awaiting execution. */
|
|
125
|
+
card(callId: string): ToolCallCard | undefined {
|
|
126
|
+
return this.#toolCards.get(callId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Every card still awaiting execution. */
|
|
130
|
+
cards(): IterableIterator<ToolCallCard> {
|
|
131
|
+
return this.#toolCards.values();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Stop tracking a call's card as awaiting execution. */
|
|
135
|
+
forgetCard(callId: string): void {
|
|
136
|
+
this.#toolCards.delete(callId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Remember the element a call's card is, for a renderer to place itself against. */
|
|
140
|
+
setCardElement(callId: string, element: HTMLElement): void {
|
|
141
|
+
this.#cardElements.set(callId, element);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Record that a call's card was settled from a streamed server-side result. */
|
|
145
|
+
markServerSettled(callId: string): void {
|
|
146
|
+
this.#serverSettled.add(callId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether a call's card was settled from a streamed server-side result. */
|
|
150
|
+
isServerSettled(callId: string): boolean {
|
|
151
|
+
return this.#serverSettled.has(callId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Fold the reasoning region away, once the answer has begun. */
|
|
155
|
+
collapseThoughts(): void {
|
|
156
|
+
this.#thoughts?.collapse();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Close the turn's answer group. Drop it if the turn rendered nothing
|
|
161
|
+
* (e.g. a server-only round that streamed no text/card) so an opted-in
|
|
162
|
+
* well leaves no empty box behind.
|
|
163
|
+
*/
|
|
164
|
+
closeGroup(): void {
|
|
165
|
+
if (this.#currentGroup !== null && this.#currentGroup.childElementCount === 0) {
|
|
166
|
+
this.#currentGroup.remove();
|
|
167
|
+
this.updateEmptyState();
|
|
168
|
+
}
|
|
169
|
+
this.#currentGroup = null;
|
|
170
|
+
this.#thoughts = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Let go of the open turn: its group, its reasoning region, its pending dots
|
|
175
|
+
* and the cards awaiting execution. The first half of wiping the transcript.
|
|
176
|
+
*/
|
|
177
|
+
releaseTurn(): void {
|
|
178
|
+
this.#currentGroup = null;
|
|
179
|
+
this.#thoughts = null;
|
|
180
|
+
this.hidePending();
|
|
181
|
+
this.#toolCards.clear();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Forget which cards settled from the server and where each card is. */
|
|
185
|
+
forgetCards(): void {
|
|
186
|
+
this.#serverSettled.clear();
|
|
187
|
+
this.#cardElements.clear();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Empty the list back to its empty-state region. The last half of wiping it. */
|
|
191
|
+
empty(): void {
|
|
192
|
+
this.#host.messages.replaceChildren(this.#host.emptyWrap);
|
|
193
|
+
this.updateEmptyState();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The bubble behind `AgUiChat.appendMessage`, whose doc is the contract. */
|
|
197
|
+
append(role: MessageRole, content: string): HTMLDivElement {
|
|
198
|
+
const bubble = document.createElement("div");
|
|
199
|
+
bubble.className = `message message--${role}`;
|
|
200
|
+
bubble.setAttribute("part", `message message-${role}`);
|
|
201
|
+
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
202
|
+
bubble.innerHTML = renderMarkdown(content, { allowImages: this.#host.allowImages() });
|
|
203
|
+
// A finished bubble: rehydrated history, or a whole message appended at
|
|
204
|
+
// once. The streaming bubble gets its buttons in onTextEnd instead.
|
|
205
|
+
attachCopyButtons(bubble, this.#host.strings());
|
|
206
|
+
this.ensureGroup().appendChild(bubble);
|
|
207
|
+
} else {
|
|
208
|
+
this.#currentGroup = null;
|
|
209
|
+
bubble.textContent = content;
|
|
210
|
+
this.#host.messages.appendChild(bubble);
|
|
211
|
+
}
|
|
212
|
+
this.updateEmptyState();
|
|
213
|
+
// A user bubble means someone just pressed Send, which is as deliberate as
|
|
214
|
+
// pressing the jump button -- so it goes to the bottom even if they had
|
|
215
|
+
// scrolled away to re-read something before typing.
|
|
216
|
+
if (role === MESSAGE_ROLE.USER) {
|
|
217
|
+
this.jump();
|
|
218
|
+
} else {
|
|
219
|
+
this.follow();
|
|
220
|
+
}
|
|
221
|
+
return bubble;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The open answer group, creating and appending it on first use. Everything a
|
|
226
|
+
* single assistant turn renders (text, tool cards, the pending indicator)
|
|
227
|
+
* goes inside it, so the opt-in `data-answer-well` styling can box the whole
|
|
228
|
+
* turn. Idempotent across the turn's runs — it persists until the run's
|
|
229
|
+
* settle closes it, see {@link closeGroup}.
|
|
230
|
+
*/
|
|
231
|
+
ensureGroup(): HTMLDivElement {
|
|
232
|
+
if (this.#currentGroup === null) {
|
|
233
|
+
const group = document.createElement("div");
|
|
234
|
+
group.className = "answer";
|
|
235
|
+
group.setAttribute("part", "answer");
|
|
236
|
+
this.#currentGroup = group;
|
|
237
|
+
this.#host.messages.appendChild(group);
|
|
238
|
+
this.updateEmptyState();
|
|
239
|
+
}
|
|
240
|
+
return this.#currentGroup;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Word-by-word reveal for the `word` text-animation mode, applied to a
|
|
245
|
+
* completed assistant bubble. `fade` is pure CSS (no JS); `none` is a no-op.
|
|
246
|
+
*/
|
|
247
|
+
revealWords(bubble: HTMLDivElement): void {
|
|
248
|
+
if (this.#host.element.getAttribute("data-text-animation") === "word") {
|
|
249
|
+
wrapWords(bubble);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hide the empty-state region once the message list holds anything else, and
|
|
255
|
+
* say so on the host as `data-empty`.
|
|
256
|
+
*
|
|
257
|
+
* Stamped on the host because the layout has to answer it: where the
|
|
258
|
+
* composer sits is decided outside the list this region lives in, and no
|
|
259
|
+
* selector reaches from inside the list back up to the list's siblings. It is
|
|
260
|
+
* also a documented styling hook for a host's own chrome around a full-page
|
|
261
|
+
* chat.
|
|
262
|
+
*/
|
|
263
|
+
updateEmptyState(): void {
|
|
264
|
+
this.#host.emptyWrap.hidden = this.#host.messages.childElementCount > 1;
|
|
265
|
+
this.#host.element.toggleAttribute("data-empty", !this.#host.emptyWrap.hidden);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
|
|
269
|
+
appendStoppedNote(): void {
|
|
270
|
+
const note = document.createElement("div");
|
|
271
|
+
note.className = "stopped-note";
|
|
272
|
+
note.setAttribute("part", "stopped");
|
|
273
|
+
note.setAttribute("role", "status");
|
|
274
|
+
note.textContent = this.#host.strings().stopped;
|
|
275
|
+
this.ensureGroup().appendChild(note);
|
|
276
|
+
this.updateEmptyState();
|
|
277
|
+
this.follow();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
282
|
+
* silent stretch before the first token and the gap after a tool result
|
|
283
|
+
* while the next round is requested. Idempotent.
|
|
284
|
+
*/
|
|
285
|
+
showPending(): void {
|
|
286
|
+
if (this.#pending !== null) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const pending = document.createElement("div");
|
|
290
|
+
pending.className = "pending";
|
|
291
|
+
pending.setAttribute("part", "pending");
|
|
292
|
+
pending.setAttribute("role", "status");
|
|
293
|
+
pending.setAttribute("aria-label", this.#host.strings().thinking);
|
|
294
|
+
for (let i = 0; i < 3; i += 1) {
|
|
295
|
+
const dot = document.createElement("span");
|
|
296
|
+
dot.className = "pending-dot";
|
|
297
|
+
pending.appendChild(dot);
|
|
298
|
+
}
|
|
299
|
+
this.#pending = pending;
|
|
300
|
+
this.ensureGroup().appendChild(pending);
|
|
301
|
+
this.updateEmptyState();
|
|
302
|
+
this.follow();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Remove the pending indicator if shown. */
|
|
306
|
+
hidePending(): void {
|
|
307
|
+
this.#pending?.remove();
|
|
308
|
+
this.#pending = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The current turn's thoughts region, creating it (at the top of the answer
|
|
313
|
+
* group, above any streamed text or tool cards) on first sight. Idempotent
|
|
314
|
+
* across a turn's reasoning tokens.
|
|
315
|
+
*/
|
|
316
|
+
showThoughts(): ThoughtsBlock {
|
|
317
|
+
if (this.#thoughts === null) {
|
|
318
|
+
this.#thoughts = new ThoughtsBlock(this.#host.strings());
|
|
319
|
+
const group = this.ensureGroup();
|
|
320
|
+
group.insertBefore(this.#thoughts.element, group.firstChild);
|
|
321
|
+
this.updateEmptyState();
|
|
322
|
+
this.follow();
|
|
323
|
+
}
|
|
324
|
+
return this.#thoughts;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Render a skill notice for a `load_capability` call; ``true`` when handled.
|
|
329
|
+
*
|
|
330
|
+
* Shared by the live stream and history replay so the transcript looks the
|
|
331
|
+
* same before and after a reload.
|
|
332
|
+
*/
|
|
333
|
+
noticeIfSkillLoad(call: AgUiToolCall): boolean {
|
|
334
|
+
const skill = skillNameFrom(call);
|
|
335
|
+
if (skill === null) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
this.appendNotice(
|
|
339
|
+
"✨",
|
|
340
|
+
fillUiString(this.#host.strings().usingSkill, { name: skill }),
|
|
341
|
+
"skill",
|
|
342
|
+
);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* An inline notice about something the run did.
|
|
348
|
+
*
|
|
349
|
+
* Goes through {@link ensureGroup} like a tool card so it lands *inside* the
|
|
350
|
+
* assistant turn it annotates rather than floating between turns, and does
|
|
351
|
+
* the same empty-state and scroll bookkeeping afterwards.
|
|
352
|
+
*
|
|
353
|
+
* `undo` is offered only where the agent rearranged the user's own window --
|
|
354
|
+
* see {@link renderRunNotice} for why a notice may carry that one control and
|
|
355
|
+
* nothing else.
|
|
356
|
+
*/
|
|
357
|
+
appendNotice(
|
|
358
|
+
icon: string,
|
|
359
|
+
text: string,
|
|
360
|
+
kind: string,
|
|
361
|
+
undo?: { readonly label: string; readonly onActivate: () => void },
|
|
362
|
+
): void {
|
|
363
|
+
this.ensureGroup().appendChild(renderRunNotice(icon, text, kind, undo));
|
|
364
|
+
this.updateEmptyState();
|
|
365
|
+
this.follow();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Place a tool's rendered node against its own card.
|
|
370
|
+
*
|
|
371
|
+
* Anchored rather than appended because a client tool's handler does not run
|
|
372
|
+
* until the round is over: appending would put the node after everything the
|
|
373
|
+
* model said next, visibly detached from the call that produced it, and in a
|
|
374
|
+
* different order than the same transcript takes on reload. The card was
|
|
375
|
+
* created inline, in the right place, so anchoring makes *when* the handler
|
|
376
|
+
* runs stop mattering.
|
|
377
|
+
*/
|
|
378
|
+
renderToolOutput(render: ChartRenderer, call: AgUiToolCall): void {
|
|
379
|
+
const node = renderOrWarn(() => render(call.args), `tool ${call.name}`);
|
|
380
|
+
if (node === null) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// `after` rather than an insert-or-append branch: both callers set the card
|
|
384
|
+
// element immediately before calling, and a parentless anchor makes `after`
|
|
385
|
+
// a no-op, so the alternative would be a branch nothing can reach.
|
|
386
|
+
this.#cardElements.get(call.id)?.after(node);
|
|
387
|
+
this.afterGrew();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Update the empty state and follow the foot, after the transcript grew. */
|
|
391
|
+
afterGrew(): void {
|
|
392
|
+
this.updateEmptyState();
|
|
393
|
+
this.follow();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The card for ``call``, creating and appending it on first sight.
|
|
398
|
+
*
|
|
399
|
+
* The run's tool-call handler creates the card (pending) during the run; the
|
|
400
|
+
* tool's execution later retrieves the same card to settle it.
|
|
401
|
+
*/
|
|
402
|
+
cardFor(call: AgUiToolCall): ToolCallCard {
|
|
403
|
+
const existing = this.#toolCards.get(call.id);
|
|
404
|
+
if (existing !== undefined) {
|
|
405
|
+
return existing;
|
|
406
|
+
}
|
|
407
|
+
// Prefer the tool's own `x-summary`; then an explicit `toolSummaries`
|
|
408
|
+
// entry; then the fetched server catalog (`data-tools-url`). All cover
|
|
409
|
+
// server-side tools whose schema never reached the browser.
|
|
410
|
+
const labelled = this.#host.resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
411
|
+
const summary =
|
|
412
|
+
typeof labelled === "string"
|
|
413
|
+
? labelled
|
|
414
|
+
: (this.#host.toolSummaries()[call.name] ??
|
|
415
|
+
this.#host.serverSummary(call.name) ??
|
|
416
|
+
prettifyToolName(call.name));
|
|
417
|
+
const card = new ToolCallCard(call.name, call.args, summary, this.#host.strings(), {
|
|
418
|
+
// A thunk over the live property, not the property itself: the card keeps
|
|
419
|
+
// this for the life of the call, and the result region is filled when the
|
|
420
|
+
// tool settles -- which can be long after a host set the hook.
|
|
421
|
+
formatPayload: (payload) => this.#host.formatToolPayload(payload),
|
|
422
|
+
});
|
|
423
|
+
this.#toolCards.set(call.id, card);
|
|
424
|
+
this.ensureGroup().appendChild(card.element);
|
|
425
|
+
this.updateEmptyState();
|
|
426
|
+
this.follow();
|
|
427
|
+
return card;
|
|
428
|
+
}
|
|
429
|
+
}
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -47,6 +47,13 @@ export interface UiStrings {
|
|
|
47
47
|
toggleTheme: string;
|
|
48
48
|
|
|
49
49
|
// ── Messages region ─────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* The greeting over an empty conversation under `placement="page"` (or an
|
|
52
|
+
* `embedded` panel that opted in). Token: `{name}`, the `user-name` attribute.
|
|
53
|
+
*/
|
|
54
|
+
greeting: string;
|
|
55
|
+
/** The same greeting when the host named nobody: no `user-name`, or a blank one. */
|
|
56
|
+
greetingNoName: string;
|
|
50
57
|
/** `aria-label` of the scrolling message log. */
|
|
51
58
|
conversation: string;
|
|
52
59
|
/** The button offering to return to the foot of the transcript. */
|
|
@@ -70,10 +77,25 @@ export interface UiStrings {
|
|
|
70
77
|
stopped: string;
|
|
71
78
|
/** Error shown when the stream drops without a terminal AG-UI event. */
|
|
72
79
|
connectionLost: string;
|
|
73
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Formerly the card text for a tool call that produced no result.
|
|
82
|
+
*
|
|
83
|
+
* @deprecated No longer drawn: such a call is now answered with
|
|
84
|
+
* {@link callNotFinished}, on its card and in the conversation alike. Kept so a
|
|
85
|
+
* string table that sets it still type-checks.
|
|
86
|
+
*/
|
|
74
87
|
noResult: string;
|
|
88
|
+
/**
|
|
89
|
+
* Tool-result content (and card text) for a call that never produced a result
|
|
90
|
+
* and that nobody refused: the run was stopped or failed first, the page
|
|
91
|
+
* reloaded, or no tool here answered it. Sent to the agent, so it must not say
|
|
92
|
+
* a person declined.
|
|
93
|
+
*/
|
|
94
|
+
callNotFinished: string;
|
|
75
95
|
/** Tool-result content when the user declines a confirmed action. */
|
|
76
96
|
declinedAction: string;
|
|
97
|
+
/** Tool-result content (and card text) when `confirmPredicate` threw, so the call was refused. */
|
|
98
|
+
confirmCheckFailed: string;
|
|
77
99
|
/** A navigating tool's card text while the page reloads. */
|
|
78
100
|
navigating: string;
|
|
79
101
|
/** Notice shown when the server replaced the conversation wholesale. */
|
|
@@ -96,6 +118,8 @@ export interface UiStrings {
|
|
|
96
118
|
notConnected: string;
|
|
97
119
|
/** Composer hint when a run continuation was picked with an empty composer. */
|
|
98
120
|
continueNeedsTurn: string;
|
|
121
|
+
/** Composer hint when a run continuation was picked while a run is in flight. */
|
|
122
|
+
continueWhileRunning: string;
|
|
99
123
|
|
|
100
124
|
// ── Composer ────────────────────────────────────────────────────────────────
|
|
101
125
|
/** `aria-label` of the message textarea. */
|
|
@@ -134,6 +158,8 @@ export interface UiStrings {
|
|
|
134
158
|
toolError: string;
|
|
135
159
|
/** Status pill on a declined call. */
|
|
136
160
|
toolDeclined: string;
|
|
161
|
+
/** Status pill on a call that ended without a result nobody refused. */
|
|
162
|
+
toolInterrupted: string;
|
|
137
163
|
/** Accessible label on the panel resize handle. */
|
|
138
164
|
resizePanel: string;
|
|
139
165
|
/** Note on a tool card whose call a human approved. */
|
|
@@ -148,6 +174,8 @@ export interface UiStrings {
|
|
|
148
174
|
errorLabel: string;
|
|
149
175
|
/** Heading over a tool card's result region when the call was declined. */
|
|
150
176
|
declinedLabel: string;
|
|
177
|
+
/** Heading over a tool card's result region when the call did not finish. */
|
|
178
|
+
interruptedLabel: string;
|
|
151
179
|
/** Label on the toggle that expands a tool card's body. */
|
|
152
180
|
details: string;
|
|
153
181
|
|
|
@@ -327,6 +355,8 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
327
355
|
forkRun: "Fork",
|
|
328
356
|
forkedRun: "branched",
|
|
329
357
|
|
|
358
|
+
greeting: "Hello, {name}",
|
|
359
|
+
greetingNoName: "Hello there",
|
|
330
360
|
conversation: "Conversation",
|
|
331
361
|
jumpToLatest: "Jump to latest",
|
|
332
362
|
announceResponding: "Assistant is responding",
|
|
@@ -339,7 +369,11 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
339
369
|
stopped: "⏹ Stopped",
|
|
340
370
|
connectionLost: "Connection lost",
|
|
341
371
|
noResult: "No result returned.",
|
|
372
|
+
callNotFinished:
|
|
373
|
+
"Not finished: the run ended or moved on before this tool call returned a result.",
|
|
342
374
|
declinedAction: "User declined the action.",
|
|
375
|
+
confirmCheckFailed:
|
|
376
|
+
"Not run: the check that decides whether this action needs the user's confirmation failed.",
|
|
343
377
|
navigating: "Navigating…",
|
|
344
378
|
historyReplaced:
|
|
345
379
|
"The server replaced this conversation's history. Reload to see the updated transcript.",
|
|
@@ -353,6 +387,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
353
387
|
"{n} file still uploading — it was not sent with this message and is still attached.",
|
|
354
388
|
notConnected: "This chat isn’t connected to an agent, so the message wasn’t sent.",
|
|
355
389
|
continueNeedsTurn: "Type the next turn in the composer first, then pick a run to continue.",
|
|
390
|
+
continueWhileRunning: "Wait for the current answer or stop it, then pick a run to continue.",
|
|
356
391
|
skillNeeds: "“{title}” needs {fields} — fill it in below, then send.",
|
|
357
392
|
|
|
358
393
|
message: "Message",
|
|
@@ -371,6 +406,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
371
406
|
toolDone: "✓ done",
|
|
372
407
|
toolError: "⚠ error",
|
|
373
408
|
toolDeclined: "⊘ declined",
|
|
409
|
+
toolInterrupted: "◌ not finished",
|
|
374
410
|
resizePanel: "Resize the chat panel",
|
|
375
411
|
decisionApproved: "approved by you",
|
|
376
412
|
decisionDeclined: "declined by you",
|
|
@@ -378,6 +414,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
378
414
|
resultLabel: "Result",
|
|
379
415
|
errorLabel: "Error",
|
|
380
416
|
declinedLabel: "Declined",
|
|
417
|
+
interruptedLabel: "Not finished",
|
|
381
418
|
details: "Details",
|
|
382
419
|
|
|
383
420
|
subAgentWorking: "Working…",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.40.0";
|