@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +140 -6
  3. package/dist/ag-ui-web-component.bundle.js +145 -47
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/core/ag_ui_chat.d.ts +16 -0
  6. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  7. package/dist/core/agui_client.d.ts +16 -0
  8. package/dist/core/agui_client.d.ts.map +1 -1
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +672 -228
  12. package/dist/index.js.map +3 -3
  13. package/dist/tools/page_action_tools.d.ts +31 -0
  14. package/dist/tools/page_action_tools.d.ts.map +1 -0
  15. package/dist/ui/attachment_tray.d.ts +3 -0
  16. package/dist/ui/attachment_tray.d.ts.map +1 -1
  17. package/dist/ui/confirmation_card.d.ts +4 -1
  18. package/dist/ui/confirmation_card.d.ts.map +1 -1
  19. package/dist/ui/relative_time.d.ts +5 -3
  20. package/dist/ui/relative_time.d.ts.map +1 -1
  21. package/dist/ui/styles.d.ts +1 -1
  22. package/dist/ui/styles.d.ts.map +1 -1
  23. package/dist/ui/thread_drawer.d.ts +7 -1
  24. package/dist/ui/thread_drawer.d.ts.map +1 -1
  25. package/dist/ui/tool_call_card.d.ts +6 -2
  26. package/dist/ui/tool_call_card.d.ts.map +1 -1
  27. package/dist/ui/ui_strings.d.ts +126 -0
  28. package/dist/ui/ui_strings.d.ts.map +1 -0
  29. package/package.json +1 -1
  30. package/src/core/ag_ui_chat.ts +213 -41
  31. package/src/core/agui_client.ts +33 -2
  32. package/src/index.ts +7 -0
  33. package/src/tools/page_action_tools.ts +130 -0
  34. package/src/ui/attachment_tray.ts +13 -7
  35. package/src/ui/confirmation_card.ts +15 -5
  36. package/src/ui/relative_time.ts +15 -8
  37. package/src/ui/styles.ts +98 -0
  38. package/src/ui/thread_drawer.ts +53 -25
  39. package/src/ui/tool_call_card.ts +40 -17
  40. package/src/ui/ui_strings.ts +208 -0
  41. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -104,6 +104,211 @@ function isNavigates(parameters) {
104
104
  return parameters[X_NAVIGATES_KEY] === true;
105
105
  }
106
106
 
