@artooi/ag-ui-web-component 0.9.0 → 0.11.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 (61) hide show
  1. package/CHANGELOG.md +104 -7
  2. package/README.md +89 -7
  3. package/dist/ag-ui-web-component.bundle.js +168 -47
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/core/ag_ui_chat.d.ts +39 -1
  6. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  7. package/dist/core/agui_client.d.ts +28 -1
  8. package/dist/core/agui_client.d.ts.map +1 -1
  9. package/dist/core/attachment.d.ts +5 -0
  10. package/dist/core/attachment.d.ts.map +1 -1
  11. package/dist/core/conversation_store.d.ts +8 -0
  12. package/dist/core/conversation_store.d.ts.map +1 -1
  13. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  14. package/dist/core/upload_attachment.d.ts +8 -2
  15. package/dist/core/upload_attachment.d.ts.map +1 -1
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +887 -128
  19. package/dist/index.js.map +4 -4
  20. package/dist/ui/approval_card.d.ts +51 -0
  21. package/dist/ui/approval_card.d.ts.map +1 -0
  22. package/dist/ui/attachment_chips.d.ts.map +1 -1
  23. package/dist/ui/attachment_tray.d.ts +7 -1
  24. package/dist/ui/attachment_tray.d.ts.map +1 -1
  25. package/dist/ui/question_card.d.ts +52 -0
  26. package/dist/ui/question_card.d.ts.map +1 -0
  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/skills_menu.d.ts.map +1 -1
  30. package/dist/ui/styles.d.ts +1 -1
  31. package/dist/ui/styles.d.ts.map +1 -1
  32. package/dist/ui/thoughts_block.d.ts +2 -2
  33. package/dist/ui/thoughts_block.d.ts.map +1 -1
  34. package/dist/ui/thread_drawer.d.ts.map +1 -1
  35. package/dist/ui/tool_call_card.d.ts.map +1 -1
  36. package/dist/ui/ui_strings.d.ts +16 -0
  37. package/dist/ui/ui_strings.d.ts.map +1 -1
  38. package/dist/ui/voice_input.d.ts +9 -1
  39. package/dist/ui/voice_input.d.ts.map +1 -1
  40. package/dist/version.d.ts.map +1 -1
  41. package/package.json +4 -4
  42. package/src/core/ag_ui_chat.ts +251 -14
  43. package/src/core/agui_client.ts +95 -9
  44. package/src/core/attachment.ts +21 -1
  45. package/src/core/conversation_store.ts +84 -18
  46. package/src/core/remote_conversation_store.ts +24 -3
  47. package/src/core/upload_attachment.ts +8 -1
  48. package/src/index.ts +14 -0
  49. package/src/ui/approval_card.ts +119 -0
  50. package/src/ui/attachment_chips.ts +5 -0
  51. package/src/ui/attachment_tray.ts +50 -5
  52. package/src/ui/question_card.ts +216 -0
  53. package/src/ui/relative_time.ts +8 -3
  54. package/src/ui/skills_menu.ts +6 -0
  55. package/src/ui/styles.ts +130 -9
  56. package/src/ui/thoughts_block.ts +3 -2
  57. package/src/ui/thread_drawer.ts +94 -9
  58. package/src/ui/tool_call_card.ts +6 -0
  59. package/src/ui/ui_strings.ts +30 -0
  60. package/src/ui/voice_input.ts +21 -1
  61. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -443,62 +443,6 @@ function createStateHookTools(hook) {
443
443
  return tools;
444
444
  }
445
445
 
446
- // src/ui/attachment_chips.ts
447
- function renderAttachmentChips(refs) {
448
- const list = document.createElement("div");
449
- list.className = "attachment-chips";
450
- for (const ref of refs) {
451
- list.appendChild(renderChip(ref));
452
- }
453
- return list;
454
- }
455
- function renderChip(ref) {
456
- const chip = document.createElement("div");
457
- chip.className = "attachment-chip attachment-chip--ready";
458
- const icon = document.createElement("span");
459
- icon.className = "attachment-chip-icon";
460
- icon.textContent = iconFor(ref.mime);
461
- icon.setAttribute("aria-hidden", "true");
462
- const name = document.createElement("span");
463
- name.className = "attachment-chip-name";
464
- name.textContent = ref.name;
465
- name.title = ref.name;
466
- const size = document.createElement("span");
467
- size.className = "attachment-chip-size";
468
- size.textContent = formatBytes(ref.size);
469
- chip.append(icon, name, size);
470
- return chip;
471
- }
472
- function iconFor(mime) {
473
- if (mime.startsWith("image/")) {
474
- return "\u{1F5BC}";
475
- }
476
- if (mime === "application/pdf") {
477
- return "\u{1F4D5}";
478
- }
479
- if (mime.startsWith("text/")) {
480
- return "\u{1F4C4}";
481
- }
482
- return "\u{1F4CE}";
483
- }
484
- function formatBytes(bytes) {
485
- if (bytes < 1024) {
486
- return `${bytes} B`;
487
- }
488
- const units = ["KB", "MB", "GB"];
489
- let value = bytes / 1024;
490
- let unit = 0;
491
- while (value >= 1024 && unit < units.length - 1) {
492
- value /= 1024;
493
- unit += 1;
494
- }
495
- const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
496
- return `${rounded} ${units[unit]}`;
497
- }
498
-
499
- // src/ui/attachment_tray.ts
500
- import { randomUUID } from "@ag-ui/client";
501
-
502
446
  // src/ui/ui_strings.ts
