@artooi/ag-ui-web-component 0.6.0 → 0.8.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 (44) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +180 -9
  3. package/dist/ag-ui-web-component.bundle.js +268 -47
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +5 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +20 -0
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +16 -0
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +847 -237
  14. package/dist/index.js.map +3 -3
  15. package/dist/tools/page_action_tools.d.ts +31 -0
  16. package/dist/tools/page_action_tools.d.ts.map +1 -0
  17. package/dist/ui/attachment_tray.d.ts +3 -0
  18. package/dist/ui/attachment_tray.d.ts.map +1 -1
  19. package/dist/ui/confirmation_card.d.ts +4 -1
  20. package/dist/ui/confirmation_card.d.ts.map +1 -1
  21. package/dist/ui/relative_time.d.ts +5 -3
  22. package/dist/ui/relative_time.d.ts.map +1 -1
  23. package/dist/ui/styles.d.ts +1 -1
  24. package/dist/ui/styles.d.ts.map +1 -1
  25. package/dist/ui/thread_drawer.d.ts +7 -1
  26. package/dist/ui/thread_drawer.d.ts.map +1 -1
  27. package/dist/ui/tool_call_card.d.ts +19 -8
  28. package/dist/ui/tool_call_card.d.ts.map +1 -1
  29. package/dist/ui/ui_strings.d.ts +126 -0
  30. package/dist/ui/ui_strings.d.ts.map +1 -0
  31. package/package.json +1 -1
  32. package/src/constants.ts +5 -0
  33. package/src/core/ag_ui_chat.ts +267 -46
  34. package/src/core/agui_client.ts +33 -2
  35. package/src/index.ts +7 -0
  36. package/src/tools/page_action_tools.ts +130 -0
  37. package/src/ui/attachment_tray.ts +13 -7
  38. package/src/ui/confirmation_card.ts +15 -5
  39. package/src/ui/relative_time.ts +15 -8
  40. package/src/ui/styles.ts +221 -0
  41. package/src/ui/thread_drawer.ts +53 -25
  42. package/src/ui/tool_call_card.ts +63 -25
  43. package/src/ui/ui_strings.ts +208 -0
  44. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ var ATTACHMENT_STATUS = {
24
24
  };
25
25
  var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
