@artooi/ag-ui-web-component 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +208 -7
  3. package/dist/ag-ui-web-component.bundle.js +268 -58
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +16 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +33 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +23 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/core/attachment.d.ts +35 -0
  12. package/dist/core/attachment.d.ts.map +1 -0
  13. package/dist/core/upload_attachment.d.ts +32 -0
  14. package/dist/core/upload_attachment.d.ts.map +1 -0
  15. package/dist/index.d.ts +5 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1247 -245
  18. package/dist/index.js.map +4 -4
  19. package/dist/tools/page_action_tools.d.ts +31 -0
  20. package/dist/tools/page_action_tools.d.ts.map +1 -0
  21. package/dist/ui/attachment_chips.d.ts +13 -0
  22. package/dist/ui/attachment_chips.d.ts.map +1 -0
  23. package/dist/ui/attachment_tray.d.ts +45 -0
  24. package/dist/ui/attachment_tray.d.ts.map +1 -0
  25. package/dist/ui/confirmation_card.d.ts +4 -1
  26. package/dist/ui/confirmation_card.d.ts.map +1 -1
  27. package/dist/ui/relative_time.d.ts +5 -3
  28. package/dist/ui/relative_time.d.ts.map +1 -1
  29. package/dist/ui/styles.d.ts +1 -1
  30. package/dist/ui/styles.d.ts.map +1 -1
  31. package/dist/ui/thread_drawer.d.ts +7 -1
  32. package/dist/ui/thread_drawer.d.ts.map +1 -1
  33. package/dist/ui/tool_call_card.d.ts +6 -2
  34. package/dist/ui/tool_call_card.d.ts.map +1 -1
  35. package/dist/ui/ui_strings.d.ts +126 -0
  36. package/dist/ui/ui_strings.d.ts.map +1 -0
  37. package/package.json +1 -1
  38. package/src/constants.ts +18 -0
  39. package/src/core/ag_ui_chat.ts +389 -51
  40. package/src/core/agui_client.ts +48 -4
  41. package/src/core/attachment.ts +39 -0
  42. package/src/core/upload_attachment.ts +113 -0
  43. package/src/index.ts +13 -0
  44. package/src/tools/page_action_tools.ts +130 -0
  45. package/src/ui/attachment_chips.ts +68 -0
  46. package/src/ui/attachment_tray.ts +243 -0
  47. package/src/ui/confirmation_card.ts +15 -5
  48. package/src/ui/relative_time.ts +15 -8
  49. package/src/ui/styles.ts +208 -0
  50. package/src/ui/thread_drawer.ts +53 -25
  51. package/src/ui/tool_call_card.ts +40 -17
  52. package/src/ui/ui_strings.ts +208 -0
  53. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -17,6 +17,12 @@ var TOOL_CALL_STATUS = {
17
17
  ERROR: "error",
18
18
  DECLINED: "declined"
19
19
  };
