@artooi/ag-ui-web-component 0.6.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 +44 -1
- package/README.md +140 -6
- package/dist/ag-ui-web-component.bundle.js +145 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +16 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +16 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +672 -228
- package/dist/index.js.map +3 -3
- 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_tray.d.ts +3 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- 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/core/ag_ui_chat.ts +213 -41
- package/src/core/agui_client.ts +33 -2
- package/src/index.ts +7 -0
- package/src/tools/page_action_tools.ts +130 -0
- package/src/ui/attachment_tray.ts +13 -7
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/relative_time.ts +15 -8
- package/src/ui/styles.ts +98 -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
|
@@ -15,6 +15,7 @@ import type { Skill } from "../skills/skill.js";
|
|
|
15
15
|
import { type ClientTool, ClientToolRegistry } from "../tools/client_tool_registry.js";
|
|
16
16
|
import { isDestructive } from "../tools/is_destructive.js";
|
|
17
17
|
import { isNavigates } from "../tools/is_navigates.js";
|
|
18
|
+
import { createPageActionTools, type ResolvePageTarget } from "../tools/page_action_tools.js";
|
|
18
19
|
import { createPageMapContext, type PageMap } from "../tools/page_map.js";
|
|
19
20
|
import { parseToolCatalog } from "../tools/parse_tool_catalog.js";
|
|
20
21
|
import { createRouteTools, type RouteMap } from "../tools/route_map.js";
|
|
@@ -29,6 +30,7 @@ import { SkillsMenu } from "../ui/skills_menu.js";
|
|
|
29
30
|
import { STYLES } from "../ui/styles.js";
|
|
30
31
|
import { ThreadDrawer } from "../ui/thread_drawer.js";
|
|
31
32
|
import { ToolCallCard, type ToolDisplayMode } from "../ui/tool_call_card.js";
|
|
33
|
+
import { DEFAULT_UI_STRINGS, mergeUiStrings, type UiStrings } from "../ui/ui_strings.js";
|
|
32
34
|
import {
|
|
33
35
|
AgUiClient,
|
|
34
36
|
type AgUiClientHandlers,
|
|
@@ -194,12 +196,30 @@ export class AgUiChat extends HTMLElement {
|
|
|
194
196
|
*/
|
|
195
197
|
toolSummaries: Record<string, string> = {};
|
|
196
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
|
+
|
|
197
215
|
/**
|
|
198
216
|
* Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
|
|
199
217
|
* tool name. The base layer behind {@link toolSummaries}: an explicit entry in
|
|
200
218
|
* `toolSummaries` wins, this fills the rest. Populated once on connect.
|
|
201
219
|
*/
|
|
202
220
|
#toolCatalog: Record<string, string> = {};
|
|
221
|
+
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
222
|
+
#strings: UiStrings = DEFAULT_UI_STRINGS;
|
|
203
223
|
|
|
204
224
|
readonly #toolRegistry = new ClientToolRegistry();
|
|
205
225
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
@@ -223,6 +243,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
223
243
|
readonly #attachButton: HTMLButtonElement;
|
|
224
244
|
readonly #fileInput: HTMLInputElement;
|
|
225
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;
|
|
226
250
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
227
251
|
#attachTray: AttachmentTray | null = null;
|
|
228
252
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
@@ -261,6 +285,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
261
285
|
this.#attachButton = document.createElement("button");
|
|
262
286
|
this.#fileInput = document.createElement("input");
|
|
263
287
|
this.#attachSlot = document.createElement("div");
|
|
288
|
+
this.#rail = document.createElement("button");
|
|
289
|
+
this.#emptyWrap = document.createElement("div");
|
|
264
290
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
265
291
|
this.#drawer = new ThreadDrawer({
|
|
266
292
|
onSelect: (threadId) => {
|
|
@@ -287,8 +313,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
287
313
|
|
|
288
314
|
attributeChangedCallback(_name: string, _previous: string | null, value: string | null): void {
|
|
289
315
|
// Only `title-text` is observed (other attributes are read at use-time or
|
|
290
|
-
// are CSS-reactive), so update the header title directly.
|
|
291
|
-
|
|
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;
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
/** Declare a frontend tool the agent may call. */
|
|
@@ -341,9 +368,29 @@ export class AgUiChat extends HTMLElement {
|
|
|
341
368
|
];
|
|
342
369
|
}
|
|
343
370
|
|
|
344
|
-
/**
|
|
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. */
|
|
345
392
|
#builtinTools(): ClientTool[] {
|
|
346
|
-
return [...this.#routeTools(), ...this.#pageTools()];
|
|
393
|
+
return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
|
|
347
394
|
}
|
|
348
395
|
|
|
349
396
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
@@ -384,10 +431,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
384
431
|
}
|
|
385
432
|
|
|
386
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 });
|
|
387
437
|
this.#render();
|
|
438
|
+
this.#drawer.setStrings(this.#strings);
|
|
388
439
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
389
440
|
this.setAttribute("collapsed", "");
|
|
390
441
|
}
|
|
442
|
+
this.#syncRail();
|
|
391
443
|
this.#initSkills();
|
|
392
444
|
void this.#fetchToolCatalog();
|
|
393
445
|
this.#wireThreadStore();
|
|
@@ -396,6 +448,23 @@ export class AgUiChat extends HTMLElement {
|
|
|
396
448
|
void this.#rehydrate();
|
|
397
449
|
}
|
|
398
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
|
+
|
|
399
468
|
/**
|
|
400
469
|
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
401
470
|
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
@@ -414,6 +483,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
414
483
|
upload,
|
|
415
484
|
maxBytes: this.#attachmentMaxBytes(),
|
|
416
485
|
accept,
|
|
486
|
+
strings: this.#strings,
|
|
417
487
|
});
|
|
418
488
|
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
419
489
|
this.#fileInput.accept = accept;
|
|
@@ -578,7 +648,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
578
648
|
#applySkill(skill: Skill): void {
|
|
579
649
|
const { text, missing } = fillTemplate(skill.prompt, this.skillContext());
|
|
580
650
|
if (missing.length > 0) {
|
|
581
|
-
this.#skillHint.textContent =
|
|
651
|
+
this.#skillHint.textContent = this.#strings.skillNeeds
|
|
652
|
+
.replace("{title}", skill.title)
|
|
653
|
+
.replace("{fields}", missing.join(", "));
|
|
582
654
|
this.#skillHint.hidden = false;
|
|
583
655
|
return;
|
|
584
656
|
}
|
|
@@ -614,6 +686,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
614
686
|
this.removeAttribute("collapsed");
|
|
615
687
|
}
|
|
616
688
|
sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
689
|
+
this.#syncRail();
|
|
617
690
|
this.dispatchEvent(
|
|
618
691
|
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
619
692
|
detail: { collapsed },
|
|
@@ -652,7 +725,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
652
725
|
this.#initialMessages = [];
|
|
653
726
|
this.#runAttachments = [];
|
|
654
727
|
this.#attachTray?.clear();
|
|
655
|
-
|
|
728
|
+
// Keep the empty-state region; everything else clears.
|
|
729
|
+
this.#messages.replaceChildren(this.#emptyWrap);
|
|
730
|
+
this.#updateEmptyState();
|
|
656
731
|
}
|
|
657
732
|
|
|
658
733
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
@@ -795,12 +870,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
795
870
|
appendMessage(role: MessageRole, content: string): HTMLDivElement {
|
|
796
871
|
const bubble = document.createElement("div");
|
|
797
872
|
bubble.className = `message message--${role}`;
|
|
873
|
+
bubble.setAttribute("part", `message message-${role}`);
|
|
798
874
|
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
799
875
|
bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
|
|
800
876
|
} else {
|
|
801
877
|
bubble.textContent = content;
|
|
802
878
|
}
|
|
803
879
|
this.#messages.appendChild(bubble);
|
|
880
|
+
this.#updateEmptyState();
|
|
804
881
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
805
882
|
return bubble;
|
|
806
883
|
}
|
|
@@ -810,67 +887,83 @@ export class AgUiChat extends HTMLElement {
|
|
|
810
887
|
style.textContent = STYLES;
|
|
811
888
|
|
|
812
889
|
this.#chat.className = "chat";
|
|
890
|
+
this.#chat.setAttribute("part", "panel");
|
|
813
891
|
|
|
814
892
|
const header = document.createElement("div");
|
|
815
893
|
header.className = "header";
|
|
894
|
+
header.setAttribute("part", "header");
|
|
816
895
|
|
|
817
896
|
const title = this.#title;
|
|
818
897
|
title.className = "header-title";
|
|
819
|
-
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";
|
|
820
914
|
|
|
821
915
|
const controls = document.createElement("div");
|
|
822
916
|
controls.className = "header-controls";
|
|
917
|
+
controls.setAttribute("part", "header-controls");
|
|
823
918
|
|
|
824
|
-
const history =
|
|
825
|
-
history.type = "button";
|
|
826
|
-
history.className = "header-btn header-btn--history";
|
|
827
|
-
history.title = "Chat history";
|
|
828
|
-
history.setAttribute("aria-label", "Chat history");
|
|
829
|
-
history.textContent = "☰";
|
|
919
|
+
const history = this.#headerButton("history", this.#strings.chatHistory, "☰");
|
|
830
920
|
history.addEventListener("click", () => {
|
|
831
921
|
void this.#refreshDrawer();
|
|
832
922
|
this.#drawer.open();
|
|
833
923
|
});
|
|
834
924
|
|
|
835
|
-
const newChat =
|
|
836
|
-
newChat.type = "button";
|
|
837
|
-
newChat.className = "header-btn header-btn--new";
|
|
838
|
-
newChat.title = "New chat";
|
|
839
|
-
newChat.setAttribute("aria-label", "New chat");
|
|
840
|
-
newChat.textContent = "✚";
|
|
925
|
+
const newChat = this.#headerButton("new", this.#strings.newChat, "✚");
|
|
841
926
|
newChat.addEventListener("click", () => this.newChat());
|
|
842
927
|
|
|
843
|
-
const collapse =
|
|
844
|
-
collapse.type = "button";
|
|
845
|
-
collapse.className = "header-btn header-btn--collapse";
|
|
846
|
-
collapse.title = "Collapse";
|
|
847
|
-
collapse.setAttribute("aria-label", "Collapse");
|
|
848
|
-
collapse.textContent = "—";
|
|
928
|
+
const collapse = this.#headerButton("collapse", this.#strings.collapse, "—");
|
|
849
929
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
850
930
|
|
|
851
931
|
controls.append(history, newChat, collapse);
|
|
852
|
-
header.append(title, controls);
|
|
932
|
+
header.append(title, headerActions, controls);
|
|
853
933
|
|
|
854
934
|
this.#messages.className = "messages";
|
|
935
|
+
this.#messages.setAttribute("part", "messages");
|
|
855
936
|
// Screen readers announce streamed messages as they arrive.
|
|
856
937
|
this.#messages.setAttribute("role", "log");
|
|
857
938
|
this.#messages.setAttribute("aria-live", "polite");
|
|
858
|
-
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);
|
|
859
949
|
|
|
860
950
|
const inputRow = document.createElement("div");
|
|
861
951
|
inputRow.className = "input-row";
|
|
952
|
+
inputRow.setAttribute("part", "composer");
|
|
862
953
|
|
|
863
954
|
this.#input.className = "input";
|
|
864
|
-
this.#input.setAttribute("
|
|
955
|
+
this.#input.setAttribute("part", "input");
|
|
956
|
+
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
865
957
|
this.#input.rows = 2;
|
|
866
|
-
this.#input.placeholder =
|
|
958
|
+
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
867
959
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
868
960
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
869
961
|
|
|
870
962
|
this.#send.className = "send";
|
|
871
963
|
this.#send.type = "button";
|
|
872
|
-
this.#send.
|
|
873
|
-
this.#send.
|
|
964
|
+
this.#send.setAttribute("part", "send");
|
|
965
|
+
this.#send.textContent = this.#strings.send;
|
|
966
|
+
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
874
967
|
this.#send.dataset["state"] = "idle";
|
|
875
968
|
this.#send.addEventListener("click", () => {
|
|
876
969
|
// One button, two states: Send while idle, Stop while a run is in
|
|
@@ -890,9 +983,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
890
983
|
// whole shell (wired in #enableDragAndDrop).
|
|
891
984
|
this.#attachButton.className = "attach-btn";
|
|
892
985
|
this.#attachButton.type = "button";
|
|
986
|
+
this.#attachButton.setAttribute("part", "attach-button");
|
|
893
987
|
this.#attachButton.textContent = "📎";
|
|
894
|
-
this.#attachButton.title =
|
|
895
|
-
this.#attachButton.setAttribute("aria-label",
|
|
988
|
+
this.#attachButton.title = this.#strings.attachFiles;
|
|
989
|
+
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
896
990
|
this.#attachButton.hidden = true;
|
|
897
991
|
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
898
992
|
|
|
@@ -904,6 +998,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
904
998
|
|
|
905
999
|
this.#attachSlot.className = "attachment-slot";
|
|
906
1000
|
|
|
1001
|
+
// A coarse footer slot below the composer.
|
|
1002
|
+
const footer = document.createElement("slot");
|
|
1003
|
+
footer.name = "footer";
|
|
1004
|
+
|
|
907
1005
|
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
908
1006
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
909
1007
|
// the missing-placeholder hint, and the pending-attachments tray.
|
|
@@ -915,9 +1013,67 @@ export class AgUiChat extends HTMLElement {
|
|
|
915
1013
|
this.#skillHint,
|
|
916
1014
|
this.#attachSlot,
|
|
917
1015
|
inputRow,
|
|
1016
|
+
footer,
|
|
918
1017
|
this.#drawer.element,
|
|
919
1018
|
);
|
|
920
|
-
|
|
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;
|
|
921
1077
|
}
|
|
922
1078
|
|
|
923
1079
|
/** Forward input changes to the skills palette and clear any stale hint. */
|
|
@@ -959,7 +1115,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
959
1115
|
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
960
1116
|
#setRunning(running: boolean): void {
|
|
961
1117
|
this.#running = running;
|
|
962
|
-
const label = running ?
|
|
1118
|
+
const label = running ? this.#strings.stop : this.#strings.send;
|
|
963
1119
|
this.#send.textContent = label;
|
|
964
1120
|
this.#send.setAttribute("aria-label", label);
|
|
965
1121
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
@@ -1018,6 +1174,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1018
1174
|
getContext: () => this.getContext(),
|
|
1019
1175
|
executeTool: (call) => this.#executeTool(call),
|
|
1020
1176
|
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
1177
|
+
connectionLostMessage: this.#strings.connectionLost,
|
|
1021
1178
|
});
|
|
1022
1179
|
}
|
|
1023
1180
|
return this.#client;
|
|
@@ -1047,7 +1204,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1047
1204
|
// pending indicator: nothing here triggers another client round, so it
|
|
1048
1205
|
// would hang after the run ended.
|
|
1049
1206
|
if (!this.#serverSettled.has(call.id)) {
|
|
1050
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
1207
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
1051
1208
|
}
|
|
1052
1209
|
return null;
|
|
1053
1210
|
}
|
|
@@ -1062,12 +1219,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1062
1219
|
this.#confirmAbort = new AbortController();
|
|
1063
1220
|
const decision = requestConfirmation(this.#messages, request, {
|
|
1064
1221
|
signal: this.#confirmAbort.signal,
|
|
1222
|
+
strings: this.#strings,
|
|
1065
1223
|
});
|
|
1224
|
+
this.#updateEmptyState();
|
|
1066
1225
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1067
1226
|
const accepted = await decision;
|
|
1068
1227
|
this.#confirmAbort = null;
|
|
1069
1228
|
if (!accepted) {
|
|
1070
|
-
const message =
|
|
1229
|
+
const message = this.#strings.declinedAction;
|
|
1071
1230
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
1072
1231
|
this.#showPending();
|
|
1073
1232
|
return { content: message };
|
|
@@ -1085,7 +1244,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1085
1244
|
try {
|
|
1086
1245
|
const result = await tool.handler(call.args);
|
|
1087
1246
|
if (navigates) {
|
|
1088
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
1247
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
1089
1248
|
return { content: "", halt: true };
|
|
1090
1249
|
}
|
|
1091
1250
|
const content = JSON.stringify(result ?? null);
|
|
@@ -1164,6 +1323,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1164
1323
|
// The attachment manifest was for this run only; the model has read what
|
|
1165
1324
|
// it needed (results now live in history).
|
|
1166
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
|
+
}
|
|
1167
1334
|
},
|
|
1168
1335
|
};
|
|
1169
1336
|
}
|
|
@@ -1172,9 +1339,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
1172
1339
|
#appendStoppedNote(): void {
|
|
1173
1340
|
const note = document.createElement("div");
|
|
1174
1341
|
note.className = "stopped-note";
|
|
1342
|
+
note.setAttribute("part", "stopped");
|
|
1175
1343
|
note.setAttribute("role", "status");
|
|
1176
|
-
note.textContent =
|
|
1344
|
+
note.textContent = this.#strings.stopped;
|
|
1177
1345
|
this.#messages.appendChild(note);
|
|
1346
|
+
this.#updateEmptyState();
|
|
1178
1347
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1179
1348
|
}
|
|
1180
1349
|
|
|
@@ -1189,8 +1358,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1189
1358
|
}
|
|
1190
1359
|
const pending = document.createElement("div");
|
|
1191
1360
|
pending.className = "pending";
|
|
1361
|
+
pending.setAttribute("part", "pending");
|
|
1192
1362
|
pending.setAttribute("role", "status");
|
|
1193
|
-
pending.setAttribute("aria-label",
|
|
1363
|
+
pending.setAttribute("aria-label", this.#strings.thinking);
|
|
1194
1364
|
for (let i = 0; i < 3; i += 1) {
|
|
1195
1365
|
const dot = document.createElement("span");
|
|
1196
1366
|
dot.className = "pending-dot";
|
|
@@ -1198,6 +1368,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1198
1368
|
}
|
|
1199
1369
|
this.#pending = pending;
|
|
1200
1370
|
this.#messages.appendChild(pending);
|
|
1371
|
+
this.#updateEmptyState();
|
|
1201
1372
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1202
1373
|
}
|
|
1203
1374
|
|
|
@@ -1238,9 +1409,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1238
1409
|
: (this.toolSummaries[call.name] ??
|
|
1239
1410
|
this.#toolCatalog[call.name] ??
|
|
1240
1411
|
prettifyToolName(call.name));
|
|
1241
|
-
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);
|
|
1242
1413
|
this.#toolCards.set(call.id, card);
|
|
1243
1414
|
this.#messages.appendChild(card.element);
|
|
1415
|
+
this.#updateEmptyState();
|
|
1244
1416
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1245
1417
|
return card;
|
|
1246
1418
|
}
|
package/src/core/agui_client.ts
CHANGED
|
@@ -93,6 +93,26 @@ export interface AgUiClientConfig extends AgUiRunInputs {
|
|
|
93
93
|
* conversation in-memory only.
|
|
94
94
|
*/
|
|
95
95
|
onPersist?: (messages: readonly Message[]) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Error text surfaced to {@link AgUiClientHandlers.onError} when a run's
|
|
98
|
+
* stream closes without a terminal AG-UI event (`RUN_FINISHED`/`RUN_ERROR`) —
|
|
99
|
+
* a dropped connection. Defaults to `"Connection lost"`; the host passes its
|
|
100
|
+
* localized string.
|
|
101
|
+
*/
|
|
102
|
+
connectionLostMessage?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Raised when a run's stream closes cleanly at the transport level but never
|
|
107
|
+
* emits a terminal AG-UI event, so the run neither finished nor errored. Routed
|
|
108
|
+
* to {@link AgUiClientHandlers.onError} (it is not an abort), turning a silent
|
|
109
|
+
* "stuck pending" into a visible "connection lost".
|
|
110
|
+
*/
|
|
111
|
+
export class ConnectionLostError extends Error {
|
|
112
|
+
constructor(message: string) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "ConnectionLostError";
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
/**
|
|
@@ -109,6 +129,7 @@ export class AgUiClient {
|
|
|
109
129
|
readonly #getContext: () => Context[];
|
|
110
130
|
readonly #executeTool: ExecuteTool | null;
|
|
111
131
|
readonly #onPersist: (messages: readonly Message[]) => void;
|
|
132
|
+
readonly #connectionLostMessage: string;
|
|
112
133
|
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
113
134
|
// a cancel between frontend-tool rounds doesn't start another round.
|
|
114
135
|
#cancelled = false;
|
|
@@ -120,6 +141,7 @@ export class AgUiClient {
|
|
|
120
141
|
this.#getContext = config.getContext ?? (() => []);
|
|
121
142
|
this.#executeTool = config.executeTool ?? null;
|
|
122
143
|
this.#onPersist = config.onPersist ?? (() => {});
|
|
144
|
+
this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
/** Whether a run is currently in flight. */
|
|
@@ -220,9 +242,10 @@ export class AgUiClient {
|
|
|
220
242
|
return;
|
|
221
243
|
}
|
|
222
244
|
const pending: AgUiToolCall[] = [];
|
|
245
|
+
const runState = { terminal: false };
|
|
223
246
|
await this.#agent.runAgent(
|
|
224
247
|
{ tools: this.#getTools(), context: this.#getContext() },
|
|
225
|
-
this.#buildSubscriber(pending),
|
|
248
|
+
this.#buildSubscriber(pending, runState),
|
|
226
249
|
);
|
|
227
250
|
this.#onPersist(this.#agent.messages);
|
|
228
251
|
// Cancelled mid-stream: the user said stop — don't execute the tool
|
|
@@ -230,6 +253,12 @@ export class AgUiClient {
|
|
|
230
253
|
if (this.#cancelled) {
|
|
231
254
|
return;
|
|
232
255
|
}
|
|
256
|
+
// The stream resolved without RUN_FINISHED / RUN_ERROR: the transport
|
|
257
|
+
// dropped mid-run. Surface it as an error so the UI doesn't rest silently
|
|
258
|
+
// with a stuck pending indicator (caught by #run → onError).
|
|
259
|
+
if (!runState.terminal) {
|
|
260
|
+
throw new ConnectionLostError(this.#connectionLostMessage);
|
|
261
|
+
}
|
|
233
262
|
if (this.#executeTool === null || pending.length === 0) {
|
|
234
263
|
return;
|
|
235
264
|
}
|
|
@@ -259,7 +288,7 @@ export class AgUiClient {
|
|
|
259
288
|
}
|
|
260
289
|
}
|
|
261
290
|
|
|
262
|
-
#buildSubscriber(pending: AgUiToolCall[]): AgentSubscriber {
|
|
291
|
+
#buildSubscriber(pending: AgUiToolCall[], runState: { terminal: boolean }): AgentSubscriber {
|
|
263
292
|
const h = this.#handlers;
|
|
264
293
|
return {
|
|
265
294
|
onRunInitialized() {
|
|
@@ -284,9 +313,11 @@ export class AgUiClient {
|
|
|
284
313
|
h.onToolResult(event.toolCallId, event.content);
|
|
285
314
|
},
|
|
286
315
|
onRunErrorEvent({ event }) {
|
|
316
|
+
runState.terminal = true;
|
|
287
317
|
h.onError(event.message);
|
|
288
318
|
},
|
|
289
319
|
onRunFinalized() {
|
|
320
|
+
runState.terminal = true;
|
|
290
321
|
h.onRunEnd();
|
|
291
322
|
},
|
|
292
323
|
};
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
type AgUiClientHandlers,
|
|
26
26
|
type AgUiRunInputs,
|
|
27
27
|
type AgUiToolCall,
|
|
28
|
+
ConnectionLostError,
|
|
28
29
|
type ExecuteTool,
|
|
29
30
|
type ToolExecution,
|
|
30
31
|
} from "./core/agui_client.js";
|
|
@@ -78,6 +79,11 @@ export type { Skill } from "./skills/skill.js";
|
|
|
78
79
|
export { type ClientTool, ClientToolRegistry } from "./tools/client_tool_registry.js";
|
|
79
80
|
export { isDestructive } from "./tools/is_destructive.js";
|
|
80
81
|
export { isNavigates } from "./tools/is_navigates.js";
|
|
82
|
+
export {
|
|
83
|
+
createPageActionTools,
|
|
84
|
+
PAGE_ACTIONS,
|
|
85
|
+
type ResolvePageTarget,
|
|
86
|
+
} from "./tools/page_action_tools.js";
|
|
81
87
|
export { createPageMapContext, type PageMap } from "./tools/page_map.js";
|
|
82
88
|
export { parseToolCatalog, type ToolCatalogEntry } from "./tools/parse_tool_catalog.js";
|
|
83
89
|
export {
|
|
@@ -100,4 +106,5 @@ export {
|
|
|
100
106
|
type ToolCallStatus,
|
|
101
107
|
type ToolDisplayMode,
|
|
102
108
|
} from "./ui/tool_call_card.js";
|
|
109
|
+
export { DEFAULT_UI_STRINGS, mergeUiStrings, type UiStrings } from "./ui/ui_strings.js";
|
|
103
110
|
export { VERSION } from "./version.js";
|