@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.
Files changed (142) hide show
  1. package/CHANGELOG.md +515 -1
  2. package/README.md +232 -56
  3. package/dist/ag-ui-web-component.bundle.js +370 -139
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +17 -14
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/activity_registry.d.ts +46 -0
  8. package/dist/core/activity_registry.d.ts.map +1 -0
  9. package/dist/core/ag_ui_chat.d.ts +23 -10
  10. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  11. package/dist/core/agui_client.d.ts +28 -0
  12. package/dist/core/agui_client.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +27 -0
  14. package/dist/core/client_seed.d.ts.map +1 -0
  15. package/dist/core/read_max_tool_rounds.d.ts +9 -0
  16. package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
  17. package/dist/core/run_handlers.d.ts +80 -0
  18. package/dist/core/run_handlers.d.ts.map +1 -0
  19. package/dist/core/storage_scope.d.ts +110 -0
  20. package/dist/core/storage_scope.d.ts.map +1 -0
  21. package/dist/core/tool_outcome.d.ts +2 -3
  22. package/dist/core/tool_outcome.d.ts.map +1 -1
  23. package/dist/core/utils.d.ts +46 -0
  24. package/dist/core/utils.d.ts.map +1 -1
  25. package/dist/index.js +16268 -14747
  26. package/dist/index.js.map +4 -4
  27. package/dist/skills/skill_catalog.d.ts +67 -0
  28. package/dist/skills/skill_catalog.d.ts.map +1 -0
  29. package/dist/skills/skill_name_from.d.ts +12 -0
  30. package/dist/skills/skill_name_from.d.ts.map +1 -0
  31. package/dist/tools/tool_catalog.d.ts +79 -0
  32. package/dist/tools/tool_catalog.d.ts.map +1 -0
  33. package/dist/tools/tool_dispatch.d.ts +104 -0
  34. package/dist/tools/tool_dispatch.d.ts.map +1 -0
  35. package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
  36. package/dist/ui/composer/auto_grow.d.ts +10 -0
  37. package/dist/ui/composer/auto_grow.d.ts.map +1 -0
  38. package/dist/ui/composer/composer_attachments.d.ts +57 -0
  39. package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
  40. package/dist/ui/composer/composer_voice.d.ts +48 -0
  41. package/dist/ui/composer/composer_voice.d.ts.map +1 -0
  42. package/dist/ui/composer/voice_input.d.ts.map +1 -1
  43. package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
  44. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
  45. package/dist/ui/fill_ui_string.d.ts +32 -0
  46. package/dist/ui/fill_ui_string.d.ts.map +1 -0
  47. package/dist/ui/history/conversation_history.d.ts +203 -0
  48. package/dist/ui/history/conversation_history.d.ts.map +1 -0
  49. package/dist/ui/history/relative_time.d.ts.map +1 -1
  50. package/dist/ui/history/thread_drawer.d.ts +0 -14
  51. package/dist/ui/history/thread_drawer.d.ts.map +1 -1
  52. package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
  53. package/dist/ui/interrupts/pending_decision.d.ts +23 -0
  54. package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
  55. package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
  56. package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
  57. package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
  58. package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
  59. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  60. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  61. package/dist/ui/placement/panel_placement.d.ts +119 -0
  62. package/dist/ui/placement/panel_placement.d.ts.map +1 -0
  63. package/dist/ui/progress/run_announcer.d.ts +41 -0
  64. package/dist/ui/progress/run_announcer.d.ts.map +1 -0
  65. package/dist/ui/progress/subagent_progress.d.ts +58 -0
  66. package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
  67. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  68. package/dist/ui/shell/adopt_styles.d.ts +23 -0
  69. package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
  70. package/dist/ui/shell/glyph_slot.d.ts +7 -0
  71. package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
  72. package/dist/ui/shell/header_button.d.ts +10 -0
  73. package/dist/ui/shell/header_button.d.ts.map +1 -0
  74. package/dist/ui/shell/icon_element.d.ts +12 -0
  75. package/dist/ui/shell/icon_element.d.ts.map +1 -0
  76. package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
  77. package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
  78. package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
  79. package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
  80. package/dist/ui/styles.d.ts +1 -1
  81. package/dist/ui/styles.d.ts.map +1 -1
  82. package/dist/ui/transcript/answer_actions.d.ts +39 -0
  83. package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
  84. package/dist/ui/transcript/answer_stream.d.ts +56 -0
  85. package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
  86. package/dist/ui/transcript/render_or_warn.d.ts +16 -0
  87. package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
  88. package/dist/ui/transcript/starter_chips.d.ts +21 -0
  89. package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
  90. package/dist/ui/transcript/transcript.d.ts +171 -0
  91. package/dist/ui/transcript/transcript.d.ts.map +1 -0
  92. package/dist/ui/ui_strings.d.ts +29 -1
  93. package/dist/ui/ui_strings.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/constants.ts +17 -15
  96. package/src/core/activity_registry.ts +133 -0
  97. package/src/core/ag_ui_chat.ts +945 -4419
  98. package/src/core/agui_client.ts +172 -16
  99. package/src/core/client_seed.ts +27 -0
  100. package/src/core/read_max_tool_rounds.ts +13 -0
  101. package/src/core/remote_conversation_store.ts +1 -1
  102. package/src/core/run_handlers.ts +501 -0
  103. package/src/core/storage_scope.ts +279 -0
  104. package/src/core/tool_outcome.ts +5 -3
  105. package/src/core/utils.ts +109 -0
  106. package/src/skills/skill_catalog.ts +166 -0
  107. package/src/skills/skill_name_from.ts +19 -0
  108. package/src/tools/tool_catalog.ts +354 -0
  109. package/src/tools/tool_dispatch.ts +521 -0
  110. package/src/ui/composer/attachment_tray.ts +6 -5
  111. package/src/ui/composer/auto_grow.ts +12 -0
  112. package/src/ui/composer/composer_attachments.ts +324 -0
  113. package/src/ui/composer/composer_voice.ts +93 -0
  114. package/src/ui/composer/voice_input.ts +4 -4
  115. package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
  116. package/src/ui/fill_ui_string.ts +41 -0
  117. package/src/ui/history/conversation_history.ts +699 -0
  118. package/src/ui/history/relative_time.ts +5 -4
  119. package/src/ui/history/thread_drawer.ts +3 -50
  120. package/src/ui/interrupts/confirmation_card.ts +4 -2
  121. package/src/ui/interrupts/pending_decision.ts +34 -0
  122. package/src/ui/placement/is_collapsible_placement.ts +19 -0
  123. package/src/ui/placement/is_draggable_placement.ts +23 -0
  124. package/src/ui/placement/launcher_drag.ts +104 -89
  125. package/src/ui/placement/panel_placement.ts +1130 -0
  126. package/src/ui/progress/run_announcer.ts +67 -0
  127. package/src/ui/progress/subagent_progress.ts +201 -0
  128. package/src/ui/progress/tool_call_card.ts +2 -0
  129. package/src/ui/shell/adopt_styles.ts +28 -0
  130. package/src/ui/shell/glyph_slot.ts +12 -0
  131. package/src/ui/shell/header_button.ts +21 -0
  132. package/src/ui/shell/icon_element.ts +33 -0
  133. package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
  134. package/src/ui/shell/read_launcher_icon_url.ts +9 -0
  135. package/src/ui/styles.ts +301 -70
  136. package/src/ui/transcript/answer_actions.ts +139 -0
  137. package/src/ui/transcript/answer_stream.ts +123 -0
  138. package/src/ui/transcript/render_or_warn.ts +22 -0
  139. package/src/ui/transcript/starter_chips.ts +42 -0
  140. package/src/ui/transcript/transcript.ts +429 -0
  141. package/src/ui/ui_strings.ts +38 -1
  142. 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
+ }
@@ -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
- /** Fallback when a tool call produced no result. */
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.38.0";
1
+ export const VERSION: string = "0.40.0";