107
+ // src/dom/native_setter.ts
108
+ function prototypeSetter(proto, prop) {
109
+ const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
110
+ return setter;
111
+ }
112
+ var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
113
+ var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
114
+ var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
115
+ var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
116
+ function setNativeValue(el, value) {
117
+ if (el instanceof HTMLTextAreaElement) {
118
+ setTextareaValue.call(el, value);
119
+ } else if (el instanceof HTMLSelectElement) {
120
+ setSelectValue.call(el, value);
121
+ } else {
122
+ setInputValue.call(el, value);
123
+ }
124
+ }
125
+ function setNativeChecked(el, checked) {
126
+ setInputChecked.call(el, checked);
127
+ }
128
+
129
+ // src/dom/animations.ts
130
+ var ACCENT = "#4f46e5";
131
+ function delay(ms) {
132
+ return new Promise((resolve) => {
133
+ setTimeout(resolve, ms);
134
+ });
135
+ }
136
+ async function typeInto(el, value, options = {}) {
137
+ const charDelayMs = options.charDelayMs ?? 35;
138
+ setNativeValue(el, "");
139
+ el.dispatchEvent(new Event("input", { bubbles: true }));
140
+ for (const char of value) {
141
+ setNativeValue(el, el.value + char);
142
+ el.dispatchEvent(new Event("input", { bubbles: true }));
143
+ if (charDelayMs > 0) {
144
+ await delay(charDelayMs);
145
+ }
146
+ }
147
+ el.dispatchEvent(new Event("change", { bubbles: true }));
148
+ }
149
+ async function highlightThenClick(el, options = {}) {
150
+ const highlightMs = options.highlightMs ?? 280;
151
+ const previousOutline = el.style.outline;
152
+ const previousOffset = el.style.outlineOffset;
153
+ el.style.outline = `2px solid ${ACCENT}`;
154
+ el.style.outlineOffset = "2px";
155
+ await delay(highlightMs);
156
+ el.style.outline = previousOutline;
157
+ el.style.outlineOffset = previousOffset;
158
+ el.click();
159
+ }
160
+ function scrollIntoCenterView(el) {
161
+ el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
162
+ }
163
+ async function focusWithFlash(el, options = {}) {
164
+ const flashMs = options.flashMs ?? 200;
165
+ el.focus();
166
+ const previousShadow = el.style.boxShadow;
167
+ el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
168
+ await delay(flashMs);
169
+ el.style.boxShadow = previousShadow;
170
+ }
171
+ var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
172
+ function prefersReducedMotion() {
173
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
174
+ }
175
+ function motionDelay(ms) {
176
+ if (ms <= 0 || prefersReducedMotion()) {
177
+ return Promise.resolve();
178
+ }
179
+ return delay(ms);
180
+ }
181
+ async function pressThenClick(el, options = {}) {
182
+ const pressMs = options.pressMs ?? 140;
183
+ const previousTransform = el.style.transform;
184
+ const previousTransition = el.style.transition;
185
+ const previousShadow = el.style.boxShadow;
186
+ el.style.transition = "transform 80ms ease";
187
+ el.style.transform = "scale(0.96)";
188
+ el.style.boxShadow = RING;
189
+ await motionDelay(pressMs);
190
+ el.style.transform = previousTransform;
191
+ el.style.transition = previousTransition;
192
+ el.style.boxShadow = previousShadow;
193
+ el.click();
194
+ }
195
+ function findOption(el, value) {
196
+ for (const option of Array.from(el.options)) {
197
+ if (option.value === value || option.text === value) {
198
+ return option;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ async function selectOption(el, value, options = {}) {
204
+ const option = findOption(el, value);
205
+ if (option === null) {
206
+ throw new Error(`no <option> matching "${value}"`);
207
+ }
208
+ const highlightMs = options.highlightMs ?? 220;
209
+ const previousOutline = el.style.outline;
210
+ const previousOffset = el.style.outlineOffset;
211
+ el.style.outline = `2px solid ${ACCENT}`;
212
+ el.style.outlineOffset = "2px";
213
+ await motionDelay(highlightMs);
214
+ setNativeValue(el, option.value);
215
+ el.dispatchEvent(new Event("input", { bubbles: true }));
216
+ el.dispatchEvent(new Event("change", { bubbles: true }));
217
+ el.style.outline = previousOutline;
218
+ el.style.outlineOffset = previousOffset;
219
+ }
220
+ async function toggleControl(el, checked, options = {}) {
221
+ const flashMs = options.flashMs ?? 200;
222
+ const previousShadow = el.style.boxShadow;
223
+ el.style.boxShadow = RING;
224
+ await motionDelay(flashMs);
225
+ setNativeChecked(el, checked);
226
+ el.dispatchEvent(new Event("input", { bubbles: true }));
227
+ el.dispatchEvent(new Event("change", { bubbles: true }));
228
+ el.style.boxShadow = previousShadow;
229
+ }
230
+
231
+ // src/tools/page_action_tools.ts
232
+ var PAGE_ACTIONS = {
233
+ SCROLL: "scroll",
234
+ DRAG: "drag"
235
+ };
236
+ function createPageActionTools(enabled, resolveTarget) {
237
+ const tools = [];
238
+ if (enabled.has(PAGE_ACTIONS.SCROLL)) {
239
+ tools.push(scrollTool(resolveTarget));
240
+ }
241
+ if (enabled.has(PAGE_ACTIONS.DRAG)) {
242
+ tools.push(dragTool(resolveTarget));
243
+ }
244
+ return tools;
245
+ }
246
+ function scrollTool(resolveTarget) {
247
+ return {
248
+ name: "scroll_to",
249
+ 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.",
250
+ parameters: {
251
+ type: "object",
252
+ properties: { target: { type: "string" } },
253
+ required: ["target"],
254
+ [X_SUMMARY_KEY]: "Scroll into view"
255
+ },
256
+ handler: (args) => {
257
+ const target = String(args["target"] ?? "");
258
+ if (target === "top" || target === "bottom") {
259
+ const top = target === "top" ? 0 : document.body.scrollHeight;
260
+ window.scrollTo({ top, behavior: "smooth" });
261
+ return { scrolled: true, target };
262
+ }
263
+ const element = resolveTarget(target);
264
+ if (element === null) {
265
+ throw new Error(`no element matching "${target}"`);
266
+ }
267
+ scrollIntoCenterView(element);
268
+ return { scrolled: true, target };
269
+ }
270
+ };
271
+ }
272
+ function dragTool(resolveTarget) {
273
+ return {
274
+ name: "drag_and_drop",
275
+ 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.",
276
+ parameters: {
277
+ type: "object",
278
+ properties: { from: { type: "string" }, to: { type: "string" } },
279
+ required: ["from", "to"],
280
+ [X_SUMMARY_KEY]: "Drag and drop"
281
+ },
282
+ handler: (args) => {
283
+ const fromTarget = String(args["from"] ?? "");
284
+ const toTarget = String(args["to"] ?? "");
285
+ const from = resolveTarget(fromTarget);
286
+ if (from === null) {
287
+ throw new Error(`no element matching "${fromTarget}"`);
288
+ }
289
+ const to = resolveTarget(toTarget);
290
+ if (to === null) {
291
+ throw new Error(`no element matching "${toTarget}"`);
292
+ }
293
+ dispatchDragSequence(from, to);
294
+ return { dragged: true, from: fromTarget, to: toTarget };
295
+ }
296
+ };
297
+ }
298
+ function dispatchDragSequence(from, to) {
299
+ const dataTransfer = new DataTransfer();
300
+ fire(from, "dragstart", dataTransfer);
301
+ fire(to, "dragenter", dataTransfer);
302
+ fire(to, "dragover", dataTransfer);
303
+ fire(to, "drop", dataTransfer);
304
+ fire(from, "dragend", dataTransfer);
305
+ }
306
+ function fire(target, type, dataTransfer) {
307
+ const event = new Event(type, { bubbles: true, cancelable: true });
308
+ event.dataTransfer = dataTransfer;
309
+ target.dispatchEvent(event);
310
+ }
311
+
107
312
  // src/tools/page_map.ts
108
313
  function createPageMapContext(getPageMap, autoInject) {
109
314
  if (!autoInject || getPageMap === null) {
@@ -292,15 +497,83 @@ function formatBytes(bytes) {
292
497
 
293
498
  // src/ui/attachment_tray.ts
294
499
  import { randomUUID } from "@ag-ui/client";
500
+
501
+ // src/ui/ui_strings.ts
502
+ var DEFAULT_UI_STRINGS = {
503
+ title: "Assistant",
504
+ chatHistory: "Chat history",
505
+ newChat: "New chat",
506
+ collapse: "Collapse",
507
+ expand: "Expand",
508
+ conversation: "Conversation",
509
+ thinking: "Assistant is thinking\u2026",
510
+ stopped: "\u23F9 Stopped",
511
+ connectionLost: "Connection lost",
512
+ noResult: "No result returned.",
513
+ declinedAction: "User declined the action.",
514
+ navigating: "Navigating\u2026",
515
+ skillNeeds: "\u201C{title}\u201D needs: {fields}",
516
+ message: "Message",
517
+ inputPlaceholder: "Ask anything\u2026",
518
+ send: "Send",
519
+ stop: "Stop",
520
+ attachFiles: "Attach files",
521
+ toolRunning: "running\u2026",
522
+ toolDone: "\u2713 done",
523
+ toolError: "\u26A0 error",
524
+ toolDeclined: "\u2298 declined",
525
+ resultLabel: "Result",
526
+ errorLabel: "Error",
527
+ declinedLabel: "Declined",
528
+ details: "Details",
529
+ confirmAction: "Confirm action",
530
+ confirmRun: "Run \u201C{tool}\u201D?",
531
+ confirm: "Confirm",
532
+ cancel: "Cancel",
533
+ chats: "Chats",
534
+ noConversations: "No conversations yet.",
535
+ rename: "Rename",
536
+ renameConversation: "Rename conversation",
537
+ delete: "Delete",
538
+ deleteConversation: "Delete conversation",
539
+ deletePrompt: "Delete?",
540
+ tooLarge: "Too large (max {size})",
541
+ fileTypeNotAllowed: "File type not allowed",
542
+ uploadFailed: "upload failed",
543
+ retry: "Retry",
544
+ retryUpload: "Retry upload",
545
+ remove: "Remove",
546
+ removeAttachment: "Remove attachment",
547
+ justNow: "just now",
548
+ minutesAgo: "{n}m ago",
549
+ hoursAgo: "{n}h ago",
550
+ daysAgo: "{n}d ago",
551
+ weeksAgo: "{n}w ago"
552
+ };
553
+ function mergeUiStrings(overrides) {
554
+ const merged = { ...DEFAULT_UI_STRINGS };
555
+ for (const key of Object.keys(overrides)) {
556
+ const value = overrides[key];
557
+ if (value !== void 0) {
558
+ merged[key] = value;
559
+ }
560
+ }
561
+ return merged;
562
+ }
563
+
564
+ // src/ui/attachment_tray.ts
295
565
  var AttachmentTray = class {
296
566
  /** The tray root; append above the input row. Hidden while empty. */
297
567
  element;
298
568
  #config;
569
+ #strings;
299
570
  #items = [];
300
571
  constructor(config) {
301
572
  this.#config = config;
573
+ this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
302
574
  this.element = document.createElement("div");
303
575
  this.element.className = "attachment-tray";
576
+ this.element.setAttribute("part", "attachment-tray");
304
577
  this.element.hidden = true;
305
578
  }
306
579
  /** Queue a file: reject oversize/disallowed into an error chip, else upload. */
@@ -357,10 +630,10 @@ var AttachmentTray = class {
357
630
  /** The size/type rejection reason for a file, or `null` when accepted. */
358
631
  #reject(file) {
359
632
  if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
360
- return `Too large (max ${formatBytes(this.#config.maxBytes)})`;
633
+ return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
361
634
  }
362
635
  if (!accepts(this.#config.accept, file)) {
363
- return "File type not allowed";
636
+ return this.#strings.fileTypeNotAllowed;
364
637
  }
365
638
  return null;
366
639
  }
@@ -377,7 +650,7 @@ var AttachmentTray = class {
377
650
  item.ref = ref;
378
651
  }).catch((error) => {
379
652
  item.status = ATTACHMENT_STATUS.ERROR;
380
- item.error = error instanceof Error ? error.message : "upload failed";
653
+ item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
381
654
  }).finally(() => {
382
655
  this.#render();
383
656
  this.#config.onChange?.();
@@ -423,8 +696,8 @@ var AttachmentTray = class {
423
696
  const retry = document.createElement("button");
424
697
  retry.type = "button";
425
698
  retry.className = "attachment-chip-retry";
426
- retry.title = "Retry";
427
- retry.setAttribute("aria-label", "Retry upload");
699
+ retry.title = this.#strings.retry;
700
+ retry.setAttribute("aria-label", this.#strings.retryUpload);
428
701
  retry.textContent = "\u21BB";
429
702
  retry.addEventListener("click", () => this.#upload(item));
430
703
  chip.appendChild(retry);
@@ -432,8 +705,8 @@ var AttachmentTray = class {
432
705
  const remove = document.createElement("button");
433
706
  remove.type = "button";
434
707
  remove.className = "attachment-chip-remove";
435
- remove.title = "Remove";
436
- remove.setAttribute("aria-label", "Remove attachment");
708
+ remove.title = this.#strings.remove;
709
+ remove.setAttribute("aria-label", this.#strings.removeAttachment);
437
710
  remove.textContent = "\u2715";
438
711
  remove.addEventListener("click", () => this.#remove(item));
439
712
  chip.appendChild(remove);
@@ -463,26 +736,32 @@ function actionButton(modifier, label) {
463
736
  const button = document.createElement("button");
464
737
  button.type = "button";
465
738
  button.className = `confirm-btn confirm-btn--${modifier}`;
739
+ button.setAttribute("part", `confirm-button confirm-${modifier}`);
466
740
  button.textContent = label;
467
741
  return button;
468
742
  }
469
743
  function requestConfirmation(host, request, options = {}) {
744
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
470
745
  return new Promise((resolve) => {
471
746
  const card = document.createElement("div");
472
747
  card.className = "confirm";
748
+ card.setAttribute("part", "confirm");
473
749
  card.setAttribute("data-tool-name", request.toolName);
474
750
  card.setAttribute("role", "group");
475
- card.setAttribute("aria-label", "Confirm action");
751
+ card.setAttribute("aria-label", strings.confirmAction);
476
752
  const body = document.createElement("div");
477
753
  body.className = "confirm-body";
478
- body.textContent = request.message ?? `Run \u201C${request.toolName}\u201D?`;
754
+ body.setAttribute("part", "confirm-body");
755
+ body.textContent = request.message ?? strings.confirmRun.replace("{tool}", request.toolName);
479
756
  const args = document.createElement("pre");
480
757
  args.className = "confirm-args";
758
+ args.setAttribute("part", "confirm-args");
481
759
  args.textContent = JSON.stringify(request.args, null, 2);
482
760
  const actions = document.createElement("div");
483
761
  actions.className = "confirm-actions";
484
- const cancel = actionButton("cancel", "Cancel");
485
- const confirm = actionButton("confirm", "Confirm");
762
+ actions.setAttribute("part", "confirm-actions");
763
+ const cancel = actionButton("cancel", strings.cancel);
764
+ const confirm = actionButton("confirm", strings.confirm);
486
765
  let settled = false;
487
766
  const close = (accepted) => {
488
767
  if (settled) {
@@ -3318,6 +3597,71 @@ var STYLES = `
3318
3597
  --ag-ui-radius: 0;
3319
3598
  }
3320
3599
 
3600
+ /* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
3601
+ collapses to a slim icon rail (not the floating launcher). Docked right by
3602
+ default; data-side="left" docks it left. Overlay by default \u2014 set
3603
+ --ag-ui-position: static (and place this element in your own layout) for a
3604
+ host-managed push instead. */
3605
+ :host([placement="sidebar"]) {
3606
+ --ag-ui-inset: 0 0 0 auto;
3607
+ --ag-ui-width: 420px;
3608
+ --ag-ui-height: 100vh;
3609
+ --ag-ui-max-height: 100vh;
3610
+ --ag-ui-radius: 0;
3611
+ --ag-ui-rail-width: 52px;
3612
+ transition: width 0.28s ease;
3613
+ }
3614
+
3615
+ :host([placement="sidebar"][data-side="left"]) {
3616
+ --ag-ui-inset: 0 auto 0 0;
3617
+ }
3618
+
3619
+ :host([placement="sidebar"]) .chat {
3620
+ transition: transform 0.28s ease;
3621
+ }
3622
+
3623
+ /* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
3624
+ reveal the rail. Higher specificity than the generic collapse rules, so it
3625
+ wins regardless of source order. */
3626
+ :host([placement="sidebar"][collapsed]) {
3627
+ width: var(--ag-ui-rail-width);
3628
+ height: 100vh;
3629
+ max-height: 100vh;
3630
+ bottom: 0;
3631
+ }
3632
+
3633
+ :host([placement="sidebar"][collapsed]) .chat {
3634
+ display: none;
3635
+ }
3636
+
3637
+ /* The rail is a sibling of the panel (so it survives the panel being hidden);
3638
+ shown only for a collapsed sidebar. */
3639
+ .rail {
3640
+ display: none;
3641
+ border: none;
3642
+ font: inherit;
3643
+ }
3644
+
3645
+ :host([placement="sidebar"][collapsed]) .rail {
3646
+ display: flex;
3647
+ position: absolute;
3648
+ inset: 0;
3649
+ align-items: flex-start;
3650
+ justify-content: center;
3651
+ padding-top: 16px;
3652
+ border: 1px solid var(--ag-ui-border);
3653
+ background: var(--ag-ui-header-bg);
3654
+ color: var(--ag-ui-header-fg);
3655
+ cursor: pointer;
3656
+ }
3657
+
3658
+ @media (prefers-reduced-motion: reduce) {
3659
+ :host([placement="sidebar"]),
3660
+ :host([placement="sidebar"]) .chat {
3661
+ transition: none;
3662
+ }
3663
+ }
3664
+
3321
3665
  /* Embedded: drop the floating chrome and the high z-index stacking context so
3322
3666
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
3323
3667
  :host([placement="embedded"]) {
@@ -3355,12 +3699,33 @@ var STYLES = `
3355
3699
  }
3356
3700
 
3357
3701
  .header-title {
3702
+ flex: 1;
3703
+ min-width: 0;
3358
3704
  font-weight: 600;
3359
3705
  overflow: hidden;
3360
3706
  text-overflow: ellipsis;
3361
3707
  white-space: nowrap;
3362
3708
  }
3363
3709
 
3710
+ /* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
3711
+ fallback, sized via --ag-ui-icon-size. */
3712
+ .icon-holder {
3713
+ display: inline-flex;
3714
+ align-items: center;
3715
+ justify-content: center;
3716
+ flex: none;
3717
+ width: var(--ag-ui-icon-size, 22px);
3718
+ height: var(--ag-ui-icon-size, 22px);
3719
+ line-height: 1;
3720
+ }
3721
+
3722
+ .icon-img {
3723
+ width: 100%;
3724
+ height: 100%;
3725
+ object-fit: contain;
3726
+ border-radius: var(--ag-ui-icon-radius, 4px);
3727
+ }
3728
+
3364
3729
  .header-controls {
3365
3730
  display: flex;
3366
3731
  gap: 2px;
@@ -3416,6 +3781,18 @@ var STYLES = `
3416
3781
  gap: var(--ag-ui-space);
3417
3782
  }
3418
3783
 
3784
+ /* Empty-state region (CUST-1 slot): centred while it's the only thing in the
3785
+ list, hidden as soon as a message, card, or pending indicator renders. */
3786
+ .empty {
3787
+ margin: auto;
3788
+ text-align: center;
3789
+ color: var(--ag-ui-muted);
3790
+ }
3791
+
3792
+ .empty[hidden] {
3793
+ display: none;
3794
+ }
3795
+
3419
3796
  .message {
3420
3797
  max-width: 80%;
3421
3798
  padding: var(--ag-ui-msg-pad);
@@ -4126,24 +4503,24 @@ var STYLES = `
4126
4503
  `;
4127
4504
 
4128
4505
  // src/ui/relative_time.ts
4129
- function relativeTime(timestamp, now = Date.now()) {
4506
+ function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
4130
4507
  const seconds = Math.round((now - timestamp) / 1e3);
4131
4508
  if (seconds < 60) {
4132
- return "just now";
4509
+ return strings.justNow;
4133
4510
  }
4134
4511
  const minutes = Math.round(seconds / 60);
4135
4512
  if (minutes < 60) {
4136
- return `${minutes}m ago`;
4513
+ return strings.minutesAgo.replace("{n}", String(minutes));
4137
4514
  }
4138
4515
  const hours = Math.round(minutes / 60);
4139
4516
  if (hours < 24) {
4140
- return `${hours}h ago`;
4517
+ return strings.hoursAgo.replace("{n}", String(hours));
4141
4518
  }
4142
4519
  const days = Math.round(hours / 24);
4143
4520
  if (days < 7) {
4144
- return `${days}d ago`;
4521
+ return strings.daysAgo.replace("{n}", String(days));
4145
4522
  }
4146
- return `${Math.round(days / 7)}w ago`;
4523
+ return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
4147
4524
  }
4148
4525
 
4149
4526
  // src/ui/thread_drawer.ts
@@ -4151,39 +4528,59 @@ var ThreadDrawer = class {
4151
4528
  /** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
4152
4529
  element;
4153
4530
  #callbacks;
4531
+ #panel;
4532
+ #heading;
4533
+ #newButton;
4154
4534
  #list;
4535
+ #strings;
4155
4536
  #threads = [];
4156
4537
  #activeId = "";
4157
- constructor(callbacks) {
4538
+ constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
4158
4539
  this.#callbacks = callbacks;
4540
+ this.#strings = strings;
4159
4541
  this.element = document.createElement("div");
4160
4542
  this.element.className = "drawer";
4543
+ this.element.setAttribute("part", "drawer");
4161
4544
  this.element.hidden = true;
4162
4545
  const backdrop = document.createElement("div");
4163
4546
  backdrop.className = "drawer-backdrop";
4547
+ backdrop.setAttribute("part", "drawer-backdrop");
4164
4548
  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");
4549
+ this.#panel = document.createElement("div");
4550
+ this.#panel.className = "drawer-panel";
4551
+ this.#panel.setAttribute("part", "drawer-panel");
4552
+ this.#panel.setAttribute("role", "dialog");
4553
+ this.#panel.setAttribute("aria-label", strings.chatHistory);
4169
4554
  const header = document.createElement("div");
4170
4555
  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", () => {
4556
+ header.setAttribute("part", "drawer-header");
4557
+ this.#heading = document.createElement("span");
4558
+ this.#heading.className = "drawer-title";
4559
+ this.#heading.setAttribute("part", "drawer-title");
4560
+ this.#heading.textContent = strings.chats;
4561
+ this.#newButton = document.createElement("button");
4562
+ this.#newButton.type = "button";
4563
+ this.#newButton.className = "drawer-new";
4564
+ this.#newButton.setAttribute("part", "drawer-new");
4565
+ this.#newButton.textContent = strings.newChat;
4566
+ this.#newButton.addEventListener("click", () => {
4179
4567
  this.close();
4180
4568
  this.#callbacks.onNew();
4181
4569
  });
4182
- header.append(heading, newButton);
4570
+ header.append(this.#heading, this.#newButton);
4183
4571
  this.#list = document.createElement("div");
4184
4572
  this.#list.className = "drawer-list";
4185
- panel.append(header, this.#list);
4186
- this.element.append(backdrop, panel);
4573
+ this.#list.setAttribute("part", "drawer-list");
4574
+ this.#panel.append(header, this.#list);
4575
+ this.element.append(backdrop, this.#panel);
4576
+ }
4577
+ /** Re-localize the drawer's chrome and rows (the host calls this on connect). */
4578
+ setStrings(strings) {
4579
+ this.#strings = strings;
4580
+ this.#panel.setAttribute("aria-label", strings.chatHistory);
4581
+ this.#heading.textContent = strings.chats;
4582
+ this.#newButton.textContent = strings.newChat;
4583
+ this.#renderList();
4187
4584
  }
4188
4585
  isOpen() {
4189
4586
  return !this.element.hidden;
@@ -4208,7 +4605,8 @@ var ThreadDrawer = class {
4208
4605
  if (this.#threads.length === 0) {
4209
4606
  const empty = document.createElement("div");
4210
4607
  empty.className = "drawer-empty";
4211
- empty.textContent = "No conversations yet.";
4608
+ empty.setAttribute("part", "drawer-empty");
4609
+ empty.textContent = this.#strings.noConversations;
4212
4610
  this.#list.appendChild(empty);
4213
4611
  return;
4214
4612
  }
@@ -4219,18 +4617,20 @@ var ThreadDrawer = class {
4219
4617
  #renderRow(meta) {
4220
4618
  const row = document.createElement("div");
4221
4619
  row.className = "drawer-row";
4620
+ row.setAttribute("part", "drawer-row");
4222
4621
  if (meta.threadId === this.#activeId) {
4223
4622
  row.classList.add("drawer-row--active");
4224
4623
  }
4225
4624
  const select = document.createElement("button");
4226
4625
  select.type = "button";
4227
4626
  select.className = "drawer-row-select";
4627
+ select.setAttribute("part", "drawer-row-select");
4228
4628
  const title = document.createElement("span");
4229
4629
  title.className = "drawer-row-title";
4230
4630
  title.textContent = meta.title;
4231
4631
  const time = document.createElement("span");
4232
4632
  time.className = "drawer-row-time";
4233
- time.textContent = relativeTime(meta.updatedAt);
4633
+ time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
4234
4634
  const preview = document.createElement("span");
4235
4635
  preview.className = "drawer-row-preview";
4236
4636
  preview.textContent = meta.preview;
@@ -4242,15 +4642,15 @@ var ThreadDrawer = class {
4242
4642
  const rename = document.createElement("button");
4243
4643
  rename.type = "button";
4244
4644
  rename.className = "drawer-row-rename";
4245
- rename.title = "Rename";
4246
- rename.setAttribute("aria-label", "Rename conversation");
4645
+ rename.title = this.#strings.rename;
4646
+ rename.setAttribute("aria-label", this.#strings.renameConversation);
4247
4647
  rename.textContent = "\u270E";
4248
4648
  rename.addEventListener("click", () => this.#startRename(row, meta));
4249
4649
  const remove = document.createElement("button");
4250
4650
  remove.type = "button";
4251
4651
  remove.className = "drawer-row-delete";
4252
- remove.title = "Delete";
4253
- remove.setAttribute("aria-label", "Delete conversation");
4652
+ remove.title = this.#strings.delete;
4653
+ remove.setAttribute("aria-label", this.#strings.deleteConversation);
4254
4654
  remove.textContent = "\u{1F5D1}";
4255
4655
  remove.addEventListener("click", () => this.#confirmDelete(row, meta));
4256
4656
  const actions = document.createElement("div");
@@ -4287,16 +4687,16 @@ var ThreadDrawer = class {
4287
4687
  confirm.className = "drawer-confirm";
4288
4688
  const label = document.createElement("span");
4289
4689
  label.className = "drawer-confirm-label";
4290
- label.textContent = "Delete?";
4690
+ label.textContent = this.#strings.deletePrompt;
4291
4691
  const yes = document.createElement("button");
4292
4692
  yes.type = "button";
4293
4693
  yes.className = "drawer-confirm-yes";
4294
- yes.textContent = "Delete";
4694
+ yes.textContent = this.#strings.delete;
4295
4695
  yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
4296
4696
  const no = document.createElement("button");
4297
4697
  no.type = "button";
4298
4698
  no.className = "drawer-confirm-no";
4299
- no.textContent = "Cancel";
4699
+ no.textContent = this.#strings.cancel;
4300
4700
  no.addEventListener("click", () => this.#renderList());
4301
4701
  confirm.append(label, yes, no);
4302
4702
  row.replaceChildren(confirm);
@@ -4304,73 +4704,92 @@ var ThreadDrawer = class {
4304
4704
  };
4305
4705
 
4306
4706
  // 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
- };
4707
+ function statusLabels(strings) {
4708
+ return {
4709
+ [TOOL_CALL_STATUS.PENDING]: strings.toolRunning,
4710
+ [TOOL_CALL_STATUS.DONE]: strings.toolDone,
4711
+ [TOOL_CALL_STATUS.ERROR]: strings.toolError,
4712
+ [TOOL_CALL_STATUS.DECLINED]: strings.toolDeclined
4713
+ };
4714
+ }
4715
+ function resultLabels(strings) {
4716
+ return {
4717
+ [TOOL_CALL_STATUS.DONE]: strings.resultLabel,
4718
+ [TOOL_CALL_STATUS.ERROR]: strings.errorLabel,
4719
+ [TOOL_CALL_STATUS.DECLINED]: strings.declinedLabel
4720
+ };
4721
+ }
4318
4722
  var ToolCallCard = class {
4319
4723
  /** The card's root element; append this into the message list. */
4320
4724
  element;
4321
4725
  #status;
4322
4726
  #mode;
4323
4727
  #args;
4324
- constructor(name, args, mode = TOOL_DISPLAY.FULL, summary) {
4728
+ #strings;
4729
+ #settled = false;
4730
+ constructor(name, args, mode = TOOL_DISPLAY.FULL, summary, strings = DEFAULT_UI_STRINGS) {
4325
4731
  this.#mode = mode;
4326
4732
  this.#args = args;
4733
+ this.#strings = strings;
4327
4734
  this.element = document.createElement("div");
4328
4735
  this.element.className = "tool-call";
4736
+ this.element.setAttribute("part", "tool-card");
4329
4737
  this.element.setAttribute("data-tool-name", name);
4330
4738
  this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
4331
4739
  this.element.setAttribute("data-display", mode);
4332
4740
  const head = document.createElement("div");
4333
4741
  head.className = "tool-call-head";
4742
+ head.setAttribute("part", "tool-card-head");
4334
4743
  const label = document.createElement("span");
4335
4744
  label.className = "tool-call-name";
4745
+ label.setAttribute("part", "tool-card-name");
4336
4746
  label.textContent = `\u{1F527} ${summary ?? name}`;
4337
4747
  this.#status = document.createElement("span");
4338
4748
  this.#status.className = "tool-call-status";
4339
- this.#status.textContent = STATUS_LABEL[TOOL_CALL_STATUS.PENDING];
4749
+ this.#status.setAttribute("part", "tool-card-status");
4750
+ this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
4340
4751
  head.append(label, this.#status);
4341
4752
  this.element.append(head);
4342
4753
  if (mode === TOOL_DISPLAY.FULL) {
4343
4754
  const argsEl = document.createElement("pre");
4344
4755
  argsEl.className = "tool-call-args";
4756
+ argsEl.setAttribute("part", "tool-card-args");
4345
4757
  argsEl.textContent = JSON.stringify(args, null, 2);
4346
4758
  this.element.append(argsEl);
4347
4759
  }
4348
4760
  }
4761
+ /** Whether {@link settle} has already run (so a terminal sweep can skip it). */
4762
+ get settled() {
4763
+ return this.#settled;
4764
+ }
4349
4765
  /**
4350
4766
  * Flip the status pill to ``status`` and, unless in `minimal` mode, append a
4351
4767
  * collapsed body behind a click-to-expand toggle: the result alone (`full`),
4352
4768
  * or the args + result together (`compact`).
4353
4769
  */
4354
4770
  settle(status, text2) {
4771
+ this.#settled = true;
4355
4772
  this.element.setAttribute("data-status", status);
4356
- this.#status.textContent = STATUS_LABEL[status];
4773
+ this.#status.textContent = statusLabels(this.#strings)[status];
4357
4774
  if (this.#mode === TOOL_DISPLAY.MINIMAL) {
4358
4775
  return;
4359
4776
  }
4360
4777
  const toggle = document.createElement("button");
4361
4778
  toggle.type = "button";
4362
4779
  toggle.className = "tool-call-toggle";
4780
+ toggle.setAttribute("part", "tool-card-toggle");
4363
4781
  toggle.setAttribute("aria-expanded", "false");
4364
4782
  const output = document.createElement("pre");
4365
4783
  output.className = "tool-call-result";
4784
+ output.setAttribute("part", "tool-card-result");
4366
4785
  output.hidden = true;
4367
4786
  if (this.#mode === TOOL_DISPLAY.COMPACT) {
4368
- toggle.textContent = "Details";
4787
+ toggle.textContent = this.#strings.details;
4369
4788
  output.textContent = `args: ${JSON.stringify(this.#args)}
4370
4789
 
4371
4790
  ${text2}`;
4372
4791
  } else {
4373
- toggle.textContent = RESULT_LABEL[status];
4792
+ toggle.textContent = resultLabels(this.#strings)[status];
4374
4793
  output.textContent = text2;
4375
4794
  }
4376
4795
  toggle.addEventListener("click", () => {
@@ -4384,6 +4803,12 @@ ${text2}`;
4384
4803
 
4385
4804
  // src/core/agui_client.ts
4386
4805
  import { randomUUID as randomUUID2 } from "@ag-ui/client";
4806
+ var ConnectionLostError = class extends Error {
4807
+ constructor(message) {
4808
+ super(message);
4809
+ this.name = "ConnectionLostError";
4810
+ }
4811
+ };
4387
4812
  var AgUiClient = class {
4388
4813
  #agent;
4389
4814
  #handlers;
@@ -4391,6 +4816,7 @@ var AgUiClient = class {
4391
4816
  #getContext;
4392
4817
  #executeTool;
4393
4818
  #onPersist;
4819
+ #connectionLostMessage;
4394
4820
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
4395
4821
  // a cancel between frontend-tool rounds doesn't start another round.
4396
4822
  #cancelled = false;
@@ -4402,6 +4828,7 @@ var AgUiClient = class {
4402
4828
  this.#executeTool = config.executeTool ?? null;
4403
4829
  this.#onPersist = config.onPersist ?? (() => {
4404
4830
  });
4831
+ this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
4405
4832
  }
4406
4833
  /** Whether a run is currently in flight. */
4407
4834
  get running() {
@@ -4483,14 +4910,18 @@ var AgUiClient = class {
4483
4910
  return;
4484
4911
  }
4485
4912
  const pending = [];
4913
+ const runState = { terminal: false };
4486
4914
  await this.#agent.runAgent(
4487
4915
  { tools: this.#getTools(), context: this.#getContext() },
4488
- this.#buildSubscriber(pending)
4916
+ this.#buildSubscriber(pending, runState)
4489
4917
  );
4490
4918
  this.#onPersist(this.#agent.messages);
4491
4919
  if (this.#cancelled) {
4492
4920
  return;
4493
4921
  }
4922
+ if (!runState.terminal) {
4923
+ throw new ConnectionLostError(this.#connectionLostMessage);
4924
+ }
4494
4925
  if (this.#executeTool === null || pending.length === 0) {
4495
4926
  return;
4496
4927
  }
@@ -4517,7 +4948,7 @@ var AgUiClient = class {
4517
4948
  }
4518
4949
  }
4519
4950
  }
4520
- #buildSubscriber(pending) {
4951
+ #buildSubscriber(pending, runState) {
4521
4952
  const h = this.#handlers;
4522
4953
  return {
4523
4954
  onRunInitialized() {
@@ -4542,9 +4973,11 @@ var AgUiClient = class {
4542
4973
  h.onToolResult(event.toolCallId, event.content);
4543
4974
  },
4544
4975
  onRunErrorEvent({ event }) {
4976
+ runState.terminal = true;
4545
4977
  h.onError(event.message);
4546
4978
  },
4547
4979
  onRunFinalized() {
4980
+ runState.terminal = true;
4548
4981
  h.onRunEnd();
4549
4982
  }
4550
4983
  };
@@ -4982,12 +5415,28 @@ var AgUiChat = class extends HTMLElement {
4982
5415
  * schema; this map is the seam for everything else.
4983
5416
  */
4984
5417
  toolSummaries = {};
5418
+ /**
5419
+ * Localizable UI strings — a partial override merged over the English
5420
+ * {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
5421
+ * element is appended); the `data-strings` JSON attribute is the markup
5422
+ * equivalent, and this property wins key-by-key over it.
5423
+ */
5424
+ strings = {};
5425
+ /**
5426
+ * Resolve a `scroll_to` / `drag_and_drop` target string to a host-page
5427
+ * element (or `null`). Defaults to a CSS-selector lookup; override to map
5428
+ * page-map element ids. The page-action tools are opt-in via the
5429
+ * `data-page-actions` attribute (`"scroll"` / `"drag"`).
5430
+ */
5431
+ resolvePageTarget = (target) => document.querySelector(target);
4985
5432
  /**
4986
5433
  * Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
4987
5434
  * tool name. The base layer behind {@link toolSummaries}: an explicit entry in
4988
5435
  * `toolSummaries` wins, this fills the rest. Populated once on connect.
4989
5436
  */
4990
5437
  #toolCatalog = {};
5438
+ /** The resolved string table (defaults ← `data-strings` ← `strings`). */
5439
+ #strings = DEFAULT_UI_STRINGS;
4991
5440
  #toolRegistry = new ClientToolRegistry();
4992
5441
  /** Tool-call cards awaiting execution, keyed by call id. */
4993
5442
  #toolCards = /* @__PURE__ */ new Map();
@@ -5010,6 +5459,10 @@ var AgUiChat = class extends HTMLElement {
5010
5459
  #attachButton;
5011
5460
  #fileInput;
5012
5461
  #attachSlot;
5462
+ /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
5463
+ #rail;
5464
+ /** Empty-state region at the top of the message list; hidden once anything renders. */
5465
+ #emptyWrap;
5013
5466
  /** Upload tray; created on connect only when `data-attachments-url` is set. */
5014
5467
  #attachTray = null;
5015
5468
  /** Refs attached to the message currently being sent (the context manifest). */
@@ -5046,6 +5499,8 @@ var AgUiChat = class extends HTMLElement {
5046
5499
  this.#attachButton = document.createElement("button");
5047
5500
  this.#fileInput = document.createElement("input");
5048
5501
  this.#attachSlot = document.createElement("div");
5502
+ this.#rail = document.createElement("button");
5503
+ this.#emptyWrap = document.createElement("div");
5049
5504
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
5050
5505
  this.#drawer = new ThreadDrawer({
5051
5506
  onSelect: (threadId) => {
@@ -5069,7 +5524,7 @@ var AgUiChat = class extends HTMLElement {
5069
5524
  return ["title-text"];
5070
5525
  }
5071
5526
  attributeChangedCallback(_name, _previous, value) {
5072
- this.#title.textContent = value ?? "Assistant";
5527
+ this.#title.textContent = value ?? this.#strings.title;
5073
5528
  }
5074
5529
  /** Declare a frontend tool the agent may call. */
5075
5530
  registerTool(tool) {
@@ -5115,9 +5570,25 @@ var AgUiChat = class extends HTMLElement {
5115
5570
  }
5116
5571
  ];
5117
5572
  }
5118
- /** All built-in (route + page) frontend tools. */
5573
+ /**
5574
+ * Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
5575
+ * via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
5576
+ * through {@link resolvePageTarget} so a host controls the agent's interaction
5577
+ * surface; absent attribute ⇒ no tools registered.
5578
+ */
5579
+ #pageActionTools() {
5580
+ const attr = this.getAttribute("data-page-actions");
5581
+ if (attr === null) {
5582
+ return [];
5583
+ }
5584
+ const enabled = new Set(
5585
+ attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
5586
+ );
5587
+ return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
5588
+ }
5589
+ /** All built-in (route + page + page-action) frontend tools. */
5119
5590
  #builtinTools() {
5120
- return [...this.#routeTools(), ...this.#pageTools()];
5591
+ return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
5121
5592
  }
5122
5593
  /** Resolve a tool by name: built-in tools first, then the registry. */
5123
5594
  #resolveTool(name) {
@@ -5152,10 +5623,13 @@ var AgUiChat = class extends HTMLElement {
5152
5623
  this.setAttribute("data-tool-display", value);
5153
5624
  }
5154
5625
  connectedCallback() {
5626
+ this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
5155
5627
  this.#render();
5628
+ this.#drawer.setStrings(this.#strings);
5156
5629
  if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
5157
5630
  this.setAttribute("collapsed", "");
5158
5631
  }
5632
+ this.#syncRail();
5159
5633
  this.#initSkills();
5160
5634
  void this.#fetchToolCatalog();
5161
5635
  this.#wireThreadStore();
@@ -5163,6 +5637,21 @@ var AgUiChat = class extends HTMLElement {
5163
5637
  this.#threadId = this.conversationStore.threadId();
5164
5638
  void this.#rehydrate();
5165
5639
  }
5640
+ /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
5641
+ #readStringOverrides() {
5642
+ const raw = this.getAttribute("data-strings");
5643
+ if (raw === null) {
5644
+ return {};
5645
+ }
5646
+ try {
5647
+ const parsed = JSON.parse(raw);
5648
+ if (typeof parsed === "object" && parsed !== null) {
5649
+ return parsed;
5650
+ }
5651
+ } catch {
5652
+ }
5653
+ return {};
5654
+ }
5166
5655
  /**
5167
5656
  * Enable the composer's file-upload tray when uploads are possible — either a
5168
5657
  * custom {@link uploadHandler} is set or `data-attachments-url` provides the
@@ -5180,7 +5669,8 @@ var AgUiChat = class extends HTMLElement {
5180
5669
  this.#attachTray = new AttachmentTray({
5181
5670
  upload,
5182
5671
  maxBytes: this.#attachmentMaxBytes(),
5183
- accept
5672
+ accept,
5673
+ strings: this.#strings
5184
5674
  });
5185
5675
  this.#attachSlot.appendChild(this.#attachTray.element);
5186
5676
  this.#fileInput.accept = accept;
@@ -5330,7 +5820,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5330
5820
  #applySkill(skill) {
5331
5821
  const { text: text2, missing } = fillTemplate(skill.prompt, this.skillContext());
5332
5822
  if (missing.length > 0) {
5333
- this.#skillHint.textContent = `\u201C${skill.title}\u201D needs: ${missing.join(", ")}`;
5823
+ this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
5334
5824
  this.#skillHint.hidden = false;
5335
5825
  return;
5336
5826
  }
@@ -5363,6 +5853,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5363
5853
  this.removeAttribute("collapsed");
5364
5854
  }
5365
5855
  sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
5856
+ this.#syncRail();
5366
5857
  this.dispatchEvent(
5367
5858
  new CustomEvent(TOGGLE_EVENT, {
5368
5859
  detail: { collapsed },
@@ -5396,7 +5887,8 @@ Use the read_attachment tool with an id to read a file's contents.`
5396
5887
  this.#initialMessages = [];
5397
5888
  this.#runAttachments = [];
5398
5889
  this.#attachTray?.clear();
5399
- this.#messages.replaceChildren();
5890
+ this.#messages.replaceChildren(this.#emptyWrap);
5891
+ this.#updateEmptyState();
5400
5892
  }
5401
5893
  /** Switch the active conversation to an existing thread and replay it. */
5402
5894
  async #switchThread(threadId) {
@@ -5524,12 +6016,14 @@ Use the read_attachment tool with an id to read a file's contents.`
5524
6016
  appendMessage(role, content) {
5525
6017
  const bubble = document.createElement("div");
5526
6018
  bubble.className = `message message--${role}`;
6019
+ bubble.setAttribute("part", `message message-${role}`);
5527
6020
  if (role === MESSAGE_ROLE.ASSISTANT) {
5528
6021
  bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
5529
6022
  } else {
5530
6023
  bubble.textContent = content;
5531
6024
  }
5532
6025
  this.#messages.appendChild(bubble);
6026
+ this.#updateEmptyState();
5533
6027
  this.#messages.scrollTop = this.#messages.scrollHeight;
5534
6028
  return bubble;
5535
6029
  }
@@ -5537,55 +6031,59 @@ Use the read_attachment tool with an id to read a file's contents.`
5537
6031
  const style = document.createElement("style");
5538
6032
  style.textContent = STYLES;
5539
6033
  this.#chat.className = "chat";
6034
+ this.#chat.setAttribute("part", "panel");
5540
6035
  const header = document.createElement("div");
5541
6036
  header.className = "header";
6037
+ header.setAttribute("part", "header");
5542
6038
  const title = this.#title;
5543
6039
  title.className = "header-title";
5544
- title.textContent = this.getAttribute("title-text") ?? "Assistant";
6040
+ title.setAttribute("part", "title");
6041
+ title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
6042
+ if (this.querySelector('[slot="icon"]') !== null || this.getAttribute("data-icon-url") !== null) {
6043
+ header.append(this.#iconElement("icon", "icon", null));
6044
+ }
6045
+ const headerActions = document.createElement("slot");
6046
+ headerActions.name = "header-actions";
5545
6047
  const controls = document.createElement("div");
5546
6048
  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";
6049
+ controls.setAttribute("part", "header-controls");
6050
+ const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
5553
6051
  history.addEventListener("click", () => {
5554
6052
  void this.#refreshDrawer();
5555
6053
  this.#drawer.open();
5556
6054
  });
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";
6055
+ const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
5563
6056
  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";
6057
+ const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
5570
6058
  collapse.addEventListener("click", () => this.toggleCollapsed());
5571
6059
  controls.append(history, newChat, collapse);
5572
- header.append(title, controls);
6060
+ header.append(title, headerActions, controls);
5573
6061
  this.#messages.className = "messages";
6062
+ this.#messages.setAttribute("part", "messages");
5574
6063
  this.#messages.setAttribute("role", "log");
5575
6064
  this.#messages.setAttribute("aria-live", "polite");
5576
- this.#messages.setAttribute("aria-label", "Conversation");
6065
+ this.#messages.setAttribute("aria-label", this.#strings.conversation);
6066
+ this.#emptyWrap.className = "empty";
6067
+ this.#emptyWrap.setAttribute("part", "empty");
6068
+ const emptySlot = document.createElement("slot");
6069
+ emptySlot.name = "empty";
6070
+ this.#emptyWrap.append(emptySlot);
6071
+ this.#messages.append(this.#emptyWrap);
5577
6072
  const inputRow = document.createElement("div");
5578
6073
  inputRow.className = "input-row";
6074
+ inputRow.setAttribute("part", "composer");
5579
6075
  this.#input.className = "input";
5580
- this.#input.setAttribute("aria-label", "Message");
6076
+ this.#input.setAttribute("part", "input");
6077
+ this.#input.setAttribute("aria-label", this.#strings.message);
5581
6078
  this.#input.rows = 2;
5582
- this.#input.placeholder = "Ask anything\u2026";
6079
+ this.#input.placeholder = this.#strings.inputPlaceholder;
5583
6080
  this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
5584
6081
  this.#input.addEventListener("input", () => this.#onInput());
5585
6082
  this.#send.className = "send";
5586
6083
  this.#send.type = "button";
5587
- this.#send.textContent = "Send";
5588
- this.#send.setAttribute("aria-label", "Send");
6084
+ this.#send.setAttribute("part", "send");
6085
+ this.#send.textContent = this.#strings.send;
6086
+ this.#send.setAttribute("aria-label", this.#strings.send);
5589
6087
  this.#send.dataset["state"] = "idle";
5590
6088
  this.#send.addEventListener("click", () => {
5591
6089
  if (this.#running) {
@@ -5598,9 +6096,10 @@ Use the read_attachment tool with an id to read a file's contents.`
5598
6096
  this.#skillHint.hidden = true;
5599
6097
  this.#attachButton.className = "attach-btn";
5600
6098
  this.#attachButton.type = "button";
6099
+ this.#attachButton.setAttribute("part", "attach-button");
5601
6100
  this.#attachButton.textContent = "\u{1F4CE}";
5602
- this.#attachButton.title = "Attach files";
5603
- this.#attachButton.setAttribute("aria-label", "Attach files");
6101
+ this.#attachButton.title = this.#strings.attachFiles;
6102
+ this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
5604
6103
  this.#attachButton.hidden = true;
5605
6104
  this.#attachButton.addEventListener("click", () => this.#fileInput.click());
5606
6105
  this.#fileInput.className = "attach-input";
@@ -5609,6 +6108,8 @@ Use the read_attachment tool with an id to read a file's contents.`
5609
6108
  this.#fileInput.hidden = true;
5610
6109
  this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
5611
6110
  this.#attachSlot.className = "attachment-slot";
6111
+ const footer = document.createElement("slot");
6112
+ footer.name = "footer";
5612
6113
  inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
5613
6114
  this.#chat.append(
5614
6115
  header,
@@ -5618,9 +6119,58 @@ Use the read_attachment tool with an id to read a file's contents.`
5618
6119
  this.#skillHint,
5619
6120
  this.#attachSlot,
5620
6121
  inputRow,
6122
+ footer,
5621
6123
  this.#drawer.element
5622
6124
  );
5623
- this.#root.append(style, this.#chat);
6125
+ this.#rail.className = "rail";
6126
+ this.#rail.type = "button";
6127
+ this.#rail.setAttribute("part", "launcher");
6128
+ this.#rail.setAttribute("aria-label", this.#strings.expand);
6129
+ this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
6130
+ this.#rail.addEventListener("click", () => this.setCollapsed(false));
6131
+ this.#root.append(style, this.#chat, this.#rail);
6132
+ }
6133
+ /** Build a header control button (icon glyph + localized title/aria). */
6134
+ #headerButton(modifier, label, glyph) {
6135
+ const button = document.createElement("button");
6136
+ button.type = "button";
6137
+ button.className = `header-btn header-btn--${modifier}`;
6138
+ button.setAttribute("part", `header-button ${modifier}-button`);
6139
+ button.title = label;
6140
+ button.setAttribute("aria-label", label);
6141
+ button.textContent = glyph;
6142
+ return button;
6143
+ }
6144
+ /**
6145
+ * An icon holder wrapping a `<slot>` so a host can project custom markup; with
6146
+ * a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
6147
+ */
6148
+ #iconElement(slotName, part, fallbackGlyph) {
6149
+ const holder = document.createElement("span");
6150
+ holder.className = "icon-holder";
6151
+ holder.setAttribute("part", part);
6152
+ const slot = document.createElement("slot");
6153
+ slot.name = slotName;
6154
+ const iconUrl = this.getAttribute("data-icon-url");
6155
+ if (iconUrl !== null) {
6156
+ const img = document.createElement("img");
6157
+ img.className = "icon-img";
6158
+ img.src = iconUrl;
6159
+ img.alt = "";
6160
+ slot.append(img);
6161
+ } else if (fallbackGlyph !== null) {
6162
+ slot.append(document.createTextNode(fallbackGlyph));
6163
+ }
6164
+ holder.append(slot);
6165
+ return holder;
6166
+ }
6167
+ /** Reflect the collapsed state on the rail's `aria-expanded`. */
6168
+ #syncRail() {
6169
+ this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
6170
+ }
6171
+ /** Hide the empty-state region once the message list holds anything else. */
6172
+ #updateEmptyState() {
6173
+ this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
5624
6174
  }
5625
6175
  /** Forward input changes to the skills palette and clear any stale hint. */
5626
6176
  #onInput() {
@@ -5655,7 +6205,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5655
6205
  /** Swap the composer button between Send (idle) and Stop (running). */
5656
6206
  #setRunning(running) {
5657
6207
  this.#running = running;
5658
- const label = running ? "Stop" : "Send";
6208
+ const label = running ? this.#strings.stop : this.#strings.send;
5659
6209
  this.#send.textContent = label;
5660
6210
  this.#send.setAttribute("aria-label", label);
5661
6211
  this.#send.dataset["state"] = running ? "running" : "idle";
@@ -5706,7 +6256,8 @@ Use the read_attachment tool with an id to read a file's contents.`
5706
6256
  getTools: () => this.getTools(),
5707
6257
  getContext: () => this.getContext(),
5708
6258
  executeTool: (call) => this.#executeTool(call),
5709
- onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
6259
+ onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
6260
+ connectionLostMessage: this.#strings.connectionLost
5710
6261
  });
5711
6262
  }
5712
6263
  return this.#client;
@@ -5727,7 +6278,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5727
6278
  const tool = this.#resolveTool(call.name);
5728
6279
  if (tool === null) {
5729
6280
  if (!this.#serverSettled.has(call.id)) {
5730
- card.settle(TOOL_CALL_STATUS.DONE, "No result returned.");
6281
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
5731
6282
  }
5732
6283
  return null;
5733
6284
  }
@@ -5739,13 +6290,15 @@ Use the read_attachment tool with an id to read a file's contents.`
5739
6290
  }
5740
6291
  this.#confirmAbort = new AbortController();
5741
6292
  const decision = requestConfirmation(this.#messages, request, {
5742
- signal: this.#confirmAbort.signal
6293
+ signal: this.#confirmAbort.signal,
6294
+ strings: this.#strings
5743
6295
  });
6296
+ this.#updateEmptyState();
5744
6297
  this.#messages.scrollTop = this.#messages.scrollHeight;
5745
6298
  const accepted = await decision;
5746
6299
  this.#confirmAbort = null;
5747
6300
  if (!accepted) {
5748
- const message = "User declined the action.";
6301
+ const message = this.#strings.declinedAction;
5749
6302
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
5750
6303
  this.#showPending();
5751
6304
  return { content: message };
@@ -5758,7 +6311,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5758
6311
  try {
5759
6312
  const result = await tool.handler(call.args);
5760
6313
  if (navigates) {
5761
- card.settle(TOOL_CALL_STATUS.DONE, "Navigating\u2026");
6314
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
5762
6315
  return { content: "", halt: true };
5763
6316
  }
5764
6317
  const content = JSON.stringify(result ?? null);
@@ -5824,6 +6377,11 @@ Use the read_attachment tool with an id to read a file's contents.`
5824
6377
  this.#setRunning(false);
5825
6378
  this.#streamingBubble = null;
5826
6379
  this.#runAttachments = [];
6380
+ for (const card of this.#toolCards.values()) {
6381
+ if (!card.settled) {
6382
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
6383
+ }
6384
+ }
5827
6385
  }
5828
6386
  };
5829
6387
  }
@@ -5831,9 +6389,11 @@ Use the read_attachment tool with an id to read a file's contents.`
5831
6389
  #appendStoppedNote() {
5832
6390
  const note = document.createElement("div");
5833
6391
  note.className = "stopped-note";
6392
+ note.setAttribute("part", "stopped");
5834
6393
  note.setAttribute("role", "status");
5835
- note.textContent = "\u23F9 Stopped";
6394
+ note.textContent = this.#strings.stopped;
5836
6395
  this.#messages.appendChild(note);
6396
+ this.#updateEmptyState();
5837
6397
  this.#messages.scrollTop = this.#messages.scrollHeight;
5838
6398
  }
5839
6399
  /**
@@ -5847,8 +6407,9 @@ Use the read_attachment tool with an id to read a file's contents.`
5847
6407
  }
5848
6408
  const pending = document.createElement("div");
5849
6409
  pending.className = "pending";
6410
+ pending.setAttribute("part", "pending");
5850
6411
  pending.setAttribute("role", "status");
5851
- pending.setAttribute("aria-label", "Assistant is thinking\u2026");
6412
+ pending.setAttribute("aria-label", this.#strings.thinking);
5852
6413
  for (let i = 0; i < 3; i += 1) {
5853
6414
  const dot = document.createElement("span");
5854
6415
  dot.className = "pending-dot";
@@ -5856,6 +6417,7 @@ Use the read_attachment tool with an id to read a file's contents.`
5856
6417
  }
5857
6418
  this.#pending = pending;
5858
6419
  this.#messages.appendChild(pending);
6420
+ this.#updateEmptyState();
5859
6421
  this.#messages.scrollTop = this.#messages.scrollHeight;
5860
6422
  }
5861
6423
  /** Remove the pending indicator if shown. */
@@ -5885,9 +6447,10 @@ Use the read_attachment tool with an id to read a file's contents.`
5885
6447
  }
5886
6448
  const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
5887
6449
  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);
6450
+ const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
5889
6451
  this.#toolCards.set(call.id, card);
5890
6452
  this.#messages.appendChild(card.element);
6453
+ this.#updateEmptyState();
5891
6454
  this.#messages.scrollTop = this.#messages.scrollHeight;
5892
6455
  return card;
5893
6456
  }
@@ -5900,130 +6463,6 @@ function defineAgUiChat() {
5900
6463
  }
5901
6464
  }
5902
6465
 
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
6466
  // src/dom/dom_driver.ts
6028
6467
  async function fillField(el, value, options = {}) {
6029
6468
  scrollIntoCenterView(el);
@@ -6057,14 +6496,17 @@ function setControlValue(el, value) {
6057
6496
  }
6058
6497
 
6059
6498
  // src/version.ts
6060
- var VERSION = "0.6.0";
6499
+ var VERSION = "0.7.0";
6061
6500
  export {
6062
6501
  AgUiChat,
6063
6502
  AgUiClient,
6064
6503
  ClientToolRegistry,
6504
+ ConnectionLostError,
6505
+ DEFAULT_UI_STRINGS,
6065
6506
  ELEMENT_TAG,
6066
6507
  MAX_TOOL_ROUNDS,
6067
6508
  MESSAGE_ROLE,
6509
+ PAGE_ACTIONS,
6068
6510
  RemoteConversationStore,
6069
6511
  SUBMIT_EVENT,
6070
6512
  SessionStorageStore,
@@ -6079,6 +6521,7 @@ export {
6079
6521
  X_SUMMARY_KEY,
6080
6522
  clickElement,
6081
6523
  createHttpAgent,
6524
+ createPageActionTools,
6082
6525
  createPageMapContext,
6083
6526
  createRouteTools,
6084
6527
  createStateHookTools,
@@ -6088,6 +6531,7 @@ export {
6088
6531
  highlightThenClick,
6089
6532
  isDestructive,
6090
6533
  isNavigates,
6534
+ mergeUiStrings,
6091
6535
  messageAttachments,
6092
6536
  parseToolCatalog,
6093
6537
  prefersReducedMotion,