20
+ var ATTACHMENT_STATUS = {
21
+ UPLOADING: "uploading",
22
+ READY: "ready",
23
+ ERROR: "error"
24
+ };
25
+ var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
20
26
  var TOOL_DISPLAY = {
21
27
  MINIMAL: "minimal",
22
28
  COMPACT: "compact",
@@ -98,6 +104,211 @@ function isNavigates(parameters) {
98
104
  return parameters[X_NAVIGATES_KEY] === true;
99
105
  }
100
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
+
101
312
  // src/tools/page_map.ts
102
313
  function createPageMapContext(getPageMap, autoInject) {
103
314
  if (!autoInject || getPageMap === null) {
@@ -231,31 +442,326 @@ function createStateHookTools(hook) {
231
442
  return tools;
232
443
  }
233
444
 
445
+ // src/ui/attachment_chips.ts
446
+ function renderAttachmentChips(refs) {
447
+ const list = document.createElement("div");
448
+ list.className = "attachment-chips";
449
+ for (const ref of refs) {
450
+ list.appendChild(renderChip(ref));
451
+ }
452
+ return list;
453
+ }
454
+ function renderChip(ref) {
455
+ const chip = document.createElement("div");
456
+ chip.className = "attachment-chip attachment-chip--ready";
457
+ const icon = document.createElement("span");
458
+ icon.className = "attachment-chip-icon";
459
+ icon.textContent = iconFor(ref.mime);
460
+ icon.setAttribute("aria-hidden", "true");
461
+ const name = document.createElement("span");
462
+ name.className = "attachment-chip-name";
463
+ name.textContent = ref.name;
464
+ name.title = ref.name;
465
+ const size = document.createElement("span");
466
+ size.className = "attachment-chip-size";
467
+ size.textContent = formatBytes(ref.size);
468
+ chip.append(icon, name, size);
469
+ return chip;
470
+ }
471
+ function iconFor(mime) {
472
+ if (mime.startsWith("image/")) {
473
+ return "\u{1F5BC}";
474
+ }
475
+ if (mime === "application/pdf") {
476
+ return "\u{1F4D5}";
477
+ }
478
+ if (mime.startsWith("text/")) {
479
+ return "\u{1F4C4}";
480
+ }
481
+ return "\u{1F4CE}";
482
+ }
483
+ function formatBytes(bytes) {
484
+ if (bytes < 1024) {
485
+ return `${bytes} B`;
486
+ }
487
+ const units = ["KB", "MB", "GB"];
488
+ let value = bytes / 1024;
489
+ let unit = 0;
490
+ while (value >= 1024 && unit < units.length - 1) {
491
+ value /= 1024;
492
+ unit += 1;
493
+ }
494
+ const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
495
+ return `${rounded} ${units[unit]}`;
496
+ }
497
+
498
+ // src/ui/attachment_tray.ts
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
565
+ var AttachmentTray = class {
566
+ /** The tray root; append above the input row. Hidden while empty. */
567
+ element;
568
+ #config;
569
+ #strings;
570
+ #items = [];
571
+ constructor(config) {
572
+ this.#config = config;
573
+ this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
574
+ this.element = document.createElement("div");
575
+ this.element.className = "attachment-tray";
576
+ this.element.setAttribute("part", "attachment-tray");
577
+ this.element.hidden = true;
578
+ }
579
+ /** Queue a file: reject oversize/disallowed into an error chip, else upload. */
580
+ add(file) {
581
+ const item = {
582
+ localId: randomUUID(),
583
+ file,
584
+ status: ATTACHMENT_STATUS.UPLOADING,
585
+ progress: 0,
586
+ ref: null,
587
+ error: ""
588
+ };
589
+ this.#items.push(item);
590
+ const rejection = this.#reject(file);
591
+ if (rejection !== null) {
592
+ item.status = ATTACHMENT_STATUS.ERROR;
593
+ item.error = rejection;
594
+ this.#render();
595
+ this.#config.onChange?.();
596
+ return;
597
+ }
598
+ this.#render();
599
+ this.#config.onChange?.();
600
+ this.#upload(item);
601
+ }
602
+ /** The durable refs of every chip that finished uploading. */
603
+ readyRefs() {
604
+ const refs = [];
605
+ for (const item of this.#items) {
606
+ if (item.ref !== null) {
607
+ refs.push(item.ref);
608
+ }
609
+ }
610
+ return refs;
611
+ }
612
+ /** Whether any chip is still uploading (a send would drop nothing if false). */
613
+ hasPending() {
614
+ return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
615
+ }
616
+ /** Whether the tray holds no chips. */
617
+ isEmpty() {
618
+ return this.#items.length === 0;
619
+ }
620
+ /** Drop the settled (ready / error) chips, leaving any still uploading. */
621
+ clearReady() {
622
+ this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
623
+ this.#render();
624
+ }
625
+ /** Drop every chip (a reset / new-chat). */
626
+ clear() {
627
+ this.#items = [];
628
+ this.#render();
629
+ }
630
+ /** The size/type rejection reason for a file, or `null` when accepted. */
631
+ #reject(file) {
632
+ if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
633
+ return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
634
+ }
635
+ if (!accepts(this.#config.accept, file)) {
636
+ return this.#strings.fileTypeNotAllowed;
637
+ }
638
+ return null;
639
+ }
640
+ #upload(item) {
641
+ item.status = ATTACHMENT_STATUS.UPLOADING;
642
+ item.progress = 0;
643
+ item.error = "";
644
+ this.#render();
645
+ this.#config.upload(item.file, (fraction) => {
646
+ item.progress = fraction;
647
+ this.#render();
648
+ }).then((ref) => {
649
+ item.status = ATTACHMENT_STATUS.READY;
650
+ item.ref = ref;
651
+ }).catch((error) => {
652
+ item.status = ATTACHMENT_STATUS.ERROR;
653
+ item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
654
+ }).finally(() => {
655
+ this.#render();
656
+ this.#config.onChange?.();
657
+ });
658
+ }
659
+ #remove(item) {
660
+ this.#items = this.#items.filter((other) => other !== item);
661
+ this.#render();
662
+ this.#config.onChange?.();
663
+ }
664
+ #render() {
665
+ this.element.replaceChildren();
666
+ this.element.hidden = this.#items.length === 0;
667
+ for (const item of this.#items) {
668
+ this.element.appendChild(this.#renderChip(item));
669
+ }
670
+ }
671
+ #renderChip(item) {
672
+ const chip = document.createElement("div");
673
+ chip.className = `attachment-chip attachment-chip--${item.status}`;
674
+ const icon = document.createElement("span");
675
+ icon.className = "attachment-chip-icon";
676
+ icon.textContent = iconFor(item.file.type);
677
+ icon.setAttribute("aria-hidden", "true");
678
+ const name = document.createElement("span");
679
+ name.className = "attachment-chip-name";
680
+ name.textContent = item.file.name;
681
+ name.title = item.file.name;
682
+ const meta = document.createElement("span");
683
+ meta.className = "attachment-chip-size";
684
+ meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
685
+ chip.append(icon, name, meta);
686
+ if (item.status === ATTACHMENT_STATUS.UPLOADING) {
687
+ const bar = document.createElement("div");
688
+ bar.className = "attachment-chip-bar";
689
+ const fill = document.createElement("div");
690
+ fill.className = "attachment-chip-bar-fill";
691
+ fill.style.width = `${Math.round(item.progress * 100)}%`;
692
+ bar.appendChild(fill);
693
+ chip.appendChild(bar);
694
+ }
695
+ if (item.status === ATTACHMENT_STATUS.ERROR) {
696
+ const retry = document.createElement("button");
697
+ retry.type = "button";
698
+ retry.className = "attachment-chip-retry";
699
+ retry.title = this.#strings.retry;
700
+ retry.setAttribute("aria-label", this.#strings.retryUpload);
701
+ retry.textContent = "\u21BB";
702
+ retry.addEventListener("click", () => this.#upload(item));
703
+ chip.appendChild(retry);
704
+ }
705
+ const remove = document.createElement("button");
706
+ remove.type = "button";
707
+ remove.className = "attachment-chip-remove";
708
+ remove.title = this.#strings.remove;
709
+ remove.setAttribute("aria-label", this.#strings.removeAttachment);
710
+ remove.textContent = "\u2715";
711
+ remove.addEventListener("click", () => this.#remove(item));
712
+ chip.appendChild(remove);
713
+ return chip;
714
+ }
715
+ };
716
+ function accepts(accept, file) {
717
+ const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
718
+ if (tokens.length === 0) {
719
+ return true;
720
+ }
721
+ const mime = file.type.toLowerCase();
722
+ const name = file.name.toLowerCase();
723
+ return tokens.some((token) => {
724
+ if (token.startsWith(".")) {
725
+ return name.endsWith(token);
726
+ }
727
+ if (token.endsWith("/*")) {
728
+ return mime.startsWith(token.slice(0, -1));
729
+ }
730
+ return mime === token;
731
+ });
732
+ }
733
+
234
734
  // src/ui/confirmation_card.ts
235
735
  function actionButton(modifier, label) {
236
736
  const button = document.createElement("button");
237
737
  button.type = "button";
238
738
  button.className = `confirm-btn confirm-btn--${modifier}`;
739
+ button.setAttribute("part", `confirm-button confirm-${modifier}`);
239
740
  button.textContent = label;
240
741
  return button;
241
742
  }
242
743
  function requestConfirmation(host, request, options = {}) {
744
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
243
745
  return new Promise((resolve) => {
244
746
  const card = document.createElement("div");
245
747
  card.className = "confirm";
748
+ card.setAttribute("part", "confirm");
246
749
  card.setAttribute("data-tool-name", request.toolName);
247
750
  card.setAttribute("role", "group");
248
- card.setAttribute("aria-label", "Confirm action");
751
+ card.setAttribute("aria-label", strings.confirmAction);
249
752
  const body = document.createElement("div");
250
753
  body.className = "confirm-body";
251
- 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);
252
756
  const args = document.createElement("pre");
253
757
  args.className = "confirm-args";
758
+ args.setAttribute("part", "confirm-args");
254
759
  args.textContent = JSON.stringify(request.args, null, 2);
255
760
  const actions = document.createElement("div");
256
761
  actions.className = "confirm-actions";
257
- const cancel = actionButton("cancel", "Cancel");
258
- 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);
259
765
  let settled = false;
260
766
  const close = (accepted) => {
261
767
  if (settled) {
@@ -3091,6 +3597,71 @@ var STYLES = `
3091
3597
  --ag-ui-radius: 0;
3092
3598
  }
3093
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
+
3094
3665
  /* Embedded: drop the floating chrome and the high z-index stacking context so
3095
3666
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
3096
3667
  :host([placement="embedded"]) {
@@ -3128,12 +3699,33 @@ var STYLES = `
3128
3699
  }
3129
3700
 
3130
3701
  .header-title {
3702
+ flex: 1;
3703
+ min-width: 0;
3131
3704
  font-weight: 600;
3132
3705
  overflow: hidden;
3133
3706
  text-overflow: ellipsis;
3134
3707
  white-space: nowrap;
3135
3708
  }
3136
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
+
3137
3729
  .header-controls {
3138
3730
  display: flex;
3139
3731
  gap: 2px;
@@ -3189,6 +3781,18 @@ var STYLES = `
3189
3781
  gap: var(--ag-ui-space);
3190
3782
  }
3191
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
+
3192
3796
  .message {
3193
3797
  max-width: 80%;
3194
3798
  padding: var(--ag-ui-msg-pad);
@@ -3464,14 +4068,124 @@ var STYLES = `
3464
4068
  background: var(--ag-ui-muted);
3465
4069
  }
3466
4070
 
3467
- /* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
3468
- .stopped-note {
3469
- align-self: flex-start;
3470
- color: var(--ag-ui-muted);
3471
- font-size: 12px;
3472
- padding: 2px 4px;
3473
- }
3474
-
4071
+ /* \u2500\u2500 File attachments \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4072
+ /* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
4073
+ .attach-btn {
4074
+ border: 1px solid var(--ag-ui-border);
4075
+ border-radius: 8px;
4076
+ padding: 0 10px;
4077
+ background: var(--ag-ui-input-bg);
4078
+ color: inherit;
4079
+ font: inherit;
4080
+ cursor: pointer;
4081
+ }
4082
+
4083
+ .attach-btn:hover {
4084
+ border-color: var(--ag-ui-accent);
4085
+ }
4086
+
4087
+ .attach-input {
4088
+ display: none;
4089
+ }
4090
+
4091
+ /* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
4092
+ .attachment-slot {
4093
+ display: contents;
4094
+ }
4095
+
4096
+ .attachment-tray {
4097
+ display: flex;
4098
+ flex-wrap: wrap;
4099
+ gap: 6px;
4100
+ padding: 8px 12px 0;
4101
+ }
4102
+
4103
+ .attachment-chips {
4104
+ display: flex;
4105
+ flex-wrap: wrap;
4106
+ gap: 6px;
4107
+ margin-top: 6px;
4108
+ }
4109
+
4110
+ .attachment-chip {
4111
+ display: inline-flex;
4112
+ align-items: center;
4113
+ gap: 6px;
4114
+ max-width: 100%;
4115
+ padding: 4px 8px;
4116
+ border: 1px solid var(--ag-ui-border);
4117
+ border-radius: 999px;
4118
+ background: var(--ag-ui-assistant-bg);
4119
+ font-size: 0.85em;
4120
+ position: relative;
4121
+ }
4122
+
4123
+ .attachment-chip--error {
4124
+ border-color: var(--ag-ui-danger);
4125
+ color: var(--ag-ui-danger);
4126
+ }
4127
+
4128
+ .attachment-chip-name {
4129
+ overflow: hidden;
4130
+ text-overflow: ellipsis;
4131
+ white-space: nowrap;
4132
+ max-width: 14ch;
4133
+ }
4134
+
4135
+ .attachment-chip-size {
4136
+ color: var(--ag-ui-muted);
4137
+ white-space: nowrap;
4138
+ }
4139
+
4140
+ .attachment-chip--error .attachment-chip-size {
4141
+ color: var(--ag-ui-danger);
4142
+ }
4143
+
4144
+ /* The progress bar fills as the file uploads. */
4145
+ .attachment-chip-bar {
4146
+ flex-basis: 100%;
4147
+ height: 3px;
4148
+ border-radius: 2px;
4149
+ background: var(--ag-ui-border);
4150
+ overflow: hidden;
4151
+ }
4152
+
4153
+ .attachment-chip-bar-fill {
4154
+ height: 100%;
4155
+ background: var(--ag-ui-accent);
4156
+ transition: width 0.15s ease;
4157
+ }
4158
+
4159
+ .attachment-chip-remove,
4160
+ .attachment-chip-retry {
4161
+ border: none;
4162
+ background: none;
4163
+ color: inherit;
4164
+ cursor: pointer;
4165
+ padding: 0;
4166
+ line-height: 1;
4167
+ opacity: 0.7;
4168
+ }
4169
+
4170
+ .attachment-chip-remove:hover,
4171
+ .attachment-chip-retry:hover {
4172
+ opacity: 1;
4173
+ }
4174
+
4175
+ /* A subtle outline while a file is dragged over the shell. */
4176
+ .chat--dragover {
4177
+ outline: 2px dashed var(--ag-ui-accent);
4178
+ outline-offset: -4px;
4179
+ }
4180
+
4181
+ /* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
4182
+ .stopped-note {
4183
+ align-self: flex-start;
4184
+ color: var(--ag-ui-muted);
4185
+ font-size: 12px;
4186
+ padding: 2px 4px;
4187
+ }
4188
+
3475
4189
  /* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */
3476
4190
  .confirm {
3477
4191
  align-self: stretch;
@@ -3789,24 +4503,24 @@ var STYLES = `
3789
4503
  `;
3790
4504
 
3791
4505
  // src/ui/relative_time.ts
3792
- function relativeTime(timestamp, now = Date.now()) {
4506
+ function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
3793
4507
  const seconds = Math.round((now - timestamp) / 1e3);
3794
4508
  if (seconds < 60) {
3795
- return "just now";
4509
+ return strings.justNow;
3796
4510
  }
3797
4511
  const minutes = Math.round(seconds / 60);
3798
4512
  if (minutes < 60) {
3799
- return `${minutes}m ago`;
4513
+ return strings.minutesAgo.replace("{n}", String(minutes));
3800
4514
  }
3801
4515
  const hours = Math.round(minutes / 60);
3802
4516
  if (hours < 24) {
3803
- return `${hours}h ago`;
4517
+ return strings.hoursAgo.replace("{n}", String(hours));
3804
4518
  }
3805
4519
  const days = Math.round(hours / 24);
3806
4520
  if (days < 7) {
3807
- return `${days}d ago`;
4521
+ return strings.daysAgo.replace("{n}", String(days));
3808
4522
  }
3809
- return `${Math.round(days / 7)}w ago`;
4523
+ return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
3810
4524
  }
3811
4525
 
3812
4526
  // src/ui/thread_drawer.ts
@@ -3814,39 +4528,59 @@ var ThreadDrawer = class {
3814
4528
  /** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
3815
4529
  element;
3816
4530
  #callbacks;
4531
+ #panel;
4532
+ #heading;
4533
+ #newButton;
3817
4534
  #list;
4535
+ #strings;
3818
4536
  #threads = [];
3819
4537
  #activeId = "";
3820
- constructor(callbacks) {
4538
+ constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
3821
4539
  this.#callbacks = callbacks;
4540
+ this.#strings = strings;
3822
4541
  this.element = document.createElement("div");
3823
4542
  this.element.className = "drawer";
4543
+ this.element.setAttribute("part", "drawer");
3824
4544
  this.element.hidden = true;
3825
4545
  const backdrop = document.createElement("div");
3826
4546
  backdrop.className = "drawer-backdrop";
4547
+ backdrop.setAttribute("part", "drawer-backdrop");
3827
4548
  backdrop.addEventListener("click", () => this.close());
3828
- const panel = document.createElement("div");
3829
- panel.className = "drawer-panel";
3830
- panel.setAttribute("role", "dialog");
3831
- 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);
3832
4554
  const header = document.createElement("div");
3833
4555
  header.className = "drawer-header";
3834
- const heading = document.createElement("span");
3835
- heading.className = "drawer-title";
3836
- heading.textContent = "Chats";
3837
- const newButton = document.createElement("button");
3838
- newButton.type = "button";
3839
- newButton.className = "drawer-new";
3840
- newButton.textContent = "New chat";
3841
- 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", () => {
3842
4567
  this.close();
3843
4568
  this.#callbacks.onNew();
3844
4569
  });
3845
- header.append(heading, newButton);
4570
+ header.append(this.#heading, this.#newButton);
3846
4571
  this.#list = document.createElement("div");
3847
4572
  this.#list.className = "drawer-list";
3848
- panel.append(header, this.#list);
3849
- 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();
3850
4584
  }
3851
4585
  isOpen() {
3852
4586
  return !this.element.hidden;
@@ -3871,7 +4605,8 @@ var ThreadDrawer = class {
3871
4605
  if (this.#threads.length === 0) {
3872
4606
  const empty = document.createElement("div");
3873
4607
  empty.className = "drawer-empty";
3874
- empty.textContent = "No conversations yet.";
4608
+ empty.setAttribute("part", "drawer-empty");
4609
+ empty.textContent = this.#strings.noConversations;
3875
4610
  this.#list.appendChild(empty);
3876
4611
  return;
3877
4612
  }
@@ -3882,18 +4617,20 @@ var ThreadDrawer = class {
3882
4617
  #renderRow(meta) {
3883
4618
  const row = document.createElement("div");
3884
4619
  row.className = "drawer-row";
4620
+ row.setAttribute("part", "drawer-row");
3885
4621
  if (meta.threadId === this.#activeId) {
3886
4622
  row.classList.add("drawer-row--active");
3887
4623
  }
3888
4624
  const select = document.createElement("button");
3889
4625
  select.type = "button";
3890
4626
  select.className = "drawer-row-select";
4627
+ select.setAttribute("part", "drawer-row-select");
3891
4628
  const title = document.createElement("span");
3892
4629
  title.className = "drawer-row-title";
3893
4630
  title.textContent = meta.title;
3894
4631
  const time = document.createElement("span");
3895
4632
  time.className = "drawer-row-time";
3896
- time.textContent = relativeTime(meta.updatedAt);
4633
+ time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
3897
4634
  const preview = document.createElement("span");
3898
4635
  preview.className = "drawer-row-preview";
3899
4636
  preview.textContent = meta.preview;
@@ -3905,15 +4642,15 @@ var ThreadDrawer = class {
3905
4642
  const rename = document.createElement("button");
3906
4643
  rename.type = "button";
3907
4644
  rename.className = "drawer-row-rename";
3908
- rename.title = "Rename";
3909
- rename.setAttribute("aria-label", "Rename conversation");
4645
+ rename.title = this.#strings.rename;
4646
+ rename.setAttribute("aria-label", this.#strings.renameConversation);
3910
4647
  rename.textContent = "\u270E";
3911
4648
  rename.addEventListener("click", () => this.#startRename(row, meta));
3912
4649
  const remove = document.createElement("button");
3913
4650
  remove.type = "button";
3914
4651
  remove.className = "drawer-row-delete";
3915
- remove.title = "Delete";
3916
- remove.setAttribute("aria-label", "Delete conversation");
4652
+ remove.title = this.#strings.delete;
4653
+ remove.setAttribute("aria-label", this.#strings.deleteConversation);
3917
4654
  remove.textContent = "\u{1F5D1}";
3918
4655
  remove.addEventListener("click", () => this.#confirmDelete(row, meta));
3919
4656
  const actions = document.createElement("div");
@@ -3950,16 +4687,16 @@ var ThreadDrawer = class {
3950
4687
  confirm.className = "drawer-confirm";
3951
4688
  const label = document.createElement("span");
3952
4689
  label.className = "drawer-confirm-label";
3953
- label.textContent = "Delete?";
4690
+ label.textContent = this.#strings.deletePrompt;
3954
4691
  const yes = document.createElement("button");
3955
4692
  yes.type = "button";
3956
4693
  yes.className = "drawer-confirm-yes";
3957
- yes.textContent = "Delete";
4694
+ yes.textContent = this.#strings.delete;
3958
4695
  yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
3959
4696
  const no = document.createElement("button");
3960
4697
  no.type = "button";
3961
4698
  no.className = "drawer-confirm-no";
3962
- no.textContent = "Cancel";
4699
+ no.textContent = this.#strings.cancel;
3963
4700
  no.addEventListener("click", () => this.#renderList());
3964
4701
  confirm.append(label, yes, no);
3965
4702
  row.replaceChildren(confirm);
@@ -3967,73 +4704,92 @@ var ThreadDrawer = class {
3967
4704
  };
3968
4705
 
3969
4706
  // src/ui/tool_call_card.ts
3970
- var STATUS_LABEL = {
3971
- [TOOL_CALL_STATUS.PENDING]: "running\u2026",
3972
- [TOOL_CALL_STATUS.DONE]: "\u2713 done",
3973
- [TOOL_CALL_STATUS.ERROR]: "\u26A0 error",
3974
- [TOOL_CALL_STATUS.DECLINED]: "\u2298 declined"
3975
- };
3976
- var RESULT_LABEL = {
3977
- [TOOL_CALL_STATUS.DONE]: "Result",
3978
- [TOOL_CALL_STATUS.ERROR]: "Error",
3979
- [TOOL_CALL_STATUS.DECLINED]: "Declined"
3980
- };
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
+ }
3981
4722
  var ToolCallCard = class {
3982
4723
  /** The card's root element; append this into the message list. */
3983
4724
  element;
3984
4725
  #status;
3985
4726
  #mode;
3986
4727
  #args;
3987
- 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) {
3988
4731
  this.#mode = mode;
3989
4732
  this.#args = args;
4733
+ this.#strings = strings;
3990
4734
  this.element = document.createElement("div");
3991
4735
  this.element.className = "tool-call";
4736
+ this.element.setAttribute("part", "tool-card");
3992
4737
  this.element.setAttribute("data-tool-name", name);
3993
4738
  this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
3994
4739
  this.element.setAttribute("data-display", mode);
3995
4740
  const head = document.createElement("div");
3996
4741
  head.className = "tool-call-head";
4742
+ head.setAttribute("part", "tool-card-head");
3997
4743
  const label = document.createElement("span");
3998
4744
  label.className = "tool-call-name";
4745
+ label.setAttribute("part", "tool-card-name");
3999
4746
  label.textContent = `\u{1F527} ${summary ?? name}`;
4000
4747
  this.#status = document.createElement("span");
4001
4748
  this.#status.className = "tool-call-status";
4002
- 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];
4003
4751
  head.append(label, this.#status);
4004
4752
  this.element.append(head);
4005
4753
  if (mode === TOOL_DISPLAY.FULL) {
4006
4754
  const argsEl = document.createElement("pre");
4007
4755
  argsEl.className = "tool-call-args";
4756
+ argsEl.setAttribute("part", "tool-card-args");
4008
4757
  argsEl.textContent = JSON.stringify(args, null, 2);
4009
4758
  this.element.append(argsEl);
4010
4759
  }
4011
4760
  }
4761
+ /** Whether {@link settle} has already run (so a terminal sweep can skip it). */
4762
+ get settled() {
4763
+ return this.#settled;
4764
+ }
4012
4765
  /**
4013
4766
  * Flip the status pill to ``status`` and, unless in `minimal` mode, append a
4014
4767
  * collapsed body behind a click-to-expand toggle: the result alone (`full`),
4015
4768
  * or the args + result together (`compact`).
4016
4769
  */
4017
4770
  settle(status, text2) {
4771
+ this.#settled = true;
4018
4772
  this.element.setAttribute("data-status", status);
4019
- this.#status.textContent = STATUS_LABEL[status];
4773
+ this.#status.textContent = statusLabels(this.#strings)[status];
4020
4774
  if (this.#mode === TOOL_DISPLAY.MINIMAL) {
4021
4775
  return;
4022
4776
  }
4023
4777
  const toggle = document.createElement("button");
4024
4778
  toggle.type = "button";
4025
4779
  toggle.className = "tool-call-toggle";
4780
+ toggle.setAttribute("part", "tool-card-toggle");
4026
4781
  toggle.setAttribute("aria-expanded", "false");
4027
4782
  const output = document.createElement("pre");
4028
4783
  output.className = "tool-call-result";
4784
+ output.setAttribute("part", "tool-card-result");
4029
4785
  output.hidden = true;
4030
4786
  if (this.#mode === TOOL_DISPLAY.COMPACT) {
4031
- toggle.textContent = "Details";
4787
+ toggle.textContent = this.#strings.details;
4032
4788
  output.textContent = `args: ${JSON.stringify(this.#args)}
4033
4789
 
4034
4790
  ${text2}`;
4035
4791
  } else {
4036
- toggle.textContent = RESULT_LABEL[status];
4792
+ toggle.textContent = resultLabels(this.#strings)[status];
4037
4793
  output.textContent = text2;
4038
4794
  }
4039
4795
  toggle.addEventListener("click", () => {
@@ -4046,7 +4802,13 @@ ${text2}`;
4046
4802
  };
4047
4803
 
4048
4804
  // src/core/agui_client.ts
4049
- import { randomUUID } from "@ag-ui/client";
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
+ };
4050
4812
  var AgUiClient = class {
4051
4813
  #agent;
4052
4814
  #handlers;
@@ -4054,6 +4816,7 @@ var AgUiClient = class {
4054
4816
  #getContext;
4055
4817
  #executeTool;
4056
4818
  #onPersist;
4819
+ #connectionLostMessage;
4057
4820
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
4058
4821
  // a cancel between frontend-tool rounds doesn't start another round.
4059
4822
  #cancelled = false;
@@ -4065,6 +4828,7 @@ var AgUiClient = class {
4065
4828
  this.#executeTool = config.executeTool ?? null;
4066
4829
  this.#onPersist = config.onPersist ?? (() => {
4067
4830
  });
4831
+ this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
4068
4832
  }
4069
4833
  /** Whether a run is currently in flight. */
4070
4834
  get running() {
@@ -4080,9 +4844,18 @@ var AgUiClient = class {
4080
4844
  * When the agent calls frontend tools, this executes them and re-runs the
4081
4845
  * agent with the results, looping until the agent stops calling frontend
4082
4846
  * tools (bounded by {@link MAX_TOOL_ROUNDS}).
4847
+ *
4848
+ * `attachments` ride on the user message as a non-standard field so the
4849
+ * default client store round-trips them for history replay; the agent learns
4850
+ * the ids from the run context (the server's strict validation ignores the
4851
+ * unknown message field), then reads bytes via the `read_attachment` tool.
4083
4852
  */
4084
- async send(content) {
4085
- this.#agent.addMessage({ id: randomUUID(), role: "user", content });
4853
+ async send(content, attachments = []) {
4854
+ const message = { id: randomUUID2(), role: "user", content };
4855
+ if (attachments.length > 0) {
4856
+ message.attachments = attachments;
4857
+ }
4858
+ this.#agent.addMessage(message);
4086
4859
  this.#onPersist(this.#agent.messages);
4087
4860
  await this.#run();
4088
4861
  }
@@ -4096,7 +4869,7 @@ var AgUiClient = class {
4096
4869
  }
4097
4870
  /** Append a frontend tool result to history (used by the resume path). */
4098
4871
  addToolResult(toolCallId, content) {
4099
- this.#agent.addMessage({ id: randomUUID(), role: "tool", content, toolCallId });
4872
+ this.#agent.addMessage({ id: randomUUID2(), role: "tool", content, toolCallId });
4100
4873
  this.#onPersist(this.#agent.messages);
4101
4874
  }
4102
4875
  /**
@@ -4137,14 +4910,18 @@ var AgUiClient = class {
4137
4910
  return;
4138
4911
  }
4139
4912
  const pending = [];
4913
+ const runState = { terminal: false };
4140
4914
  await this.#agent.runAgent(
4141
4915
  { tools: this.#getTools(), context: this.#getContext() },
4142
- this.#buildSubscriber(pending)
4916
+ this.#buildSubscriber(pending, runState)
4143
4917
  );
4144
4918
  this.#onPersist(this.#agent.messages);
4145
4919
  if (this.#cancelled) {
4146
4920
  return;
4147
4921
  }
4922
+ if (!runState.terminal) {
4923
+ throw new ConnectionLostError(this.#connectionLostMessage);
4924
+ }
4148
4925
  if (this.#executeTool === null || pending.length === 0) {
4149
4926
  return;
4150
4927
  }
@@ -4158,7 +4935,7 @@ var AgUiClient = class {
4158
4935
  return;
4159
4936
  }
4160
4937
  this.#agent.addMessage({
4161
- id: randomUUID(),
4938
+ id: randomUUID2(),
4162
4939
  role: "tool",
4163
4940
  content: result.content,
4164
4941
  toolCallId: call.id
@@ -4171,7 +4948,7 @@ var AgUiClient = class {
4171
4948
  }
4172
4949
  }
4173
4950
  }
4174
- #buildSubscriber(pending) {
4951
+ #buildSubscriber(pending, runState) {
4175
4952
  const h = this.#handlers;
4176
4953
  return {
4177
4954
  onRunInitialized() {
@@ -4196,9 +4973,11 @@ var AgUiClient = class {
4196
4973
  h.onToolResult(event.toolCallId, event.content);
4197
4974
  },
4198
4975
  onRunErrorEvent({ event }) {
4976
+ runState.terminal = true;
4199
4977
  h.onError(event.message);
4200
4978
  },
4201
4979
  onRunFinalized() {
4980
+ runState.terminal = true;
4202
4981
  h.onRunEnd();
4203
4982
  }
4204
4983
  };
@@ -4208,8 +4987,14 @@ function isAbortError(error) {
4208
4987
  return error instanceof Error && error.name === "AbortError";
4209
4988
  }
4210
4989
 
4990
+ // src/core/attachment.ts
4991
+ function messageAttachments(message) {
4992
+ const refs = message.attachments;
4993
+ return Array.isArray(refs) ? refs : [];
4994
+ }
4995
+
4211
4996
  // src/core/conversation_store.ts
4212
- import { randomUUID as randomUUID2 } from "@ag-ui/client";
4997
+ import { randomUUID as randomUUID3 } from "@ag-ui/client";
4213
4998
  var THREAD_KEY = "ag-ui-chat:thread";
4214
4999
  var THREADS_KEY = "ag-ui-chat:threads";
4215
5000
  var MESSAGES_PREFIX = "ag-ui-chat:messages:";
@@ -4223,7 +5008,7 @@ var SessionStorageStore = class {
4223
5008
  if (existing !== null) {
4224
5009
  return existing;
4225
5010
  }
4226
- const id = randomUUID2();
5011
+ const id = randomUUID3();
4227
5012
  sessionStorage.setItem(THREAD_KEY, id);
4228
5013
  return id;
4229
5014
  }
@@ -4463,6 +5248,70 @@ var RemoteConversationStore = class {
4463
5248
  }
4464
5249
  };
4465
5250
 
5251
+ // src/core/upload_attachment.ts
5252
+ function uploadAttachment(file, options) {
5253
+ return new Promise((resolve, reject) => {
5254
+ const form = new FormData();
5255
+ form.append("file", file);
5256
+ const xhr = new XMLHttpRequest();
5257
+ xhr.open("POST", options.url);
5258
+ for (const [key, value] of Object.entries(options.headers ?? {})) {
5259
+ xhr.setRequestHeader(key, value);
5260
+ }
5261
+ const onProgress = options.onProgress;
5262
+ if (onProgress !== void 0) {
5263
+ xhr.upload.addEventListener("progress", (event) => {
5264
+ if (event.lengthComputable) {
5265
+ onProgress(event.total === 0 ? 0 : event.loaded / event.total);
5266
+ }
5267
+ });
5268
+ }
5269
+ xhr.addEventListener("load", () => {
5270
+ if (xhr.status >= 200 && xhr.status < 300) {
5271
+ try {
5272
+ resolve(parseRef(JSON.parse(xhr.responseText)));
5273
+ } catch {
5274
+ reject(new Error("upload returned an unreadable response"));
5275
+ }
5276
+ } else {
5277
+ reject(new Error(errorMessage(xhr)));
5278
+ }
5279
+ });
5280
+ xhr.addEventListener("error", () => reject(new Error("upload failed")));
5281
+ xhr.addEventListener("abort", () => reject(new Error("upload cancelled")));
5282
+ const signal = options.signal;
5283
+ if (signal !== void 0) {
5284
+ signal.addEventListener("abort", () => xhr.abort());
5285
+ }
5286
+ xhr.send(form);
5287
+ });
5288
+ }
5289
+ function parseRef(body) {
5290
+ if (typeof body !== "object" || body === null) {
5291
+ throw new Error("not an object");
5292
+ }
5293
+ const o = body;
5294
+ const id = o["id"];
5295
+ const name = o["name"];
5296
+ const mime = o["mime"];
5297
+ const size = o["size"];
5298
+ const url = o["url"];
5299
+ if (typeof id !== "string" || typeof name !== "string" || typeof mime !== "string" || typeof size !== "number") {
5300
+ throw new Error("missing fields");
5301
+ }
5302
+ return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
5303
+ }
5304
+ function errorMessage(xhr) {
5305
+ try {
5306
+ const body = JSON.parse(xhr.responseText);
5307
+ if (typeof body.error === "string") {
5308
+ return body.error;
5309
+ }
5310
+ } catch {
5311
+ }
5312
+ return `upload failed (${xhr.status})`;
5313
+ }
5314
+
4466
5315
  // src/core/ag_ui_chat.ts
4467
5316
  var COLLAPSED_KEY = "ag-ui-chat:collapsed";
4468
5317
  var AgUiChat = class extends HTMLElement {
@@ -4503,9 +5352,14 @@ var AgUiChat = class extends HTMLElement {
4503
5352
  ];
4504
5353
  /**
4505
5354
  * Per-run context provider. Defaults to the compact page map (when a
4506
- * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on).
5355
+ * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
5356
+ * plus a one-line manifest of the files attached to the message being sent,
5357
+ * so the agent knows which `read_attachment` ids are available.
4507
5358
  */
4508
- getContext = () => createPageMapContext(this.getPageMap, this.autoInjectPageMap);
5359
+ getContext = () => [
5360
+ ...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
5361
+ ...this.#attachmentContext()
5362
+ ];
4509
5363
  /**
4510
5364
  * Navigable routes the agent can jump to via the built-in `route.*` tools.
4511
5365
  * A compact summary also rides in each run's context.
@@ -4527,6 +5381,16 @@ var AgUiChat = class extends HTMLElement {
4527
5381
  * server-backed store for cross-tab/device durability.
4528
5382
  */
4529
5383
  conversationStore = new SessionStorageStore();
5384
+ /**
5385
+ * How attached files are uploaded. `null` (default) uses the built-in
5386
+ * multipart `POST` to `data-attachments-url`. Set a custom
5387
+ * {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
5388
+ * swap the transport (e.g. a `tus-js-client` resumable adapter or
5389
+ * direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
5390
+ * wire (refs are transport-agnostic). When set, the 📎 affordance appears even
5391
+ * with no `data-attachments-url`; the handler owns its own endpoint + headers.
5392
+ */
5393
+ uploadHandler = null;
4530
5394
  /**
4531
5395
  * Builds the tool result a navigating tool resumes with after the page
4532
5396
  * reloads. Defaults to the landed URL; a host (e.g. the admin package) can
@@ -4551,12 +5415,28 @@ var AgUiChat = class extends HTMLElement {
4551
5415
  * schema; this map is the seam for everything else.
4552
5416
  */
4553
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);
4554
5432
  /**
4555
5433
  * Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
4556
5434
  * tool name. The base layer behind {@link toolSummaries}: an explicit entry in
4557
5435
  * `toolSummaries` wins, this fills the rest. Populated once on connect.
4558
5436
  */
4559
5437
  #toolCatalog = {};
5438
+ /** The resolved string table (defaults ← `data-strings` ← `strings`). */
5439
+ #strings = DEFAULT_UI_STRINGS;
4560
5440
  #toolRegistry = new ClientToolRegistry();
4561
5441
  /** Tool-call cards awaiting execution, keyed by call id. */
4562
5442
  #toolCards = /* @__PURE__ */ new Map();
@@ -4575,6 +5455,18 @@ var AgUiChat = class extends HTMLElement {
4575
5455
  #skillsMenu;
4576
5456
  #drawer;
4577
5457
  #skillHint;
5458
+ /** File-picker button + hidden input + tray slot; the tray mounts on connect. */
5459
+ #attachButton;
5460
+ #fileInput;
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;
5466
+ /** Upload tray; created on connect only when `data-attachments-url` is set. */
5467
+ #attachTray = null;
5468
+ /** Refs attached to the message currently being sent (the context manifest). */
5469
+ #runAttachments = [];
4578
5470
  #client = null;
4579
5471
  // Whether an interaction is in flight (first onRunStart → onSettled). Drives
4580
5472
  // the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
@@ -4604,6 +5496,11 @@ var AgUiChat = class extends HTMLElement {
4604
5496
  this.#send = document.createElement("button");
4605
5497
  this.#title = document.createElement("span");
4606
5498
  this.#skillHint = document.createElement("div");
5499
+ this.#attachButton = document.createElement("button");
5500
+ this.#fileInput = document.createElement("input");
5501
+ this.#attachSlot = document.createElement("div");
5502
+ this.#rail = document.createElement("button");
5503
+ this.#emptyWrap = document.createElement("div");
4607
5504
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
4608
5505
  this.#drawer = new ThreadDrawer({
4609
5506
  onSelect: (threadId) => {
@@ -4627,7 +5524,7 @@ var AgUiChat = class extends HTMLElement {
4627
5524
  return ["title-text"];
4628
5525
  }
4629
5526
  attributeChangedCallback(_name, _previous, value) {
4630
- this.#title.textContent = value ?? "Assistant";
5527
+ this.#title.textContent = value ?? this.#strings.title;
4631
5528
  }
4632
5529
  /** Declare a frontend tool the agent may call. */
4633
5530
  registerTool(tool) {
@@ -4673,9 +5570,25 @@ var AgUiChat = class extends HTMLElement {
4673
5570
  }
4674
5571
  ];
4675
5572
  }
4676
- /** 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. */
4677
5590
  #builtinTools() {
4678
- return [...this.#routeTools(), ...this.#pageTools()];
5591
+ return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
4679
5592
  }
4680
5593
  /** Resolve a tool by name: built-in tools first, then the registry. */
4681
5594
  #resolveTool(name) {
@@ -4710,16 +5623,122 @@ var AgUiChat = class extends HTMLElement {
4710
5623
  this.setAttribute("data-tool-display", value);
4711
5624
  }
4712
5625
  connectedCallback() {
5626
+ this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
4713
5627
  this.#render();
5628
+ this.#drawer.setStrings(this.#strings);
4714
5629
  if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
4715
5630
  this.setAttribute("collapsed", "");
4716
5631
  }
5632
+ this.#syncRail();
4717
5633
  this.#initSkills();
4718
5634
  void this.#fetchToolCatalog();
4719
5635
  this.#wireThreadStore();
5636
+ this.#wireAttachments();
4720
5637
  this.#threadId = this.conversationStore.threadId();
4721
5638
  void this.#rehydrate();
4722
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
+ }
5655
+ /**
5656
+ * Enable the composer's file-upload tray when uploads are possible — either a
5657
+ * custom {@link uploadHandler} is set or `data-attachments-url` provides the
5658
+ * built-in multipart endpoint: reveal the 📎 button, wire the hidden file
5659
+ * input + drag-and-drop, and mount the tray. With neither, the affordance
5660
+ * stays hidden and the chat degrades to text-only.
5661
+ */
5662
+ #wireAttachments() {
5663
+ const url = this.getAttribute("data-attachments-url");
5664
+ const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
5665
+ if (upload === null) {
5666
+ return;
5667
+ }
5668
+ const accept = this.getAttribute("data-attachment-accept") ?? "";
5669
+ this.#attachTray = new AttachmentTray({
5670
+ upload,
5671
+ maxBytes: this.#attachmentMaxBytes(),
5672
+ accept,
5673
+ strings: this.#strings
5674
+ });
5675
+ this.#attachSlot.appendChild(this.#attachTray.element);
5676
+ this.#fileInput.accept = accept;
5677
+ this.#attachButton.hidden = false;
5678
+ this.#enableDragAndDrop();
5679
+ }
5680
+ /** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
5681
+ #defaultUploadHandler(url) {
5682
+ if (url === null) {
5683
+ return null;
5684
+ }
5685
+ return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
5686
+ }
5687
+ /** The client-side upload size cap from `data-attachment-max-bytes`. */
5688
+ #attachmentMaxBytes() {
5689
+ const attr = this.getAttribute("data-attachment-max-bytes");
5690
+ if (attr === null) {
5691
+ return DEFAULT_ATTACHMENT_MAX_BYTES;
5692
+ }
5693
+ const parsed = Number.parseInt(attr, 10);
5694
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
5695
+ }
5696
+ /** Queue every file from the picker into the tray, then reset the input. */
5697
+ #onFilesPicked() {
5698
+ const files = this.#fileInput.files;
5699
+ if (files !== null) {
5700
+ for (const file of Array.from(files)) {
5701
+ this.#attachTray?.add(file);
5702
+ }
5703
+ }
5704
+ this.#fileInput.value = "";
5705
+ }
5706
+ /** Accept files dropped anywhere on the chat shell into the tray. */
5707
+ #enableDragAndDrop() {
5708
+ this.#chat.addEventListener("dragover", (event) => {
5709
+ event.preventDefault();
5710
+ this.#chat.classList.add("chat--dragover");
5711
+ });
5712
+ this.#chat.addEventListener("dragleave", () => {
5713
+ this.#chat.classList.remove("chat--dragover");
5714
+ });
5715
+ this.#chat.addEventListener("drop", (event) => {
5716
+ event.preventDefault();
5717
+ this.#chat.classList.remove("chat--dragover");
5718
+ const files = event.dataTransfer?.files;
5719
+ if (files !== void 0) {
5720
+ for (const file of Array.from(files)) {
5721
+ this.#attachTray?.add(file);
5722
+ }
5723
+ }
5724
+ });
5725
+ }
5726
+ /** The one-line manifest of the message's attachments, for the run context. */
5727
+ #attachmentContext() {
5728
+ if (this.#runAttachments.length === 0) {
5729
+ return [];
5730
+ }
5731
+ const lines = this.#runAttachments.map(
5732
+ (ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`
5733
+ );
5734
+ return [
5735
+ {
5736
+ description: "Files the user attached to this message",
5737
+ value: `${lines.join("\n")}
5738
+ Use the read_attachment tool with an id to read a file's contents.`
5739
+ }
5740
+ ];
5741
+ }
4723
5742
  /**
4724
5743
  * When `data-threads-url` is set, route thread enumeration / load / rename /
4725
5744
  * delete through that server endpoint (wrapping the current store as the
@@ -4801,7 +5820,7 @@ var AgUiChat = class extends HTMLElement {
4801
5820
  #applySkill(skill) {
4802
5821
  const { text: text2, missing } = fillTemplate(skill.prompt, this.skillContext());
4803
5822
  if (missing.length > 0) {
4804
- 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(", "));
4805
5824
  this.#skillHint.hidden = false;
4806
5825
  return;
4807
5826
  }
@@ -4834,6 +5853,7 @@ var AgUiChat = class extends HTMLElement {
4834
5853
  this.removeAttribute("collapsed");
4835
5854
  }
4836
5855
  sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
5856
+ this.#syncRail();
4837
5857
  this.dispatchEvent(
4838
5858
  new CustomEvent(TOGGLE_EVENT, {
4839
5859
  detail: { collapsed },
@@ -4865,7 +5885,10 @@ var AgUiChat = class extends HTMLElement {
4865
5885
  this.#toolCards.clear();
4866
5886
  this.#serverSettled.clear();
4867
5887
  this.#initialMessages = [];
4868
- this.#messages.replaceChildren();
5888
+ this.#runAttachments = [];
5889
+ this.#attachTray?.clear();
5890
+ this.#messages.replaceChildren(this.#emptyWrap);
5891
+ this.#updateEmptyState();
4869
5892
  }
4870
5893
  /** Switch the active conversation to an existing thread and replay it. */
4871
5894
  async #switchThread(threadId) {
@@ -4924,8 +5947,12 @@ var AgUiChat = class extends HTMLElement {
4924
5947
  #renderHistoricMessage(message) {
4925
5948
  const text2 = typeof message.content === "string" ? message.content : "";
4926
5949
  if (message.role === MESSAGE_ROLE.USER) {
4927
- if (text2 !== "") {
4928
- this.appendMessage(MESSAGE_ROLE.USER, text2);
5950
+ const attachments = messageAttachments(message);
5951
+ if (text2 !== "" || attachments.length > 0) {
5952
+ const bubble = this.appendMessage(MESSAGE_ROLE.USER, text2);
5953
+ if (attachments.length > 0) {
5954
+ bubble.appendChild(renderAttachmentChips(attachments));
5955
+ }
4929
5956
  }
4930
5957
  return;
4931
5958
  }
@@ -4989,12 +6016,14 @@ var AgUiChat = class extends HTMLElement {
4989
6016
  appendMessage(role, content) {
4990
6017
  const bubble = document.createElement("div");
4991
6018
  bubble.className = `message message--${role}`;
6019
+ bubble.setAttribute("part", `message message-${role}`);
4992
6020
  if (role === MESSAGE_ROLE.ASSISTANT) {
4993
6021
  bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
4994
6022
  } else {
4995
6023
  bubble.textContent = content;
4996
6024
  }
4997
6025
  this.#messages.appendChild(bubble);
6026
+ this.#updateEmptyState();
4998
6027
  this.#messages.scrollTop = this.#messages.scrollHeight;
4999
6028
  return bubble;
5000
6029
  }
@@ -5002,55 +6031,59 @@ var AgUiChat = class extends HTMLElement {
5002
6031
  const style = document.createElement("style");
5003
6032
  style.textContent = STYLES;
5004
6033
  this.#chat.className = "chat";
6034
+ this.#chat.setAttribute("part", "panel");
5005
6035
  const header = document.createElement("div");
5006
6036
  header.className = "header";
6037
+ header.setAttribute("part", "header");
5007
6038
  const title = this.#title;
5008
6039
  title.className = "header-title";
5009
- 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";
5010
6047
  const controls = document.createElement("div");
5011
6048
  controls.className = "header-controls";
5012
- const history = document.createElement("button");
5013
- history.type = "button";
5014
- history.className = "header-btn header-btn--history";
5015
- history.title = "Chat history";
5016
- history.setAttribute("aria-label", "Chat history");
5017
- history.textContent = "\u2630";
6049
+ controls.setAttribute("part", "header-controls");
6050
+ const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
5018
6051
  history.addEventListener("click", () => {
5019
6052
  void this.#refreshDrawer();
5020
6053
  this.#drawer.open();
5021
6054
  });
5022
- const newChat = document.createElement("button");
5023
- newChat.type = "button";
5024
- newChat.className = "header-btn header-btn--new";
5025
- newChat.title = "New chat";
5026
- newChat.setAttribute("aria-label", "New chat");
5027
- newChat.textContent = "\u271A";
6055
+ const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
5028
6056
  newChat.addEventListener("click", () => this.newChat());
5029
- const collapse = document.createElement("button");
5030
- collapse.type = "button";
5031
- collapse.className = "header-btn header-btn--collapse";
5032
- collapse.title = "Collapse";
5033
- collapse.setAttribute("aria-label", "Collapse");
5034
- collapse.textContent = "\u2014";
6057
+ const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
5035
6058
  collapse.addEventListener("click", () => this.toggleCollapsed());
5036
6059
  controls.append(history, newChat, collapse);
5037
- header.append(title, controls);
6060
+ header.append(title, headerActions, controls);
5038
6061
  this.#messages.className = "messages";
6062
+ this.#messages.setAttribute("part", "messages");
5039
6063
  this.#messages.setAttribute("role", "log");
5040
6064
  this.#messages.setAttribute("aria-live", "polite");
5041
- 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);
5042
6072
  const inputRow = document.createElement("div");
5043
6073
  inputRow.className = "input-row";
6074
+ inputRow.setAttribute("part", "composer");
5044
6075
  this.#input.className = "input";
5045
- this.#input.setAttribute("aria-label", "Message");
6076
+ this.#input.setAttribute("part", "input");
6077
+ this.#input.setAttribute("aria-label", this.#strings.message);
5046
6078
  this.#input.rows = 2;
5047
- this.#input.placeholder = "Ask anything\u2026";
6079
+ this.#input.placeholder = this.#strings.inputPlaceholder;
5048
6080
  this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
5049
6081
  this.#input.addEventListener("input", () => this.#onInput());
5050
6082
  this.#send.className = "send";
5051
6083
  this.#send.type = "button";
5052
- this.#send.textContent = "Send";
5053
- 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);
5054
6087
  this.#send.dataset["state"] = "idle";
5055
6088
  this.#send.addEventListener("click", () => {
5056
6089
  if (this.#running) {
@@ -5061,17 +6094,83 @@ var AgUiChat = class extends HTMLElement {
5061
6094
  });
5062
6095
  this.#skillHint.className = "skill-hint";
5063
6096
  this.#skillHint.hidden = true;
5064
- inputRow.append(this.#input, this.#send);
6097
+ this.#attachButton.className = "attach-btn";
6098
+ this.#attachButton.type = "button";
6099
+ this.#attachButton.setAttribute("part", "attach-button");
6100
+ this.#attachButton.textContent = "\u{1F4CE}";
6101
+ this.#attachButton.title = this.#strings.attachFiles;
6102
+ this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
6103
+ this.#attachButton.hidden = true;
6104
+ this.#attachButton.addEventListener("click", () => this.#fileInput.click());
6105
+ this.#fileInput.className = "attach-input";
6106
+ this.#fileInput.type = "file";
6107
+ this.#fileInput.multiple = true;
6108
+ this.#fileInput.hidden = true;
6109
+ this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
6110
+ this.#attachSlot.className = "attachment-slot";
6111
+ const footer = document.createElement("slot");
6112
+ footer.name = "footer";
6113
+ inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
5065
6114
  this.#chat.append(
5066
6115
  header,
5067
6116
  this.#messages,
5068
6117
  this.#skillsMenu.palette,
5069
6118
  this.#skillsMenu.chips,
5070
6119
  this.#skillHint,
6120
+ this.#attachSlot,
5071
6121
  inputRow,
6122
+ footer,
5072
6123
  this.#drawer.element
5073
6124
  );
5074
- 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;
5075
6174
  }
5076
6175
  /** Forward input changes to the skills palette and clear any stale hint. */
5077
6176
  #onInput() {
@@ -5106,32 +6205,38 @@ var AgUiChat = class extends HTMLElement {
5106
6205
  /** Swap the composer button between Send (idle) and Stop (running). */
5107
6206
  #setRunning(running) {
5108
6207
  this.#running = running;
5109
- const label = running ? "Stop" : "Send";
6208
+ const label = running ? this.#strings.stop : this.#strings.send;
5110
6209
  this.#send.textContent = label;
5111
6210
  this.#send.setAttribute("aria-label", label);
5112
6211
  this.#send.dataset["state"] = running ? "running" : "idle";
5113
6212
  }
5114
6213
  async #submit() {
5115
6214
  const content = this.#input.value.trim();
5116
- if (content === "") {
6215
+ const attachments = this.#attachTray?.readyRefs() ?? [];
6216
+ if (content === "" && attachments.length === 0) {
5117
6217
  return;
5118
6218
  }
5119
- this.appendMessage(MESSAGE_ROLE.USER, content);
6219
+ const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
6220
+ if (attachments.length > 0) {
6221
+ bubble.appendChild(renderAttachmentChips(attachments));
6222
+ }
5120
6223
  this.#input.value = "";
6224
+ this.#attachTray?.clearReady();
6225
+ this.#runAttachments = attachments;
5121
6226
  this.dispatchEvent(
5122
6227
  new CustomEvent(SUBMIT_EVENT, {
5123
- detail: { content },
6228
+ detail: { content, attachments },
5124
6229
  bubbles: true,
5125
6230
  composed: true
5126
6231
  })
5127
6232
  );
5128
- await this.#client_send(content);
6233
+ await this.#client_send(content, attachments);
5129
6234
  }
5130
- async #client_send(content) {
6235
+ async #client_send(content, attachments) {
5131
6236
  if (this.endpoint === "") {
5132
6237
  return;
5133
6238
  }
5134
- await this.#ensureClient().send(content);
6239
+ await this.#ensureClient().send(content, attachments);
5135
6240
  }
5136
6241
  #ensureClient() {
5137
6242
  if (this.#client === null) {
@@ -5151,7 +6256,8 @@ var AgUiChat = class extends HTMLElement {
5151
6256
  getTools: () => this.getTools(),
5152
6257
  getContext: () => this.getContext(),
5153
6258
  executeTool: (call) => this.#executeTool(call),
5154
- onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
6259
+ onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
6260
+ connectionLostMessage: this.#strings.connectionLost
5155
6261
  });
5156
6262
  }
5157
6263
  return this.#client;
@@ -5172,7 +6278,7 @@ var AgUiChat = class extends HTMLElement {
5172
6278
  const tool = this.#resolveTool(call.name);
5173
6279
  if (tool === null) {
5174
6280
  if (!this.#serverSettled.has(call.id)) {
5175
- card.settle(TOOL_CALL_STATUS.DONE, "No result returned.");
6281
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
5176
6282
  }
5177
6283
  return null;
5178
6284
  }
@@ -5184,13 +6290,15 @@ var AgUiChat = class extends HTMLElement {
5184
6290
  }
5185
6291
  this.#confirmAbort = new AbortController();
5186
6292
  const decision = requestConfirmation(this.#messages, request, {
5187
- signal: this.#confirmAbort.signal
6293
+ signal: this.#confirmAbort.signal,
6294
+ strings: this.#strings
5188
6295
  });
6296
+ this.#updateEmptyState();
5189
6297
  this.#messages.scrollTop = this.#messages.scrollHeight;
5190
6298
  const accepted = await decision;
5191
6299
  this.#confirmAbort = null;
5192
6300
  if (!accepted) {
5193
- const message = "User declined the action.";
6301
+ const message = this.#strings.declinedAction;
5194
6302
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
5195
6303
  this.#showPending();
5196
6304
  return { content: message };
@@ -5203,7 +6311,7 @@ var AgUiChat = class extends HTMLElement {
5203
6311
  try {
5204
6312
  const result = await tool.handler(call.args);
5205
6313
  if (navigates) {
5206
- card.settle(TOOL_CALL_STATUS.DONE, "Navigating\u2026");
6314
+ card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
5207
6315
  return { content: "", halt: true };
5208
6316
  }
5209
6317
  const content = JSON.stringify(result ?? null);
@@ -5268,6 +6376,12 @@ var AgUiChat = class extends HTMLElement {
5268
6376
  this.#hidePending();
5269
6377
  this.#setRunning(false);
5270
6378
  this.#streamingBubble = null;
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
+ }
5271
6385
  }
5272
6386
  };
5273
6387
  }
@@ -5275,9 +6389,11 @@ var AgUiChat = class extends HTMLElement {
5275
6389
  #appendStoppedNote() {
5276
6390
  const note = document.createElement("div");
5277
6391
  note.className = "stopped-note";
6392
+ note.setAttribute("part", "stopped");
5278
6393
  note.setAttribute("role", "status");
5279
- note.textContent = "\u23F9 Stopped";
6394
+ note.textContent = this.#strings.stopped;
5280
6395
  this.#messages.appendChild(note);
6396
+ this.#updateEmptyState();
5281
6397
  this.#messages.scrollTop = this.#messages.scrollHeight;
5282
6398
  }
5283
6399
  /**
@@ -5291,8 +6407,9 @@ var AgUiChat = class extends HTMLElement {
5291
6407
  }
5292
6408
  const pending = document.createElement("div");
5293
6409
  pending.className = "pending";
6410
+ pending.setAttribute("part", "pending");
5294
6411
  pending.setAttribute("role", "status");
5295
- pending.setAttribute("aria-label", "Assistant is thinking\u2026");
6412
+ pending.setAttribute("aria-label", this.#strings.thinking);
5296
6413
  for (let i = 0; i < 3; i += 1) {
5297
6414
  const dot = document.createElement("span");
5298
6415
  dot.className = "pending-dot";
@@ -5300,6 +6417,7 @@ var AgUiChat = class extends HTMLElement {
5300
6417
  }
5301
6418
  this.#pending = pending;
5302
6419
  this.#messages.appendChild(pending);
6420
+ this.#updateEmptyState();
5303
6421
  this.#messages.scrollTop = this.#messages.scrollHeight;
5304
6422
  }
5305
6423
  /** Remove the pending indicator if shown. */
@@ -5329,9 +6447,10 @@ var AgUiChat = class extends HTMLElement {
5329
6447
  }
5330
6448
  const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
5331
6449
  const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
5332
- 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);
5333
6451
  this.#toolCards.set(call.id, card);
5334
6452
  this.#messages.appendChild(card.element);
6453
+ this.#updateEmptyState();
5335
6454
  this.#messages.scrollTop = this.#messages.scrollHeight;
5336
6455
  return card;
5337
6456
  }
@@ -5344,130 +6463,6 @@ function defineAgUiChat() {
5344
6463
  }
5345
6464
  }
5346
6465
 
5347
- // src/dom/native_setter.ts
5348
- function prototypeSetter(proto, prop) {
5349
- const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
5350
- return setter;
5351
- }
5352
- var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
5353
- var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
5354
- var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
5355
- var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
5356
- function setNativeValue(el, value) {
5357
- if (el instanceof HTMLTextAreaElement) {
5358
- setTextareaValue.call(el, value);
5359
- } else if (el instanceof HTMLSelectElement) {
5360
- setSelectValue.call(el, value);
5361
- } else {
5362
- setInputValue.call(el, value);
5363
- }
5364
- }
5365
- function setNativeChecked(el, checked) {
5366
- setInputChecked.call(el, checked);
5367
- }
5368
-
5369
- // src/dom/animations.ts
5370
- var ACCENT = "#4f46e5";
5371
- function delay(ms) {
5372
- return new Promise((resolve) => {
5373
- setTimeout(resolve, ms);
5374
- });
5375
- }
5376
- async function typeInto(el, value, options = {}) {
5377
- const charDelayMs = options.charDelayMs ?? 35;
5378
- setNativeValue(el, "");
5379
- el.dispatchEvent(new Event("input", { bubbles: true }));
5380
- for (const char of value) {
5381
- setNativeValue(el, el.value + char);
5382
- el.dispatchEvent(new Event("input", { bubbles: true }));
5383
- if (charDelayMs > 0) {
5384
- await delay(charDelayMs);
5385
- }
5386
- }
5387
- el.dispatchEvent(new Event("change", { bubbles: true }));
5388
- }
5389
- async function highlightThenClick(el, options = {}) {
5390
- const highlightMs = options.highlightMs ?? 280;
5391
- const previousOutline = el.style.outline;
5392
- const previousOffset = el.style.outlineOffset;
5393
- el.style.outline = `2px solid ${ACCENT}`;
5394
- el.style.outlineOffset = "2px";
5395
- await delay(highlightMs);
5396
- el.style.outline = previousOutline;
5397
- el.style.outlineOffset = previousOffset;
5398
- el.click();
5399
- }
5400
- function scrollIntoCenterView(el) {
5401
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
5402
- }
5403
- async function focusWithFlash(el, options = {}) {
5404
- const flashMs = options.flashMs ?? 200;
5405
- el.focus();
5406
- const previousShadow = el.style.boxShadow;
5407
- el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
5408
- await delay(flashMs);
5409
- el.style.boxShadow = previousShadow;
5410
- }
5411
- var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
5412
- function prefersReducedMotion() {
5413
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5414
- }
5415
- function motionDelay(ms) {
5416
- if (ms <= 0 || prefersReducedMotion()) {
5417
- return Promise.resolve();
5418
- }
5419
- return delay(ms);
5420
- }
5421
- async function pressThenClick(el, options = {}) {
5422
- const pressMs = options.pressMs ?? 140;
5423
- const previousTransform = el.style.transform;
5424
- const previousTransition = el.style.transition;
5425
- const previousShadow = el.style.boxShadow;
5426
- el.style.transition = "transform 80ms ease";
5427
- el.style.transform = "scale(0.96)";
5428
- el.style.boxShadow = RING;
5429
- await motionDelay(pressMs);
5430
- el.style.transform = previousTransform;
5431
- el.style.transition = previousTransition;
5432
- el.style.boxShadow = previousShadow;
5433
- el.click();
5434
- }
5435
- function findOption(el, value) {
5436
- for (const option of Array.from(el.options)) {
5437
- if (option.value === value || option.text === value) {
5438
- return option;
5439
- }
5440
- }
5441
- return null;
5442
- }
5443
- async function selectOption(el, value, options = {}) {
5444
- const option = findOption(el, value);
5445
- if (option === null) {
5446
- throw new Error(`no <option> matching "${value}"`);
5447
- }
5448
- const highlightMs = options.highlightMs ?? 220;
5449
- const previousOutline = el.style.outline;
5450
- const previousOffset = el.style.outlineOffset;
5451
- el.style.outline = `2px solid ${ACCENT}`;
5452
- el.style.outlineOffset = "2px";
5453
- await motionDelay(highlightMs);
5454
- setNativeValue(el, option.value);
5455
- el.dispatchEvent(new Event("input", { bubbles: true }));
5456
- el.dispatchEvent(new Event("change", { bubbles: true }));
5457
- el.style.outline = previousOutline;
5458
- el.style.outlineOffset = previousOffset;
5459
- }
5460
- async function toggleControl(el, checked, options = {}) {
5461
- const flashMs = options.flashMs ?? 200;
5462
- const previousShadow = el.style.boxShadow;
5463
- el.style.boxShadow = RING;
5464
- await motionDelay(flashMs);
5465
- setNativeChecked(el, checked);
5466
- el.dispatchEvent(new Event("input", { bubbles: true }));
5467
- el.dispatchEvent(new Event("change", { bubbles: true }));
5468
- el.style.boxShadow = previousShadow;
5469
- }
5470
-
5471
6466
  // src/dom/dom_driver.ts
5472
6467
  async function fillField(el, value, options = {}) {
5473
6468
  scrollIntoCenterView(el);
@@ -5501,14 +6496,17 @@ function setControlValue(el, value) {
5501
6496
  }
5502
6497
 
5503
6498
  // src/version.ts
5504
- var VERSION = "0.5.0";
6499
+ var VERSION = "0.7.0";
5505
6500
  export {
5506
6501
  AgUiChat,
5507
6502
  AgUiClient,
5508
6503
  ClientToolRegistry,
6504
+ ConnectionLostError,
6505
+ DEFAULT_UI_STRINGS,
5509
6506
  ELEMENT_TAG,
5510
6507
  MAX_TOOL_ROUNDS,
5511
6508
  MESSAGE_ROLE,
6509
+ PAGE_ACTIONS,
5512
6510
  RemoteConversationStore,
5513
6511
  SUBMIT_EVENT,
5514
6512
  SessionStorageStore,
@@ -5523,6 +6521,7 @@ export {
5523
6521
  X_SUMMARY_KEY,
5524
6522
  clickElement,
5525
6523
  createHttpAgent,
6524
+ createPageActionTools,
5526
6525
  createPageMapContext,
5527
6526
  createRouteTools,
5528
6527
  createStateHookTools,
@@ -5532,6 +6531,8 @@ export {
5532
6531
  highlightThenClick,
5533
6532
  isDestructive,
5534
6533
  isNavigates,
6534
+ mergeUiStrings,
6535
+ messageAttachments,
5535
6536
  parseToolCatalog,
5536
6537
  prefersReducedMotion,
5537
6538
  pressButton,
@@ -5547,7 +6548,8 @@ export {
5547
6548
  setNativeValue,
5548
6549
  toggleCheckbox,
5549
6550
  toggleControl,
5550
- typeInto
6551
+ typeInto,
6552
+ uploadAttachment
5551
6553
  };
5552
6554
  /*! Bundled license information:
5553
6555