503
447
  var DEFAULT_UI_STRINGS = {
504
448
  title: "Assistant",
@@ -537,6 +481,14 @@ var DEFAULT_UI_STRINGS = {
537
481
  confirmRun: "Run \u201C{tool}\u201D?",
538
482
  confirm: "Confirm",
539
483
  cancel: "Cancel",
484
+ approveAction: "Approve action",
485
+ approvalPrompt: "Approve this action?",
486
+ approve: "Approve",
487
+ deny: "Deny",
488
+ askUserAction: "Question",
489
+ otherOption: "Other\u2026",
490
+ answerPlaceholder: "Type your answer\u2026",
491
+ submit: "Submit",
540
492
  chats: "Chats",
541
493
  noConversations: "No conversations yet.",
542
494
  rename: "Rename",
@@ -568,7 +520,120 @@ function mergeUiStrings(overrides) {
568
520
  return merged;
569
521
  }
570
522
 
523
+ // src/ui/approval_card.ts
524
+ function actionButton(modifier, label) {
525
+ const button = document.createElement("button");
526
+ button.type = "button";
527
+ button.className = `approval-btn approval-btn--${modifier}`;
528
+ button.setAttribute("part", `approval-button approval-${modifier}`);
529
+ button.textContent = label;
530
+ return button;
531
+ }
532
+ function requestApproval(host, request, options = {}) {
533
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
534
+ return new Promise((resolve) => {
535
+ const card = document.createElement("div");
536
+ card.className = "approval";
537
+ card.setAttribute("part", "approval");
538
+ if (request.toolName !== void 0) {
539
+ card.setAttribute("data-tool-name", request.toolName);
540
+ }
541
+ card.setAttribute("role", "group");
542
+ card.setAttribute("aria-label", strings.approveAction);
543
+ const body = document.createElement("div");
544
+ body.className = "approval-body";
545
+ body.setAttribute("part", "approval-body");
546
+ body.textContent = request.message ?? strings.approvalPrompt;
547
+ const actions = document.createElement("div");
548
+ actions.className = "approval-actions";
549
+ actions.setAttribute("part", "approval-actions");
550
+ const deny = actionButton("deny", strings.deny);
551
+ const approve = actionButton("approve", strings.approve);
552
+ let settled = false;
553
+ const close = (approved) => {
554
+ if (settled) {
555
+ return;
556
+ }
557
+ settled = true;
558
+ deny.disabled = true;
559
+ approve.disabled = true;
560
+ card.setAttribute("data-resolved", approved ? "approved" : "denied");
561
+ resolve(approved);
562
+ };
563
+ deny.addEventListener("click", () => close(false));
564
+ approve.addEventListener("click", () => close(true));
565
+ options.signal?.addEventListener("abort", () => close(false), { once: true });
566
+ actions.append(deny, approve);
567
+ card.append(body, actions);
568
+ host.appendChild(card);
569
+ if (options.signal?.aborted === true) {
570
+ close(false);
571
+ return;
572
+ }
573
+ approve.focus();
574
+ });
575
+ }
576
+
577
+ // src/ui/attachment_chips.ts
578
+ function renderAttachmentChips(refs) {
579
+ const list = document.createElement("div");
580
+ list.className = "attachment-chips";
581
+ list.setAttribute("part", "attachment-chips");
582
+ for (const ref of refs) {
583
+ list.appendChild(renderChip(ref));
584
+ }
585
+ return list;
586
+ }
587
+ function renderChip(ref) {
588
+ const chip = document.createElement("div");
589
+ chip.className = "attachment-chip attachment-chip--ready";
590
+ chip.setAttribute("part", "attachment-chip");
591
+ const icon = document.createElement("span");
592
+ icon.className = "attachment-chip-icon";
593
+ icon.setAttribute("part", "attachment-chip-icon");
594
+ icon.textContent = iconFor(ref.mime);
595
+ icon.setAttribute("aria-hidden", "true");
596
+ const name = document.createElement("span");
597
+ name.className = "attachment-chip-name";
598
+ name.setAttribute("part", "attachment-chip-name");
599
+ name.textContent = ref.name;
600
+ name.title = ref.name;
601
+ const size = document.createElement("span");
602
+ size.className = "attachment-chip-size";
603
+ size.setAttribute("part", "attachment-chip-size");
604
+ size.textContent = formatBytes(ref.size);
605
+ chip.append(icon, name, size);
606
+ return chip;
607
+ }
608
+ function iconFor(mime) {
609
+ if (mime.startsWith("image/")) {
610
+ return "\u{1F5BC}";
611
+ }
612
+ if (mime === "application/pdf") {
613
+ return "\u{1F4D5}";
614
+ }
615
+ if (mime.startsWith("text/")) {
616
+ return "\u{1F4C4}";
617
+ }
618
+ return "\u{1F4CE}";
619
+ }
620
+ function formatBytes(bytes) {
621
+ if (bytes < 1024) {
622
+ return `${bytes} B`;
623
+ }
624
+ const units = ["KB", "MB", "GB"];
625
+ let value = bytes / 1024;
626
+ let unit = 0;
627
+ while (value >= 1024 && unit < units.length - 1) {
628
+ value /= 1024;
629
+ unit += 1;
630
+ }
631
+ const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
632
+ return `${rounded} ${units[unit]}`;
633
+ }
634
+
571
635
  // src/ui/attachment_tray.ts
636
+ import { randomUUID } from "@ag-ui/client";
572
637
  var AttachmentTray = class {
573
638
  /** The tray root; append above the input row. Hidden while empty. */
574
639
  element;
@@ -591,7 +656,8 @@ var AttachmentTray = class {
591
656
  status: ATTACHMENT_STATUS.UPLOADING,
592
657
  progress: 0,
593
658
  ref: null,
594
- error: ""
659
+ error: "",
660
+ controller: null
595
661
  };
596
662
  this.#items.push(item);
597
663
  const rejection = this.#reject(file);
@@ -629,11 +695,24 @@ var AttachmentTray = class {
629
695
  this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
630
696
  this.#render();
631
697
  }
632
- /** Drop every chip (a reset / new-chat). */
698
+ /** Drop every chip (a reset / new-chat), aborting any in-flight upload. */
633
699
  clear() {
700
+ for (const item of this.#items) {
701
+ item.controller?.abort();
702
+ }
634
703
  this.#items = [];
635
704
  this.#render();
636
705
  }
706
+ /**
707
+ * Abort every in-flight upload without touching the rendered chips — the
708
+ * teardown path when the host element is removed mid-upload, so a cancelled
709
+ * transfer doesn't orphan a server-side file.
710
+ */
711
+ dispose() {
712
+ for (const item of this.#items) {
713
+ item.controller?.abort();
714
+ }
715
+ }
637
716
  /** The size/type rejection reason for a file, or `null` when accepted. */
638
717
  #reject(file) {
639
718
  if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
@@ -645,25 +724,41 @@ var AttachmentTray = class {
645
724
  return null;
646
725
  }
647
726
  #upload(item) {
727
+ const rejection = this.#reject(item.file);
728
+ if (rejection !== null) {
729
+ item.status = ATTACHMENT_STATUS.ERROR;
730
+ item.error = rejection;
731
+ this.#render();
732
+ this.#config.onChange?.();
733
+ return;
734
+ }
648
735
  item.status = ATTACHMENT_STATUS.UPLOADING;
649
736
  item.progress = 0;
650
737
  item.error = "";
738
+ const controller = new AbortController();
739
+ item.controller = controller;
651
740
  this.#render();
652
- this.#config.upload(item.file, (fraction) => {
653
- item.progress = fraction;
654
- this.#render();
655
- }).then((ref) => {
741
+ this.#config.upload(
742
+ item.file,
743
+ (fraction) => {
744
+ item.progress = fraction;
745
+ this.#render();
746
+ },
747
+ controller.signal
748
+ ).then((ref) => {
656
749
  item.status = ATTACHMENT_STATUS.READY;
657
750
  item.ref = ref;
658
751
  }).catch((error) => {
659
752
  item.status = ATTACHMENT_STATUS.ERROR;
660
753
  item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
661
754
  }).finally(() => {
755
+ item.controller = null;
662
756
  this.#render();
663
757
  this.#config.onChange?.();
664
758
  });
665
759
  }
666
760
  #remove(item) {
761
+ item.controller?.abort();
667
762
  this.#items = this.#items.filter((other) => other !== item);
668
763
  this.#render();
669
764
  this.#config.onChange?.();
@@ -678,23 +773,29 @@ var AttachmentTray = class {
678
773
  #renderChip(item) {
679
774
  const chip = document.createElement("div");
680
775
  chip.className = `attachment-chip attachment-chip--${item.status}`;
776
+ chip.setAttribute("part", "attachment-chip");
681
777
  const icon = document.createElement("span");
682
778
  icon.className = "attachment-chip-icon";
779
+ icon.setAttribute("part", "attachment-chip-icon");
683
780
  icon.textContent = iconFor(item.file.type);
684
781
  icon.setAttribute("aria-hidden", "true");
685
782
  const name = document.createElement("span");
686
783
  name.className = "attachment-chip-name";
784
+ name.setAttribute("part", "attachment-chip-name");
687
785
  name.textContent = item.file.name;
688
786
  name.title = item.file.name;
689
787
  const meta = document.createElement("span");
690
788
  meta.className = "attachment-chip-size";
789
+ meta.setAttribute("part", "attachment-chip-size");
691
790
  meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
692
791
  chip.append(icon, name, meta);
693
792
  if (item.status === ATTACHMENT_STATUS.UPLOADING) {
694
793
  const bar = document.createElement("div");
695
794
  bar.className = "attachment-chip-bar";
795
+ bar.setAttribute("part", "attachment-chip-bar");
696
796
  const fill = document.createElement("div");
697
797
  fill.className = "attachment-chip-bar-fill";
798
+ fill.setAttribute("part", "attachment-chip-bar-fill");
698
799
  fill.style.width = `${Math.round(item.progress * 100)}%`;
699
800
  bar.appendChild(fill);
700
801
  chip.appendChild(bar);
@@ -703,6 +804,7 @@ var AttachmentTray = class {
703
804
  const retry = document.createElement("button");
704
805
  retry.type = "button";
705
806
  retry.className = "attachment-chip-retry";
807
+ retry.setAttribute("part", "attachment-chip-retry");
706
808
  retry.title = this.#strings.retry;
707
809
  retry.setAttribute("aria-label", this.#strings.retryUpload);
708
810
  retry.textContent = "\u21BB";
@@ -712,6 +814,7 @@ var AttachmentTray = class {
712
814
  const remove = document.createElement("button");
713
815
  remove.type = "button";
714
816
  remove.className = "attachment-chip-remove";
817
+ remove.setAttribute("part", "attachment-chip-remove");
715
818
  remove.title = this.#strings.remove;
716
819
  remove.setAttribute("aria-label", this.#strings.removeAttachment);
717
820
  remove.textContent = "\u2715";
@@ -739,7 +842,7 @@ function accepts(accept, file) {
739
842
  }
740
843
 
741
844
  // src/ui/confirmation_card.ts
742
- function actionButton(modifier, label) {
845
+ function actionButton2(modifier, label) {
743
846
  const button = document.createElement("button");
744
847
  button.type = "button";
745
848
  button.className = `confirm-btn confirm-btn--${modifier}`;
@@ -767,8 +870,8 @@ function requestConfirmation(host, request, options = {}) {
767
870
  const actions = document.createElement("div");
768
871
  actions.className = "confirm-actions";
769
872
  actions.setAttribute("part", "confirm-actions");
770
- const cancel = actionButton("cancel", strings.cancel);
771
- const confirm = actionButton("confirm", strings.confirm);
873
+ const cancel = actionButton2("cancel", strings.cancel);
874
+ const confirm = actionButton2("confirm", strings.confirm);
772
875
  let settled = false;
773
876
  const close = (accepted) => {
774
877
  if (settled) {
@@ -803,6 +906,149 @@ function prettifyToolName(name) {
803
906
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
804
907
  }
805
908
 
909
+ // src/ui/question_card.ts
910
+ function answerInput(placeholder) {
911
+ const input = document.createElement("input");
912
+ input.type = "text";
913
+ input.className = "question-input";
914
+ input.setAttribute("part", "question-input");
915
+ input.placeholder = placeholder;
916
+ return input;
917
+ }
918
+ function requestQuestion(host, request, options = {}) {
919
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
920
+ const choices = request.options ?? [];
921
+ const hasChoices = choices.length > 0;
922
+ const allowsText = !hasChoices || request.allowCustom === true;
923
+ return new Promise((resolve) => {
924
+ const card = document.createElement("div");
925
+ card.className = "question";
926
+ card.setAttribute("part", "question");
927
+ card.setAttribute("role", "group");
928
+ card.setAttribute("aria-label", strings.askUserAction);
929
+ const body = document.createElement("div");
930
+ body.className = "question-body";
931
+ body.setAttribute("part", "question-body");
932
+ body.textContent = request.question;
933
+ const form = document.createElement("div");
934
+ form.className = "question-options";
935
+ form.setAttribute("part", "question-options");
936
+ const group = `q-${choices.length}-${request.question.length}`;
937
+ const radios = [];
938
+ for (const choice of choices) {
939
+ const label = document.createElement("label");
940
+ label.className = "question-choice";
941
+ label.setAttribute("part", "question-choice");
942
+ const radio = document.createElement("input");
943
+ radio.type = "radio";
944
+ radio.name = group;
945
+ radio.value = choice;
946
+ radio.setAttribute("part", "question-radio");
947
+ const text2 = document.createElement("span");
948
+ text2.setAttribute("part", "question-choice-text");
949
+ text2.textContent = choice;
950
+ label.append(radio, text2);
951
+ form.appendChild(label);
952
+ radios.push(radio);
953
+ }
954
+ let otherRadio = null;
955
+ let input = null;
956
+ if (allowsText) {
957
+ input = answerInput(strings.answerPlaceholder);
958
+ if (hasChoices) {
959
+ const label = document.createElement("label");
960
+ label.className = "question-choice";
961
+ label.setAttribute("part", "question-choice");
962
+ otherRadio = document.createElement("input");
963
+ otherRadio.type = "radio";
964
+ otherRadio.name = group;
965
+ otherRadio.value = "";
966
+ otherRadio.setAttribute("part", "question-radio");
967
+ const text2 = document.createElement("span");
968
+ text2.setAttribute("part", "question-choice-text");
969
+ text2.textContent = strings.otherOption;
970
+ label.append(otherRadio, text2);
971
+ form.appendChild(label);
972
+ input.disabled = true;
973
+ }
974
+ form.appendChild(input);
975
+ }
976
+ const actions = document.createElement("div");
977
+ actions.className = "question-actions";
978
+ actions.setAttribute("part", "question-actions");
979
+ const submit = document.createElement("button");
980
+ submit.type = "button";
981
+ submit.className = "question-btn";
982
+ submit.setAttribute("part", "question-button");
983
+ submit.textContent = strings.submit;
984
+ actions.appendChild(submit);
985
+ let settled = false;
986
+ const answerFor = () => {
987
+ const picked = radios.find((r) => r.checked);
988
+ if (picked !== void 0) {
989
+ return picked.value;
990
+ }
991
+ if (input !== null && (otherRadio === null || otherRadio.checked)) {
992
+ const typed = input.value.trim();
993
+ return typed === "" ? null : typed;
994
+ }
995
+ return null;
996
+ };
997
+ const refresh = () => {
998
+ if (input !== null && otherRadio !== null) {
999
+ input.disabled = !otherRadio.checked;
1000
+ }
1001
+ submit.disabled = answerFor() === null;
1002
+ };
1003
+ const close = (answer) => {
1004
+ if (settled) {
1005
+ return;
1006
+ }
1007
+ settled = true;
1008
+ submit.disabled = true;
1009
+ for (const radio of radios) {
1010
+ radio.disabled = true;
1011
+ }
1012
+ if (otherRadio !== null) {
1013
+ otherRadio.disabled = true;
1014
+ }
1015
+ if (input !== null) {
1016
+ input.disabled = true;
1017
+ }
1018
+ card.setAttribute("data-resolved", answer === "" ? "cancelled" : "answered");
1019
+ resolve(answer);
1020
+ };
1021
+ for (const radio of [...radios, ...otherRadio !== null ? [otherRadio] : []]) {
1022
+ radio.addEventListener("change", refresh);
1023
+ }
1024
+ input?.addEventListener("input", refresh);
1025
+ input?.addEventListener("keydown", (event) => {
1026
+ if (event.key === "Enter") {
1027
+ event.preventDefault();
1028
+ const answer = answerFor();
1029
+ if (answer !== null) {
1030
+ close(answer);
1031
+ }
1032
+ }
1033
+ });
1034
+ submit.addEventListener("click", () => {
1035
+ const answer = answerFor();
1036
+ if (answer !== null) {
1037
+ close(answer);
1038
+ }
1039
+ });
1040
+ options.signal?.addEventListener("abort", () => close(""), { once: true });
1041
+ card.append(body, form, actions);
1042
+ host.appendChild(card);
1043
+ if (options.signal?.aborted === true) {
1044
+ close("");
1045
+ return;
1046
+ }
1047
+ refresh();
1048
+ (hasChoices ? radios[0] : input)?.focus();
1049
+ });
1050
+ }
1051
+
806
1052
  // node_modules/.pnpm/dompurify@3.4.7/node_modules/dompurify/dist/purify.es.mjs
807
1053
  function _arrayLikeToArray(r, a) {
808
1054
  (null == a || a > r.length) && (a = r.length);
@@ -2003,7 +2249,7 @@ function createDOMPurify() {
2003
2249
  }
2004
2250
  var purify = createDOMPurify();
2005
2251
 
2006
- // node_modules/.pnpm/marked@18.0.4/node_modules/marked/lib/marked.esm.js
2252
+ // node_modules/.pnpm/marked@18.0.5/node_modules/marked/lib/marked.esm.js
2007
2253
  function M() {
2008
2254
  return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
2009
2255
  }
@@ -2047,15 +2293,15 @@ var F = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|
2047
2293
  var $e = /^[^\n]+/;
2048
2294
  var U = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
2049
2295
  var Le = d(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label", U).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex();
2050
- var _e = d(/^(bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g, j).getRegex();
2296
+ var _e = d(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g, j).getRegex();
2051
2297
  var H = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul";
2052
2298
  var K = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
2053
2299
  var ze = d("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))", "i").replace("comment", K).replace("tag", H).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex();
2054
- var le = d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex();
2300
+ var le = d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex();
2055
2301
  var Me = d(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", le).getRegex();
2056
2302
  var W = { blockquote: Me, code: we, def: Le, fences: ye, heading: Pe, hr: B, html: ze, lheading: ae, list: _e, newline: Oe, paragraph: le, table: _, text: $e };
2057
2303
  var se = d("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", "(?: {4}| {0,3} )[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex();
2058
- var Ee = { ...W, lheading: Se, table: se, paragraph: d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", se).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex() };
2304
+ var Ee = { ...W, lheading: Se, table: se, paragraph: d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", se).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex() };
2059
2305
  var Ie = { ...W, html: d(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment", K).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, heading: /^(#{1,6})(.*)(?:\n+|$)/, fences: _, lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, paragraph: d(F).replace("hr", B).replace("heading", ` *#{1,6} *[^
2060
2306
  ]`).replace("lheading", ae).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() };
2061
2307
  var Ae = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/;
@@ -3346,9 +3592,11 @@ var SkillsMenu = class {
3346
3592
  this.#onPick = onPick;
3347
3593
  this.chips = document.createElement("div");
3348
3594
  this.chips.className = "skill-chips";
3595
+ this.chips.setAttribute("part", "skill-chips");
3349
3596
  this.chips.hidden = true;
3350
3597
  this.palette = document.createElement("div");
3351
3598
  this.palette.className = "skill-palette";
3599
+ this.palette.setAttribute("part", "skill-palette");
3352
3600
  this.palette.setAttribute("role", "listbox");
3353
3601
  this.palette.hidden = true;
3354
3602
  }
@@ -3443,6 +3691,7 @@ var SkillsMenu = class {
3443
3691
  const button = document.createElement("button");
3444
3692
  button.type = "button";
3445
3693
  button.className = "skill-chip";
3694
+ button.setAttribute("part", "skill-chip");
3446
3695
  button.textContent = skill.title;
3447
3696
  button.addEventListener("click", () => this.#pick(skill));
3448
3697
  this.chips.appendChild(button);
@@ -3454,15 +3703,18 @@ var SkillsMenu = class {
3454
3703
  const item = document.createElement("button");
3455
3704
  item.type = "button";
3456
3705
  item.className = "skill-item";
3706
+ item.setAttribute("part", "skill-item");
3457
3707
  item.setAttribute("role", "option");
3458
3708
  item.setAttribute("aria-selected", index === this.#activeIndex ? "true" : "false");
3459
3709
  const title = document.createElement("span");
3460
3710
  title.className = "skill-item-title";
3711
+ title.setAttribute("part", "skill-item-title");
3461
3712
  title.textContent = skill.title;
3462
3713
  item.appendChild(title);
3463
3714
  if (skill.description !== void 0) {
3464
3715
  const desc = document.createElement("span");
3465
3716
  desc.className = "skill-item-desc";
3717
+ desc.setAttribute("part", "skill-item-desc");
3466
3718
  desc.textContent = skill.description;
3467
3719
  item.appendChild(desc);
3468
3720
  }
@@ -3617,7 +3869,7 @@ var STYLES = `
3617
3869
  --ag-ui-radius: 0;
3618
3870
  }
3619
3871
 
3620
- /* Page (PAGE-1): full-bleed background with a centred reading column. Unlike
3872
+ /* Page: full-bleed background with a centred reading column. Unlike
3621
3873
  "full" (edge-to-edge, left-aligned messages) the content sits in a column
3622
3874
  capped at --ag-ui-content-max-width. The column is produced by symmetric auto
3623
3875
  padding on the scroll area + composer (no per-row wrapper), so user pills
@@ -3659,7 +3911,7 @@ var STYLES = `
3659
3911
  max-width: 100%;
3660
3912
  }
3661
3913
 
3662
- /* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
3914
+ /* Sidebar: a full-height docked panel that slides open/closed and
3663
3915
  collapses to a slim icon rail (not the floating launcher). Docked right by
3664
3916
  default; data-side="left" docks it left. Overlay by default \u2014 set
3665
3917
  --ag-ui-position: static (and place this element in your own layout) for a
@@ -3769,7 +4021,7 @@ var STYLES = `
3769
4021
  white-space: nowrap;
3770
4022
  }
3771
4023
 
3772
- /* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
4024
+ /* Header / launcher icon holder: a slot, with a data-icon-url <img>
3773
4025
  fallback, sized via --ag-ui-icon-size. */
3774
4026
  .icon-holder {
3775
4027
  display: inline-flex;
@@ -3843,7 +4095,7 @@ var STYLES = `
3843
4095
  gap: var(--ag-ui-space);
3844
4096
  }
3845
4097
 
3846
- /* Empty-state region (CUST-1 slot): centred while it's the only thing in the
4098
+ /* Empty-state region (slot): centred while it's the only thing in the
3847
4099
  list, hidden as soon as a message, card, or pending indicator renders. */
3848
4100
  .empty {
3849
4101
  margin: auto;
@@ -3855,7 +4107,7 @@ var STYLES = `
3855
4107
  display: none;
3856
4108
  }
3857
4109
 
3858
- /* \u2500\u2500 Answer group / well (WELL-1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4110
+ /* \u2500\u2500 Answer group / well \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
3859
4111
  One .answer per assistant turn wraps the streamed text, its tool cards,
3860
4112
  and the pending indicator so a whole answer reads (and can be boxed) as one
3861
4113
  unit. A flex column on the message-list gap, stretched to the list width so
@@ -4024,7 +4276,7 @@ var STYLES = `
4024
4276
  }
4025
4277
  }
4026
4278
 
4027
- /* \u2500\u2500 Thoughts region (THINK-1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4279
+ /* \u2500\u2500 Thoughts region \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
4028
4280
  A muted, collapsible chain-of-thought at the top of the answer group: open
4029
4281
  while the model reasons, folded once the answer text starts. */
4030
4282
  .thoughts {
@@ -4117,7 +4369,7 @@ var STYLES = `
4117
4369
  word-break: break-word;
4118
4370
  }
4119
4371
 
4120
- /* Leading status icon (CARD-1). Empty in the DOM \u2014 the glyph/spinner is drawn
4372
+ /* Leading status icon. Empty in the DOM \u2014 the glyph/spinner is drawn
4121
4373
  here from the card's data-status, so it stays themeable. */
4122
4374
  .tool-call-icon {
4123
4375
  flex: none;
@@ -4165,7 +4417,7 @@ var STYLES = `
4165
4417
  }
4166
4418
  }
4167
4419
 
4168
- /* Inline display mode (CARD-1): the lightest card \u2014 drop the box chrome so the
4420
+ /* Inline display mode: the lightest card \u2014 drop the box chrome so the
4169
4421
  status row reads as one line of the answer; the result toggle still expands
4170
4422
  below it. */
4171
4423
  .tool-call[data-display="inline"] {
@@ -4295,7 +4547,7 @@ var STYLES = `
4295
4547
  display: none;
4296
4548
  }
4297
4549
 
4298
- /* The \u{1F3A4} mic button (VOICE-1); shown only once #wireVoice mounts it. */
4550
+ /* The \u{1F3A4} mic button; shown only once #wireVoice mounts it. */
4299
4551
  .voice-slot {
4300
4552
  display: contents;
4301
4553
  }
@@ -4499,6 +4751,127 @@ var STYLES = `
4499
4751
  color: #ffffff;
4500
4752
  }
4501
4753
 
4754
+ /* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */
4755
+ .approval {
4756
+ align-self: stretch;
4757
+ box-sizing: border-box;
4758
+ display: flex;
4759
+ flex-direction: column;
4760
+ gap: 8px;
4761
+ padding: 12px;
4762
+ background: var(--ag-ui-bg);
4763
+ border: 1px solid var(--ag-ui-accent);
4764
+ border-radius: 10px;
4765
+ }
4766
+
4767
+ .approval[data-resolved] {
4768
+ opacity: 0.7;
4769
+ border-color: var(--ag-ui-border);
4770
+ }
4771
+
4772
+ .approval-body {
4773
+ font-weight: 600;
4774
+ }
4775
+
4776
+ .approval-actions {
4777
+ display: flex;
4778
+ gap: 8px;
4779
+ justify-content: flex-end;
4780
+ }
4781
+
4782
+ .approval-btn {
4783
+ border: 1px solid var(--ag-ui-border);
4784
+ border-radius: 8px;
4785
+ padding: 8px 14px;
4786
+ font: inherit;
4787
+ font-weight: 600;
4788
+ cursor: pointer;
4789
+ background: var(--ag-ui-bg);
4790
+ color: var(--ag-ui-fg);
4791
+ }
4792
+
4793
+ .approval-btn:disabled {
4794
+ cursor: default;
4795
+ opacity: 0.6;
4796
+ }
4797
+
4798
+ .approval-btn--approve {
4799
+ border-color: var(--ag-ui-accent);
4800
+ background: var(--ag-ui-accent);
4801
+ color: #ffffff;
4802
+ }
4803
+
4804
+ /* Question card \u2014 the built-in ask_user prompt (radios and/or free text). */
4805
+ .question {
4806
+ align-self: stretch;
4807
+ box-sizing: border-box;
4808
+ display: flex;
4809
+ flex-direction: column;
4810
+ gap: 8px;
4811
+ padding: 12px;
4812
+ background: var(--ag-ui-bg);
4813
+ border: 1px solid var(--ag-ui-accent);
4814
+ border-radius: 10px;
4815
+ }
4816
+
4817
+ .question[data-resolved] {
4818
+ opacity: 0.7;
4819
+ border-color: var(--ag-ui-border);
4820
+ }
4821
+
4822
+ .question-body {
4823
+ font-weight: 600;
4824
+ }
4825
+
4826
+ .question-options {
4827
+ display: flex;
4828
+ flex-direction: column;
4829
+ gap: 6px;
4830
+ }
4831
+
4832
+ .question-choice {
4833
+ display: flex;
4834
+ align-items: center;
4835
+ gap: 8px;
4836
+ cursor: pointer;
4837
+ }
4838
+
4839
+ .question-input {
4840
+ box-sizing: border-box;
4841
+ width: 100%;
4842
+ padding: 8px 10px;
4843
+ font: inherit;
4844
+ color: var(--ag-ui-fg);
4845
+ background: var(--ag-ui-bg);
4846
+ border: 1px solid var(--ag-ui-border);
4847
+ border-radius: 8px;
4848
+ }
4849
+
4850
+ .question-input:disabled {
4851
+ opacity: 0.6;
4852
+ }
4853
+
4854
+ .question-actions {
4855
+ display: flex;
4856
+ justify-content: flex-end;
4857
+ }
4858
+
4859
+ .question-btn {
4860
+ border: 1px solid var(--ag-ui-accent);
4861
+ border-radius: 8px;
4862
+ padding: 8px 14px;
4863
+ font: inherit;
4864
+ font-weight: 600;
4865
+ cursor: pointer;
4866
+ background: var(--ag-ui-accent);
4867
+ color: #ffffff;
4868
+ }
4869
+
4870
+ .question-btn:disabled {
4871
+ cursor: default;
4872
+ opacity: 0.6;
4873
+ }
4874
+
4502
4875
  /* Skills \u2014 chips row + the /-command palette, above the input. */
4503
4876
  .skill-chips {
4504
4877
  display: flex;
@@ -4774,6 +5147,7 @@ var ThoughtsBlock = class {
4774
5147
  this.#toggle.setAttribute("aria-expanded", "true");
4775
5148
  this.#label = document.createElement("span");
4776
5149
  this.#label.className = "thoughts-label";
5150
+ this.#label.setAttribute("part", "thoughts-label");
4777
5151
  this.#label.textContent = strings.thinking;
4778
5152
  this.#toggle.append(this.#label);
4779
5153
  this.#body = document.createElement("pre");
@@ -4810,6 +5184,9 @@ var ThoughtsBlock = class {
4810
5184
 
4811
5185
  // src/ui/relative_time.ts
4812
5186
  function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
5187
+ if (!Number.isFinite(timestamp)) {
5188
+ return strings.justNow;
5189
+ }
4813
5190
  const seconds = Math.round((now - timestamp) / 1e3);
4814
5191
  if (seconds < 60) {
4815
5192
  return strings.justNow;
@@ -4841,6 +5218,8 @@ var ThreadDrawer = class {
4841
5218
  #strings;
4842
5219
  #threads = [];
4843
5220
  #activeId = "";
5221
+ /** The element focused before the drawer opened, restored on close. */
5222
+ #lastFocused = null;
4844
5223
  constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
4845
5224
  this.#callbacks = callbacks;
4846
5225
  this.#strings = strings;
@@ -4856,7 +5235,9 @@ var ThreadDrawer = class {
4856
5235
  this.#panel.className = "drawer-panel";
4857
5236
  this.#panel.setAttribute("part", "drawer-panel");
4858
5237
  this.#panel.setAttribute("role", "dialog");
5238
+ this.#panel.setAttribute("aria-modal", "true");
4859
5239
  this.#panel.setAttribute("aria-label", strings.chatHistory);
5240
+ this.#panel.addEventListener("keydown", (event) => this.#onPanelKeydown(event));
4860
5241
  const header = document.createElement("div");
4861
5242
  header.className = "drawer-header";
4862
5243
  header.setAttribute("part", "drawer-header");
@@ -4892,13 +5273,55 @@ var ThreadDrawer = class {
4892
5273
  return !this.element.hidden;
4893
5274
  }
4894
5275
  open() {
5276
+ if (this.isOpen()) {
5277
+ return;
5278
+ }
5279
+ this.#lastFocused = this.#activeElement();
4895
5280
  this.element.hidden = false;
5281
+ this.#newButton.focus();
4896
5282
  }
4897
5283
  close() {
5284
+ if (!this.isOpen()) {
5285
+ return;
5286
+ }
4898
5287
  this.element.hidden = true;
5288
+ this.#lastFocused?.focus();
5289
+ this.#lastFocused = null;
4899
5290
  }
4900
5291
  toggle() {
4901
- this.element.hidden = !this.element.hidden;
5292
+ if (this.isOpen()) {
5293
+ this.close();
5294
+ } else {
5295
+ this.open();
5296
+ }
5297
+ }
5298
+ /** The currently-focused element within the drawer's root (shadow-aware). */
5299
+ #activeElement() {
5300
+ return this.element.getRootNode().activeElement;
5301
+ }
5302
+ /** Escape-to-close and a Tab focus trap while the dialog is open. */
5303
+ #onPanelKeydown(event) {
5304
+ if (event.key === "Escape") {
5305
+ event.preventDefault();
5306
+ this.close();
5307
+ return;
5308
+ }
5309
+ if (event.key !== "Tab") {
5310
+ return;
5311
+ }
5312
+ const focusables = Array.from(
5313
+ this.#panel.querySelectorAll("button, input, [tabindex]")
5314
+ ).filter((el) => !el.hidden);
5315
+ const first = focusables[0];
5316
+ const last = focusables[focusables.length - 1];
5317
+ const active = this.#activeElement();
5318
+ if (event.shiftKey && active === first) {
5319
+ event.preventDefault();
5320
+ last?.focus();
5321
+ } else if (!event.shiftKey && active === last) {
5322
+ event.preventDefault();
5323
+ first?.focus();
5324
+ }
4902
5325
  }
4903
5326
  /** Render the rows (or the empty state), highlighting the active thread. */
4904
5327
  setThreads(threads, activeId) {
@@ -4933,12 +5356,15 @@ var ThreadDrawer = class {
4933
5356
  select.setAttribute("part", "drawer-row-select");
4934
5357
  const title = document.createElement("span");
4935
5358
  title.className = "drawer-row-title";
5359
+ title.setAttribute("part", "drawer-row-title");
4936
5360
  title.textContent = meta.title;
4937
5361
  const time = document.createElement("span");
4938
5362
  time.className = "drawer-row-time";
5363
+ time.setAttribute("part", "drawer-row-time");
4939
5364
  time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
4940
5365
  const preview = document.createElement("span");
4941
5366
  preview.className = "drawer-row-preview";
5367
+ preview.setAttribute("part", "drawer-row-preview");
4942
5368
  preview.textContent = meta.preview;
4943
5369
  select.append(title, time, preview);
4944
5370
  select.addEventListener("click", () => {
@@ -4948,6 +5374,7 @@ var ThreadDrawer = class {
4948
5374
  const rename = document.createElement("button");
4949
5375
  rename.type = "button";
4950
5376
  rename.className = "drawer-row-rename";
5377
+ rename.setAttribute("part", "drawer-row-rename");
4951
5378
  rename.title = this.#strings.rename;
4952
5379
  rename.setAttribute("aria-label", this.#strings.renameConversation);
4953
5380
  rename.textContent = "\u270E";
@@ -4955,34 +5382,56 @@ var ThreadDrawer = class {
4955
5382
  const remove = document.createElement("button");
4956
5383
  remove.type = "button";
4957
5384
  remove.className = "drawer-row-delete";
5385
+ remove.setAttribute("part", "drawer-row-delete");
4958
5386
  remove.title = this.#strings.delete;
4959
5387
  remove.setAttribute("aria-label", this.#strings.deleteConversation);
4960
5388
  remove.textContent = "\u{1F5D1}";
4961
5389
  remove.addEventListener("click", () => this.#confirmDelete(row, meta));
4962
5390
  const actions = document.createElement("div");
4963
5391
  actions.className = "drawer-row-actions";
5392
+ actions.setAttribute("part", "drawer-row-actions");
4964
5393
  actions.append(rename, remove);
4965
5394
  row.append(select, actions);
4966
5395
  return row;
4967
5396
  }
4968
- /** Swap a row for an inline rename input; Enter commits, Escape cancels. */
5397
+ /** Swap a row for an inline rename input; Enter/blur commits, Escape cancels. */
4969
5398
  #startRename(row, meta) {
4970
5399
  const input = document.createElement("input");
4971
5400
  input.type = "text";
4972
5401
  input.className = "drawer-rename-input";
5402
+ input.setAttribute("part", "drawer-rename-input");
4973
5403
  input.value = meta.title;
5404
+ let done = false;
5405
+ const commit = () => {
5406
+ if (done) {
5407
+ return;
5408
+ }
5409
+ done = true;
5410
+ const value = input.value.trim();
5411
+ if (value === "" || value === meta.title) {
5412
+ this.#renderList();
5413
+ } else {
5414
+ this.#callbacks.onRename(meta.threadId, value);
5415
+ }
5416
+ };
5417
+ const cancel = () => {
5418
+ if (done) {
5419
+ return;
5420
+ }
5421
+ done = true;
5422
+ this.#renderList();
5423
+ };
4974
5424
  input.addEventListener("keydown", (event) => {
4975
5425
  if (event.key === "Enter") {
4976
- const value = input.value.trim();
4977
- if (value === "") {
4978
- this.#renderList();
4979
- } else {
4980
- this.#callbacks.onRename(meta.threadId, value);
4981
- }
5426
+ event.preventDefault();
5427
+ commit();
4982
5428
  } else if (event.key === "Escape") {
4983
- this.#renderList();
5429
+ event.preventDefault();
5430
+ event.stopPropagation();
5431
+ cancel();
4984
5432
  }
4985
5433
  });
5434
+ input.addEventListener("blur", () => commit());
4986
5435
  row.replaceChildren(input);
4987
5436
  input.focus();
4988
5437
  input.select();
@@ -4991,17 +5440,21 @@ var ThreadDrawer = class {
4991
5440
  #confirmDelete(row, meta) {
4992
5441
  const confirm = document.createElement("div");
4993
5442
  confirm.className = "drawer-confirm";
5443
+ confirm.setAttribute("part", "drawer-confirm");
4994
5444
  const label = document.createElement("span");
4995
5445
  label.className = "drawer-confirm-label";
5446
+ label.setAttribute("part", "drawer-confirm-label");
4996
5447
  label.textContent = this.#strings.deletePrompt;
4997
5448
  const yes = document.createElement("button");
4998
5449
  yes.type = "button";
4999
5450
  yes.className = "drawer-confirm-yes";
5451
+ yes.setAttribute("part", "drawer-confirm-yes");
5000
5452
  yes.textContent = this.#strings.delete;
5001
5453
  yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
5002
5454
  const no = document.createElement("button");
5003
5455
  no.type = "button";
5004
5456
  no.className = "drawer-confirm-no";
5457
+ no.setAttribute("part", "drawer-confirm-no");
5005
5458
  no.textContent = this.#strings.cancel;
5006
5459
  no.addEventListener("click", () => this.#renderList());
5007
5460
  confirm.append(label, yes, no);
@@ -5078,6 +5531,9 @@ var ToolCallCard = class {
5078
5531
  * `inline`), or the args + result together (`compact`).
5079
5532
  */
5080
5533
  settle(status, text2) {
5534
+ if (this.#settled) {
5535
+ return;
5536
+ }
5081
5537
  this.#settled = true;
5082
5538
  this.element.setAttribute("data-status", status);
5083
5539
  this.#status.textContent = statusLabels(this.#strings)[status];
@@ -5122,6 +5578,7 @@ var VoiceInput = class {
5122
5578
  #recorder = null;
5123
5579
  #stream = null;
5124
5580
  #chunks = [];
5581
+ #disposed = false;
5125
5582
  constructor(options) {
5126
5583
  this.#transcribe = options.transcribe;
5127
5584
  this.#onText = options.onText;
@@ -5171,7 +5628,25 @@ var VoiceInput = class {
5171
5628
  #stop() {
5172
5629
  this.#recorder?.stop();
5173
5630
  }
5631
+ /**
5632
+ * Tear the control down — the teardown path when the host element is removed
5633
+ * mid-recording. Stops any live `MediaRecorder`, releases the mic tracks (so
5634
+ * the browser's recording indicator clears), and suppresses the pending
5635
+ * transcription: a disconnected control must not fire `onText` back into a
5636
+ * detached element.
5637
+ */
5638
+ dispose() {
5639
+ this.#disposed = true;
5640
+ if (this.#recorder !== null && this.#recorder.state !== "inactive") {
5641
+ this.#recorder.stop();
5642
+ }
5643
+ this.#recorder = null;
5644
+ this.#releaseStream();
5645
+ }
5174
5646
  async #finish(mimeType) {
5647
+ if (this.#disposed) {
5648
+ return;
5649
+ }
5175
5650
  this.#releaseStream();
5176
5651
  this.#setState("transcribing");
5177
5652
  const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
@@ -5221,7 +5696,10 @@ var VoiceInput = class {
5221
5696
  };
5222
5697
 
5223
5698
  // src/core/agui_client.ts
5224
- import { randomUUID as randomUUID2 } from "@ag-ui/client";
5699
+ import {
5700
+ buildResumeArray,
5701
+ randomUUID as randomUUID2
5702
+ } from "@ag-ui/client";
5225
5703
  var ConnectionLostError = class extends Error {
5226
5704
  constructor(message) {
5227
5705
  super(message);
@@ -5234,6 +5712,7 @@ var AgUiClient = class {
5234
5712
  #getTools;
5235
5713
  #getContext;
5236
5714
  #executeTool;
5715
+ #resolveInterrupts;
5237
5716
  #onPersist;
5238
5717
  #connectionLostMessage;
5239
5718
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
@@ -5245,6 +5724,7 @@ var AgUiClient = class {
5245
5724
  this.#getTools = config.getTools ?? (() => []);
5246
5725
  this.#getContext = config.getContext ?? (() => []);
5247
5726
  this.#executeTool = config.executeTool ?? null;
5727
+ this.#resolveInterrupts = config.resolveInterrupts ?? null;
5248
5728
  this.#onPersist = config.onPersist ?? (() => {
5249
5729
  });
5250
5730
  this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
@@ -5324,16 +5804,22 @@ var AgUiClient = class {
5324
5804
  this.#handlers.onCancelled();
5325
5805
  }
5326
5806
  async #runLoop() {
5807
+ let resume;
5327
5808
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
5328
5809
  if (this.#cancelled) {
5329
5810
  return;
5330
5811
  }
5331
5812
  const pending = [];
5332
- const runState = { terminal: false };
5333
- await this.#agent.runAgent(
5334
- { tools: this.#getTools(), context: this.#getContext() },
5335
- this.#buildSubscriber(pending, runState)
5336
- );
5813
+ const runState = { terminal: false, errored: false, interrupts: [] };
5814
+ const params = {
5815
+ tools: this.#getTools(),
5816
+ context: this.#getContext()
5817
+ };
5818
+ if (resume !== void 0) {
5819
+ params.resume = resume;
5820
+ }
5821
+ await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
5822
+ resume = void 0;
5337
5823
  this.#onPersist(this.#agent.messages);
5338
5824
  if (this.#cancelled) {
5339
5825
  return;
@@ -5341,6 +5827,20 @@ var AgUiClient = class {
5341
5827
  if (!runState.terminal) {
5342
5828
  throw new ConnectionLostError(this.#connectionLostMessage);
5343
5829
  }
5830
+ if (runState.errored) {
5831
+ return;
5832
+ }
5833
+ if (runState.interrupts.length > 0) {
5834
+ if (this.#resolveInterrupts === null) {
5835
+ return;
5836
+ }
5837
+ const responses = await this.#resolveInterrupts(runState.interrupts);
5838
+ if (this.#cancelled) {
5839
+ return;
5840
+ }
5841
+ resume = buildResumeArray(runState.interrupts, responses);
5842
+ continue;
5843
+ }
5344
5844
  if (this.#executeTool === null || pending.length === 0) {
5345
5845
  return;
5346
5846
  }
@@ -5391,7 +5891,7 @@ var AgUiClient = class {
5391
5891
  onToolCallResultEvent({ event }) {
5392
5892
  h.onToolResult(event.toolCallId, event.content);
5393
5893
  },
5394
- // Reasoning (THINK-1). `@ag-ui/client` already maps the deprecated
5894
+ // Reasoning. `@ag-ui/client` already maps the deprecated
5395
5895
  // THINKING_* events onto these REASONING_* callbacks, so handling the
5396
5896
  // reasoning family alone covers both protocol versions.
5397
5897
  onReasoningStartEvent() {
@@ -5403,8 +5903,15 @@ var AgUiClient = class {
5403
5903
  onReasoningEndEvent() {
5404
5904
  h.onReasoningEnd();
5405
5905
  },
5906
+ onRunFinishedEvent(params) {
5907
+ runState.terminal = true;
5908
+ if (params.outcome === "interrupt") {
5909
+ runState.interrupts = params.interrupts;
5910
+ }
5911
+ },
5406
5912
  onRunErrorEvent({ event }) {
5407
5913
  runState.terminal = true;
5914
+ runState.errored = true;
5408
5915
  h.onError(event.message);
5409
5916
  },
5410
5917
  onRunFinalized() {
@@ -5421,40 +5928,56 @@ function isAbortError(error) {
5421
5928
  // src/core/attachment.ts
5422
5929
  function messageAttachments(message) {
5423
5930
  const refs = message.attachments;
5424
- return Array.isArray(refs) ? refs : [];
5931
+ return Array.isArray(refs) ? refs.filter(isAttachmentRef) : [];
5932
+ }
5933
+ function isAttachmentRef(value) {
5934
+ if (typeof value !== "object" || value === null) {
5935
+ return false;
5936
+ }
5937
+ const ref = value;
5938
+ return typeof ref["id"] === "string" && typeof ref["name"] === "string" && typeof ref["mime"] === "string" && typeof ref["size"] === "number" && (ref["url"] === void 0 || typeof ref["url"] === "string");
5425
5939
  }
5426
5940
 
5427
5941
  // src/core/conversation_store.ts
5428
5942
  import { randomUUID as randomUUID3 } from "@ag-ui/client";
5429
- var THREAD_KEY = "ag-ui-chat:thread";
5430
- var THREADS_KEY = "ag-ui-chat:threads";
5431
- var MESSAGES_PREFIX = "ag-ui-chat:messages:";
5432
- var CHECKPOINT_PREFIX = "ag-ui-chat:checkpoint:";
5943
+ var KEY_ROOT = "ag-ui-chat";
5944
+ var THREAD_SUFFIX = "thread";
5945
+ var THREADS_SUFFIX = "threads";
5946
+ var MESSAGES_SUFFIX = "messages:";
5947
+ var CHECKPOINT_SUFFIX = "checkpoint:";
5433
5948
  var TITLE_LIMIT = 60;
5434
5949
  var PREVIEW_LIMIT = 100;
5435
5950
  var DEFAULT_TITLE = "New conversation";
5436
5951
  var SessionStorageStore = class {
5952
+ #root;
5953
+ constructor(namespace = "") {
5954
+ this.#root = namespace === "" ? KEY_ROOT : `${KEY_ROOT}@${namespace}`;
5955
+ if (namespace !== "") {
5956
+ this.#migrateLegacyKeys();
5957
+ }
5958
+ }
5437
5959
  threadId() {
5438
- const existing = sessionStorage.getItem(THREAD_KEY);
5960
+ const key = this.#key(THREAD_SUFFIX);
5961
+ const existing = sessionStorage.getItem(key);
5439
5962
  if (existing !== null) {
5440
5963
  return existing;
5441
5964
  }
5442
5965
  const id = randomUUID3();
5443
- sessionStorage.setItem(THREAD_KEY, id);
5966
+ sessionStorage.setItem(key, id);
5444
5967
  return id;
5445
5968
  }
5446
5969
  loadMessages(threadId) {
5447
- return Promise.resolve(this.#readJson(MESSAGES_PREFIX + threadId));
5970
+ return Promise.resolve(this.#readJson(this.#key(MESSAGES_SUFFIX + threadId)));
5448
5971
  }
5449
5972
  saveMessages(threadId, messages) {
5450
- sessionStorage.setItem(MESSAGES_PREFIX + threadId, JSON.stringify(messages));
5973
+ sessionStorage.setItem(this.#key(MESSAGES_SUFFIX + threadId), JSON.stringify(messages));
5451
5974
  this.#touchThread(threadId, messages);
5452
5975
  }
5453
5976
  loadCheckpoint(threadId) {
5454
- return this.#readJson(CHECKPOINT_PREFIX + threadId);
5977
+ return this.#readJson(this.#key(CHECKPOINT_SUFFIX + threadId));
5455
5978
  }
5456
5979
  saveCheckpoint(threadId, checkpoint) {
5457
- const key = CHECKPOINT_PREFIX + threadId;
5980
+ const key = this.#key(CHECKPOINT_SUFFIX + threadId);
5458
5981
  if (checkpoint === null) {
5459
5982
  sessionStorage.removeItem(key);
5460
5983
  return;
@@ -5462,11 +5985,11 @@ var SessionStorageStore = class {
5462
5985
  sessionStorage.setItem(key, JSON.stringify(checkpoint));
5463
5986
  }
5464
5987
  clear(threadId) {
5465
- sessionStorage.removeItem(MESSAGES_PREFIX + threadId);
5466
- sessionStorage.removeItem(CHECKPOINT_PREFIX + threadId);
5988
+ sessionStorage.removeItem(this.#key(MESSAGES_SUFFIX + threadId));
5989
+ sessionStorage.removeItem(this.#key(CHECKPOINT_SUFFIX + threadId));
5467
5990
  this.#writeThreads(this.#readThreads().filter((thread) => thread.threadId !== threadId));
5468
- if (sessionStorage.getItem(THREAD_KEY) === threadId) {
5469
- sessionStorage.removeItem(THREAD_KEY);
5991
+ if (sessionStorage.getItem(this.#key(THREAD_SUFFIX)) === threadId) {
5992
+ sessionStorage.removeItem(this.#key(THREAD_SUFFIX));
5470
5993
  }
5471
5994
  }
5472
5995
  listThreads() {
@@ -5474,7 +5997,7 @@ var SessionStorageStore = class {
5474
5997
  return Promise.resolve(metas);
5475
5998
  }
5476
5999
  setActiveThread(threadId) {
5477
- sessionStorage.setItem(THREAD_KEY, threadId);
6000
+ sessionStorage.setItem(this.#key(THREAD_SUFFIX), threadId);
5478
6001
  }
5479
6002
  renameThread(threadId, title) {
5480
6003
  const threads = this.#readThreads();
@@ -5510,14 +6033,48 @@ var SessionStorageStore = class {
5510
6033
  this.#writeThreads(threads);
5511
6034
  }
5512
6035
  #readThreads() {
5513
- return this.#readJson(THREADS_KEY) ?? [];
6036
+ return this.#readJson(this.#key(THREADS_SUFFIX)) ?? [];
5514
6037
  }
5515
6038
  #writeThreads(threads) {
6039
+ const key = this.#key(THREADS_SUFFIX);
5516
6040
  if (threads.length === 0) {
5517
- sessionStorage.removeItem(THREADS_KEY);
6041
+ sessionStorage.removeItem(key);
5518
6042
  return;
5519
6043
  }
5520
- sessionStorage.setItem(THREADS_KEY, JSON.stringify(threads));
6044
+ sessionStorage.setItem(key, JSON.stringify(threads));
6045
+ }
6046
+ /** This store's fully-qualified key for a suffix (namespaced when set). */
6047
+ #key(suffix) {
6048
+ return `${this.#root}:${suffix}`;
6049
+ }
6050
+ /**
6051
+ * One-time move of pre-namespacing (`ag-ui-chat:*`) keys into this instance's
6052
+ * namespace, so an existing conversation isn't orphaned by the upgrade. Only
6053
+ * this store's own keys move (thread pointer, drawer index, per-thread
6054
+ * messages/checkpoints) — the element's `collapsed`/`theme` keys are left
6055
+ * alone. The first namespaced instance to mount adopts the legacy data; a
6056
+ * second namespace finds it gone and starts fresh.
6057
+ */
6058
+ #migrateLegacyKeys() {
6059
+ const legacyRoot = `${KEY_ROOT}:`;
6060
+ const moves = [];
6061
+ for (let i = 0; i < sessionStorage.length; i += 1) {
6062
+ const key = sessionStorage.key(i);
6063
+ if (key === null || !key.startsWith(legacyRoot)) {
6064
+ continue;
6065
+ }
6066
+ const suffix = key.slice(legacyRoot.length);
6067
+ if (isOwnedSuffix(suffix)) {
6068
+ moves.push([key, this.#key(suffix)]);
6069
+ }
6070
+ }
6071
+ for (const [from, to] of moves) {
6072
+ const value = sessionStorage.getItem(from);
6073
+ if (value !== null && sessionStorage.getItem(to) === null) {
6074
+ sessionStorage.setItem(to, value);
6075
+ }
6076
+ sessionStorage.removeItem(from);
6077
+ }
5521
6078
  }
5522
6079
  /** Parse a stored JSON value, returning `null` when absent or corrupt. */
5523
6080
  #readJson(key) {
@@ -5532,6 +6089,9 @@ var SessionStorageStore = class {
5532
6089
  }
5533
6090
  }
5534
6091
  };
6092
+ function isOwnedSuffix(suffix) {
6093
+ return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX);
6094
+ }
5535
6095
  function deriveTitle(messages) {
5536
6096
  for (const message of messages) {
5537
6097
  if (message.role === "user") {
@@ -5638,7 +6198,10 @@ var RemoteConversationStore = class {
5638
6198
  if (response === null || !response.ok) {
5639
6199
  return this.#local.loadMessages(threadId);
5640
6200
  }
5641
- const body = await response.json();
6201
+ const body = await this.#readJson(response);
6202
+ if (body === null) {
6203
+ return this.#local.loadMessages(threadId);
6204
+ }
5642
6205
  return body.messages ?? null;
5643
6206
  }
5644
6207
  async #fetchThreads() {
@@ -5646,14 +6209,28 @@ var RemoteConversationStore = class {
5646
6209
  if (response === null || !response.ok) {
5647
6210
  return null;
5648
6211
  }
5649
- const body = await response.json();
6212
+ const body = await this.#readJson(response);
6213
+ if (body === null) {
6214
+ return null;
6215
+ }
5650
6216
  return body.threads ?? [];
5651
6217
  }
6218
+ /** Parse a `Response` body as JSON, or `null` when it isn't valid JSON. */
6219
+ async #readJson(response) {
6220
+ try {
6221
+ return await response.json();
6222
+ } catch {
6223
+ return null;
6224
+ }
6225
+ }
5652
6226
  #toMeta(row) {
5653
6227
  return {
5654
6228
  threadId: row.thread_id,
5655
6229
  title: this.#renamed.get(row.thread_id) ?? row.title,
5656
- updatedAt: row.updated_at === null ? 0 : Date.parse(row.updated_at),
6230
+ // `null` or an unparseable date both become `NaN` (Date.parse's own
6231
+ // signal), which `relativeTime` renders as a neutral label rather than
6232
+ // "~2950w ago" (epoch 0) or "NaNw ago".
6233
+ updatedAt: row.updated_at === null ? Number.NaN : Date.parse(row.updated_at),
5657
6234
  preview: row.preview
5658
6235
  };
5659
6236
  }
@@ -5789,6 +6366,33 @@ var AgUiChat = class extends HTMLElement {
5789
6366
  allowImages = false;
5790
6367
  /** When true, destructive tools execute without a confirmation modal. */
5791
6368
  autoConfirm = false;
6369
+ /**
6370
+ * When true, the built-in `ask_user` frontend tool is offered to the agent:
6371
+ * calling it renders an inline question card (radio choices and/or a free-text
6372
+ * field) and returns the user's answer. Off by default — like the other
6373
+ * built-in tool groups (route / page-action), it is opt-in so it doesn't
6374
+ * change the advertised catalog until a host asks for it.
6375
+ */
6376
+ askUser = false;
6377
+ /**
6378
+ * Optional full replacement for the `ask_user` question UI. When set, calling
6379
+ * `ask_user` invokes this instead of the built-in inline card: the host
6380
+ * renders whatever it likes (a native modal, a framework component, …) and
6381
+ * resolves with the answer. Unset (default) uses the built-in
6382
+ * {@link requestQuestion} card — style that via the `strings` override and the
6383
+ * `question*` CSS `::part()`s. Requires {@link askUser} to be enabled.
6384
+ */
6385
+ askUserRenderer = null;
6386
+ /**
6387
+ * Optional full replacement for the server-side-tool approval UI. When set, an
6388
+ * approval interrupt invokes this instead of the built-in inline approval
6389
+ * card: the host renders whatever it likes and resolves `true` to approve /
6390
+ * `false` to deny. Unset (default) uses the built-in {@link requestApproval}
6391
+ * card — style that via the `strings` override and the `approval*` CSS
6392
+ * `::part()`s. The gate itself is enabled server-side; this only changes how
6393
+ * the decision is collected.
6394
+ */
6395
+ approvalRenderer = null;
5792
6396
  /**
5793
6397
  * Optional per-call confirmation predicate. When set, it is authoritative:
5794
6398
  * given a tool name + args it decides whether *this* call needs confirmation
@@ -5929,7 +6533,7 @@ var AgUiChat = class extends HTMLElement {
5929
6533
  #attachButton;
5930
6534
  #fileInput;
5931
6535
  #attachSlot;
5932
- /** Optional built-in header theme toggle (THEME-1); shown only with `data-theme-toggle`. */
6536
+ /** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
5933
6537
  #themeToggle;
5934
6538
  /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
5935
6539
  #rail;
@@ -5957,18 +6561,25 @@ var AgUiChat = class extends HTMLElement {
5957
6561
  // it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
5958
6562
  #streamDeltas = 0;
5959
6563
  #pending = null;
5960
- // The current assistant turn's grouping container (WELL-1). One `.answer`
6564
+ // The current assistant turn's grouping container. One `.answer`
5961
6565
  // wraps everything a single answer produces — streamed text, tool cards, the
5962
6566
  // pending indicator — so it can be boxed as one "well" by CSS. Opened on the
5963
6567
  // turn's first run start, closed at settle, so it spans the whole multi-round
5964
6568
  // frontend-tool loop (which is several AG-UI runs), not one run. `null`
5965
6569
  // between turns; user bubbles never enter it.
5966
6570
  #currentGroup = null;
5967
- // The current turn's streamed-reasoning region (THINK-1), shown at the top of
6571
+ // The current turn's streamed-reasoning region, shown at the top of
5968
6572
  // the answer group while a reasoning model thinks and collapsed once the
5969
6573
  // answer's first text token arrives. `null` outside a reasoning turn.
5970
6574
  #thoughts = null;
5971
6575
  #threadId = "";
6576
+ // Per-instance suffix for the origin-scoped storage keys (collapsed / theme /
6577
+ // active thread), so two instances on one origin don't clobber each other.
6578
+ // Empty ⇒ the pre-namespacing global keys (back-compat). Resolved on connect.
6579
+ #storageNs = "";
6580
+ // Bumped on every #rehydrate; a replay whose generation is stale (a newer
6581
+ // thread switch started while it awaited a slow store) drops its result.
6582
+ #rehydrateGeneration = 0;
5972
6583
  #initialMessages = [];
5973
6584
  // Skill catalog by source; merged backend → embed → client (later wins).
5974
6585
  #backendSkills = [];
@@ -6075,9 +6686,73 @@ var AgUiChat = class extends HTMLElement {
6075
6686
  );
6076
6687
  return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
6077
6688
  }
6078
- /** All built-in (route + page + page-action) frontend tools. */
6689
+ /** All built-in (route + page + page-action + ask_user) frontend tools. */
6079
6690
  #builtinTools() {
6080
- return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
6691
+ return [
6692
+ ...this.#routeTools(),
6693
+ ...this.#pageTools(),
6694
+ ...this.#pageActionTools(),
6695
+ ...this.#askUserTool()
6696
+ ];
6697
+ }
6698
+ /**
6699
+ * The built-in `ask_user` frontend tool, or `[]` when {@link askUser} is off.
6700
+ *
6701
+ * A generic "ask the user a typed question" primitive: the agent calls it, the
6702
+ * client executes it locally via the normal frontend-tool path (rendering a
6703
+ * {@link requestQuestion} card), and the chosen/typed answer flows back as the
6704
+ * tool result — no new protocol, reusing the machinery already in place.
6705
+ */
6706
+ #askUserTool() {
6707
+ if (!this.askUser) {
6708
+ return [];
6709
+ }
6710
+ return [
6711
+ {
6712
+ name: "ask_user",
6713
+ description: "Ask the user a question and wait for their answer. Provide `options` for a multiple-choice prompt; set `allow_custom` to also accept a free-text answer.",
6714
+ parameters: {
6715
+ type: "object",
6716
+ properties: {
6717
+ question: { type: "string", description: "The question to ask the user." },
6718
+ options: {
6719
+ type: "array",
6720
+ items: { type: "string" },
6721
+ description: "Preset choices offered as radio buttons."
6722
+ },
6723
+ allow_custom: {
6724
+ type: "boolean",
6725
+ description: "Allow a free-text answer in addition to any options."
6726
+ }
6727
+ },
6728
+ required: ["question"]
6729
+ },
6730
+ handler: (args) => this.#askUser(args)
6731
+ }
6732
+ ];
6733
+ }
6734
+ /** Render the `ask_user` question card and resolve with the user's answer. */
6735
+ async #askUser(args) {
6736
+ const question = typeof args["question"] === "string" ? args["question"] : "";
6737
+ const request = { question };
6738
+ const rawOptions = args["options"];
6739
+ if (Array.isArray(rawOptions)) {
6740
+ request.options = rawOptions.filter((option) => typeof option === "string");
6741
+ }
6742
+ if (args["allow_custom"] === true) {
6743
+ request.allowCustom = true;
6744
+ }
6745
+ this.#confirmAbort = new AbortController();
6746
+ const signal = this.#confirmAbort.signal;
6747
+ this.#hidePending();
6748
+ const answer = this.askUserRenderer !== null ? await this.askUserRenderer(request, { signal }) : await requestQuestion(this.#ensureGroup(), request, {
6749
+ signal,
6750
+ strings: this.#strings
6751
+ });
6752
+ this.#confirmAbort = null;
6753
+ this.#updateEmptyState();
6754
+ this.#messages.scrollTop = this.#messages.scrollHeight;
6755
+ return answer;
6081
6756
  }
6082
6757
  /** Resolve a tool by name: built-in tools first, then the registry. */
6083
6758
  #resolveTool(name) {
@@ -6112,27 +6787,44 @@ var AgUiChat = class extends HTMLElement {
6112
6787
  this.setAttribute("data-tool-display", value);
6113
6788
  }
6114
6789
  connectedCallback() {
6790
+ this.#storageNs = this.id !== "" ? this.id : this.endpoint;
6115
6791
  this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
6116
6792
  if (this.getAttribute("data-theme-toggle") !== null) {
6117
- const saved = sessionStorage.getItem(THEME_KEY);
6793
+ const saved = this.#readScopedItem(THEME_KEY);
6118
6794
  if (saved !== null) {
6119
6795
  this.setAttribute("theme", saved);
6120
6796
  }
6121
6797
  }
6122
6798
  this.#render();
6123
6799
  this.#drawer.setStrings(this.#strings);
6124
- if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
6800
+ if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
6125
6801
  this.setAttribute("collapsed", "");
6126
6802
  }
6127
6803
  this.#syncRail();
6128
6804
  this.#initSkills();
6129
6805
  void this.#fetchToolCatalog();
6806
+ if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
6807
+ this.conversationStore = new SessionStorageStore(this.#storageNs);
6808
+ }
6130
6809
  this.#wireThreadStore();
6131
6810
  this.#wireAttachments();
6132
6811
  this.#wireVoice();
6133
6812
  this.#threadId = this.conversationStore.threadId();
6134
6813
  void this.#rehydrate();
6135
6814
  }
6815
+ /**
6816
+ * Tear down live resources when the element leaves the DOM (a removed node, a
6817
+ * client-side route swap): cancel the in-flight run so its SSE stream closes,
6818
+ * abort any in-flight uploads so they don't orphan server-side files, and
6819
+ * release the mic so the browser's recording indicator clears. Without this a
6820
+ * removed `<ag-ui-chat>` leaks a streaming request, uploads, and a live
6821
+ * `MediaRecorder`.
6822
+ */
6823
+ disconnectedCallback() {
6824
+ this.#cancelRun();
6825
+ this.#attachTray?.dispose();
6826
+ this.#voice?.dispose();
6827
+ }
6136
6828
  /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
6137
6829
  #readStringOverrides() {
6138
6830
  const raw = this.getAttribute("data-strings");
@@ -6178,7 +6870,7 @@ var AgUiChat = class extends HTMLElement {
6178
6870
  if (url === null) {
6179
6871
  return null;
6180
6872
  }
6181
- return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
6873
+ return (file, onProgress, signal) => uploadAttachment(file, { url, headers: this.headers, onProgress, signal });
6182
6874
  }
6183
6875
  /**
6184
6876
  * Reveal the composer's 🎤 mic button when transcription is possible — either
@@ -6382,7 +7074,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6382
7074
  } else {
6383
7075
  this.removeAttribute("collapsed");
6384
7076
  }
6385
- sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
7077
+ sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
6386
7078
  this.#syncRail();
6387
7079
  this.dispatchEvent(
6388
7080
  new CustomEvent(TOGGLE_EVENT, {
@@ -6405,9 +7097,25 @@ Use the read_attachment tool with an id to read a file's contents.`
6405
7097
  toggleTheme() {
6406
7098
  const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
6407
7099
  this.setAttribute("theme", next);
6408
- sessionStorage.setItem(THEME_KEY, next);
7100
+ sessionStorage.setItem(this.#storageKey(THEME_KEY), next);
6409
7101
  this.#syncThemeGlyph();
6410
7102
  }
7103
+ /** This instance's namespaced form of an origin-scoped storage key. */
7104
+ #storageKey(base) {
7105
+ return this.#storageNs === "" ? base : `${base}:${this.#storageNs}`;
7106
+ }
7107
+ /**
7108
+ * Read a namespaced origin-scoped value, falling back once to the legacy
7109
+ * pre-namespacing global key (left in place) so an existing collapsed/theme
7110
+ * preference survives the upgrade.
7111
+ */
7112
+ #readScopedItem(base) {
7113
+ const scoped = sessionStorage.getItem(this.#storageKey(base));
7114
+ if (scoped !== null || this.#storageNs === "") {
7115
+ return scoped;
7116
+ }
7117
+ return sessionStorage.getItem(base);
7118
+ }
6411
7119
  /** Reflect the current theme on the toggle: show the destination's glyph. */
6412
7120
  #syncThemeGlyph() {
6413
7121
  const dark = this.getAttribute("theme") === "dark";
@@ -6475,7 +7183,12 @@ Use the read_attachment tool with an id to read a file's contents.`
6475
7183
  * result from the page we landed on.
6476
7184
  */
6477
7185
  async #rehydrate() {
7186
+ this.#rehydrateGeneration += 1;
7187
+ const generation = this.#rehydrateGeneration;
6478
7188
  const messages = await this.conversationStore.loadMessages(this.#threadId);
7189
+ if (generation !== this.#rehydrateGeneration) {
7190
+ return;
7191
+ }
6479
7192
  if (messages !== null) {
6480
7193
  this.#initialMessages = messages;
6481
7194
  for (const message of messages) {
@@ -6562,7 +7275,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6562
7275
  * stays literal text (no need to parse what the user typed, and it avoids
6563
7276
  * rendering user-authored markup).
6564
7277
  *
6565
- * Assistant bubbles land in the current answer group (WELL-1), opening one if
7278
+ * Assistant bubbles land in the current answer group, opening one if
6566
7279
  * needed; a user bubble closes the prior group and sits directly in the list
6567
7280
  * (the well wraps the *assistant* turn, the user message precedes it).
6568
7281
  */
@@ -6677,6 +7390,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6677
7390
  void this.#submit();
6678
7391
  });
6679
7392
  this.#skillHint.className = "skill-hint";
7393
+ this.#skillHint.setAttribute("part", "skill-hint");
6680
7394
  this.#skillHint.hidden = true;
6681
7395
  this.#attachButton.className = "attach-btn";
6682
7396
  this.#attachButton.type = "button";
@@ -6796,6 +7510,9 @@ Use the read_attachment tool with an id to read a file's contents.`
6796
7510
  this.#send.dataset["state"] = running ? "running" : "idle";
6797
7511
  }
6798
7512
  async #submit() {
7513
+ if (this.#running) {
7514
+ return;
7515
+ }
6799
7516
  const content = this.#input.value.trim();
6800
7517
  const attachments = this.#attachTray?.readyRefs() ?? [];
6801
7518
  if (content === "" && attachments.length === 0) {
@@ -6841,6 +7558,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6841
7558
  getTools: () => this.getTools(),
6842
7559
  getContext: () => this.getContext(),
6843
7560
  executeTool: (call) => this.#executeTool(call),
7561
+ resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
6844
7562
  onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
6845
7563
  connectionLostMessage: this.#strings.connectionLost
6846
7564
  });
@@ -6913,6 +7631,45 @@ Use the read_attachment tool with an id to read a file's contents.`
6913
7631
  return { content: `Error: ${message}`, error: message };
6914
7632
  }
6915
7633
  }
7634
+ /**
7635
+ * Render an approval card per server-side-tool interrupt and collect the
7636
+ * user's decisions (approve → run it, deny → decline it).
7637
+ *
7638
+ * The run is suspended on these cards; a Stop while any is open aborts the
7639
+ * shared {@link #confirmAbort} controller, resolving every still-open card as
7640
+ * denied (and the client loop then sees the cancellation and stops). An
7641
+ * approved tool runs on the follow-up (resume) run and streams its result
7642
+ * back into the same pending card; a denied one is settled here, since no
7643
+ * result will ever arrive for it.
7644
+ */
7645
+ async #resolveInterrupts(interrupts) {
7646
+ const responses = {};
7647
+ this.#confirmAbort = new AbortController();
7648
+ this.#hidePending();
7649
+ for (const interrupt of interrupts) {
7650
+ const request = {};
7651
+ if (interrupt.message !== void 0) {
7652
+ request.message = interrupt.message;
7653
+ }
7654
+ const card = interrupt.toolCallId !== void 0 ? this.#toolCards.get(interrupt.toolCallId) : void 0;
7655
+ const toolName = card?.element.getAttribute("data-tool-name");
7656
+ if (toolName !== null && toolName !== void 0) {
7657
+ request.toolName = toolName;
7658
+ }
7659
+ const signal = this.#confirmAbort.signal;
7660
+ const approved = this.approvalRenderer !== null ? await this.approvalRenderer(request, { signal }) : await requestApproval(this.#ensureGroup(), request, { signal, strings: this.#strings });
7661
+ this.#updateEmptyState();
7662
+ this.#messages.scrollTop = this.#messages.scrollHeight;
7663
+ if (approved) {
7664
+ responses[interrupt.id] = { status: "resolved", payload: { approved: true } };
7665
+ } else {
7666
+ responses[interrupt.id] = { status: "cancelled" };
7667
+ card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
7668
+ }
7669
+ }
7670
+ this.#confirmAbort = null;
7671
+ return responses;
7672
+ }
6916
7673
  #handlers() {
6917
7674
  return {
6918
7675
  onRunStart: () => {
@@ -7113,7 +7870,7 @@ function setControlValue(el, value) {
7113
7870
  }
7114
7871
 
7115
7872
  // src/version.ts
7116
- var VERSION = "0.9.0";
7873
+ var VERSION = "0.11.0";
7117
7874
  export {
7118
7875
  AgUiChat,
7119
7876
  AgUiClient,
@@ -7156,7 +7913,9 @@ export {
7156
7913
  pressThenClick,
7157
7914
  prettifyToolName,
7158
7915
  renderMarkdown,
7916
+ requestApproval,
7159
7917
  requestConfirmation,
7918
+ requestQuestion,
7160
7919
  scrollIntoCenterView,
7161
7920
  selectControl,
7162
7921
  selectOption,