@artooi/ag-ui-web-component 0.5.0 → 0.7.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 +73 -1
- package/README.md +208 -7
- package/dist/ag-ui-web-component.bundle.js +268 -58
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +33 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +23 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +35 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +32 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1247 -245
- package/dist/index.js.map +4 -4
- package/dist/tools/page_action_tools.d.ts +31 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -0
- package/dist/ui/attachment_chips.d.ts +13 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +45 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -0
- package/dist/ui/confirmation_card.d.ts +4 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +7 -1
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +6 -2
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +126 -0
- package/dist/ui/ui_strings.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +18 -0
- package/src/core/ag_ui_chat.ts +389 -51
- package/src/core/agui_client.ts +48 -4
- package/src/core/attachment.ts +39 -0
- package/src/core/upload_attachment.ts +113 -0
- package/src/index.ts +13 -0
- package/src/tools/page_action_tools.ts +130 -0
- package/src/ui/attachment_chips.ts +68 -0
- package/src/ui/attachment_tray.ts +243 -0
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/relative_time.ts +15 -8
- package/src/ui/styles.ts +208 -0
- package/src/ui/thread_drawer.ts +53 -25
- package/src/ui/tool_call_card.ts +40 -17
- package/src/ui/ui_strings.ts +208 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, Message, Tool } from "@ag-ui/core";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
3
4
|
MESSAGE_ROLE,
|
|
4
5
|
SUBMIT_EVENT,
|
|
5
6
|
TOGGLE_EVENT,
|
|
@@ -14,10 +15,13 @@ import type { Skill } from "../skills/skill.js";
|
|
|
14
15
|
import { type ClientTool, ClientToolRegistry } from "../tools/client_tool_registry.js";
|
|
15
16
|
import { isDestructive } from "../tools/is_destructive.js";
|
|
16
17
|
import { isNavigates } from "../tools/is_navigates.js";
|
|
18
|
+
import { createPageActionTools, type ResolvePageTarget } from "../tools/page_action_tools.js";
|
|
17
19
|
import { createPageMapContext, type PageMap } from "../tools/page_map.js";
|
|
18
20
|
import { parseToolCatalog } from "../tools/parse_tool_catalog.js";
|
|
19
21
|
import { createRouteTools, type RouteMap } from "../tools/route_map.js";
|
|
20
22
|
import { createStateHookTools, type StateHook } from "../tools/state_hook.js";
|
|
23
|
+
import { renderAttachmentChips } from "../ui/attachment_chips.js";
|
|
24
|
+
import { AttachmentTray } from "../ui/attachment_tray.js";
|
|
21
25
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
22
26
|
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
23
27
|
import { renderMarkdown } from "../ui/render_markdown.js";
|
|
@@ -26,12 +30,14 @@ import { SkillsMenu } from "../ui/skills_menu.js";
|
|
|
26
30
|
import { STYLES } from "../ui/styles.js";
|
|
27
31
|
import { ThreadDrawer } from "../ui/thread_drawer.js";
|
|
28
32
|
import { ToolCallCard, type ToolDisplayMode } from "../ui/tool_call_card.js";
|
|
33
|
+
import { DEFAULT_UI_STRINGS, mergeUiStrings, type UiStrings } from "../ui/ui_strings.js";
|
|
29
34
|
import {
|
|
30
35
|
AgUiClient,
|
|
31
36
|
type AgUiClientHandlers,
|
|
32
37
|
type AgUiToolCall,
|
|
33
38
|
type ToolExecution,
|
|
34
39
|
} from "./agui_client.js";
|
|
40
|
+
import { type AttachmentRef, messageAttachments } from "./attachment.js";
|
|
35
41
|
import {
|
|
36
42
|
type ClientConversationStore,
|
|
37
43
|
type NavigationCheckpoint,
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
} from "./conversation_store.js";
|
|
40
46
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
41
47
|
import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
48
|
+
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
42
49
|
|
|
43
50
|
/** The role a rendered chat message takes. */
|
|
44
51
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -46,6 +53,8 @@ export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
|
46
53
|
/** `detail` shape of the {@link SUBMIT_EVENT} CustomEvent. */
|
|
47
54
|
export interface SubmitDetail {
|
|
48
55
|
readonly content: string;
|
|
56
|
+
/** Durable refs for the files attached to this message (empty when none). */
|
|
57
|
+
readonly attachments: readonly AttachmentRef[];
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
/** `detail` shape of the {@link TOGGLE_EVENT} CustomEvent. */
|
|
@@ -114,9 +123,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
114
123
|
|
|
115
124
|
/**
|
|
116
125
|
* Per-run context provider. Defaults to the compact page map (when a
|
|
117
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
126
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
127
|
+
* plus a one-line manifest of the files attached to the message being sent,
|
|
128
|
+
* so the agent knows which `read_attachment` ids are available.
|
|
118
129
|
*/
|
|
119
|
-
getContext: () => Context[] = () =>
|
|
130
|
+
getContext: () => Context[] = () => [
|
|
131
|
+
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
132
|
+
...this.#attachmentContext(),
|
|
133
|
+
];
|
|
120
134
|
|
|
121
135
|
/**
|
|
122
136
|
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
@@ -144,6 +158,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
144
158
|
*/
|
|
145
159
|
conversationStore: ClientConversationStore = new SessionStorageStore();
|
|
146
160
|
|
|
161
|
+
/**
|
|
162
|
+
* How attached files are uploaded. `null` (default) uses the built-in
|
|
163
|
+
* multipart `POST` to `data-attachments-url`. Set a custom
|
|
164
|
+
* {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
|
|
165
|
+
* swap the transport (e.g. a `tus-js-client` resumable adapter or
|
|
166
|
+
* direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
|
|
167
|
+
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
168
|
+
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
169
|
+
*/
|
|
170
|
+
uploadHandler: UploadHandler | null = null;
|
|
171
|
+
|
|
147
172
|
/**
|
|
148
173
|
* Builds the tool result a navigating tool resumes with after the page
|
|
149
174
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -171,12 +196,30 @@ export class AgUiChat extends HTMLElement {
|
|
|
171
196
|
*/
|
|
172
197
|
toolSummaries: Record<string, string> = {};
|
|
173
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Localizable UI strings — a partial override merged over the English
|
|
201
|
+
* {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
|
|
202
|
+
* element is appended); the `data-strings` JSON attribute is the markup
|
|
203
|
+
* equivalent, and this property wins key-by-key over it.
|
|
204
|
+
*/
|
|
205
|
+
strings: Partial<UiStrings> = {};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a `scroll_to` / `drag_and_drop` target string to a host-page
|
|
209
|
+
* element (or `null`). Defaults to a CSS-selector lookup; override to map
|
|
210
|
+
* page-map element ids. The page-action tools are opt-in via the
|
|
211
|
+
* `data-page-actions` attribute (`"scroll"` / `"drag"`).
|
|
212
|
+
*/
|
|
213
|
+
resolvePageTarget: ResolvePageTarget = (target) => document.querySelector<HTMLElement>(target);
|
|
214
|
+
|
|
174
215
|
/**
|
|
175
216
|
* Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
|
|
176
217
|
* tool name. The base layer behind {@link toolSummaries}: an explicit entry in
|
|
177
218
|
* `toolSummaries` wins, this fills the rest. Populated once on connect.
|
|
178
219
|
*/
|
|
179
220
|
#toolCatalog: Record<string, string> = {};
|
|
221
|
+
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
222
|
+
#strings: UiStrings = DEFAULT_UI_STRINGS;
|
|
180
223
|
|
|
181
224
|
readonly #toolRegistry = new ClientToolRegistry();
|
|
182
225
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
@@ -196,6 +239,18 @@ export class AgUiChat extends HTMLElement {
|
|
|
196
239
|
readonly #skillsMenu: SkillsMenu;
|
|
197
240
|
readonly #drawer: ThreadDrawer;
|
|
198
241
|
readonly #skillHint: HTMLDivElement;
|
|
242
|
+
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
243
|
+
readonly #attachButton: HTMLButtonElement;
|
|
244
|
+
readonly #fileInput: HTMLInputElement;
|
|
245
|
+
readonly #attachSlot: HTMLDivElement;
|
|
246
|
+
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
247
|
+
readonly #rail: HTMLButtonElement;
|
|
248
|
+
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
249
|
+
readonly #emptyWrap: HTMLDivElement;
|
|
250
|
+
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
251
|
+
#attachTray: AttachmentTray | null = null;
|
|
252
|
+
/** Refs attached to the message currently being sent (the context manifest). */
|
|
253
|
+
#runAttachments: readonly AttachmentRef[] = [];
|
|
199
254
|
|
|
200
255
|
#client: AgUiClient | null = null;
|
|
201
256
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
@@ -227,6 +282,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
227
282
|
this.#send = document.createElement("button");
|
|
228
283
|
this.#title = document.createElement("span");
|
|
229
284
|
this.#skillHint = document.createElement("div");
|
|
285
|
+
this.#attachButton = document.createElement("button");
|
|
286
|
+
this.#fileInput = document.createElement("input");
|
|
287
|
+
this.#attachSlot = document.createElement("div");
|
|
288
|
+
this.#rail = document.createElement("button");
|
|
289
|
+
this.#emptyWrap = document.createElement("div");
|
|
230
290
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
231
291
|
this.#drawer = new ThreadDrawer({
|
|
232
292
|
onSelect: (threadId) => {
|
|
@@ -253,8 +313,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
253
313
|
|
|
254
314
|
attributeChangedCallback(_name: string, _previous: string | null, value: string | null): void {
|
|
255
315
|
// Only `title-text` is observed (other attributes are read at use-time or
|
|
256
|
-
// are CSS-reactive), so update the header title directly.
|
|
257
|
-
|
|
316
|
+
// are CSS-reactive), so update the header title directly. `#strings` is the
|
|
317
|
+
// resolved table once connected, the English defaults before then.
|
|
318
|
+
this.#title.textContent = value ?? this.#strings.title;
|
|
258
319
|
}
|
|
259
320
|
|
|
260
321
|
/** Declare a frontend tool the agent may call. */
|
|
@@ -307,9 +368,29 @@ export class AgUiChat extends HTMLElement {
|
|
|
307
368
|
];
|
|
308
369
|
}
|
|
309
370
|
|
|
310
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
|
|
373
|
+
* via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
|
|
374
|
+
* through {@link resolvePageTarget} so a host controls the agent's interaction
|
|
375
|
+
* surface; absent attribute ⇒ no tools registered.
|
|
376
|
+
*/
|
|
377
|
+
#pageActionTools(): ClientTool[] {
|
|
378
|
+
const attr = this.getAttribute("data-page-actions");
|
|
379
|
+
if (attr === null) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
const enabled = new Set(
|
|
383
|
+
attr
|
|
384
|
+
.split(",")
|
|
385
|
+
.map((token) => token.trim())
|
|
386
|
+
.filter((token) => token !== ""),
|
|
387
|
+
);
|
|
388
|
+
return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** All built-in (route + page + page-action) frontend tools. */
|
|
311
392
|
#builtinTools(): ClientTool[] {
|
|
312
|
-
return [...this.#routeTools(), ...this.#pageTools()];
|
|
393
|
+
return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
|
|
313
394
|
}
|
|
314
395
|
|
|
315
396
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
@@ -350,17 +431,133 @@ export class AgUiChat extends HTMLElement {
|
|
|
350
431
|
}
|
|
351
432
|
|
|
352
433
|
connectedCallback(): void {
|
|
434
|
+
// Resolve the string table before rendering any chrome (defaults are the
|
|
435
|
+
// floor; `data-strings` then the `strings` property layer over them).
|
|
436
|
+
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
353
437
|
this.#render();
|
|
438
|
+
this.#drawer.setStrings(this.#strings);
|
|
354
439
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
355
440
|
this.setAttribute("collapsed", "");
|
|
356
441
|
}
|
|
442
|
+
this.#syncRail();
|
|
357
443
|
this.#initSkills();
|
|
358
444
|
void this.#fetchToolCatalog();
|
|
359
445
|
this.#wireThreadStore();
|
|
446
|
+
this.#wireAttachments();
|
|
360
447
|
this.#threadId = this.conversationStore.threadId();
|
|
361
448
|
void this.#rehydrate();
|
|
362
449
|
}
|
|
363
450
|
|
|
451
|
+
/** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
|
|
452
|
+
#readStringOverrides(): Partial<UiStrings> {
|
|
453
|
+
const raw = this.getAttribute("data-strings");
|
|
454
|
+
if (raw === null) {
|
|
455
|
+
return {};
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const parsed: unknown = JSON.parse(raw);
|
|
459
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
460
|
+
return parsed as Partial<UiStrings>;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
// Malformed JSON — fall back to the defaults rather than failing to mount.
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
470
|
+
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
471
|
+
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
472
|
+
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
473
|
+
* stays hidden and the chat degrades to text-only.
|
|
474
|
+
*/
|
|
475
|
+
#wireAttachments(): void {
|
|
476
|
+
const url = this.getAttribute("data-attachments-url");
|
|
477
|
+
const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
|
|
478
|
+
if (upload === null) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
482
|
+
this.#attachTray = new AttachmentTray({
|
|
483
|
+
upload,
|
|
484
|
+
maxBytes: this.#attachmentMaxBytes(),
|
|
485
|
+
accept,
|
|
486
|
+
strings: this.#strings,
|
|
487
|
+
});
|
|
488
|
+
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
489
|
+
this.#fileInput.accept = accept;
|
|
490
|
+
this.#attachButton.hidden = false;
|
|
491
|
+
this.#enableDragAndDrop();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
|
|
495
|
+
#defaultUploadHandler(url: string | null): UploadHandler | null {
|
|
496
|
+
if (url === null) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
503
|
+
#attachmentMaxBytes(): number {
|
|
504
|
+
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
505
|
+
if (attr === null) {
|
|
506
|
+
return DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
507
|
+
}
|
|
508
|
+
const parsed = Number.parseInt(attr, 10);
|
|
509
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Queue every file from the picker into the tray, then reset the input. */
|
|
513
|
+
#onFilesPicked(): void {
|
|
514
|
+
const files = this.#fileInput.files;
|
|
515
|
+
if (files !== null) {
|
|
516
|
+
for (const file of Array.from(files)) {
|
|
517
|
+
this.#attachTray?.add(file);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Reset so re-picking the same file fires `change` again.
|
|
521
|
+
this.#fileInput.value = "";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
525
|
+
#enableDragAndDrop(): void {
|
|
526
|
+
this.#chat.addEventListener("dragover", (event) => {
|
|
527
|
+
event.preventDefault();
|
|
528
|
+
this.#chat.classList.add("chat--dragover");
|
|
529
|
+
});
|
|
530
|
+
this.#chat.addEventListener("dragleave", () => {
|
|
531
|
+
this.#chat.classList.remove("chat--dragover");
|
|
532
|
+
});
|
|
533
|
+
this.#chat.addEventListener("drop", (event) => {
|
|
534
|
+
event.preventDefault();
|
|
535
|
+
this.#chat.classList.remove("chat--dragover");
|
|
536
|
+
const files = event.dataTransfer?.files;
|
|
537
|
+
if (files !== undefined) {
|
|
538
|
+
for (const file of Array.from(files)) {
|
|
539
|
+
this.#attachTray?.add(file);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** The one-line manifest of the message's attachments, for the run context. */
|
|
546
|
+
#attachmentContext(): Context[] {
|
|
547
|
+
if (this.#runAttachments.length === 0) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
const lines = this.#runAttachments.map(
|
|
551
|
+
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`,
|
|
552
|
+
);
|
|
553
|
+
return [
|
|
554
|
+
{
|
|
555
|
+
description: "Files the user attached to this message",
|
|
556
|
+
value: `${lines.join("\n")}\nUse the read_attachment tool with an id to read a file's contents.`,
|
|
557
|
+
},
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
|
|
364
561
|
/**
|
|
365
562
|
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
366
563
|
* delete through that server endpoint (wrapping the current store as the
|
|
@@ -451,7 +648,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
451
648
|
#applySkill(skill: Skill): void {
|
|
452
649
|
const { text, missing } = fillTemplate(skill.prompt, this.skillContext());
|
|
453
650
|
if (missing.length > 0) {
|
|
454
|
-
this.#skillHint.textContent =
|
|
651
|
+
this.#skillHint.textContent = this.#strings.skillNeeds
|
|
652
|
+
.replace("{title}", skill.title)
|
|
653
|
+
.replace("{fields}", missing.join(", "));
|
|
455
654
|
this.#skillHint.hidden = false;
|
|
456
655
|
return;
|
|
457
656
|
}
|
|
@@ -487,6 +686,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
487
686
|
this.removeAttribute("collapsed");
|
|
488
687
|
}
|
|
489
688
|
sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
689
|
+
this.#syncRail();
|
|
490
690
|
this.dispatchEvent(
|
|
491
691
|
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
492
692
|
detail: { collapsed },
|
|
@@ -523,7 +723,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
523
723
|
this.#toolCards.clear();
|
|
524
724
|
this.#serverSettled.clear();
|
|
525
725
|
this.#initialMessages = [];
|
|
526
|
-
this.#
|
|
726
|
+
this.#runAttachments = [];
|
|
727
|
+
this.#attachTray?.clear();
|
|
728
|
+
// Keep the empty-state region; everything else clears.
|
|
729
|
+
this.#messages.replaceChildren(this.#emptyWrap);
|
|
730
|
+
this.#updateEmptyState();
|
|
527
731
|
}
|
|
528
732
|
|
|
529
733
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
@@ -587,8 +791,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
587
791
|
#renderHistoricMessage(message: Message): void {
|
|
588
792
|
const text = typeof message.content === "string" ? message.content : "";
|
|
589
793
|
if (message.role === MESSAGE_ROLE.USER) {
|
|
590
|
-
|
|
591
|
-
|
|
794
|
+
const attachments = messageAttachments(message);
|
|
795
|
+
if (text !== "" || attachments.length > 0) {
|
|
796
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, text);
|
|
797
|
+
if (attachments.length > 0) {
|
|
798
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
799
|
+
}
|
|
592
800
|
}
|
|
593
801
|
return;
|
|
594
802
|
}
|
|
@@ -662,12 +870,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
662
870
|
appendMessage(role: MessageRole, content: string): HTMLDivElement {
|
|
663
871
|
const bubble = document.createElement("div");
|
|
664
872
|
bubble.className = `message message--${role}`;
|
|
873
|
+
bubble.setAttribute("part", `message message-${role}`);
|
|
665
874
|
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
666
875
|
bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
|
|
667
876
|
} else {
|
|
668
877
|
bubble.textContent = content;
|
|
669
878
|
}
|
|
670
879
|
this.#messages.appendChild(bubble);
|
|
880
|
+
this.#updateEmptyState();
|
|
671
881
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
672
882
|
return bubble;
|
|
673
883
|
}
|
|
@@ -677,67 +887,83 @@ export class AgUiChat extends HTMLElement {
|
|
|
677
887
|
style.textContent = STYLES;
|
|
678
888
|
|
|
679
889
|
this.#chat.className = "chat";
|
|
890
|
+
this.#chat.setAttribute("part", "panel");
|
|
680
891
|
|
|
681
892
|
const header = document.createElement("div");
|
|
682
893
|
header.className = "header";
|
|
894
|
+
header.setAttribute("part", "header");
|
|
683
895
|
|
|
684
896
|
const title = this.#title;
|
|
685
897
|
title.className = "header-title";
|
|
686
|
-
title.
|
|
898
|
+
title.setAttribute("part", "title");
|
|
899
|
+
title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
|
|
900
|
+
|
|
901
|
+
// Optional header icon: a slot (any markup) with a `data-icon-url` <img>
|
|
902
|
+
// fallback. Rendered only when one of the two is provided, so the header has
|
|
903
|
+
// no phantom gap otherwise.
|
|
904
|
+
if (
|
|
905
|
+
this.querySelector('[slot="icon"]') !== null ||
|
|
906
|
+
this.getAttribute("data-icon-url") !== null
|
|
907
|
+
) {
|
|
908
|
+
header.append(this.#iconElement("icon", "icon", null));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// A coarse slot for host-provided header actions, between title and controls.
|
|
912
|
+
const headerActions = document.createElement("slot");
|
|
913
|
+
headerActions.name = "header-actions";
|
|
687
914
|
|
|
688
915
|
const controls = document.createElement("div");
|
|
689
916
|
controls.className = "header-controls";
|
|
917
|
+
controls.setAttribute("part", "header-controls");
|
|
690
918
|
|
|
691
|
-
const history =
|
|
692
|
-
history.type = "button";
|
|
693
|
-
history.className = "header-btn header-btn--history";
|
|
694
|
-
history.title = "Chat history";
|
|
695
|
-
history.setAttribute("aria-label", "Chat history");
|
|
696
|
-
history.textContent = "☰";
|
|
919
|
+
const history = this.#headerButton("history", this.#strings.chatHistory, "☰");
|
|
697
920
|
history.addEventListener("click", () => {
|
|
698
921
|
void this.#refreshDrawer();
|
|
699
922
|
this.#drawer.open();
|
|
700
923
|
});
|
|
701
924
|
|
|
702
|
-
const newChat =
|
|
703
|
-
newChat.type = "button";
|
|
704
|
-
newChat.className = "header-btn header-btn--new";
|
|
705
|
-
newChat.title = "New chat";
|
|
706
|
-
newChat.setAttribute("aria-label", "New chat");
|
|
707
|
-
newChat.textContent = "✚";
|
|
925
|
+
const newChat = this.#headerButton("new", this.#strings.newChat, "✚");
|
|
708
926
|
newChat.addEventListener("click", () => this.newChat());
|
|
709
927
|
|
|
710
|
-
const collapse =
|
|
711
|
-
collapse.type = "button";
|
|
712
|
-
collapse.className = "header-btn header-btn--collapse";
|
|
713
|
-
collapse.title = "Collapse";
|
|
714
|
-
collapse.setAttribute("aria-label", "Collapse");
|
|
715
|
-
collapse.textContent = "—";
|
|
928
|
+
const collapse = this.#headerButton("collapse", this.#strings.collapse, "—");
|
|
716
929
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
717
930
|
|
|
718
931
|
controls.append(history, newChat, collapse);
|
|
719
|
-
header.append(title, controls);
|
|
932
|
+
header.append(title, headerActions, controls);
|
|
720
933
|
|
|
721
934
|
this.#messages.className = "messages";
|
|
935
|
+
this.#messages.setAttribute("part", "messages");
|
|
722
936
|
// Screen readers announce streamed messages as they arrive.
|
|
723
937
|
this.#messages.setAttribute("role", "log");
|
|
724
938
|
this.#messages.setAttribute("aria-live", "polite");
|
|
725
|
-
this.#messages.setAttribute("aria-label",
|
|
939
|
+
this.#messages.setAttribute("aria-label", this.#strings.conversation);
|
|
940
|
+
|
|
941
|
+
// Empty-state region: a host slot at the top of the list, hidden as soon as
|
|
942
|
+
// anything renders.
|
|
943
|
+
this.#emptyWrap.className = "empty";
|
|
944
|
+
this.#emptyWrap.setAttribute("part", "empty");
|
|
945
|
+
const emptySlot = document.createElement("slot");
|
|
946
|
+
emptySlot.name = "empty";
|
|
947
|
+
this.#emptyWrap.append(emptySlot);
|
|
948
|
+
this.#messages.append(this.#emptyWrap);
|
|
726
949
|
|
|
727
950
|
const inputRow = document.createElement("div");
|
|
728
951
|
inputRow.className = "input-row";
|
|
952
|
+
inputRow.setAttribute("part", "composer");
|
|
729
953
|
|
|
730
954
|
this.#input.className = "input";
|
|
731
|
-
this.#input.setAttribute("
|
|
955
|
+
this.#input.setAttribute("part", "input");
|
|
956
|
+
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
732
957
|
this.#input.rows = 2;
|
|
733
|
-
this.#input.placeholder =
|
|
958
|
+
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
734
959
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
735
960
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
736
961
|
|
|
737
962
|
this.#send.className = "send";
|
|
738
963
|
this.#send.type = "button";
|
|
739
|
-
this.#send.
|
|
740
|
-
this.#send.
|
|
964
|
+
this.#send.setAttribute("part", "send");
|
|
965
|
+
this.#send.textContent = this.#strings.send;
|
|
966
|
+
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
741
967
|
this.#send.dataset["state"] = "idle";
|
|
742
968
|
this.#send.addEventListener("click", () => {
|
|
743
969
|
// One button, two states: Send while idle, Stop while a run is in
|
|
@@ -752,19 +978,102 @@ export class AgUiChat extends HTMLElement {
|
|
|
752
978
|
this.#skillHint.className = "skill-hint";
|
|
753
979
|
this.#skillHint.hidden = true;
|
|
754
980
|
|
|
755
|
-
|
|
981
|
+
// File-upload affordance: a 📎 button (hidden until `data-attachments-url`
|
|
982
|
+
// is wired) opening a hidden multi-file input. Drag-and-drop covers the
|
|
983
|
+
// whole shell (wired in #enableDragAndDrop).
|
|
984
|
+
this.#attachButton.className = "attach-btn";
|
|
985
|
+
this.#attachButton.type = "button";
|
|
986
|
+
this.#attachButton.setAttribute("part", "attach-button");
|
|
987
|
+
this.#attachButton.textContent = "📎";
|
|
988
|
+
this.#attachButton.title = this.#strings.attachFiles;
|
|
989
|
+
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
990
|
+
this.#attachButton.hidden = true;
|
|
991
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
992
|
+
|
|
993
|
+
this.#fileInput.className = "attach-input";
|
|
994
|
+
this.#fileInput.type = "file";
|
|
995
|
+
this.#fileInput.multiple = true;
|
|
996
|
+
this.#fileInput.hidden = true;
|
|
997
|
+
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
998
|
+
|
|
999
|
+
this.#attachSlot.className = "attachment-slot";
|
|
1000
|
+
|
|
1001
|
+
// A coarse footer slot below the composer.
|
|
1002
|
+
const footer = document.createElement("slot");
|
|
1003
|
+
footer.name = "footer";
|
|
1004
|
+
|
|
1005
|
+
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
756
1006
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
757
|
-
//
|
|
1007
|
+
// the missing-placeholder hint, and the pending-attachments tray.
|
|
758
1008
|
this.#chat.append(
|
|
759
1009
|
header,
|
|
760
1010
|
this.#messages,
|
|
761
1011
|
this.#skillsMenu.palette,
|
|
762
1012
|
this.#skillsMenu.chips,
|
|
763
1013
|
this.#skillHint,
|
|
1014
|
+
this.#attachSlot,
|
|
764
1015
|
inputRow,
|
|
1016
|
+
footer,
|
|
765
1017
|
this.#drawer.element,
|
|
766
1018
|
);
|
|
767
|
-
|
|
1019
|
+
|
|
1020
|
+
// The collapsed-sidebar rail: a slim edge strip (the expand affordance),
|
|
1021
|
+
// sibling of the panel so it survives the panel being hidden. CSS shows it
|
|
1022
|
+
// only for `placement="sidebar"` + `collapsed`.
|
|
1023
|
+
this.#rail.className = "rail";
|
|
1024
|
+
this.#rail.type = "button";
|
|
1025
|
+
this.#rail.setAttribute("part", "launcher");
|
|
1026
|
+
this.#rail.setAttribute("aria-label", this.#strings.expand);
|
|
1027
|
+
this.#rail.append(this.#iconElement("launcher", "launcher-icon", "💬"));
|
|
1028
|
+
this.#rail.addEventListener("click", () => this.setCollapsed(false));
|
|
1029
|
+
|
|
1030
|
+
this.#root.append(style, this.#chat, this.#rail);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Build a header control button (icon glyph + localized title/aria). */
|
|
1034
|
+
#headerButton(modifier: string, label: string, glyph: string): HTMLButtonElement {
|
|
1035
|
+
const button = document.createElement("button");
|
|
1036
|
+
button.type = "button";
|
|
1037
|
+
button.className = `header-btn header-btn--${modifier}`;
|
|
1038
|
+
button.setAttribute("part", `header-button ${modifier}-button`);
|
|
1039
|
+
button.title = label;
|
|
1040
|
+
button.setAttribute("aria-label", label);
|
|
1041
|
+
button.textContent = glyph;
|
|
1042
|
+
return button;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup; with
|
|
1047
|
+
* a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
|
|
1048
|
+
*/
|
|
1049
|
+
#iconElement(slotName: string, part: string, fallbackGlyph: string | null): HTMLSpanElement {
|
|
1050
|
+
const holder = document.createElement("span");
|
|
1051
|
+
holder.className = "icon-holder";
|
|
1052
|
+
holder.setAttribute("part", part);
|
|
1053
|
+
const slot = document.createElement("slot");
|
|
1054
|
+
slot.name = slotName;
|
|
1055
|
+
const iconUrl = this.getAttribute("data-icon-url");
|
|
1056
|
+
if (iconUrl !== null) {
|
|
1057
|
+
const img = document.createElement("img");
|
|
1058
|
+
img.className = "icon-img";
|
|
1059
|
+
img.src = iconUrl;
|
|
1060
|
+
img.alt = "";
|
|
1061
|
+
slot.append(img);
|
|
1062
|
+
} else if (fallbackGlyph !== null) {
|
|
1063
|
+
slot.append(document.createTextNode(fallbackGlyph));
|
|
1064
|
+
}
|
|
1065
|
+
holder.append(slot);
|
|
1066
|
+
return holder;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Reflect the collapsed state on the rail's `aria-expanded`. */
|
|
1070
|
+
#syncRail(): void {
|
|
1071
|
+
this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Hide the empty-state region once the message list holds anything else. */
|
|
1075
|
+
#updateEmptyState(): void {
|
|
1076
|
+
this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
|
|
768
1077
|
}
|
|
769
1078
|
|
|
770
1079
|
/** Forward input changes to the skills palette and clear any stale hint. */
|
|
@@ -806,7 +1115,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
806
1115
|
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
807
1116
|
#setRunning(running: boolean): void {
|
|
808
1117
|
this.#running = running;
|
|
809
|
-
const label = running ?
|
|
1118
|
+
const label = running ? this.#strings.stop : this.#strings.send;
|
|
810
1119
|
this.#send.textContent = label;
|
|
811
1120
|
this.#send.setAttribute("aria-label", label);
|
|
812
1121
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
@@ -814,26 +1123,36 @@ export class AgUiChat extends HTMLElement {
|
|
|
814
1123
|
|
|
815
1124
|
async #submit(): Promise<void> {
|
|
816
1125
|
const content = this.#input.value.trim();
|
|
817
|
-
|
|
1126
|
+
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
1127
|
+
// Allow an attachments-only message (no typed text), but nothing empty.
|
|
1128
|
+
if (content === "" && attachments.length === 0) {
|
|
818
1129
|
return;
|
|
819
1130
|
}
|
|
820
|
-
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
1131
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
1132
|
+
if (attachments.length > 0) {
|
|
1133
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
1134
|
+
}
|
|
821
1135
|
this.#input.value = "";
|
|
1136
|
+
// The refs are now on the bubble; drop the settled chips, keep any still
|
|
1137
|
+
// uploading for a follow-up message.
|
|
1138
|
+
this.#attachTray?.clearReady();
|
|
1139
|
+
// Surfaced to the run via the context manifest until the run settles.
|
|
1140
|
+
this.#runAttachments = attachments;
|
|
822
1141
|
this.dispatchEvent(
|
|
823
1142
|
new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
|
|
824
|
-
detail: { content },
|
|
1143
|
+
detail: { content, attachments },
|
|
825
1144
|
bubbles: true,
|
|
826
1145
|
composed: true,
|
|
827
1146
|
}),
|
|
828
1147
|
);
|
|
829
|
-
await this.#client_send(content);
|
|
1148
|
+
await this.#client_send(content, attachments);
|
|
830
1149
|
}
|
|
831
1150
|
|
|
832
|
-
async #client_send(content: string): Promise<void> {
|
|
1151
|
+
async #client_send(content: string, attachments: readonly AttachmentRef[]): Promise<void> {
|
|
833
1152
|
if (this.endpoint === "") {
|
|
834
1153
|
return;
|
|
835
1154
|
}
|
|
836
|
-
await this.#ensureClient().send(content);
|
|
1155
|
+
await this.#ensureClient().send(content, attachments);
|
|
837
1156
|
}
|
|
838
1157
|
|
|
839
1158
|
#ensureClient(): AgUiClient {
|
|
@@ -855,6 +1174,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
855
1174
|
getContext: () => this.getContext(),
|
|
856
1175
|
executeTool: (call) => this.#executeTool(call),
|
|
857
1176
|
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
1177
|
+
connectionLostMessage: this.#strings.connectionLost,
|
|
858
1178
|
});
|
|
859
1179
|
}
|
|
860
1180
|
return this.#client;
|
|
@@ -884,7 +1204,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
884
1204
|
// pending indicator: nothing here triggers another client round, so it
|
|
885
1205
|
// would hang after the run ended.
|
|
886
1206
|
if (!this.#serverSettled.has(call.id)) {
|
|
887
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
1207
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
888
1208
|
}
|
|
889
1209
|
return null;
|
|
890
1210
|
}
|
|
@@ -899,12 +1219,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
899
1219
|
this.#confirmAbort = new AbortController();
|
|
900
1220
|
const decision = requestConfirmation(this.#messages, request, {
|
|
901
1221
|
signal: this.#confirmAbort.signal,
|
|
1222
|
+
strings: this.#strings,
|
|
902
1223
|
});
|
|
1224
|
+
this.#updateEmptyState();
|
|
903
1225
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
904
1226
|
const accepted = await decision;
|
|
905
1227
|
this.#confirmAbort = null;
|
|
906
1228
|
if (!accepted) {
|
|
907
|
-
const message =
|
|
1229
|
+
const message = this.#strings.declinedAction;
|
|
908
1230
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
909
1231
|
this.#showPending();
|
|
910
1232
|
return { content: message };
|
|
@@ -922,7 +1244,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
922
1244
|
try {
|
|
923
1245
|
const result = await tool.handler(call.args);
|
|
924
1246
|
if (navigates) {
|
|
925
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
1247
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
926
1248
|
return { content: "", halt: true };
|
|
927
1249
|
}
|
|
928
1250
|
const content = JSON.stringify(result ?? null);
|
|
@@ -998,6 +1320,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
998
1320
|
this.#hidePending();
|
|
999
1321
|
this.#setRunning(false);
|
|
1000
1322
|
this.#streamingBubble = null;
|
|
1323
|
+
// The attachment manifest was for this run only; the model has read what
|
|
1324
|
+
// it needed (results now live in history).
|
|
1325
|
+
this.#runAttachments = [];
|
|
1326
|
+
// Belt-and-suspenders: a tool card still pending at settle (e.g. a
|
|
1327
|
+
// server tool whose result never streamed because the connection
|
|
1328
|
+
// dropped) would hang forever — settle it to the no-result fallback.
|
|
1329
|
+
for (const card of this.#toolCards.values()) {
|
|
1330
|
+
if (!card.settled) {
|
|
1331
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1001
1334
|
},
|
|
1002
1335
|
};
|
|
1003
1336
|
}
|
|
@@ -1006,9 +1339,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
1006
1339
|
#appendStoppedNote(): void {
|
|
1007
1340
|
const note = document.createElement("div");
|
|
1008
1341
|
note.className = "stopped-note";
|
|
1342
|
+
note.setAttribute("part", "stopped");
|
|
1009
1343
|
note.setAttribute("role", "status");
|
|
1010
|
-
note.textContent =
|
|
1344
|
+
note.textContent = this.#strings.stopped;
|
|
1011
1345
|
this.#messages.appendChild(note);
|
|
1346
|
+
this.#updateEmptyState();
|
|
1012
1347
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1013
1348
|
}
|
|
1014
1349
|
|
|
@@ -1023,8 +1358,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1023
1358
|
}
|
|
1024
1359
|
const pending = document.createElement("div");
|
|
1025
1360
|
pending.className = "pending";
|
|
1361
|
+
pending.setAttribute("part", "pending");
|
|
1026
1362
|
pending.setAttribute("role", "status");
|
|
1027
|
-
pending.setAttribute("aria-label",
|
|
1363
|
+
pending.setAttribute("aria-label", this.#strings.thinking);
|
|
1028
1364
|
for (let i = 0; i < 3; i += 1) {
|
|
1029
1365
|
const dot = document.createElement("span");
|
|
1030
1366
|
dot.className = "pending-dot";
|
|
@@ -1032,6 +1368,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1032
1368
|
}
|
|
1033
1369
|
this.#pending = pending;
|
|
1034
1370
|
this.#messages.appendChild(pending);
|
|
1371
|
+
this.#updateEmptyState();
|
|
1035
1372
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1036
1373
|
}
|
|
1037
1374
|
|
|
@@ -1072,9 +1409,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1072
1409
|
: (this.toolSummaries[call.name] ??
|
|
1073
1410
|
this.#toolCatalog[call.name] ??
|
|
1074
1411
|
prettifyToolName(call.name));
|
|
1075
|
-
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
|
|
1412
|
+
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
|
|
1076
1413
|
this.#toolCards.set(call.id, card);
|
|
1077
1414
|
this.#messages.appendChild(card.element);
|
|
1415
|
+
this.#updateEmptyState();
|
|
1078
1416
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1079
1417
|
return card;
|
|
1080
1418
|
}
|