26
26
  var TOOL_DISPLAY = {
27
+ INLINE: "inline",
27
28
  MINIMAL: "minimal",
28
29
  COMPACT: "compact",
29
30
  FULL: "full"
@@ -104,6 +105,211 @@ function isNavigates(parameters) {
104
105
  return parameters[X_NAVIGATES_KEY] === true;
105
106
  }
106
107
 
108
+ // src/dom/native_setter.ts
109
+ function prototypeSetter(proto, prop) {
110
+ const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
111
+ return setter;
112
+ }
113
+ var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
114
+ var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
115
+ var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
116
+ var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
117
+ function setNativeValue(el, value) {
118
+ if (el instanceof HTMLTextAreaElement) {
119
+ setTextareaValue.call(el, value);
120
+ } else if (el instanceof HTMLSelectElement) {
121
+ setSelectValue.call(el, value);
122
+ } else {
123
+ setInputValue.call(el, value);
124
+ }
125
+ }
126
+ function setNativeChecked(el, checked) {
127
+ setInputChecked.call(el, checked);
128
+ }
129
+
130
+ // src/dom/animations.ts
131
+ var ACCENT = "#4f46e5";
132
+ function delay(ms) {
133
+ return new Promise((resolve) => {
134
+ setTimeout(resolve, ms);
135
+ });
136
+ }
137
+ async function typeInto(el, value, options = {}) {
138
+ const charDelayMs = options.charDelayMs ?? 35;
139
+ setNativeValue(el, "");
140
+ el.dispatchEvent(new Event("input", { bubbles: true }));
141
+ for (const char of value) {
142
+ setNativeValue(el, el.value + char);
143
+ el.dispatchEvent(new Event("input", { bubbles: true }));
144
+ if (charDelayMs > 0) {
145
+ await delay(charDelayMs);
146
+ }
147
+ }
148
+ el.dispatchEvent(new Event("change", { bubbles: true }));
149
+ }
150
+ async function highlightThenClick(el, options = {}) {
151
+ const highlightMs = options.highlightMs ?? 280;
152
+ const previousOutline = el.style.outline;
153
+ const previousOffset = el.style.outlineOffset;
154
+ el.style.outline = `2px solid ${ACCENT}`;
155
+ el.style.outlineOffset = "2px";
156
+ await delay(highlightMs);
157
+ el.style.outline = previousOutline;
158
+ el.style.outlineOffset = previousOffset;
159
+ el.click();
160
+ }
161
+ function scrollIntoCenterView(el) {
162
+ el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
163
+ }
164
+ async function focusWithFlash(el, options = {}) {
165
+ const flashMs = options.flashMs ?? 200;
166
+ el.focus();
167
+ const previousShadow = el.style.boxShadow;
168
+ el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
169
+ await delay(flashMs);
170
+ el.style.boxShadow = previousShadow;
171
+ }
172
+ var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
173
+ function prefersReducedMotion() {
174
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
175
+ }
176
+ function motionDelay(ms) {
177
+ if (ms <= 0 || prefersReducedMotion()) {
178
+ return Promise.resolve();
179
+ }
180
+ return delay(ms);
181
+ }
182
+ async function pressThenClick(el, options = {}) {
183
+ const pressMs = options.pressMs ?? 140;
184
+ const previousTransform = el.style.transform;
185
+ const previousTransition = el.style.transition;
186
+ const previousShadow = el.style.boxShadow;
187
+ el.style.transition = "transform 80ms ease";
188
+ el.style.transform = "scale(0.96)";
189
+ el.style.boxShadow = RING;
190
+ await motionDelay(pressMs);
191
+ el.style.transform = previousTransform;
192
+ el.style.transition = previousTransition;
193
+ el.style.boxShadow = previousShadow;
194
+ el.click();
195
+ }
196
+ function findOption(el, value) {
197
+ for (const option of Array.from(el.options)) {
198
+ if (option.value === value || option.text === value) {
199
+ return option;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ async function selectOption(el, value, options = {}) {
205
+ const option = findOption(el, value);
206
+ if (option === null) {
207
+ throw new Error(`no <option> matching "${value}"`);
208
+ }
209
+ const highlightMs = options.highlightMs ?? 220;
210
+ const previousOutline = el.style.outline;
211
+ const previousOffset = el.style.outlineOffset;
212
+ el.style.outline = `2px solid ${ACCENT}`;
213
+ el.style.outlineOffset = "2px";
214
+ await motionDelay(highlightMs);
215
+ setNativeValue(el, option.value);
216
+ el.dispatchEvent(new Event("input", { bubbles: true }));
217
+ el.dispatchEvent(new Event("change", { bubbles: true }));
218
+ el.style.outline = previousOutline;
219
+ el.style.outlineOffset = previousOffset;
220
+ }
221
+ async function toggleControl(el, checked, options = {}) {
222
+ const flashMs = options.flashMs ?? 200;
223
+ const previousShadow = el.style.boxShadow;
224
+ el.style.boxShadow = RING;
225
+ await motionDelay(flashMs);
226
+ setNativeChecked(el, checked);
227
+ el.dispatchEvent(new Event("input", { bubbles: true }));
228
+ el.dispatchEvent(new Event("change", { bubbles: true }));
229
+ el.style.boxShadow = previousShadow;
230
+ }
231
+
232
+ // src/tools/page_action_tools.ts
233
+ var PAGE_ACTIONS = {
234
+ SCROLL: "scroll",
235
+ DRAG: "drag"
236
+ };
237
+ function createPageActionTools(enabled, resolveTarget) {
238
+ const tools = [];
239
+ if (enabled.has(PAGE_ACTIONS.SCROLL)) {
240
+ tools.push(scrollTool(resolveTarget));
241
+ }
242
+ if (enabled.has(PAGE_ACTIONS.DRAG)) {
243
+ tools.push(dragTool(resolveTarget));
244
+ }
245
+ return tools;
246
+ }
247
+ function scrollTool(resolveTarget) {
248
+ return {
249
+ name: "scroll_to",
250
+ description: "Scroll a target into view. `target` is `top`, `bottom`, or a CSS selector / page-map element id. Read-only: it changes nothing on the page.",
251
+ parameters: {
252
+ type: "object",
253
+ properties: { target: { type: "string" } },
254
+ required: ["target"],
255
+ [X_SUMMARY_KEY]: "Scroll into view"
256
+ },
257
+ handler: (args) => {
258
+ const target = String(args["target"] ?? "");
259
+ if (target === "top" || target === "bottom") {
260
+ const top = target === "top" ? 0 : document.body.scrollHeight;
261
+ window.scrollTo({ top, behavior: "smooth" });
262
+ return { scrolled: true, target };
263
+ }
264
+ const element = resolveTarget(target);
265
+ if (element === null) {
266
+ throw new Error(`no element matching "${target}"`);
267
+ }
268
+ scrollIntoCenterView(element);
269
+ return { scrolled: true, target };
270
+ }
271
+ };
272
+ }
273
+ function dragTool(resolveTarget) {
274
+ return {
275
+ name: "drag_and_drop",
276
+ description: "Drag the `from` element onto the `to` element (CSS selectors or page-map element ids), firing the page's native drag-and-drop. Use for reordering sortable lists. The page decides what the drop commits.",
277
+ parameters: {
278
+ type: "object",
279
+ properties: { from: { type: "string" }, to: { type: "string" } },
280
+ required: ["from", "to"],
281
+ [X_SUMMARY_KEY]: "Drag and drop"
282
+ },
283
+ handler: (args) => {
284
+ const fromTarget = String(args["from"] ?? "");
285
+ const toTarget = String(args["to"] ?? "");
286
+ const from = resolveTarget(fromTarget);
287
+ if (from === null) {
288
+ throw new Error(`no element matching "${fromTarget}"`);
289
+ }
290
+ const to = resolveTarget(toTarget);
291
+ if (to === null) {
292
+ throw new Error(`no element matching "${toTarget}"`);
293
+ }
294
+ dispatchDragSequence(from, to);
295
+ return { dragged: true, from: fromTarget, to: toTarget };
296
+ }
297
+ };
298
+ }
299
+ function dispatchDragSequence(from, to) {
300
+ const dataTransfer = new DataTransfer();
301
+ fire(from, "dragstart", dataTransfer);
302
+ fire(to, "dragenter", dataTransfer);
303
+ fire(to, "dragover", dataTransfer);
304
+ fire(to, "drop", dataTransfer);
305
+ fire(from, "dragend", dataTransfer);
306
+ }
307
+ function fire(target, type, dataTransfer) {
308
+ const event = new Event(type, { bubbles: true, cancelable: true });
309
+ event.dataTransfer = dataTransfer;
310
+ target.dispatchEvent(event);
311
+ }
312
+
107
313
  // src/tools/page_map.ts
108
314
  function createPageMapContext(getPageMap, autoInject) {
109
315
  if (!autoInject || getPageMap === null) {
@@ -292,15 +498,83 @@ function formatBytes(bytes) {
292
498
 
293
499
  // src/ui/attachment_tray.ts
294
500
  import { randomUUID } from "@ag-ui/client";
501
+
502
+ // src/ui/ui_strings.ts
503
+ var DEFAULT_UI_STRINGS = {
504
+ title: "Assistant",
505
+ chatHistory: "Chat history",
506
+ newChat: "New chat",
507
+ collapse: "Collapse",
508
+ expand: "Expand",
509
+ conversation: "Conversation",
510
+ thinking: "Assistant is thinking\u2026",
511
+ stopped: "\u23F9 Stopped",
512
+ connectionLost: "Connection lost",
513
+ noResult: "No result returned.",
514
+ declinedAction: "User declined the action.",
515
+ navigating: "Navigating\u2026",
516
+ skillNeeds: "\u201C{title}\u201D needs: {fields}",
517
+ message: "Message",
518
+ inputPlaceholder: "Ask anything\u2026",
519
+ send: "Send",
520
+ stop: "Stop",
521
+ attachFiles: "Attach files",
522
+ toolRunning: "running\u2026",
523
+ toolDone: "\u2713 done",
524
+ toolError: "\u26A0 error",
525
+ toolDeclined: "\u2298 declined",
526
+ resultLabel: "Result",
527
+ errorLabel: "Error",
528
+ declinedLabel: "Declined",
529
+ details: "Details",
530
+ confirmAction: "Confirm action",
531
+ confirmRun: "Run \u201C{tool}\u201D?",
532
+ confirm: "Confirm",
533
+ cancel: "Cancel",
534
+ chats: "Chats",
535
+ noConversations: "No conversations yet.",
536
+ rename: "Rename",
537
+ renameConversation: "Rename conversation",
538
+ delete: "Delete",
539
+ deleteConversation: "Delete conversation",
540
+ deletePrompt: "Delete?",
541
+ tooLarge: "Too large (max {size})",
542
+ fileTypeNotAllowed: "File type not allowed",
543
+ uploadFailed: "upload failed",
544
+ retry: "Retry",
545
+ retryUpload: "Retry upload",
546
+ remove: "Remove",
547
+ removeAttachment: "Remove attachment",
548
+ justNow: "just now",
549
+ minutesAgo: "{n}m ago",
550
+ hoursAgo: "{n}h ago",
551
+ daysAgo: "{n}d ago",
552
+ weeksAgo: "{n}w ago"
553
+ };
554
+ function mergeUiStrings(overrides) {
555
+ const merged = { ...DEFAULT_UI_STRINGS };
556
+ for (const key of Object.keys(overrides)) {
557
+ const value = overrides[key];
558
+ if (value !== void 0) {
559
+ merged[key] = value;
560
+ }
561
+ }
562
+ return merged;
563
+ }
564
+
565
+ // src/ui/attachment_tray.ts
295
566
  var AttachmentTray = class {
296
567
  /** The tray root; append above the input row. Hidden while empty. */
297
568
  element;
298
569
  #config;
570
+ #strings;
299
571
  #items = [];
300
572
  constructor(config) {
301
573
  this.#config = config;
574
+ this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
302
575
  this.element = document.createElement("div");
303
576
  this.element.className = "attachment-tray";
577
+ this.element.setAttribute("part", "attachment-tray");
304
578
  this.element.hidden = true;
305
579
  }
306
580
  /** Queue a file: reject oversize/disallowed into an error chip, else upload. */
@@ -357,10 +631,10 @@ var AttachmentTray = class {
357
631
  /** The size/type rejection reason for a file, or `null` when accepted. */
358
632
  #reject(file) {
359
633
  if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
360
- return `Too large (max ${formatBytes(this.#config.maxBytes)})`;
634
+ return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
361
635
  }
362
636
  if (!accepts(this.#config.accept, file)) {
363
- return "File type not allowed";
637
+ return this.#strings.fileTypeNotAllowed;
364
638
  }
365
639
  return null;
366
640
  }
@@ -377,7 +651,7 @@ var AttachmentTray = class {
377
651
  item.ref = ref;
378
652
  }).catch((error) => {
379
653
  item.status = ATTACHMENT_STATUS.ERROR;
380
- item.error = error instanceof Error ? error.message : "upload failed";
654
+ item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
381
655
  }).finally(() => {
382
656
  this.#render();
383
657
  this.#config.onChange?.();
@@ -423,8 +697,8 @@ var AttachmentTray = class {
423
697
  const retry = document.createElement("button");
424
698
  retry.type = "button";
425
699
  retry.className = "attachment-chip-retry";
426
- retry.title = "Retry";
427
- retry.setAttribute("aria-label", "Retry upload");
700
+ retry.title = this.#strings.retry;
701
+ retry.setAttribute("aria-label", this.#strings.retryUpload);
428
702
  retry.textContent = "\u21BB";
429
703
  retry.addEventListener("click", () => this.#upload(item));
430
704
  chip.appendChild(retry);
@@ -432,8 +706,8 @@ var AttachmentTray = class {
432
706
  const remove = document.createElement("button");
433
707
  remove.type = "button";
434
708
  remove.className = "attachment-chip-remove";
435
- remove.title = "Remove";
436
- remove.setAttribute("aria-label", "Remove attachment");
709
+ remove.title = this.#strings.remove;
710
+ remove.setAttribute("aria-label", this.#strings.removeAttachment);
437
711
  remove.textContent = "\u2715";
438
712
  remove.addEventListener("click", () => this.#remove(item));
439
713
  chip.appendChild(remove);
@@ -463,26 +737,32 @@ function actionButton(modifier, label) {
463
737
  const button = document.createElement("button");
464
738
  button.type = "button";
465
739
  button.className = `confirm-btn confirm-btn--${modifier}`;
740
+ button.setAttribute("part", `confirm-button confirm-${modifier}`);
466
741
  button.textContent = label;
467
742
  return button;
468
743
  }
469
744
  function requestConfirmation(host, request, options = {}) {
745
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
470
746
  return new Promise((resolve) => {
471
747
  const card = document.createElement("div");
472
748
  card.className = "confirm";
749
+ card.setAttribute("part", "confirm");
473
750
  card.setAttribute("data-tool-name", request.toolName);
474
751
  card.setAttribute("role", "group");
475
- card.setAttribute("aria-label", "Confirm action");
752
+ card.setAttribute("aria-label", strings.confirmAction);
476
753
  const body = document.createElement("div");
477
754
  body.className = "confirm-body";
478
- body.textContent = request.message ?? `Run \u201C${request.toolName}\u201D?`;
755
+ body.setAttribute("part", "confirm-body");
756
+ body.textContent = request.message ?? strings.confirmRun.replace("{tool}", request.toolName);
479
757
  const args = document.createElement("pre");
480
758
  args.className = "confirm-args";
759
+ args.setAttribute("part", "confirm-args");
481
760
  args.textContent = JSON.stringify(request.args, null, 2);
482
761
  const actions = document.createElement("div");
483
762
  actions.className = "confirm-actions";
484
- const cancel = actionButton("cancel", "Cancel");
485
- const confirm = actionButton("confirm", "Confirm");
763
+ actions.setAttribute("part", "confirm-actions");
764
+ const cancel = actionButton("cancel", strings.cancel);
765
+ const confirm = actionButton("confirm", strings.confirm);
486
766
  let settled = false;
487
767
  const close = (accepted) => {
488
768
  if (settled) {
@@ -3209,6 +3489,17 @@ var STYLES = `
3209
3489
  --ag-ui-danger: #b91c1c;
3210
3490
  --ag-ui-muted: #6b7280;
3211
3491
 
3492
+ /* Tool-call status icon glyphs (override to re-theme) + spinner speed.
3493
+ The pending state is the animated ring; the settled states use these. */
3494
+ --ag-ui-tool-icon-done: "\u2713";
3495
+ --ag-ui-tool-icon-error: "\u2715";
3496
+ --ag-ui-tool-icon-declined: "\u2298";
3497
+ --ag-ui-tool-spin-duration: 0.7s;
3498
+
3499
+ /* Answer well (opt-in via data-answer-well) \u2014 boxes a whole assistant turn. */
3500
+ --ag-ui-well-bg: transparent;
3501
+ --ag-ui-well-border: var(--ag-ui-border);
3502
+
3212
3503
  /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */
3213
3504
  --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
3214
3505
  --ag-ui-font: inherit;
@@ -3231,6 +3522,8 @@ var STYLES = `
3231
3522
  --ag-ui-inset: auto 24px 24px auto;
3232
3523
  --ag-ui-max-width: calc(100vw - 48px);
3233
3524
  --ag-ui-max-height: calc(100vh - 48px);
3525
+ /* Reading-column width for placement="page" (full-bleed, centred content). */
3526
+ --ag-ui-content-max-width: 820px;
3234
3527
 
3235
3528
  position: var(--ag-ui-position);
3236
3529
  inset: var(--ag-ui-inset);
@@ -3318,6 +3611,99 @@ var STYLES = `
3318
3611
  --ag-ui-radius: 0;
3319
3612
  }
3320
3613
 
3614
+ /* Page (PAGE-1): full-bleed background with a centred reading column. Unlike
3615
+ "full" (edge-to-edge, left-aligned messages) the content sits in a column
3616
+ capped at --ag-ui-content-max-width. The column is produced by symmetric auto
3617
+ padding on the scroll area + composer (no per-row wrapper), so user pills
3618
+ still right-align and the assistant well spans the column. */
3619
+ :host([placement="page"]) {
3620
+ --ag-ui-inset: 0;
3621
+ --ag-ui-width: 100vw;
3622
+ --ag-ui-height: 100vh;
3623
+ --ag-ui-max-width: 100vw;
3624
+ --ag-ui-max-height: 100vh;
3625
+ --ag-ui-radius: 0;
3626
+ }
3627
+
3628
+ :host([placement="page"]) .messages {
3629
+ padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
3630
+ }
3631
+
3632
+ :host([placement="page"]) .input-row {
3633
+ padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
3634
+ }
3635
+
3636
+ /* In the reading column the assistant well uses the full width; the user
3637
+ message stays a right-aligned pill (its default align-self + max-width). */
3638
+ :host([placement="page"]) .message--assistant {
3639
+ max-width: 100%;
3640
+ }
3641
+
3642
+ /* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
3643
+ collapses to a slim icon rail (not the floating launcher). Docked right by
3644
+ default; data-side="left" docks it left. Overlay by default \u2014 set
3645
+ --ag-ui-position: static (and place this element in your own layout) for a
3646
+ host-managed push instead. */
3647
+ :host([placement="sidebar"]) {
3648
+ --ag-ui-inset: 0 0 0 auto;
3649
+ --ag-ui-width: 420px;
3650
+ --ag-ui-height: 100vh;
3651
+ --ag-ui-max-height: 100vh;
3652
+ --ag-ui-radius: 0;
3653
+ --ag-ui-rail-width: 52px;
3654
+ transition: width 0.28s ease;
3655
+ }
3656
+
3657
+ :host([placement="sidebar"][data-side="left"]) {
3658
+ --ag-ui-inset: 0 auto 0 0;
3659
+ }
3660
+
3661
+ :host([placement="sidebar"]) .chat {
3662
+ transition: transform 0.28s ease;
3663
+ }
3664
+
3665
+ /* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
3666
+ reveal the rail. Higher specificity than the generic collapse rules, so it
3667
+ wins regardless of source order. */
3668
+ :host([placement="sidebar"][collapsed]) {
3669
+ width: var(--ag-ui-rail-width);
3670
+ height: 100vh;
3671
+ max-height: 100vh;
3672
+ bottom: 0;
3673
+ }
3674
+
3675
+ :host([placement="sidebar"][collapsed]) .chat {
3676
+ display: none;
3677
+ }
3678
+
3679
+ /* The rail is a sibling of the panel (so it survives the panel being hidden);
3680
+ shown only for a collapsed sidebar. */
3681
+ .rail {
3682
+ display: none;
3683
+ border: none;
3684
+ font: inherit;
3685
+ }
3686
+
3687
+ :host([placement="sidebar"][collapsed]) .rail {
3688
+ display: flex;
3689
+ position: absolute;
3690
+ inset: 0;
3691
+ align-items: flex-start;
3692
+ justify-content: center;
3693
+ padding-top: 16px;
3694
+ border: 1px solid var(--ag-ui-border);
3695
+ background: var(--ag-ui-header-bg);
3696
+ color: var(--ag-ui-header-fg);
3697
+ cursor: pointer;
3698
+ }
3699
+
3700
+ @media (prefers-reduced-motion: reduce) {
3701
+ :host([placement="sidebar"]),
3702
+ :host([placement="sidebar"]) .chat {
3703
+ transition: none;
3704
+ }
3705
+ }
3706
+
3321
3707
  /* Embedded: drop the floating chrome and the high z-index stacking context so
3322
3708
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
3323
3709
  :host([placement="embedded"]) {
@@ -3355,12 +3741,33 @@ var STYLES = `
3355
3741
  }
3356
3742
 
3357
3743
  .header-title {
3744
+ flex: 1;
3745
+ min-width: 0;
3358
3746
  font-weight: 600;
3359
3747
  overflow: hidden;
3360
3748
  text-overflow: ellipsis;
3361
3749
  white-space: nowrap;
3362
3750
  }
3363
3751
 
3752
+ /* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
3753
+ fallback, sized via --ag-ui-icon-size. */
3754
+ .icon-holder {
3755
+ display: inline-flex;
3756
+ align-items: center;
3757
+ justify-content: center;
3758
+ flex: none;
3759
+ width: var(--ag-ui-icon-size, 22px);
3760
+ height: var(--ag-ui-icon-size, 22px);
3761
+ line-height: 1;
3762
+ }
3763
+
3764
+ .icon-img {
3765
+ width: 100%;
3766
+ height: 100%;
3767
+ object-fit: contain;
3768
+ border-radius: var(--ag-ui-icon-radius, 4px);
3769
+ }
3770
+
3364
3771
  .header-controls {
3365
3772
  display: flex;
3366
3773
  gap: 2px;
@@ -3416,6 +3823,39 @@ var STYLES = `
3416
3823
  gap: var(--ag-ui-space);
3417
3824
  }
3418
3825
 
3826
+ /* Empty-state region (CUST-1 slot): centred while it's the only thing in the
3827
+ list, hidden as soon as a message, card, or pending indicator renders. */
3828
+ .empty {
3829
+ margin: auto;
3830
+ text-align: center;
3831
+ color: var(--ag-ui-muted);
3832
+ }
3833
+
3834
+ .empty[hidden] {
3835
+ display: none;
3836
+ }
3837
+
3838
+ /* \u2500\u2500 Answer group / well (WELL-1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3839
+ One .answer per assistant turn wraps the streamed text, its tool cards,
3840
+ and the pending indicator so a whole answer reads (and can be boxed) as one
3841
+ unit. A flex column on the message-list gap, stretched to the list width so
3842
+ its children keep their own left/right alignment. data-answer-well opts into
3843
+ the bordered "well"; without it the layout is today's flat stack. */
3844
+ .answer {
3845
+ display: flex;
3846
+ flex-direction: column;
3847
+ gap: var(--ag-ui-space);
3848
+ align-self: stretch;
3849
+ min-width: 0;
3850
+ }
3851
+
3852
+ :host([data-answer-well]) .answer {
3853
+ padding: var(--ag-ui-pad);
3854
+ background: var(--ag-ui-well-bg);
3855
+ border: 1px solid var(--ag-ui-well-border);
3856
+ border-radius: var(--ag-ui-radius);
3857
+ }
3858
+
3419
3859
  .message {
3420
3860
  max-width: 80%;
3421
3861
  padding: var(--ag-ui-msg-pad);
@@ -3588,10 +4028,71 @@ var STYLES = `
3588
4028
  }
3589
4029
 
3590
4030
  .tool-call-name {
4031
+ flex: 1;
4032
+ min-width: 0;
3591
4033
  font-weight: 600;
3592
4034
  word-break: break-word;
3593
4035
  }
3594
4036
 
4037
+ /* Leading status icon (CARD-1). Empty in the DOM \u2014 the glyph/spinner is drawn
4038
+ here from the card's data-status, so it stays themeable. */
4039
+ .tool-call-icon {
4040
+ flex: none;
4041
+ box-sizing: border-box;
4042
+ display: inline-flex;
4043
+ align-items: center;
4044
+ justify-content: center;
4045
+ width: 14px;
4046
+ height: 14px;
4047
+ font-size: 12px;
4048
+ line-height: 1;
4049
+ }
4050
+
4051
+ /* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
4052
+ .tool-call[data-status="pending"] .tool-call-icon {
4053
+ border: 2px solid var(--ag-ui-muted);
4054
+ border-top-color: transparent;
4055
+ border-radius: 50%;
4056
+ animation: ag-ui-tool-spin var(--ag-ui-tool-spin-duration) linear infinite;
4057
+ }
4058
+
4059
+ @keyframes ag-ui-tool-spin {
4060
+ to { transform: rotate(360deg); }
4061
+ }
4062
+
4063
+ /* Settled: a themeable glyph coloured by outcome. */
4064
+ .tool-call[data-status="done"] .tool-call-icon::before {
4065
+ content: var(--ag-ui-tool-icon-done);
4066
+ color: var(--ag-ui-success);
4067
+ }
4068
+
4069
+ .tool-call[data-status="error"] .tool-call-icon::before {
4070
+ content: var(--ag-ui-tool-icon-error);
4071
+ color: var(--ag-ui-danger);
4072
+ }
4073
+
4074
+ .tool-call[data-status="declined"] .tool-call-icon::before {
4075
+ content: var(--ag-ui-tool-icon-declined);
4076
+ color: var(--ag-ui-muted);
4077
+ }
4078
+
4079
+ @media (prefers-reduced-motion: reduce) {
4080
+ .tool-call[data-status="pending"] .tool-call-icon {
4081
+ animation: none;
4082
+ }
4083
+ }
4084
+
4085
+ /* Inline display mode (CARD-1): the lightest card \u2014 drop the box chrome so the
4086
+ status row reads as one line of the answer; the result toggle still expands
4087
+ below it. */
4088
+ .tool-call[data-display="inline"] {
4089
+ max-width: 100%;
4090
+ background: transparent;
4091
+ border: none;
4092
+ padding: 2px 0;
4093
+ gap: 2px;
4094
+ }
4095
+
3595
4096
  .tool-call-status {
3596
4097
  flex: none;
3597
4098
  padding: 1px 8px;
@@ -4126,24 +4627,24 @@ var STYLES = `
4126
4627
  `;
4127
4628
 
4128
4629
  // src/ui/relative_time.ts
4129
- function relativeTime(timestamp, now = Date.now()) {
4630
+ function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
4130
4631
  const seconds = Math.round((now - timestamp) / 1e3);
4131
4632
  if (seconds < 60) {
4132
- return "just now";
4633
+ return strings.justNow;
4133
4634
  }
4134
4635
  const minutes = Math.round(seconds / 60);
4135
4636
  if (minutes < 60) {
4136
- return `${minutes}m ago`;
4637
+ return strings.minutesAgo.replace("{n}", String(minutes));
4137
4638
  }
4138
4639
  const hours = Math.round(minutes / 60);
4139
4640
  if (hours < 24) {
4140
- return `${hours}h ago`;
4641
+ return strings.hoursAgo.replace("{n}", String(hours));
4141
4642
  }
4142
4643
  const days = Math.round(hours / 24);
4143
4644
  if (days < 7) {
4144
- return `${days}d ago`;
4645
+ return strings.daysAgo.replace("{n}", String(days));
4145
4646
  }
4146
- return `${Math.round(days / 7)}w ago`;
4647
+ return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
4147
4648
  }
4148
4649
 
4149
4650
  // src/ui/thread_drawer.ts
@@ -4151,39 +4652,59 @@ var ThreadDrawer = class {
4151
4652
  /** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
4152
4653
  element;
4153
4654
  #callbacks;
4655
+ #panel;
4656
+ #heading;
4657
+ #newButton;
4154
4658
  #list;
4659
+ #strings;
4155
4660
  #threads = [];
4156
4661
  #activeId = "";
4157
- constructor(callbacks) {
4662
+ constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
4158
4663
  this.#callbacks = callbacks;
4664
+ this.#strings = strings;
4159
4665
  this.element = document.createElement("div");
4160
4666
  this.element.className = "drawer";
4667
+ this.element.setAttribute("part", "drawer");
4161
4668
  this.element.hidden = true;
4162
4669
  const backdrop = document.createElement("div");
4163
4670
  backdrop.className = "drawer-backdrop";
4671
+ backdrop.setAttribute("part", "drawer-backdrop");
4164
4672
  backdrop.addEventListener("click", () => this.close());
4165
- const panel = document.createElement("div");
4166
- panel.className = "drawer-panel";
4167
- panel.setAttribute("role", "dialog");
4168
- panel.setAttribute("aria-label", "Chat history");
4673
+ this.#panel = document.createElement("div");
4674
+ this.#panel.className = "drawer-panel";
4675
+ this.#panel.setAttribute("part", "drawer-panel");
4676
+ this.#panel.setAttribute("role", "dialog");
4677
+ this.#panel.setAttribute("aria-label", strings.chatHistory);
4169
4678
  const header = document.createElement("div");
4170
4679
  header.className = "drawer-header";
4171
- const heading = document.createElement("span");
4172
- heading.className = "drawer-title";
4173
- heading.textContent = "Chats";
4174
- const newButton = document.createElement("button");
4175
- newButton.type = "button";
4176
- newButton.className = "drawer-new";
4177
- newButton.textContent = "New chat";
4178
- newButton.addEventListener("click", () => {
4680
+ header.setAttribute("part", "drawer-header");
4681
+ this.#heading = document.createElement("span");
4682
+ this.#heading.className = "drawer-title";
4683
+ this.#heading.setAttribute("part", "drawer-title");
4684
+ this.#heading.textContent = strings.chats;
4685
+ this.#newButton = document.createElement("button");
4686
+ this.#newButton.type = "button";
4687
+ this.#newButton.className = "drawer-new";
4688
+ this.#newButton.setAttribute("part", "drawer-new");
4689
+ this.#newButton.textContent = strings.newChat;
4690
+ this.#newButton.addEventListener("click", () => {
4179
4691
  this.close();
4180
4692
  this.#callbacks.onNew();
4181
4693
  });
4182
- header.append(heading, newButton);
4694
+ header.append(this.#heading, this.#newButton);
4183
4695
  this.#list = document.createElement("div");
4184
4696
  this.#list.className = "drawer-list";
4185
- panel.append(header, this.#list);
4186
- this.element.append(backdrop, panel);
4697
+ this.#list.setAttribute("part", "drawer-list");
4698
+ this.#panel.append(header, this.#list);
4699
+ this.element.append(backdrop, this.#panel);
4700
+ }
4701
+ /** Re-localize the drawer's chrome and rows (the host calls this on connect). */
4702
+ setStrings(strings) {
4703
+ this.#strings = strings;
4704
+ this.#panel.setAttribute("aria-label", strings.chatHistory);
4705
+ this.#heading.textContent = strings.chats;
4706
+ this.#newButton.textContent = strings.newChat;
4707
+ this.#renderList();
4187
4708
  }
4188
4709
  isOpen() {
4189
4710
  return !this.element.hidden;
@@ -4208,7 +4729,8 @@ var ThreadDrawer = class {
4208
4729
  if (this.#threads.length === 0) {
4209
4730
  const empty = document.createElement("div");
4210
4731
  empty.className = "drawer-empty";
4211
- empty.textContent = "No conversations yet.";
4732
+ empty.setAttribute("part", "drawer-empty");
4733
+ empty.textContent = this.#strings.noConversations;
4212
4734
  this.#list.appendChild(empty);
4213
4735
  return;
4214
4736
  }
@@ -4219,18 +4741,20 @@ var ThreadDrawer = class {
4219
4741
  #renderRow(meta) {
4220
4742
  const row = document.createElement("div");
4221
4743
  row.className = "drawer-row";
4744
+ row.setAttribute("part", "drawer-row");
4222
4745
  if (meta.threadId === this.#activeId) {
4223
4746
  row.classList.add("drawer-row--active");
4224
4747
  }
4225
4748
  const select = document.createElement("button");
4226
4749
  select.type = "button";
4227
4750
  select.className = "drawer-row-select";
4751
+ select.setAttribute("part", "drawer-row-select");
4228
4752
  const title = document.createElement("span");
4229
4753
  title.className = "drawer-row-title";
4230
4754
  title.textContent = meta.title;
4231
4755
  const time = document.createElement("span");
4232
4756
  time.className = "drawer-row-time";
4233
- time.textContent = relativeTime(meta.updatedAt);
4757
+ time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
4234
4758
  const preview = document.createElement("span");
4235
4759
  preview.className = "drawer-row-preview";
4236
4760
  preview.textContent = meta.preview;
@@ -4242,15 +4766,15 @@ var ThreadDrawer = class {
4242
4766
  const rename = document.createElement("button");
4243
4767
  rename.type = "button";
4244
4768
  rename.className = "drawer-row-rename";
4245
- rename.title = "Rename";
4246
- rename.setAttribute("aria-label", "Rename conversation");
4769
+ rename.title = this.#strings.rename;
4770
+ rename.setAttribute("aria-label", this.#strings.renameConversation);
4247
4771
  rename.textContent = "\u270E";
4248
4772
  rename.addEventListener("click", () => this.#startRename(row, meta));
4249
4773
  const remove = document.createElement("button");
4250
4774
  remove.type = "button";
4251
4775
  remove.className = "drawer-row-delete";
4252
- remove.title = "Delete";
4253
- remove.setAttribute("aria-label", "Delete conversation");
4776
+ remove.title = this.#strings.delete;
4777
+ remove.setAttribute("aria-label", this.#strings.deleteConversation);
4254
4778
  remove.textContent = "\u{1F5D1}";
4255
4779
  remove.addEventListener("click", () => this.#confirmDelete(row, meta));
4256
4780
  const actions = document.createElement("div");
@@ -4287,16 +4811,16 @@ var ThreadDrawer = class {
4287
4811
  confirm.className = "drawer-confirm";
4288
4812
  const label = document.createElement("span");
4289
4813
  label.className = "drawer-confirm-label";
4290
- label.textContent = "Delete?";
4814
+ label.textContent = this.#strings.deletePrompt;
4291
4815
  const yes = document.createElement("button");
4292
4816
  yes.type = "button";
4293
4817
  yes.className = "drawer-confirm-yes";
4294
- yes.textContent = "Delete";
4818
+ yes.textContent = this.#strings.delete;
4295
4819
  yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
4296
4820
  const no = document.createElement("button");
4297
4821
  no.type = "button";
4298
4822
  no.className = "drawer-confirm-no";
4299
- no.textContent = "Cancel";
4823
+ no.textContent = this.#strings.cancel;
4300
4824
  no.addEventListener("click", () => this.#renderList());
4301
4825
  confirm.append(label, yes, no);
4302
4826
  row.replaceChildren(confirm);
@@ -4304,73 +4828,96 @@ var ThreadDrawer = class {
4304
4828
  };
4305
4829
 
4306
4830
  // src/ui/tool_call_card.ts
4307
- var STATUS_LABEL = {
4308
- [TOOL_CALL_STATUS.PENDING]: "running\u2026",
4309
- [TOOL_CALL_STATUS.DONE]: "\u2713 done",
4310
- [TOOL_CALL_STATUS.ERROR]: "\u26A0 error",
4311
- [TOOL_CALL_STATUS.DECLINED]: "\u2298 declined"
4312
- };
4313
- var RESULT_LABEL = {
4314
- [TOOL_CALL_STATUS.DONE]: "Result",
4315
- [TOOL_CALL_STATUS.ERROR]: "Error",
4316
- [TOOL_CALL_STATUS.DECLINED]: "Declined"
4317
- };
4831
+ function statusLabels(strings) {
4832
+ return {
4833
+ [TOOL_CALL_STATUS.PENDING]: strings.toolRunning,
4834
+ [TOOL_CALL_STATUS.DONE]: strings.toolDone,
4835
+ [TOOL_CALL_STATUS.ERROR]: strings.toolError,
4836
+ [TOOL_CALL_STATUS.DECLINED]: strings.toolDeclined
4837
+ };
4838
+ }
4839
+ function resultLabels(strings) {
4840
+ return {
4841
+ [TOOL_CALL_STATUS.DONE]: strings.resultLabel,
4842
+ [TOOL_CALL_STATUS.ERROR]: strings.errorLabel,
4843
+ [TOOL_CALL_STATUS.DECLINED]: strings.declinedLabel
4844
+ };
4845
+ }
4318
4846
  var ToolCallCard = class {
4319
4847
  /** The card's root element; append this into the message list. */
4320
4848
  element;
4321
4849
  #status;
4322
4850
  #mode;
4323
4851
  #args;
4324
- constructor(name, args, mode = TOOL_DISPLAY.FULL, summary) {
4852
+ #strings;
4853
+ #settled = false;
4854
+ constructor(name, args, mode = TOOL_DISPLAY.FULL, summary, strings = DEFAULT_UI_STRINGS) {
4325
4855
  this.#mode = mode;
4326
4856
  this.#args = args;
4857
+ this.#strings = strings;
4327
4858
  this.element = document.createElement("div");
4328
4859
  this.element.className = "tool-call";
4860
+ this.element.setAttribute("part", "tool-card");
4329
4861
  this.element.setAttribute("data-tool-name", name);
4330
4862
  this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
4331
4863
  this.element.setAttribute("data-display", mode);
4332
4864
  const head = document.createElement("div");
4333
4865
  head.className = "tool-call-head";
4866
+ head.setAttribute("part", "tool-card-head");
4867
+ const icon = document.createElement("span");
4868
+ icon.className = "tool-call-icon";
4869
+ icon.setAttribute("part", "tool-card-icon");
4870
+ icon.setAttribute("aria-hidden", "true");
4334
4871
  const label = document.createElement("span");
4335
4872
  label.className = "tool-call-name";
4336
- label.textContent = `\u{1F527} ${summary ?? name}`;
4873
+ label.setAttribute("part", "tool-card-name");
4874
+ label.textContent = summary ?? name;
4337
4875
  this.#status = document.createElement("span");
4338
4876
  this.#status.className = "tool-call-status";
4339
- this.#status.textContent = STATUS_LABEL[TOOL_CALL_STATUS.PENDING];
4340
- head.append(label, this.#status);
4877
+ this.#status.setAttribute("part", "tool-card-status");
4878
+ this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
4879
+ head.append(icon, label, this.#status);
4341
4880
  this.element.append(head);
4342
4881
  if (mode === TOOL_DISPLAY.FULL) {
4343
4882
  const argsEl = document.createElement("pre");
4344
4883
  argsEl.className = "tool-call-args";
4884
+ argsEl.setAttribute("part", "tool-card-args");
4345
4885
  argsEl.textContent = JSON.stringify(args, null, 2);
4346
4886
  this.element.append(argsEl);
4347
4887
  }
4348
4888
  }
4889
+ /** Whether {@link settle} has already run (so a terminal sweep can skip it). */
4890
+ get settled() {
4891
+ return this.#settled;
4892
+ }
4349
4893
  /**
4350
4894
  * Flip the status pill to ``status`` and, unless in `minimal` mode, append a
4351
- * collapsed body behind a click-to-expand toggle: the result alone (`full`),
4352
- * or the args + result together (`compact`).
4895
+ * collapsed body behind a click-to-expand toggle: the result alone (`full` /
4896
+ * `inline`), or the args + result together (`compact`).
4353
4897
  */
4354
4898
  settle(status, text2) {
4899
+ this.#settled = true;
4355
4900
  this.element.setAttribute("data-status", status);
4356
- this.#status.textContent = STATUS_LABEL[status];
4901
+ this.#status.textContent = statusLabels(this.#strings)[status];
4357
4902
  if (this.#mode === TOOL_DISPLAY.MINIMAL) {
4358
4903
  return;
4359
4904
  }
4360
4905
  const toggle = document.createElement("button");
4361
4906
  toggle.type = "button";
4362
4907
  toggle.className = "tool-call-toggle";
4908
+ toggle.setAttribute("part", "tool-card-toggle");
4363
4909
  toggle.setAttribute("aria-expanded", "false");
4364
4910
  const output = document.createElement("pre");
4365
4911
  output.className = "tool-call-result";
4912
+ output.setAttribute("part", "tool-card-result");
4366
4913
  output.hidden = true;
4367
4914
  if (this.#mode === TOOL_DISPLAY.COMPACT) {
4368
- toggle.textContent = "Details";
4915
+ toggle.textContent = this.#strings.details;
4369
4916
  output.textContent = `args: ${JSON.stringify(this.#args)}
4370
4917
 
4371
4918
  ${text2}`;
4372
4919
  } else {
4373
- toggle.textContent = RESULT_LABEL[status];
4920
+ toggle.textContent = resultLabels(this.#strings)[status];
4374
4921
  output.textContent = text2;
4375
4922
  }
4376
4923
  toggle.addEventListener("click", () => {
@@ -4384,6 +4931,12 @@ ${text2}`;
4384
4931
 
4385
4932
  // src/core/agui_client.ts
4386
4933
  import { randomUUID as randomUUID2 } from "@ag-ui/client";
4934
+ var ConnectionLostError = class extends Error {
4935
+ constructor(message) {
4936
+ super(message);
4937
+ this.name = "ConnectionLostError";
4938
+ }
4939
+ };
4387
4940
  var AgUiClient = class {
4388
4941
  #agent;
4389
4942
  #handlers;
@@ -4391,6 +4944,7 @@ var AgUiClient = class {
4391
4944
  #getContext;
4392
4945
  #executeTool;
4393
4946
  #onPersist;
4947
+ #connectionLostMessage;
4394
4948
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
4395
4949
  // a cancel between frontend-tool rounds doesn't start another round.
4396
4950
  #cancelled = false;
@@ -4402,6 +4956,7 @@ var AgUiClient = class {
4402
4956
  this.#executeTool = config.executeTool ?? null;
4403
4957
  this.#onPersist = config.onPersist ?? (() => {
4404
4958
  });
4959
+ this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
4405
4960
  }
4406
4961
  /** Whether a run is currently in flight. */
4407
4962
  get running() {
@@ -4483,14 +5038,18 @@ var AgUiClient = class {
4483
5038
  return;
4484
5039
  }
4485
5040
  const pending = [];
5041
+ const runState = { terminal: false };
4486
5042
  await this.#agent.runAgent(
4487
5043
  { tools: this.#getTools(), context: this.#getContext() },
4488
- this.#buildSubscriber(pending)
5044
+ this.#buildSubscriber(pending, runState)
4489
5045
  );
4490
5046
  this.#onPersist(this.#agent.messages);
4491
5047
  if (this.#cancelled) {
4492
5048
  return;
4493
5049
  }
5050
+ if (!runState.terminal) {
5051
+ throw new ConnectionLostError(this.#connectionLostMessage);
5052
+ }
4494
5053
  if (this.#executeTool === null || pending.length === 0) {
4495
5054
  return;
4496
5055
  }
@@ -4517,7 +5076,7 @@ var AgUiClient = class {
4517
5076
  }
4518
5077
  }
4519
5078
  }
4520
- #buildSubscriber(pending) {
5079
+ #buildSubscriber(pending, runState) {
4521
5080
  const h = this.#handlers;
4522
5081
  return {
4523
5082
  onRunInitialized() {
@@ -4542,9 +5101,11 @@ var AgUiClient = class {
4542
5101
  h.onToolResult(event.toolCallId, event.content);
4543
5102
  },
4544
5103
  onRunErrorEvent({ event }) {
5104
+ runState.terminal = true;
4545
5105
  h.onError(event.message);
4546
5106
  },
4547
5107
  onRunFinalized() {
5108
+ runState.terminal = true;
4548
5109
  h.onRunEnd();
4549
5110
  }
4550
5111
  };
@@ -4982,12 +5543,28 @@ var AgUiChat = class extends HTMLElement {
4982
5543
  * schema; this map is the seam for everything else.
4983
5544
  */
4984
5545
  toolSummaries = {};
5546
+ /**
5547
+ * Localizable UI strings — a partial override merged over the English
5548
+ * {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
5549
+ * element is appended); the `data-strings` JSON attribute is the markup
5550
+ * equivalent, and this property wins key-by-key over it.
5551
+ */
5552
+ strings = {};
5553
+ /**
5554
+ * Resolve a `scroll_to` / `drag_and_drop` target string to a host-page
5555
+ * element (or `null`). Defaults to a CSS-selector lookup; override to map
5556
+ * page-map element ids. The page-action tools are opt-in via the
5557
+ * `data-page-actions` attribute (`"scroll"` / `"drag"`).
5558
+ */
5559
+ resolvePageTarget = (target) => document.querySelector(target);
4985
5560
  /**
4986
5561
  * Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
4987
5562
  * tool name. The base layer behind {@link toolSummaries}: an explicit entry in
4988
5563
  * `toolSummaries` wins, this fills the rest. Populated once on connect.
4989
5564
  */
4990
5565
  #toolCatalog = {};
5566
+ /** The resolved string table (defaults ← `data-strings` ← `strings`). */
5567
+ #strings = DEFAULT_UI_STRINGS;
4991
5568
  #toolRegistry = new ClientToolRegistry();
4992
5569
  /** Tool-call cards awaiting execution, keyed by call id. */
4993
5570
  #toolCards = /* @__PURE__ */ new Map();
@@ -5010,6 +5587,10 @@ var AgUiChat = class extends HTMLElement {
5010
5587
  #attachButton;
5011
5588
  #fileInput;
5012
5589
  #attachSlot;
5590
+ /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
5591
+ #rail;
5592
+ /** Empty-state region at the top of the message list; hidden once anything renders. */
5593
+ #emptyWrap;
5013
5594
  /** Upload tray; created on connect only when `data-attachments-url` is set. */
5014
5595
  #attachTray = null;
5015
5596
  /** Refs attached to the message currently being sent (the context manifest). */
@@ -5028,6 +5609,13 @@ var AgUiChat = class extends HTMLElement {
5028
5609
  // it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
5029
5610
  #streamDeltas = 0;
5030
5611
  #pending = null;
5612
+ // The current assistant turn's grouping container (WELL-1). One `.answer`
5613
+ // wraps everything a single answer produces — streamed text, tool cards, the
5614
+ // pending indicator — so it can be boxed as one "well" by CSS. Opened on the
5615
+ // turn's first run start, closed at settle, so it spans the whole multi-round
5616
+ // frontend-tool loop (which is several AG-UI runs), not one run. `null`
5617
+ // between turns; user bubbles never enter it.
5618
+ #currentGroup = null;
5031
5619
  #threadId = "";
5032
5620
  #initialMessages = [];
5033
5621
  // Skill catalog by source; merged backend → embed → client (later wins).
@@ -5046,6 +5634,8 @@ var AgUiChat = class extends HTMLElement {
5046
5634
  this.#attachButton = document.createElement("button");
5047
5635
  this.#fileInput = document.createElement("input");
5048
5636
  this.#attachSlot = document.createElement("div");
5637
+ this.#rail = document.createElement("button");
5638
+ this.#emptyWrap = document.createElement("div");
5049
5639
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
5050
5640
  this.#drawer = new ThreadDrawer({
5051
5641
  onSelect: (threadId) => {
@@ -5069,7 +5659,7 @@ var AgUiChat = class extends HTMLElement {
5069
5659
  return ["title-text"];
5070
5660
  }
5071
5661
  attributeChangedCallback(_name, _previous, value) {
5072
- this.#title.textContent = value ?? "Assistant";
5662
+ this.#title.textContent = value ?? this.#strings.title;
5073
5663
  }
5074
5664
  /** Declare a frontend tool the agent may call. */
5075
5665
  registerTool(tool) {
@@ -5115,9 +5705,25 @@ var AgUiChat = class extends HTMLElement {
5115
5705
  }
5116
5706
  ];
5117
5707
  }
5118
- /** All built-in (route + page) frontend tools. */
5708
+ /**
5709
+ * Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
5710
+ * via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
5711
+ * through {@link resolvePageTarget} so a host controls the agent's interaction
5712
+ * surface; absent attribute ⇒ no tools registered.
5713
+ */
5714
+ #pageActionTools() {
5715
+ const attr = this.getAttribute("data-page-actions");
5716
+ if (attr === null) {
5717
+ return [];
5718
+ }
5719
+ const enabled = new Set(
5720
+ attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
5721
+ );
5722
+ return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
5723
+ }
5724
+ /** All built-in (route + page + page-action) frontend tools. */
5119
5725
  #builtinTools() {
5120
- return [...this.#routeTools(), ...this.#pageTools()];
5726
+ return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
5121
5727
  }
5122
5728
  /** Resolve a tool by name: built-in tools first, then the registry. */
5123
5729
  #resolveTool(name) {
@@ -5143,7 +5749,7 @@ var AgUiChat = class extends HTMLElement {
5143
5749
  */
5144
5750
  get toolDisplay() {
5145
5751
  const attr = this.getAttribute("data-tool-display");
5146
- if (attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
5752
+ if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
5147
5753
  return attr;
5148
5754
  }
5149
5755
  return TOOL_DISPLAY.FULL;
@@ -5152,10 +5758,13 @@ var AgUiChat = class extends HTMLElement {
5152
5758
  this.setAttribute("data-tool-display", value);
5153
5759
  }
5154
5760
  connectedCallback() {
5761
+ this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
5155
5762
  this.#render();
5763
+ this.#drawer.setStrings(this.#strings);
5156
5764
  if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
5157
5765
  this.setAttribute("collapsed", "");
5158
5766
  }
5767
+ this.#syncRail();
5159
5768
  this.#initSkills();
5160
5769
  void this.#fetchToolCatalog();
5161
5770
  this.#wireThreadStore();
@@ -5163,6 +5772,21 @@ var AgUiChat = class extends HTMLElement {
5163
5772
  this.#threadId = this.conversationStore.threadId();
5164
5773
  void this.#rehydrate();
5165
5774
  }
5775
+ /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
5776
+ #readStringOverrides() {
5777
+ const raw = this.getAttribute("data-strings");
5778
+ if (raw === null) {
5779
+ return {};
5780
+ }
5781
+ try {
5782
+ const parsed = JSON.parse(raw);
5783
+ if (typeof parsed === "object" && parsed !== null) {
5784
+ return parsed;
5785
+ }
5786
+ } catch {
5787
+ }
5788
+ return {};
5789
+ }
5166
5790
  /**
5167
5791
  * Enable the composer's file-upload tray when uploads are possible — either a
5168
5792
  * custom {@link uploadHandler} is set or `data-attachments-url` provides the
@@ -5180,7 +5804,8 @@ var AgUiChat = class extends HTMLElement {
5180
5804
  this.#attachTray = new AttachmentTray({
5181
5805
  upload,
5182
5806
  maxBytes: this.#attachmentMaxBytes(),
5183
- accept
5807
+ accept,
5808
+ strings: this.#strings
5184
5809
  });
5185
5810
  this.#attachSlot.appendChild(this.#attachTray.element);
5186
5811
  this.#fileInput.accept = accept;
@@ -5330,7 +5955,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5330
5955
  #applySkill(skill) {
5331
5956
  const { text: text2, missing } = fillTemplate(skill.prompt, this.skillContext());
5332
5957
  if (missing.length > 0) {
5333
- this.#skillHint.textContent = `\u201C${skill.title}\u201D needs: ${missing.join(", ")}`;
5958
+ this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
5334
5959
  this.#skillHint.hidden = false;
5335
5960
  return;
5336
5961
  }
@@ -5363,6 +5988,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5363
5988
  this.removeAttribute("collapsed");
5364
5989
  }
5365
5990
  sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
5991
+ this.#syncRail();
5366
5992
  this.dispatchEvent(
5367
5993
  new CustomEvent(TOGGLE_EVENT, {
5368
5994
  detail: { collapsed },
@@ -5390,13 +6016,15 @@ Use the read_attachment tool with an id to read a file's contents.`
5390
6016
  #resetState() {
5391
6017
  this.#client = null;
5392
6018
  this.#streamingBubble = null;
6019
+ this.#currentGroup = null;
5393
6020
  this.#hidePending();
5394
6021
  this.#toolCards.clear();
5395
6022
  this.#serverSettled.clear();
5396
6023
  this.#initialMessages = [];
5397
6024
  this.#runAttachments = [];
5398
6025
  this.#attachTray?.clear();
5399
- this.#messages.replaceChildren();
6026
+ this.#messages.replaceChildren(this.#emptyWrap);
6027
+ this.#updateEmptyState();
5400
6028
  }
5401
6029
  /** Switch the active conversation to an existing thread and replay it. */
5402
6030
  async #switchThread(threadId) {
@@ -5520,72 +6148,102 @@ Use the read_attachment tool with an id to read a file's contents.`
5520
6148
  * Assistant content is rendered as sanitised markdown/HTML; user content
5521
6149
  * stays literal text (no need to parse what the user typed, and it avoids
5522
6150
  * rendering user-authored markup).
6151
+ *
6152
+ * Assistant bubbles land in the current answer group (WELL-1), opening one if
6153
+ * needed; a user bubble closes the prior group and sits directly in the list
6154
+ * (the well wraps the *assistant* turn, the user message precedes it).
5523
6155
  */
5524
6156
  appendMessage(role, content) {
5525
6157
  const bubble = document.createElement("div");
5526
6158
  bubble.className = `message message--${role}`;
6159
+ bubble.setAttribute("part", `message message-${role}`);
5527
6160
  if (role === MESSAGE_ROLE.ASSISTANT) {
5528
6161
  bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
6162
+ this.#ensureGroup().appendChild(bubble);
5529
6163
  } else {
6164
+ this.#currentGroup = null;
5530
6165
  bubble.textContent = content;
6166
+ this.#messages.appendChild(bubble);
5531
6167
  }
5532
- this.#messages.appendChild(bubble);
6168
+ this.#updateEmptyState();
5533
6169
  this.#messages.scrollTop = this.#messages.scrollHeight;
5534
6170
  return bubble;
5535
6171
  }
6172
+ /**
6173
+ * The open answer group, creating and appending it on first use. Everything a
6174
+ * single assistant turn renders (text, tool cards, the pending indicator)
6175
+ * goes inside it, so the opt-in `data-answer-well` styling can box the whole
6176
+ * turn. Idempotent across the turn's runs — it persists until {@link #handlers}'
6177
+ * `onSettled` nulls it.
6178
+ */
6179
+ #ensureGroup() {
6180
+ if (this.#currentGroup === null) {
6181
+ const group = document.createElement("div");
6182
+ group.className = "answer";
6183
+ group.setAttribute("part", "answer");
6184
+ this.#currentGroup = group;
6185
+ this.#messages.appendChild(group);
6186
+ this.#updateEmptyState();
6187
+ }
6188
+ return this.#currentGroup;
6189
+ }
5536
6190
  #render() {
5537
6191
  const style = document.createElement("style");
5538
6192
  style.textContent = STYLES;
5539
6193
  this.#chat.className = "chat";
6194
+ this.#chat.setAttribute("part", "panel");
5540
6195
  const header = document.createElement("div");
5541
6196
  header.className = "header";
6197
+ header.setAttribute("part", "header");
5542
6198
  const title = this.#title;
5543
6199
  title.className = "header-title";
5544
- title.textContent = this.getAttribute("title-text") ?? "Assistant";
6200
+ title.setAttribute("part", "title");
6201
+ title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
6202
+ if (this.querySelector('[slot="icon"]') !== null || this.getAttribute("data-icon-url") !== null) {
6203
+ header.append(this.#iconElement("icon", "icon", null));
6204
+ }
6205
+ const headerActions = document.createElement("slot");
6206
+ headerActions.name = "header-actions";
5545
6207
  const controls = document.createElement("div");
5546
6208
  controls.className = "header-controls";
5547
- const history = document.createElement("button");
5548
- history.type = "button";
5549
- history.className = "header-btn header-btn--history";
5550
- history.title = "Chat history";
5551
- history.setAttribute("aria-label", "Chat history");
5552
- history.textContent = "\u2630";
6209
+ controls.setAttribute("part", "header-controls");
6210
+ const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
5553
6211
  history.addEventListener("click", () => {
5554
6212
  void this.#refreshDrawer();
5555
6213
  this.#drawer.open();
5556
6214
  });
5557
- const newChat = document.createElement("button");
5558
- newChat.type = "button";
5559
- newChat.className = "header-btn header-btn--new";
5560
- newChat.title = "New chat";
5561
- newChat.setAttribute("aria-label", "New chat");
5562
- newChat.textContent = "\u271A";
6215
+ const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
5563
6216
  newChat.addEventListener("click", () => this.newChat());
5564
- const collapse = document.createElement("button");
5565
- collapse.type = "button";
5566
- collapse.className = "header-btn header-btn--collapse";
5567
- collapse.title = "Collapse";
5568
- collapse.setAttribute("aria-label", "Collapse");
5569
- collapse.textContent = "\u2014";
6217
+ const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
5570
6218
  collapse.addEventListener("click", () => this.toggleCollapsed());
5571
6219
  controls.append(history, newChat, collapse);
5572
- header.append(title, controls);
6220
+ header.append(title, headerActions, controls);
5573
6221
  this.#messages.className = "messages";
6222
+ this.#messages.setAttribute("part", "messages");
5574
6223
  this.#messages.setAttribute("role", "log");
5575
6224
  this.#messages.setAttribute("aria-live", "polite");
5576
- this.#messages.setAttribute("aria-label", "Conversation");
6225
+ this.#messages.setAttribute("aria-label", this.#strings.conversation);
6226
+ this.#emptyWrap.className = "empty";
6227
+ this.#emptyWrap.setAttribute("part", "empty");
6228
+ const emptySlot = document.createElement("slot");
6229
+ emptySlot.name = "empty";
6230
+ this.#emptyWrap.append(emptySlot);
6231
+ this.#messages.append(this.#emptyWrap);
5577
6232
  const inputRow = document.createElement("div");
5578
6233
  inputRow.className = "input-row";
6234
+ inputRow.setAttribute("part", "composer");
5579
6235
  this.#input.className = "input";
5580
- this.#input.setAttribute("aria-label", "Message");
6236
+ this.#input.setAttribute("part", "input");
6237
+ this.#input.setAttribute("aria-label", this.#strings.message);
5581
6238
  this.#input.rows = 2;
5582
- this.#input.placeholder = "Ask anything\u2026";
6239
+ this.#input.placeholder = this.#strings.inputPlaceholder;
5583
6240
  this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
5584
6241
  this.#input.addEventListener("input", () => this.#onInput());
5585
6242
  this.#send.className = "send";
5586
6243
  this.#send.type = "button";
5587
- this.#send.textContent = "Send";
5588
- this.#send.setAttribute("aria-label", "Send");
6244
+ this.#send.setAttribute("part", "send");
6245
+ this.#send.textContent = this.#strings.send;
6246
+ this.#send.setAttribute("aria-label", this.#strings.send);
5589
6247
  this.#send.dataset["state"] = "idle";
5590
6248
  this.#send.addEventListener("click", () => {
5591
6249
  if (this.#running) {
@@ -5598,9 +6256,10 @@ Use the read_attachment tool with an id to read a file's contents.`
5598
6256
  this.#skillHint.hidden = true;
5599
6257
  this.#attachButton.className = "attach-btn";
5600
6258
  this.#attachButton.type = "button";
6259
+ this.#attachButton.setAttribute("part", "attach-button");
5601
6260
  this.#attachButton.textContent = "\u{1F4CE}";
5602
- this.#attachButton.title = "Attach files";
5603
- this.#attachButton.setAttribute("aria-label", "Attach files");
6261
+ this.#attachButton.title = this.#strings.attachFiles;
6262
+ this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
5604
6263
  this.#attachButton.hidden = true;
5605
6264
  this.#attachButton.addEventListener("click", () => this.#fileInput.click());
5606
6265
  this.#fileInput.className = "attach-input";
@@ -5609,6 +6268,8 @@ Use the read_attachment tool with an id to read a file's contents.`
5609
6268
  this.#fileInput.hidden = true;
5610
6269
  this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
5611
6270
  this.#attachSlot.className = "attachment-slot";
6271
+ const footer = document.createElement("slot");
6272
+ footer.name = "footer";
5612
6273
  inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
5613
6274
  this.#chat.append(
5614
6275
  header,
@@ -5618,9 +6279,58 @@ Use the read_attachment tool with an id to read a file's contents.`
5618
6279
  this.#skillHint,
5619
6280
  this.#attachSlot,
5620
6281
  inputRow,
6282
+ footer,
5621
6283
  this.#drawer.element
5622
6284
  );
5623
- this.#root.append(style, this.#chat);
6285
+ this.#rail.className = "rail";
6286
+ this.#rail.type = "button";
6287
+ this.#rail.setAttribute("part", "launcher");
6288
+ this.#rail.setAttribute("aria-label", this.#strings.expand);
6289
+ this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
6290
+ this.#rail.addEventListener("click", () => this.setCollapsed(false));
6291
+ this.#root.append(style, this.#chat, this.#rail);
6292
+ }
6293
+ /** Build a header control button (icon glyph + localized title/aria). */
6294
+ #headerButton(modifier, label, glyph) {
6295
+ const button = document.createElement("button");
6296
+ button.type = "button";
6297
+ button.className = `header-btn header-btn--${modifier}`;
6298
+ button.setAttribute("part", `header-button ${modifier}-button`);
6299
+ button.title = label;
6300
+ button.setAttribute("aria-label", label);
6301
+ button.textContent = glyph;
6302
+ return button;
6303
+ }
6304
+ /**
6305
+ * An icon holder wrapping a `<slot>` so a host can project custom markup; with
6306
+ * a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
6307
+ */
6308
+ #iconElement(slotName, part, fallbackGlyph) {
6309
+ const holder = document.createElement("span");
6310
+ holder.className = "icon-holder";
6311
+ holder.setAttribute("part", part);
6312
+ const slot = document.createElement("slot");
6313
+ slot.name = slotName;
6314
+ const iconUrl = this.getAttribute("data-icon-url");
6315
+ if (iconUrl !== null) {
6316
+ const img = document.createElement("img");
6317
+ img.className = "icon-img";
6318
+ img.src = iconUrl;
6319
+ img.alt = "";
6320
+ slot.append(img);
6321
+ } else if (fallbackGlyph !== null) {
6322
+ slot.append(document.createTextNode(fallbackGlyph));
6323
+ }
6324
+ holder.append(slot);
6325
+ return holder;
6326
+ }
6327
+ /** Reflect the collapsed state on the rail's `aria-expanded`. */
6328
+ #syncRail() {
6329
+ this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
6330
+ }
6331
+ /** Hide the empty-state region once the message list holds anything else. */
6332
+ #updateEmptyState() {
6333
+ this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
5624
6334
  }
5625
6335
  /** Forward input changes to the skills palette and clear any stale hint. */
5626
6336
  #onInput() {
@@ -5655,7 +6365,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5655
6365
  /** Swap the composer button between Send (idle) and Stop (running). */
5656
6366
  #setRunning(running) {
5657
6367
  this.#running = running;
5658
- const label = running ? "Stop" : "Send";
6368
+ const label = running ? this.#strings.stop : this.#strings.send;
5659
6369
  this.#send.textContent = label;
5660
6370
  this.#send.setAttribute("aria-label", label);
5661
6371
  this.#send.dataset["state"] = running ? "running" : "idle";
@@ -5706,7 +6416,8 @@ Use the read_attachment tool with an id to read a file's contents.`
5706
6416
  getTools: () => this.getTools(),
5707
6417
  getContext: () => this.getContext(),
5708
6418
  executeTool: (call) => this.#executeTool(call),
5709
- onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
6419
+ onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
6420
+ connectionLostMessage: this.#strings.connectionLost
5710
6421
  });
5711
6422
  }
5712
6423
  return this.#client;
@@ -5727,7 +6438,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5727
6438
  const tool = this.#resolveTool(call.name);
5728
6439
  if (tool === null) {
5729
6440
  if (!this.#serverSettled.has(call.id)) {
5730
- card.settle(TOOL_CALL_STATUS.DONE, "No result returned.");
6441
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
5731
6442
  }
5732
6443
  return null;
5733
6444
  }
@@ -5739,13 +6450,15 @@ Use the read_attachment tool with an id to read a file's contents.`
5739
6450
  }
5740
6451
  this.#confirmAbort = new AbortController();
5741
6452
  const decision = requestConfirmation(this.#messages, request, {
5742
- signal: this.#confirmAbort.signal
6453
+ signal: this.#confirmAbort.signal,
6454
+ strings: this.#strings
5743
6455
  });
6456
+ this.#updateEmptyState();
5744
6457
  this.#messages.scrollTop = this.#messages.scrollHeight;
5745
6458
  const accepted = await decision;
5746
6459
  this.#confirmAbort = null;
5747
6460
  if (!accepted) {
5748
- const message = "User declined the action.";
6461
+ const message = this.#strings.declinedAction;
5749
6462
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
5750
6463
  this.#showPending();
5751
6464
  return { content: message };
@@ -5758,7 +6471,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5758
6471
  try {
5759
6472
  const result = await tool.handler(call.args);
5760
6473
  if (navigates) {
5761
- card.settle(TOOL_CALL_STATUS.DONE, "Navigating\u2026");
6474
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
5762
6475
  return { content: "", halt: true };
5763
6476
  }
5764
6477
  const content = JSON.stringify(result ?? null);
@@ -5779,6 +6492,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5779
6492
  return {
5780
6493
  onRunStart: () => {
5781
6494
  this.#setRunning(true);
6495
+ this.#ensureGroup();
5782
6496
  this.#showPending();
5783
6497
  },
5784
6498
  onTextDelta: (buffer) => {
@@ -5824,6 +6538,16 @@ Use the read_attachment tool with an id to read a file's contents.`
5824
6538
  this.#setRunning(false);
5825
6539
  this.#streamingBubble = null;
5826
6540
  this.#runAttachments = [];
6541
+ for (const card of this.#toolCards.values()) {
6542
+ if (!card.settled) {
6543
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
6544
+ }
6545
+ }
6546
+ if (this.#currentGroup !== null && this.#currentGroup.childElementCount === 0) {
6547
+ this.#currentGroup.remove();
6548
+ this.#updateEmptyState();
6549
+ }
6550
+ this.#currentGroup = null;
5827
6551
  }
5828
6552
  };
5829
6553
  }
@@ -5831,9 +6555,11 @@ Use the read_attachment tool with an id to read a file's contents.`
5831
6555
  #appendStoppedNote() {
5832
6556
  const note = document.createElement("div");
5833
6557
  note.className = "stopped-note";
6558
+ note.setAttribute("part", "stopped");
5834
6559
  note.setAttribute("role", "status");
5835
- note.textContent = "\u23F9 Stopped";
5836
- this.#messages.appendChild(note);
6560
+ note.textContent = this.#strings.stopped;
6561
+ this.#ensureGroup().appendChild(note);
6562
+ this.#updateEmptyState();
5837
6563
  this.#messages.scrollTop = this.#messages.scrollHeight;
5838
6564
  }
5839
6565
  /**
@@ -5847,15 +6573,17 @@ Use the read_attachment tool with an id to read a file's contents.`
5847
6573
  }
5848
6574
  const pending = document.createElement("div");
5849
6575
  pending.className = "pending";
6576
+ pending.setAttribute("part", "pending");
5850
6577
  pending.setAttribute("role", "status");
5851
- pending.setAttribute("aria-label", "Assistant is thinking\u2026");
6578
+ pending.setAttribute("aria-label", this.#strings.thinking);
5852
6579
  for (let i = 0; i < 3; i += 1) {
5853
6580
  const dot = document.createElement("span");
5854
6581
  dot.className = "pending-dot";
5855
6582
  pending.appendChild(dot);
5856
6583
  }
5857
6584
  this.#pending = pending;
5858
- this.#messages.appendChild(pending);
6585
+ this.#ensureGroup().appendChild(pending);
6586
+ this.#updateEmptyState();
5859
6587
  this.#messages.scrollTop = this.#messages.scrollHeight;
5860
6588
  }
5861
6589
  /** Remove the pending indicator if shown. */
@@ -5885,9 +6613,10 @@ Use the read_attachment tool with an id to read a file's contents.`
5885
6613
  }
5886
6614
  const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
5887
6615
  const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
5888
- const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
6616
+ const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
5889
6617
  this.#toolCards.set(call.id, card);
5890
- this.#messages.appendChild(card.element);
6618
+ this.#ensureGroup().appendChild(card.element);
6619
+ this.#updateEmptyState();
5891
6620
  this.#messages.scrollTop = this.#messages.scrollHeight;
5892
6621
  return card;
5893
6622
  }
@@ -5900,130 +6629,6 @@ function defineAgUiChat() {
5900
6629
  }
5901
6630
  }
5902
6631
 
5903
- // src/dom/native_setter.ts
5904
- function prototypeSetter(proto, prop) {
5905
- const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
5906
- return setter;
5907
- }
5908
- var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
5909
- var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
5910
- var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
5911
- var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
5912
- function setNativeValue(el, value) {
5913
- if (el instanceof HTMLTextAreaElement) {
5914
- setTextareaValue.call(el, value);
5915
- } else if (el instanceof HTMLSelectElement) {
5916
- setSelectValue.call(el, value);
5917
- } else {
5918
- setInputValue.call(el, value);
5919
- }
5920
- }
5921
- function setNativeChecked(el, checked) {
5922
- setInputChecked.call(el, checked);
5923
- }
5924
-
5925
- // src/dom/animations.ts
5926
- var ACCENT = "#4f46e5";
5927
- function delay(ms) {
5928
- return new Promise((resolve) => {
5929
- setTimeout(resolve, ms);
5930
- });
5931
- }
5932
- async function typeInto(el, value, options = {}) {
5933
- const charDelayMs = options.charDelayMs ?? 35;
5934
- setNativeValue(el, "");
5935
- el.dispatchEvent(new Event("input", { bubbles: true }));
5936
- for (const char of value) {
5937
- setNativeValue(el, el.value + char);
5938
- el.dispatchEvent(new Event("input", { bubbles: true }));
5939
- if (charDelayMs > 0) {
5940
- await delay(charDelayMs);
5941
- }
5942
- }
5943
- el.dispatchEvent(new Event("change", { bubbles: true }));
5944
- }
5945
- async function highlightThenClick(el, options = {}) {
5946
- const highlightMs = options.highlightMs ?? 280;
5947
- const previousOutline = el.style.outline;
5948
- const previousOffset = el.style.outlineOffset;
5949
- el.style.outline = `2px solid ${ACCENT}`;
5950
- el.style.outlineOffset = "2px";
5951
- await delay(highlightMs);
5952
- el.style.outline = previousOutline;
5953
- el.style.outlineOffset = previousOffset;
5954
- el.click();
5955
- }
5956
- function scrollIntoCenterView(el) {
5957
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
5958
- }
5959
- async function focusWithFlash(el, options = {}) {
5960
- const flashMs = options.flashMs ?? 200;
5961
- el.focus();
5962
- const previousShadow = el.style.boxShadow;
5963
- el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
5964
- await delay(flashMs);
5965
- el.style.boxShadow = previousShadow;
5966
- }
5967
- var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
5968
- function prefersReducedMotion() {
5969
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5970
- }
5971
- function motionDelay(ms) {
5972
- if (ms <= 0 || prefersReducedMotion()) {
5973
- return Promise.resolve();
5974
- }
5975
- return delay(ms);
5976
- }
5977
- async function pressThenClick(el, options = {}) {
5978
- const pressMs = options.pressMs ?? 140;
5979
- const previousTransform = el.style.transform;
5980
- const previousTransition = el.style.transition;
5981
- const previousShadow = el.style.boxShadow;
5982
- el.style.transition = "transform 80ms ease";
5983
- el.style.transform = "scale(0.96)";
5984
- el.style.boxShadow = RING;
5985
- await motionDelay(pressMs);
5986
- el.style.transform = previousTransform;
5987
- el.style.transition = previousTransition;
5988
- el.style.boxShadow = previousShadow;
5989
- el.click();
5990
- }
5991
- function findOption(el, value) {
5992
- for (const option of Array.from(el.options)) {
5993
- if (option.value === value || option.text === value) {
5994
- return option;
5995
- }
5996
- }
5997
- return null;
5998
- }
5999
- async function selectOption(el, value, options = {}) {
6000
- const option = findOption(el, value);
6001
- if (option === null) {
6002
- throw new Error(`no <option> matching "${value}"`);
6003
- }
6004
- const highlightMs = options.highlightMs ?? 220;
6005
- const previousOutline = el.style.outline;
6006
- const previousOffset = el.style.outlineOffset;
6007
- el.style.outline = `2px solid ${ACCENT}`;
6008
- el.style.outlineOffset = "2px";
6009
- await motionDelay(highlightMs);
6010
- setNativeValue(el, option.value);
6011
- el.dispatchEvent(new Event("input", { bubbles: true }));
6012
- el.dispatchEvent(new Event("change", { bubbles: true }));
6013
- el.style.outline = previousOutline;
6014
- el.style.outlineOffset = previousOffset;
6015
- }
6016
- async function toggleControl(el, checked, options = {}) {
6017
- const flashMs = options.flashMs ?? 200;
6018
- const previousShadow = el.style.boxShadow;
6019
- el.style.boxShadow = RING;
6020
- await motionDelay(flashMs);
6021
- setNativeChecked(el, checked);
6022
- el.dispatchEvent(new Event("input", { bubbles: true }));
6023
- el.dispatchEvent(new Event("change", { bubbles: true }));
6024
- el.style.boxShadow = previousShadow;
6025
- }
6026
-
6027
6632
  // src/dom/dom_driver.ts
6028
6633
  async function fillField(el, value, options = {}) {
6029
6634
  scrollIntoCenterView(el);
@@ -6057,14 +6662,17 @@ function setControlValue(el, value) {
6057
6662
  }
6058
6663
 
6059
6664
  // src/version.ts
6060
- var VERSION = "0.6.0";
6665
+ var VERSION = "0.8.0";
6061
6666
  export {
6062
6667
  AgUiChat,
6063
6668
  AgUiClient,
6064
6669
  ClientToolRegistry,
6670
+ ConnectionLostError,
6671
+ DEFAULT_UI_STRINGS,
6065
6672
  ELEMENT_TAG,
6066
6673
  MAX_TOOL_ROUNDS,
6067
6674
  MESSAGE_ROLE,
6675
+ PAGE_ACTIONS,
6068
6676
  RemoteConversationStore,
6069
6677
  SUBMIT_EVENT,
6070
6678
  SessionStorageStore,
@@ -6079,6 +6687,7 @@ export {
6079
6687
  X_SUMMARY_KEY,
6080
6688
  clickElement,
6081
6689
  createHttpAgent,
6690
+ createPageActionTools,
6082
6691
  createPageMapContext,
6083
6692
  createRouteTools,
6084
6693
  createStateHookTools,
@@ -6088,6 +6697,7 @@ export {
6088
6697
  highlightThenClick,
6089
6698
  isDestructive,
6090
6699
  isNavigates,
6700
+ mergeUiStrings,
6091
6701
  messageAttachments,
6092
6702
  parseToolCatalog,
6093
6703
  prefersReducedMotion,