@artooi/ag-ui-web-component 0.10.0 → 0.12.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 (45) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/README.md +144 -7
  3. package/dist/ag-ui-web-component.bundle.js +237 -40
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/core/ag_ui_chat.d.ts +29 -0
  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/run_index.d.ts +50 -0
  10. package/dist/core/run_index.d.ts.map +1 -0
  11. package/dist/index.d.ts +5 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +942 -101
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/approval_card.d.ts +51 -0
  16. package/dist/ui/approval_card.d.ts.map +1 -0
  17. package/dist/ui/attachment_chips.d.ts.map +1 -1
  18. package/dist/ui/attachment_tray.d.ts.map +1 -1
  19. package/dist/ui/checkpoint_menu.d.ts +32 -0
  20. package/dist/ui/checkpoint_menu.d.ts.map +1 -0
  21. package/dist/ui/question_card.d.ts +52 -0
  22. package/dist/ui/question_card.d.ts.map +1 -0
  23. package/dist/ui/skills_menu.d.ts.map +1 -1
  24. package/dist/ui/styles.d.ts +1 -1
  25. package/dist/ui/styles.d.ts.map +1 -1
  26. package/dist/ui/thoughts_block.d.ts.map +1 -1
  27. package/dist/ui/thread_drawer.d.ts.map +1 -1
  28. package/dist/ui/ui_strings.d.ts +26 -0
  29. package/dist/ui/ui_strings.d.ts.map +1 -1
  30. package/package.json +4 -4
  31. package/src/core/ag_ui_chat.ts +263 -4
  32. package/src/core/agui_client.ts +86 -11
  33. package/src/core/run_index.ts +91 -0
  34. package/src/index.ts +16 -0
  35. package/src/ui/approval_card.ts +119 -0
  36. package/src/ui/attachment_chips.ts +5 -0
  37. package/src/ui/attachment_tray.ts +8 -0
  38. package/src/ui/checkpoint_menu.ts +153 -0
  39. package/src/ui/question_card.ts +216 -0
  40. package/src/ui/skills_menu.ts +6 -0
  41. package/src/ui/styles.ts +197 -0
  42. package/src/ui/thoughts_block.ts +1 -0
  43. package/src/ui/thread_drawer.ts +11 -0
  44. package/src/ui/ui_strings.ts +45 -0
  45. 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",
@@ -507,6 +451,11 @@ var DEFAULT_UI_STRINGS = {
507
451
  collapse: "Collapse",
508
452
  expand: "Expand",
509
453
  toggleTheme: "Toggle theme",
454
+ checkpoints: "Continue a run",
455
+ noCheckpoints: "Nothing to continue yet.",
456
+ resumeRun: "Resume",
457
+ forkRun: "Fork",
458
+ forkedRun: "branched",
510
459
  conversation: "Conversation",
511
460
  thinking: "Assistant is thinking\u2026",
512
461
  thoughts: "Thoughts",
@@ -537,6 +486,14 @@ var DEFAULT_UI_STRINGS = {
537
486
  confirmRun: "Run \u201C{tool}\u201D?",
538
487
  confirm: "Confirm",
539
488
  cancel: "Cancel",
489
+ approveAction: "Approve action",
490
+ approvalPrompt: "Approve this action?",
491
+ approve: "Approve",
492
+ deny: "Deny",
493
+ askUserAction: "Question",
494
+ otherOption: "Other\u2026",
495
+ answerPlaceholder: "Type your answer\u2026",
496
+ submit: "Submit",
540
497
  chats: "Chats",
541
498
  noConversations: "No conversations yet.",
542
499
  rename: "Rename",
@@ -568,7 +525,120 @@ function mergeUiStrings(overrides) {
568
525
  return merged;
569
526
  }
570
527
 
528
+ // src/ui/approval_card.ts
529
+ function actionButton(modifier, label) {
530
+ const button = document.createElement("button");
531
+ button.type = "button";
532
+ button.className = `approval-btn approval-btn--${modifier}`;
533
+ button.setAttribute("part", `approval-button approval-${modifier}`);
534
+ button.textContent = label;
535
+ return button;
536
+ }
537
+ function requestApproval(host, request, options = {}) {
538
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
539
+ return new Promise((resolve) => {
540
+ const card = document.createElement("div");
541
+ card.className = "approval";
542
+ card.setAttribute("part", "approval");
543
+ if (request.toolName !== void 0) {
544
+ card.setAttribute("data-tool-name", request.toolName);
545
+ }
546
+ card.setAttribute("role", "group");
547
+ card.setAttribute("aria-label", strings.approveAction);
548
+ const body = document.createElement("div");
549
+ body.className = "approval-body";
550
+ body.setAttribute("part", "approval-body");
551
+ body.textContent = request.message ?? strings.approvalPrompt;
552
+ const actions = document.createElement("div");
553
+ actions.className = "approval-actions";
554
+ actions.setAttribute("part", "approval-actions");
555
+ const deny = actionButton("deny", strings.deny);
556
+ const approve = actionButton("approve", strings.approve);
557
+ let settled = false;
558
+ const close = (approved) => {
559
+ if (settled) {
560
+ return;
561
+ }
562
+ settled = true;
563
+ deny.disabled = true;
564
+ approve.disabled = true;
565
+ card.setAttribute("data-resolved", approved ? "approved" : "denied");
566
+ resolve(approved);
567
+ };
568
+ deny.addEventListener("click", () => close(false));
569
+ approve.addEventListener("click", () => close(true));
570
+ options.signal?.addEventListener("abort", () => close(false), { once: true });
571
+ actions.append(deny, approve);
572
+ card.append(body, actions);
573
+ host.appendChild(card);
574
+ if (options.signal?.aborted === true) {
575
+ close(false);
576
+ return;
577
+ }
578
+ approve.focus();
579
+ });
580
+ }
581
+
582
+ // src/ui/attachment_chips.ts
583
+ function renderAttachmentChips(refs) {
584
+ const list = document.createElement("div");
585
+ list.className = "attachment-chips";
586
+ list.setAttribute("part", "attachment-chips");
587
+ for (const ref of refs) {
588
+ list.appendChild(renderChip(ref));
589
+ }
590
+ return list;
591
+ }
592
+ function renderChip(ref) {
593
+ const chip = document.createElement("div");
594
+ chip.className = "attachment-chip attachment-chip--ready";
595
+ chip.setAttribute("part", "attachment-chip");
596
+ const icon = document.createElement("span");
597
+ icon.className = "attachment-chip-icon";
598
+ icon.setAttribute("part", "attachment-chip-icon");
599
+ icon.textContent = iconFor(ref.mime);
600
+ icon.setAttribute("aria-hidden", "true");
601
+ const name = document.createElement("span");
602
+ name.className = "attachment-chip-name";
603
+ name.setAttribute("part", "attachment-chip-name");
604
+ name.textContent = ref.name;
605
+ name.title = ref.name;
606
+ const size = document.createElement("span");
607
+ size.className = "attachment-chip-size";
608
+ size.setAttribute("part", "attachment-chip-size");
609
+ size.textContent = formatBytes(ref.size);
610
+ chip.append(icon, name, size);
611
+ return chip;
612
+ }
613
+ function iconFor(mime) {
614
+ if (mime.startsWith("image/")) {
615
+ return "\u{1F5BC}";
616
+ }
617
+ if (mime === "application/pdf") {
618
+ return "\u{1F4D5}";
619
+ }
620
+ if (mime.startsWith("text/")) {
621
+ return "\u{1F4C4}";
622
+ }
623
+ return "\u{1F4CE}";
624
+ }
625
+ function formatBytes(bytes) {
626
+ if (bytes < 1024) {
627
+ return `${bytes} B`;
628
+ }
629
+ const units = ["KB", "MB", "GB"];
630
+ let value = bytes / 1024;
631
+ let unit = 0;
632
+ while (value >= 1024 && unit < units.length - 1) {
633
+ value /= 1024;
634
+ unit += 1;
635
+ }
636
+ const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
637
+ return `${rounded} ${units[unit]}`;
638
+ }
639
+
571
640
  // src/ui/attachment_tray.ts
641
+ import { randomUUID } from "@ag-ui/client";
572
642
  var AttachmentTray = class {
573
643
  /** The tray root; append above the input row. Hidden while empty. */
574
644
  element;
@@ -708,23 +778,29 @@ var AttachmentTray = class {
708
778
  #renderChip(item) {
709
779
  const chip = document.createElement("div");
710
780
  chip.className = `attachment-chip attachment-chip--${item.status}`;
781
+ chip.setAttribute("part", "attachment-chip");
711
782
  const icon = document.createElement("span");
712
783
  icon.className = "attachment-chip-icon";
784
+ icon.setAttribute("part", "attachment-chip-icon");
713
785
  icon.textContent = iconFor(item.file.type);
714
786
  icon.setAttribute("aria-hidden", "true");
715
787
  const name = document.createElement("span");
716
788
  name.className = "attachment-chip-name";
789
+ name.setAttribute("part", "attachment-chip-name");
717
790
  name.textContent = item.file.name;
718
791
  name.title = item.file.name;
719
792
  const meta = document.createElement("span");
720
793
  meta.className = "attachment-chip-size";
794
+ meta.setAttribute("part", "attachment-chip-size");
721
795
  meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
722
796
  chip.append(icon, name, meta);
723
797
  if (item.status === ATTACHMENT_STATUS.UPLOADING) {
724
798
  const bar = document.createElement("div");
725
799
  bar.className = "attachment-chip-bar";
800
+ bar.setAttribute("part", "attachment-chip-bar");
726
801
  const fill = document.createElement("div");
727
802
  fill.className = "attachment-chip-bar-fill";
803
+ fill.setAttribute("part", "attachment-chip-bar-fill");
728
804
  fill.style.width = `${Math.round(item.progress * 100)}%`;
729
805
  bar.appendChild(fill);
730
806
  chip.appendChild(bar);
@@ -733,6 +809,7 @@ var AttachmentTray = class {
733
809
  const retry = document.createElement("button");
734
810
  retry.type = "button";
735
811
  retry.className = "attachment-chip-retry";
812
+ retry.setAttribute("part", "attachment-chip-retry");
736
813
  retry.title = this.#strings.retry;
737
814
  retry.setAttribute("aria-label", this.#strings.retryUpload);
738
815
  retry.textContent = "\u21BB";
@@ -742,6 +819,7 @@ var AttachmentTray = class {
742
819
  const remove = document.createElement("button");
743
820
  remove.type = "button";
744
821
  remove.className = "attachment-chip-remove";
822
+ remove.setAttribute("part", "attachment-chip-remove");
745
823
  remove.title = this.#strings.remove;
746
824
  remove.setAttribute("aria-label", this.#strings.removeAttachment);
747
825
  remove.textContent = "\u2715";
@@ -768,8 +846,140 @@ function accepts(accept, file) {
768
846
  });
769
847
  }
770
848
 
849
+ // src/ui/relative_time.ts
850
+ function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
851
+ if (!Number.isFinite(timestamp)) {
852
+ return strings.justNow;
853
+ }
854
+ const seconds = Math.round((now - timestamp) / 1e3);
855
+ if (seconds < 60) {
856
+ return strings.justNow;
857
+ }
858
+ const minutes = Math.round(seconds / 60);
859
+ if (minutes < 60) {
860
+ return strings.minutesAgo.replace("{n}", String(minutes));
861
+ }
862
+ const hours = Math.round(minutes / 60);
863
+ if (hours < 24) {
864
+ return strings.hoursAgo.replace("{n}", String(hours));
865
+ }
866
+ const days = Math.round(hours / 24);
867
+ if (days < 7) {
868
+ return strings.daysAgo.replace("{n}", String(days));
869
+ }
870
+ return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
871
+ }
872
+
873
+ // src/ui/checkpoint_menu.ts
874
+ var CheckpointMenu = class {
875
+ /** The panel root. Append to the chat shell; hidden until opened. */
876
+ element;
877
+ #onPick;
878
+ #list;
879
+ #heading;
880
+ #strings;
881
+ #runs = [];
882
+ constructor(onPick, strings = DEFAULT_UI_STRINGS) {
883
+ this.#onPick = onPick;
884
+ this.#strings = strings;
885
+ this.element = document.createElement("div");
886
+ this.element.className = "checkpoints";
887
+ this.element.setAttribute("part", "checkpoints");
888
+ this.element.setAttribute("role", "dialog");
889
+ this.element.setAttribute("aria-label", strings.checkpoints);
890
+ this.element.hidden = true;
891
+ const header = document.createElement("div");
892
+ header.className = "checkpoints-header";
893
+ header.setAttribute("part", "checkpoints-header");
894
+ this.#heading = document.createElement("span");
895
+ this.#heading.className = "checkpoints-title";
896
+ this.#heading.textContent = strings.checkpoints;
897
+ header.append(this.#heading);
898
+ this.#list = document.createElement("div");
899
+ this.#list.className = "checkpoints-list";
900
+ this.#list.setAttribute("part", "checkpoints-list");
901
+ this.element.append(header, this.#list);
902
+ this.element.addEventListener("keydown", (event) => {
903
+ if (event.key === "Escape") {
904
+ event.stopPropagation();
905
+ this.close();
906
+ }
907
+ });
908
+ }
909
+ /** Replace the rows. The host passes only `continuable` runs. */
910
+ setRuns(runs) {
911
+ this.#runs = runs;
912
+ this.#render();
913
+ }
914
+ /** Re-localize a panel built before the host's strings resolved. */
915
+ setStrings(strings) {
916
+ this.#strings = strings;
917
+ this.element.setAttribute("aria-label", strings.checkpoints);
918
+ this.#heading.textContent = strings.checkpoints;
919
+ this.#render();
920
+ }
921
+ open() {
922
+ this.element.hidden = false;
923
+ }
924
+ close() {
925
+ this.element.hidden = true;
926
+ }
927
+ get open_() {
928
+ return !this.element.hidden;
929
+ }
930
+ #render() {
931
+ this.#list.replaceChildren();
932
+ if (this.#runs.length === 0) {
933
+ const empty = document.createElement("div");
934
+ empty.className = "checkpoints-empty";
935
+ empty.setAttribute("part", "checkpoints-empty");
936
+ empty.textContent = this.#strings.noCheckpoints;
937
+ this.#list.append(empty);
938
+ return;
939
+ }
940
+ for (const run of this.#runs) {
941
+ this.#list.append(this.#row(run));
942
+ }
943
+ }
944
+ #row(run) {
945
+ const row = document.createElement("div");
946
+ row.className = "checkpoint-row";
947
+ row.setAttribute("part", "checkpoint-row");
948
+ const label = document.createElement("span");
949
+ label.className = "checkpoint-label";
950
+ label.textContent = run.started_at === null ? run.run_id : relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
951
+ label.title = run.run_id;
952
+ row.append(label);
953
+ if (run.parent_run_id !== null) {
954
+ const branch = document.createElement("span");
955
+ branch.className = "checkpoint-branch";
956
+ branch.setAttribute("part", "checkpoint-branch");
957
+ branch.textContent = this.#strings.forkedRun;
958
+ branch.title = run.parent_run_id;
959
+ row.append(branch);
960
+ }
961
+ row.append(
962
+ this.#action(run.run_id, "resume", this.#strings.resumeRun),
963
+ this.#action(run.run_id, "fork", this.#strings.forkRun)
964
+ );
965
+ return row;
966
+ }
967
+ #action(runId, verb, label) {
968
+ const button = document.createElement("button");
969
+ button.type = "button";
970
+ button.className = `checkpoint-action checkpoint-${verb}`;
971
+ button.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
972
+ button.textContent = label;
973
+ button.addEventListener("click", () => {
974
+ this.close();
975
+ this.#onPick(runId, verb);
976
+ });
977
+ return button;
978
+ }
979
+ };
980
+
771
981
  // src/ui/confirmation_card.ts
772
- function actionButton(modifier, label) {
982
+ function actionButton2(modifier, label) {
773
983
  const button = document.createElement("button");
774
984
  button.type = "button";
775
985
  button.className = `confirm-btn confirm-btn--${modifier}`;
@@ -797,8 +1007,8 @@ function requestConfirmation(host, request, options = {}) {
797
1007
  const actions = document.createElement("div");
798
1008
  actions.className = "confirm-actions";
799
1009
  actions.setAttribute("part", "confirm-actions");
800
- const cancel = actionButton("cancel", strings.cancel);
801
- const confirm = actionButton("confirm", strings.confirm);
1010
+ const cancel = actionButton2("cancel", strings.cancel);
1011
+ const confirm = actionButton2("confirm", strings.confirm);
802
1012
  let settled = false;
803
1013
  const close = (accepted) => {
804
1014
  if (settled) {
@@ -833,6 +1043,149 @@ function prettifyToolName(name) {
833
1043
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
834
1044
  }
835
1045
 
1046
+ // src/ui/question_card.ts
1047
+ function answerInput(placeholder) {
1048
+ const input = document.createElement("input");
1049
+ input.type = "text";
1050
+ input.className = "question-input";
1051
+ input.setAttribute("part", "question-input");
1052
+ input.placeholder = placeholder;
1053
+ return input;
1054
+ }
1055
+ function requestQuestion(host, request, options = {}) {
1056
+ const strings = options.strings ?? DEFAULT_UI_STRINGS;
1057
+ const choices = request.options ?? [];
1058
+ const hasChoices = choices.length > 0;
1059
+ const allowsText = !hasChoices || request.allowCustom === true;
1060
+ return new Promise((resolve) => {
1061
+ const card = document.createElement("div");
1062
+ card.className = "question";
1063
+ card.setAttribute("part", "question");
1064
+ card.setAttribute("role", "group");
1065
+ card.setAttribute("aria-label", strings.askUserAction);
1066
+ const body = document.createElement("div");
1067
+ body.className = "question-body";
1068
+ body.setAttribute("part", "question-body");
1069
+ body.textContent = request.question;
1070
+ const form = document.createElement("div");
1071
+ form.className = "question-options";
1072
+ form.setAttribute("part", "question-options");
1073
+ const group = `q-${choices.length}-${request.question.length}`;
1074
+ const radios = [];
1075
+ for (const choice of choices) {
1076
+ const label = document.createElement("label");
1077
+ label.className = "question-choice";
1078
+ label.setAttribute("part", "question-choice");
1079
+ const radio = document.createElement("input");
1080
+ radio.type = "radio";
1081
+ radio.name = group;
1082
+ radio.value = choice;
1083
+ radio.setAttribute("part", "question-radio");
1084
+ const text2 = document.createElement("span");
1085
+ text2.setAttribute("part", "question-choice-text");
1086
+ text2.textContent = choice;
1087
+ label.append(radio, text2);
1088
+ form.appendChild(label);
1089
+ radios.push(radio);
1090
+ }
1091
+ let otherRadio = null;
1092
+ let input = null;
1093
+ if (allowsText) {
1094
+ input = answerInput(strings.answerPlaceholder);
1095
+ if (hasChoices) {
1096
+ const label = document.createElement("label");
1097
+ label.className = "question-choice";
1098
+ label.setAttribute("part", "question-choice");
1099
+ otherRadio = document.createElement("input");
1100
+ otherRadio.type = "radio";
1101
+ otherRadio.name = group;
1102
+ otherRadio.value = "";
1103
+ otherRadio.setAttribute("part", "question-radio");
1104
+ const text2 = document.createElement("span");
1105
+ text2.setAttribute("part", "question-choice-text");
1106
+ text2.textContent = strings.otherOption;
1107
+ label.append(otherRadio, text2);
1108
+ form.appendChild(label);
1109
+ input.disabled = true;
1110
+ }
1111
+ form.appendChild(input);
1112
+ }
1113
+ const actions = document.createElement("div");
1114
+ actions.className = "question-actions";
1115
+ actions.setAttribute("part", "question-actions");
1116
+ const submit = document.createElement("button");
1117
+ submit.type = "button";
1118
+ submit.className = "question-btn";
1119
+ submit.setAttribute("part", "question-button");
1120
+ submit.textContent = strings.submit;
1121
+ actions.appendChild(submit);
1122
+ let settled = false;
1123
+ const answerFor = () => {
1124
+ const picked = radios.find((r) => r.checked);
1125
+ if (picked !== void 0) {
1126
+ return picked.value;
1127
+ }
1128
+ if (input !== null && (otherRadio === null || otherRadio.checked)) {
1129
+ const typed = input.value.trim();
1130
+ return typed === "" ? null : typed;
1131
+ }
1132
+ return null;
1133
+ };
1134
+ const refresh = () => {
1135
+ if (input !== null && otherRadio !== null) {
1136
+ input.disabled = !otherRadio.checked;
1137
+ }
1138
+ submit.disabled = answerFor() === null;
1139
+ };
1140
+ const close = (answer) => {
1141
+ if (settled) {
1142
+ return;
1143
+ }
1144
+ settled = true;
1145
+ submit.disabled = true;
1146
+ for (const radio of radios) {
1147
+ radio.disabled = true;
1148
+ }
1149
+ if (otherRadio !== null) {
1150
+ otherRadio.disabled = true;
1151
+ }
1152
+ if (input !== null) {
1153
+ input.disabled = true;
1154
+ }
1155
+ card.setAttribute("data-resolved", answer === "" ? "cancelled" : "answered");
1156
+ resolve(answer);
1157
+ };
1158
+ for (const radio of [...radios, ...otherRadio !== null ? [otherRadio] : []]) {
1159
+ radio.addEventListener("change", refresh);
1160
+ }
1161
+ input?.addEventListener("input", refresh);
1162
+ input?.addEventListener("keydown", (event) => {
1163
+ if (event.key === "Enter") {
1164
+ event.preventDefault();
1165
+ const answer = answerFor();
1166
+ if (answer !== null) {
1167
+ close(answer);
1168
+ }
1169
+ }
1170
+ });
1171
+ submit.addEventListener("click", () => {
1172
+ const answer = answerFor();
1173
+ if (answer !== null) {
1174
+ close(answer);
1175
+ }
1176
+ });
1177
+ options.signal?.addEventListener("abort", () => close(""), { once: true });
1178
+ card.append(body, form, actions);
1179
+ host.appendChild(card);
1180
+ if (options.signal?.aborted === true) {
1181
+ close("");
1182
+ return;
1183
+ }
1184
+ refresh();
1185
+ (hasChoices ? radios[0] : input)?.focus();
1186
+ });
1187
+ }
1188
+
836
1189
  // node_modules/.pnpm/dompurify@3.4.7/node_modules/dompurify/dist/purify.es.mjs
837
1190
  function _arrayLikeToArray(r, a) {
838
1191
  (null == a || a > r.length) && (a = r.length);
@@ -2033,7 +2386,7 @@ function createDOMPurify() {
2033
2386
  }
2034
2387
  var purify = createDOMPurify();
2035
2388
 
2036
- // node_modules/.pnpm/marked@18.0.4/node_modules/marked/lib/marked.esm.js
2389
+ // node_modules/.pnpm/marked@18.0.5/node_modules/marked/lib/marked.esm.js
2037
2390
  function M() {
2038
2391
  return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
2039
2392
  }
@@ -2077,15 +2430,15 @@ var F = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|
2077
2430
  var $e = /^[^\n]+/;
2078
2431
  var U = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
2079
2432
  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();
2080
- var _e = d(/^(bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g, j).getRegex();
2433
+ var _e = d(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g, j).getRegex();
2081
2434
  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";
2082
2435
  var K = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
2083
2436
  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();
2084
- 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();
2437
+ 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();
2085
2438
  var Me = d(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", le).getRegex();
2086
2439
  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 };
2087
2440
  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();
2088
- 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() };
2441
+ 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() };
2089
2442
  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} *[^
2090
2443
  ]`).replace("lheading", ae).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() };
2091
2444
  var Ae = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/;
@@ -3376,9 +3729,11 @@ var SkillsMenu = class {
3376
3729
  this.#onPick = onPick;
3377
3730
  this.chips = document.createElement("div");
3378
3731
  this.chips.className = "skill-chips";
3732
+ this.chips.setAttribute("part", "skill-chips");
3379
3733
  this.chips.hidden = true;
3380
3734
  this.palette = document.createElement("div");
3381
3735
  this.palette.className = "skill-palette";
3736
+ this.palette.setAttribute("part", "skill-palette");
3382
3737
  this.palette.setAttribute("role", "listbox");
3383
3738
  this.palette.hidden = true;
3384
3739
  }
@@ -3473,6 +3828,7 @@ var SkillsMenu = class {
3473
3828
  const button = document.createElement("button");
3474
3829
  button.type = "button";
3475
3830
  button.className = "skill-chip";
3831
+ button.setAttribute("part", "skill-chip");
3476
3832
  button.textContent = skill.title;
3477
3833
  button.addEventListener("click", () => this.#pick(skill));
3478
3834
  this.chips.appendChild(button);
@@ -3484,15 +3840,18 @@ var SkillsMenu = class {
3484
3840
  const item = document.createElement("button");
3485
3841
  item.type = "button";
3486
3842
  item.className = "skill-item";
3843
+ item.setAttribute("part", "skill-item");
3487
3844
  item.setAttribute("role", "option");
3488
3845
  item.setAttribute("aria-selected", index === this.#activeIndex ? "true" : "false");
3489
3846
  const title = document.createElement("span");
3490
3847
  title.className = "skill-item-title";
3848
+ title.setAttribute("part", "skill-item-title");
3491
3849
  title.textContent = skill.title;
3492
3850
  item.appendChild(title);
3493
3851
  if (skill.description !== void 0) {
3494
3852
  const desc = document.createElement("span");
3495
3853
  desc.className = "skill-item-desc";
3854
+ desc.setAttribute("part", "skill-item-desc");
3496
3855
  desc.textContent = skill.description;
3497
3856
  item.appendChild(desc);
3498
3857
  }
@@ -4529,10 +4888,131 @@ var STYLES = `
4529
4888
  color: #ffffff;
4530
4889
  }
4531
4890
 
4532
- /* Skills \u2014 chips row + the /-command palette, above the input. */
4533
- .skill-chips {
4891
+ /* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */
4892
+ .approval {
4893
+ align-self: stretch;
4894
+ box-sizing: border-box;
4534
4895
  display: flex;
4535
- flex-wrap: wrap;
4896
+ flex-direction: column;
4897
+ gap: 8px;
4898
+ padding: 12px;
4899
+ background: var(--ag-ui-bg);
4900
+ border: 1px solid var(--ag-ui-accent);
4901
+ border-radius: 10px;
4902
+ }
4903
+
4904
+ .approval[data-resolved] {
4905
+ opacity: 0.7;
4906
+ border-color: var(--ag-ui-border);
4907
+ }
4908
+
4909
+ .approval-body {
4910
+ font-weight: 600;
4911
+ }
4912
+
4913
+ .approval-actions {
4914
+ display: flex;
4915
+ gap: 8px;
4916
+ justify-content: flex-end;
4917
+ }
4918
+
4919
+ .approval-btn {
4920
+ border: 1px solid var(--ag-ui-border);
4921
+ border-radius: 8px;
4922
+ padding: 8px 14px;
4923
+ font: inherit;
4924
+ font-weight: 600;
4925
+ cursor: pointer;
4926
+ background: var(--ag-ui-bg);
4927
+ color: var(--ag-ui-fg);
4928
+ }
4929
+
4930
+ .approval-btn:disabled {
4931
+ cursor: default;
4932
+ opacity: 0.6;
4933
+ }
4934
+
4935
+ .approval-btn--approve {
4936
+ border-color: var(--ag-ui-accent);
4937
+ background: var(--ag-ui-accent);
4938
+ color: #ffffff;
4939
+ }
4940
+
4941
+ /* Question card \u2014 the built-in ask_user prompt (radios and/or free text). */
4942
+ .question {
4943
+ align-self: stretch;
4944
+ box-sizing: border-box;
4945
+ display: flex;
4946
+ flex-direction: column;
4947
+ gap: 8px;
4948
+ padding: 12px;
4949
+ background: var(--ag-ui-bg);
4950
+ border: 1px solid var(--ag-ui-accent);
4951
+ border-radius: 10px;
4952
+ }
4953
+
4954
+ .question[data-resolved] {
4955
+ opacity: 0.7;
4956
+ border-color: var(--ag-ui-border);
4957
+ }
4958
+
4959
+ .question-body {
4960
+ font-weight: 600;
4961
+ }
4962
+
4963
+ .question-options {
4964
+ display: flex;
4965
+ flex-direction: column;
4966
+ gap: 6px;
4967
+ }
4968
+
4969
+ .question-choice {
4970
+ display: flex;
4971
+ align-items: center;
4972
+ gap: 8px;
4973
+ cursor: pointer;
4974
+ }
4975
+
4976
+ .question-input {
4977
+ box-sizing: border-box;
4978
+ width: 100%;
4979
+ padding: 8px 10px;
4980
+ font: inherit;
4981
+ color: var(--ag-ui-fg);
4982
+ background: var(--ag-ui-bg);
4983
+ border: 1px solid var(--ag-ui-border);
4984
+ border-radius: 8px;
4985
+ }
4986
+
4987
+ .question-input:disabled {
4988
+ opacity: 0.6;
4989
+ }
4990
+
4991
+ .question-actions {
4992
+ display: flex;
4993
+ justify-content: flex-end;
4994
+ }
4995
+
4996
+ .question-btn {
4997
+ border: 1px solid var(--ag-ui-accent);
4998
+ border-radius: 8px;
4999
+ padding: 8px 14px;
5000
+ font: inherit;
5001
+ font-weight: 600;
5002
+ cursor: pointer;
5003
+ background: var(--ag-ui-accent);
5004
+ color: #ffffff;
5005
+ }
5006
+
5007
+ .question-btn:disabled {
5008
+ cursor: default;
5009
+ opacity: 0.6;
5010
+ }
5011
+
5012
+ /* Skills \u2014 chips row + the /-command palette, above the input. */
5013
+ .skill-chips {
5014
+ display: flex;
5015
+ flex-wrap: wrap;
4536
5016
  gap: 6px;
4537
5017
  padding: 10px 12px;
4538
5018
  }
@@ -4609,6 +5089,82 @@ var STYLES = `
4609
5089
  display: none;
4610
5090
  }
4611
5091
 
5092
+ .checkpoints {
5093
+ position: absolute;
5094
+ inset-block-start: 3rem;
5095
+ inset-inline: 0.75rem;
5096
+ z-index: 6;
5097
+ display: flex;
5098
+ flex-direction: column;
5099
+ gap: 0.25rem;
5100
+ padding: 0.5rem;
5101
+ border: 1px solid var(--agui-border, #d7d7dc);
5102
+ border-radius: 0.5rem;
5103
+ background: var(--agui-surface, #fff);
5104
+ box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
5105
+ max-height: 60%;
5106
+ overflow-y: auto;
5107
+ }
5108
+
5109
+ .checkpoints[hidden] {
5110
+ display: none;
5111
+ }
5112
+
5113
+ .checkpoints-title {
5114
+ font-size: 0.75rem;
5115
+ font-weight: 600;
5116
+ opacity: 0.7;
5117
+ }
5118
+
5119
+ .checkpoints-empty {
5120
+ padding: 0.5rem 0.25rem;
5121
+ font-size: 0.8125rem;
5122
+ opacity: 0.7;
5123
+ }
5124
+
5125
+ .checkpoint-row {
5126
+ display: flex;
5127
+ align-items: center;
5128
+ gap: 0.5rem;
5129
+ padding: 0.25rem;
5130
+ border-radius: 0.375rem;
5131
+ }
5132
+
5133
+ .checkpoint-row:hover {
5134
+ background: var(--agui-hover, #f3f3f5);
5135
+ }
5136
+
5137
+ .checkpoint-label {
5138
+ flex: 1;
5139
+ font-size: 0.8125rem;
5140
+ white-space: nowrap;
5141
+ overflow: hidden;
5142
+ text-overflow: ellipsis;
5143
+ }
5144
+
5145
+ .checkpoint-branch {
5146
+ font-size: 0.6875rem;
5147
+ padding: 0 0.375rem;
5148
+ border-radius: 999px;
5149
+ background: var(--agui-hover, #f3f3f5);
5150
+ opacity: 0.8;
5151
+ }
5152
+
5153
+ .checkpoint-action {
5154
+ font: inherit;
5155
+ font-size: 0.75rem;
5156
+ cursor: pointer;
5157
+ padding: 0.125rem 0.5rem;
5158
+ border: 1px solid var(--agui-border, #d7d7dc);
5159
+ border-radius: 0.375rem;
5160
+ background: transparent;
5161
+ color: inherit;
5162
+ }
5163
+
5164
+ .checkpoint-action:hover {
5165
+ background: var(--agui-hover, #f3f3f5);
5166
+ }
5167
+
4612
5168
  .drawer-backdrop {
4613
5169
  position: absolute;
4614
5170
  inset: 0;
@@ -4804,6 +5360,7 @@ var ThoughtsBlock = class {
4804
5360
  this.#toggle.setAttribute("aria-expanded", "true");
4805
5361
  this.#label = document.createElement("span");
4806
5362
  this.#label.className = "thoughts-label";
5363
+ this.#label.setAttribute("part", "thoughts-label");
4807
5364
  this.#label.textContent = strings.thinking;
4808
5365
  this.#toggle.append(this.#label);
4809
5366
  this.#body = document.createElement("pre");
@@ -4838,30 +5395,6 @@ var ThoughtsBlock = class {
4838
5395
  }
4839
5396
  };
4840
5397
 
4841
- // src/ui/relative_time.ts
4842
- function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
4843
- if (!Number.isFinite(timestamp)) {
4844
- return strings.justNow;
4845
- }
4846
- const seconds = Math.round((now - timestamp) / 1e3);
4847
- if (seconds < 60) {
4848
- return strings.justNow;
4849
- }
4850
- const minutes = Math.round(seconds / 60);
4851
- if (minutes < 60) {
4852
- return strings.minutesAgo.replace("{n}", String(minutes));
4853
- }
4854
- const hours = Math.round(minutes / 60);
4855
- if (hours < 24) {
4856
- return strings.hoursAgo.replace("{n}", String(hours));
4857
- }
4858
- const days = Math.round(hours / 24);
4859
- if (days < 7) {
4860
- return strings.daysAgo.replace("{n}", String(days));
4861
- }
4862
- return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
4863
- }
4864
-
4865
5398
  // src/ui/thread_drawer.ts
4866
5399
  var ThreadDrawer = class {
4867
5400
  /** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
@@ -5012,12 +5545,15 @@ var ThreadDrawer = class {
5012
5545
  select.setAttribute("part", "drawer-row-select");
5013
5546
  const title = document.createElement("span");
5014
5547
  title.className = "drawer-row-title";
5548
+ title.setAttribute("part", "drawer-row-title");
5015
5549
  title.textContent = meta.title;
5016
5550
  const time = document.createElement("span");
5017
5551
  time.className = "drawer-row-time";
5552
+ time.setAttribute("part", "drawer-row-time");
5018
5553
  time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
5019
5554
  const preview = document.createElement("span");
5020
5555
  preview.className = "drawer-row-preview";
5556
+ preview.setAttribute("part", "drawer-row-preview");
5021
5557
  preview.textContent = meta.preview;
5022
5558
  select.append(title, time, preview);
5023
5559
  select.addEventListener("click", () => {
@@ -5027,6 +5563,7 @@ var ThreadDrawer = class {
5027
5563
  const rename = document.createElement("button");
5028
5564
  rename.type = "button";
5029
5565
  rename.className = "drawer-row-rename";
5566
+ rename.setAttribute("part", "drawer-row-rename");
5030
5567
  rename.title = this.#strings.rename;
5031
5568
  rename.setAttribute("aria-label", this.#strings.renameConversation);
5032
5569
  rename.textContent = "\u270E";
@@ -5034,12 +5571,14 @@ var ThreadDrawer = class {
5034
5571
  const remove = document.createElement("button");
5035
5572
  remove.type = "button";
5036
5573
  remove.className = "drawer-row-delete";
5574
+ remove.setAttribute("part", "drawer-row-delete");
5037
5575
  remove.title = this.#strings.delete;
5038
5576
  remove.setAttribute("aria-label", this.#strings.deleteConversation);
5039
5577
  remove.textContent = "\u{1F5D1}";
5040
5578
  remove.addEventListener("click", () => this.#confirmDelete(row, meta));
5041
5579
  const actions = document.createElement("div");
5042
5580
  actions.className = "drawer-row-actions";
5581
+ actions.setAttribute("part", "drawer-row-actions");
5043
5582
  actions.append(rename, remove);
5044
5583
  row.append(select, actions);
5045
5584
  return row;
@@ -5049,6 +5588,7 @@ var ThreadDrawer = class {
5049
5588
  const input = document.createElement("input");
5050
5589
  input.type = "text";
5051
5590
  input.className = "drawer-rename-input";
5591
+ input.setAttribute("part", "drawer-rename-input");
5052
5592
  input.value = meta.title;
5053
5593
  let done = false;
5054
5594
  const commit = () => {
@@ -5089,17 +5629,21 @@ var ThreadDrawer = class {
5089
5629
  #confirmDelete(row, meta) {
5090
5630
  const confirm = document.createElement("div");
5091
5631
  confirm.className = "drawer-confirm";
5632
+ confirm.setAttribute("part", "drawer-confirm");
5092
5633
  const label = document.createElement("span");
5093
5634
  label.className = "drawer-confirm-label";
5635
+ label.setAttribute("part", "drawer-confirm-label");
5094
5636
  label.textContent = this.#strings.deletePrompt;
5095
5637
  const yes = document.createElement("button");
5096
5638
  yes.type = "button";
5097
5639
  yes.className = "drawer-confirm-yes";
5640
+ yes.setAttribute("part", "drawer-confirm-yes");
5098
5641
  yes.textContent = this.#strings.delete;
5099
5642
  yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
5100
5643
  const no = document.createElement("button");
5101
5644
  no.type = "button";
5102
5645
  no.className = "drawer-confirm-no";
5646
+ no.setAttribute("part", "drawer-confirm-no");
5103
5647
  no.textContent = this.#strings.cancel;
5104
5648
  no.addEventListener("click", () => this.#renderList());
5105
5649
  confirm.append(label, yes, no);
@@ -5341,7 +5885,10 @@ var VoiceInput = class {
5341
5885
  };
5342
5886
 
5343
5887
  // src/core/agui_client.ts
5344
- import { randomUUID as randomUUID2 } from "@ag-ui/client";
5888
+ import {
5889
+ buildResumeArray,
5890
+ randomUUID as randomUUID2
5891
+ } from "@ag-ui/client";
5345
5892
  var ConnectionLostError = class extends Error {
5346
5893
  constructor(message) {
5347
5894
  super(message);
@@ -5354,6 +5901,7 @@ var AgUiClient = class {
5354
5901
  #getTools;
5355
5902
  #getContext;
5356
5903
  #executeTool;
5904
+ #resolveInterrupts;
5357
5905
  #onPersist;
5358
5906
  #connectionLostMessage;
5359
5907
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
@@ -5365,6 +5913,7 @@ var AgUiClient = class {
5365
5913
  this.#getTools = config.getTools ?? (() => []);
5366
5914
  this.#getContext = config.getContext ?? (() => []);
5367
5915
  this.#executeTool = config.executeTool ?? null;
5916
+ this.#resolveInterrupts = config.resolveInterrupts ?? null;
5368
5917
  this.#onPersist = config.onPersist ?? (() => {
5369
5918
  });
5370
5919
  this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
@@ -5444,16 +5993,22 @@ var AgUiClient = class {
5444
5993
  this.#handlers.onCancelled();
5445
5994
  }
5446
5995
  async #runLoop() {
5996
+ let resume;
5447
5997
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
5448
5998
  if (this.#cancelled) {
5449
5999
  return;
5450
6000
  }
5451
6001
  const pending = [];
5452
- const runState = { terminal: false, errored: false };
5453
- await this.#agent.runAgent(
5454
- { tools: this.#getTools(), context: this.#getContext() },
5455
- this.#buildSubscriber(pending, runState)
5456
- );
6002
+ const runState = { terminal: false, errored: false, interrupts: [] };
6003
+ const params = {
6004
+ tools: this.#getTools(),
6005
+ context: this.#getContext()
6006
+ };
6007
+ if (resume !== void 0) {
6008
+ params.resume = resume;
6009
+ }
6010
+ await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
6011
+ resume = void 0;
5457
6012
  this.#onPersist(this.#agent.messages);
5458
6013
  if (this.#cancelled) {
5459
6014
  return;
@@ -5464,6 +6019,17 @@ var AgUiClient = class {
5464
6019
  if (runState.errored) {
5465
6020
  return;
5466
6021
  }
6022
+ if (runState.interrupts.length > 0) {
6023
+ if (this.#resolveInterrupts === null) {
6024
+ return;
6025
+ }
6026
+ const responses = await this.#resolveInterrupts(runState.interrupts);
6027
+ if (this.#cancelled) {
6028
+ return;
6029
+ }
6030
+ resume = buildResumeArray(runState.interrupts, responses);
6031
+ continue;
6032
+ }
5467
6033
  if (this.#executeTool === null || pending.length === 0) {
5468
6034
  return;
5469
6035
  }
@@ -5526,6 +6092,12 @@ var AgUiClient = class {
5526
6092
  onReasoningEndEvent() {
5527
6093
  h.onReasoningEnd();
5528
6094
  },
6095
+ onRunFinishedEvent(params) {
6096
+ runState.terminal = true;
6097
+ if (params.outcome === "interrupt") {
6098
+ runState.interrupts = params.interrupts;
6099
+ }
6100
+ },
5529
6101
  onRunErrorEvent({ event }) {
5530
6102
  runState.terminal = true;
5531
6103
  runState.errored = true;
@@ -5873,6 +6445,60 @@ var RemoteConversationStore = class {
5873
6445
  }
5874
6446
  };
5875
6447
 
6448
+ // src/core/run_index.ts
6449
+ var RunIndex = class {
6450
+ #url;
6451
+ #headers;
6452
+ constructor(url, headers = () => ({})) {
6453
+ this.#url = url.endsWith("/") ? url : `${url}/`;
6454
+ this.#headers = headers;
6455
+ }
6456
+ /**
6457
+ * The user's runs, or `[]` when the endpoint is unreachable or answers with
6458
+ * an error. A history affordance that cannot load is empty, never broken:
6459
+ * the caller renders its empty state rather than surfacing a transport fault
6460
+ * the user can do nothing about.
6461
+ */
6462
+ async list() {
6463
+ try {
6464
+ const response = await fetch(this.#url, {
6465
+ method: "GET",
6466
+ headers: { Accept: "application/json", ...this.#headers() }
6467
+ });
6468
+ if (!response.ok) {
6469
+ return [];
6470
+ }
6471
+ const body = await response.json();
6472
+ return body.runs ?? [];
6473
+ } catch {
6474
+ return [];
6475
+ }
6476
+ }
6477
+ /** The rows a client may actually continue. */
6478
+ async continuable() {
6479
+ return (await this.list()).filter((run) => run.continuable);
6480
+ }
6481
+ /** The endpoint that continues `runId` as a new run. */
6482
+ resumeUrl(runId) {
6483
+ return this.#sibling("resume", runId);
6484
+ }
6485
+ /** The endpoint that branches `runId` into a new run, leaving the source untouched. */
6486
+ forkUrl(runId) {
6487
+ return this.#sibling("fork", runId);
6488
+ }
6489
+ /**
6490
+ * `<mount>/<verb>/<runId>/`, derived from the index URL's own prefix.
6491
+ *
6492
+ * Built by string surgery on the trailing `runs/` rather than with `new URL`,
6493
+ * because the configured value may be root-relative (`/agent/runs/`) — the
6494
+ * common case in a Django template — and `new URL` needs an absolute base.
6495
+ */
6496
+ #sibling(verb, runId) {
6497
+ const prefix = this.#url.slice(0, -"runs/".length);
6498
+ return `${prefix}${verb}/${encodeURIComponent(runId)}/`;
6499
+ }
6500
+ };
6501
+
5876
6502
  // src/core/transcribe_audio.ts
5877
6503
  async function transcribeAudio(audio, options) {
5878
6504
  const form = new FormData();
@@ -5983,6 +6609,33 @@ var AgUiChat = class extends HTMLElement {
5983
6609
  allowImages = false;
5984
6610
  /** When true, destructive tools execute without a confirmation modal. */
5985
6611
  autoConfirm = false;
6612
+ /**
6613
+ * When true, the built-in `ask_user` frontend tool is offered to the agent:
6614
+ * calling it renders an inline question card (radio choices and/or a free-text
6615
+ * field) and returns the user's answer. Off by default — like the other
6616
+ * built-in tool groups (route / page-action), it is opt-in so it doesn't
6617
+ * change the advertised catalog until a host asks for it.
6618
+ */
6619
+ askUser = false;
6620
+ /**
6621
+ * Optional full replacement for the `ask_user` question UI. When set, calling
6622
+ * `ask_user` invokes this instead of the built-in inline card: the host
6623
+ * renders whatever it likes (a native modal, a framework component, …) and
6624
+ * resolves with the answer. Unset (default) uses the built-in
6625
+ * {@link requestQuestion} card — style that via the `strings` override and the
6626
+ * `question*` CSS `::part()`s. Requires {@link askUser} to be enabled.
6627
+ */
6628
+ askUserRenderer = null;
6629
+ /**
6630
+ * Optional full replacement for the server-side-tool approval UI. When set, an
6631
+ * approval interrupt invokes this instead of the built-in inline approval
6632
+ * card: the host renders whatever it likes and resolves `true` to approve /
6633
+ * `false` to deny. Unset (default) uses the built-in {@link requestApproval}
6634
+ * card — style that via the `strings` override and the `approval*` CSS
6635
+ * `::part()`s. The gate itself is enabled server-side; this only changes how
6636
+ * the decision is collected.
6637
+ */
6638
+ approvalRenderer = null;
5986
6639
  /**
5987
6640
  * Optional per-call confirmation predicate. When set, it is authoritative:
5988
6641
  * given a tool name + args it decides whether *this* call needs confirmation
@@ -6118,6 +6771,10 @@ var AgUiChat = class extends HTMLElement {
6118
6771
  #title;
6119
6772
  #skillsMenu;
6120
6773
  #drawer;
6774
+ /** Checkpoint panel; rows load only when `data-runs-url` is set. */
6775
+ #checkpoints;
6776
+ /** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
6777
+ #runIndex = null;
6121
6778
  #skillHint;
6122
6779
  /** File-picker button + hidden input + tray slot; the tray mounts on connect. */
6123
6780
  #attachButton;
@@ -6208,6 +6865,70 @@ var AgUiChat = class extends HTMLElement {
6208
6865
  this.#deleteThread(threadId);
6209
6866
  }
6210
6867
  });
6868
+ this.#checkpoints = new CheckpointMenu((runId, verb) => {
6869
+ void this.#continueRun(runId, verb);
6870
+ });
6871
+ }
6872
+ /** The run index, built once from `data-runs-url`; `null` when unset. */
6873
+ #runs() {
6874
+ const url = this.getAttribute("data-runs-url");
6875
+ if (url === null || url === "") {
6876
+ return null;
6877
+ }
6878
+ if (this.#runIndex === null) {
6879
+ this.#runIndex = new RunIndex(url, () => this.headers);
6880
+ }
6881
+ return this.#runIndex;
6882
+ }
6883
+ /**
6884
+ * Continue `runId` as a **new** run, seeded server-side from its snapshot.
6885
+ *
6886
+ * Uses a short-lived agent pointed at the resume / fork endpoint and seeded
6887
+ * with **no** history, so the request carries only the turn typed here — the
6888
+ * contract those endpoints assume, since the server supplies the prior turns
6889
+ * from the snapshot and re-sending them would duplicate. Building a separate
6890
+ * agent makes that structural: the main agent keeps its own history, and
6891
+ * "only the new turn" can't be got wrong by forgetting to clear it. The
6892
+ * fresh `run_id` the endpoints also require comes free — a new agent mints
6893
+ * one per run.
6894
+ *
6895
+ * Handlers are the element's own, so the continuation streams into the same
6896
+ * transcript the user is looking at.
6897
+ */
6898
+ async #continueRun(runId, verb) {
6899
+ const index = this.#runs();
6900
+ if (index === null) {
6901
+ return;
6902
+ }
6903
+ const content = this.#input.value.trim();
6904
+ if (content === "") {
6905
+ return;
6906
+ }
6907
+ this.#input.value = "";
6908
+ const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
6909
+ const agent = this.agentFactory({
6910
+ endpoint,
6911
+ headers: this.headers,
6912
+ getHeaders: () => this.headers,
6913
+ threadId: this.#threadId,
6914
+ // The seed the endpoints assume: nothing. The snapshot is the history.
6915
+ initialMessages: []
6916
+ });
6917
+ const client = new AgUiClient({
6918
+ agent,
6919
+ handlers: this.#handlers(),
6920
+ getTools: () => this.getTools(),
6921
+ getContext: () => this.getContext(),
6922
+ executeTool: (call) => this.#executeTool(call),
6923
+ resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
6924
+ connectionLostMessage: this.#strings.connectionLost
6925
+ });
6926
+ await client.send(content);
6927
+ }
6928
+ /** Load the checkpoint panel with the runs that can actually be continued. */
6929
+ async #refreshCheckpoints() {
6930
+ const index = this.#runs();
6931
+ this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
6211
6932
  }
6212
6933
  /** Attributes whose late changes must reflect in already-rendered chrome. */
6213
6934
  static get observedAttributes() {
@@ -6276,9 +6997,73 @@ var AgUiChat = class extends HTMLElement {
6276
6997
  );
6277
6998
  return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
6278
6999
  }
6279
- /** All built-in (route + page + page-action) frontend tools. */
7000
+ /** All built-in (route + page + page-action + ask_user) frontend tools. */
6280
7001
  #builtinTools() {
6281
- return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
7002
+ return [
7003
+ ...this.#routeTools(),
7004
+ ...this.#pageTools(),
7005
+ ...this.#pageActionTools(),
7006
+ ...this.#askUserTool()
7007
+ ];
7008
+ }
7009
+ /**
7010
+ * The built-in `ask_user` frontend tool, or `[]` when {@link askUser} is off.
7011
+ *
7012
+ * A generic "ask the user a typed question" primitive: the agent calls it, the
7013
+ * client executes it locally via the normal frontend-tool path (rendering a
7014
+ * {@link requestQuestion} card), and the chosen/typed answer flows back as the
7015
+ * tool result — no new protocol, reusing the machinery already in place.
7016
+ */
7017
+ #askUserTool() {
7018
+ if (!this.askUser) {
7019
+ return [];
7020
+ }
7021
+ return [
7022
+ {
7023
+ name: "ask_user",
7024
+ 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.",
7025
+ parameters: {
7026
+ type: "object",
7027
+ properties: {
7028
+ question: { type: "string", description: "The question to ask the user." },
7029
+ options: {
7030
+ type: "array",
7031
+ items: { type: "string" },
7032
+ description: "Preset choices offered as radio buttons."
7033
+ },
7034
+ allow_custom: {
7035
+ type: "boolean",
7036
+ description: "Allow a free-text answer in addition to any options."
7037
+ }
7038
+ },
7039
+ required: ["question"]
7040
+ },
7041
+ handler: (args) => this.#askUser(args)
7042
+ }
7043
+ ];
7044
+ }
7045
+ /** Render the `ask_user` question card and resolve with the user's answer. */
7046
+ async #askUser(args) {
7047
+ const question = typeof args["question"] === "string" ? args["question"] : "";
7048
+ const request = { question };
7049
+ const rawOptions = args["options"];
7050
+ if (Array.isArray(rawOptions)) {
7051
+ request.options = rawOptions.filter((option) => typeof option === "string");
7052
+ }
7053
+ if (args["allow_custom"] === true) {
7054
+ request.allowCustom = true;
7055
+ }
7056
+ this.#confirmAbort = new AbortController();
7057
+ const signal = this.#confirmAbort.signal;
7058
+ this.#hidePending();
7059
+ const answer = this.askUserRenderer !== null ? await this.askUserRenderer(request, { signal }) : await requestQuestion(this.#ensureGroup(), request, {
7060
+ signal,
7061
+ strings: this.#strings
7062
+ });
7063
+ this.#confirmAbort = null;
7064
+ this.#updateEmptyState();
7065
+ this.#messages.scrollTop = this.#messages.scrollHeight;
7066
+ return answer;
6282
7067
  }
6283
7068
  /** Resolve a tool by name: built-in tools first, then the registry. */
6284
7069
  #resolveTool(name) {
@@ -6323,6 +7108,7 @@ var AgUiChat = class extends HTMLElement {
6323
7108
  }
6324
7109
  this.#render();
6325
7110
  this.#drawer.setStrings(this.#strings);
7111
+ this.#checkpoints.setStrings(this.#strings);
6326
7112
  if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
6327
7113
  this.setAttribute("collapsed", "");
6328
7114
  }
@@ -6864,11 +7650,20 @@ Use the read_attachment tool with an id to read a file's contents.`
6864
7650
  void this.#refreshDrawer();
6865
7651
  this.#drawer.open();
6866
7652
  });
7653
+ const checkpoints = this.#headerButton("checkpoints", this.#strings.checkpoints, "\u2B6F");
7654
+ checkpoints.addEventListener("click", () => {
7655
+ void this.#refreshCheckpoints();
7656
+ this.#checkpoints.open();
7657
+ });
6867
7658
  const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
6868
7659
  newChat.addEventListener("click", () => this.newChat());
6869
7660
  const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
6870
7661
  collapse.addEventListener("click", () => this.toggleCollapsed());
6871
- controls.append(history, newChat);
7662
+ if (this.#runs() !== null) {
7663
+ controls.append(history, checkpoints, newChat);
7664
+ } else {
7665
+ controls.append(history, newChat);
7666
+ }
6872
7667
  if (this.getAttribute("data-theme-toggle") !== null) {
6873
7668
  this.#themeToggle.type = "button";
6874
7669
  this.#themeToggle.className = "header-btn header-btn--theme";
@@ -6916,6 +7711,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6916
7711
  void this.#submit();
6917
7712
  });
6918
7713
  this.#skillHint.className = "skill-hint";
7714
+ this.#skillHint.setAttribute("part", "skill-hint");
6919
7715
  this.#skillHint.hidden = true;
6920
7716
  this.#attachButton.className = "attach-btn";
6921
7717
  this.#attachButton.type = "button";
@@ -6944,7 +7740,8 @@ Use the read_attachment tool with an id to read a file's contents.`
6944
7740
  this.#attachSlot,
6945
7741
  inputRow,
6946
7742
  footer,
6947
- this.#drawer.element
7743
+ this.#drawer.element,
7744
+ this.#checkpoints.element
6948
7745
  );
6949
7746
  this.#rail.className = "rail";
6950
7747
  this.#rail.type = "button";
@@ -7083,6 +7880,7 @@ Use the read_attachment tool with an id to read a file's contents.`
7083
7880
  getTools: () => this.getTools(),
7084
7881
  getContext: () => this.getContext(),
7085
7882
  executeTool: (call) => this.#executeTool(call),
7883
+ resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
7086
7884
  onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
7087
7885
  connectionLostMessage: this.#strings.connectionLost
7088
7886
  });
@@ -7155,6 +7953,45 @@ Use the read_attachment tool with an id to read a file's contents.`
7155
7953
  return { content: `Error: ${message}`, error: message };
7156
7954
  }
7157
7955
  }
7956
+ /**
7957
+ * Render an approval card per server-side-tool interrupt and collect the
7958
+ * user's decisions (approve → run it, deny → decline it).
7959
+ *
7960
+ * The run is suspended on these cards; a Stop while any is open aborts the
7961
+ * shared {@link #confirmAbort} controller, resolving every still-open card as
7962
+ * denied (and the client loop then sees the cancellation and stops). An
7963
+ * approved tool runs on the follow-up (resume) run and streams its result
7964
+ * back into the same pending card; a denied one is settled here, since no
7965
+ * result will ever arrive for it.
7966
+ */
7967
+ async #resolveInterrupts(interrupts) {
7968
+ const responses = {};
7969
+ this.#confirmAbort = new AbortController();
7970
+ this.#hidePending();
7971
+ for (const interrupt of interrupts) {
7972
+ const request = {};
7973
+ if (interrupt.message !== void 0) {
7974
+ request.message = interrupt.message;
7975
+ }
7976
+ const card = interrupt.toolCallId !== void 0 ? this.#toolCards.get(interrupt.toolCallId) : void 0;
7977
+ const toolName = card?.element.getAttribute("data-tool-name");
7978
+ if (toolName !== null && toolName !== void 0) {
7979
+ request.toolName = toolName;
7980
+ }
7981
+ const signal = this.#confirmAbort.signal;
7982
+ const approved = this.approvalRenderer !== null ? await this.approvalRenderer(request, { signal }) : await requestApproval(this.#ensureGroup(), request, { signal, strings: this.#strings });
7983
+ this.#updateEmptyState();
7984
+ this.#messages.scrollTop = this.#messages.scrollHeight;
7985
+ if (approved) {
7986
+ responses[interrupt.id] = { status: "resolved", payload: { approved: true } };
7987
+ } else {
7988
+ responses[interrupt.id] = { status: "cancelled" };
7989
+ card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
7990
+ }
7991
+ }
7992
+ this.#confirmAbort = null;
7993
+ return responses;
7994
+ }
7158
7995
  #handlers() {
7159
7996
  return {
7160
7997
  onRunStart: () => {
@@ -7355,10 +8192,11 @@ function setControlValue(el, value) {
7355
8192
  }
7356
8193
 
7357
8194
  // src/version.ts
7358
- var VERSION = "0.10.0";
8195
+ var VERSION = "0.12.0";
7359
8196
  export {
7360
8197
  AgUiChat,
7361
8198
  AgUiClient,
8199
+ CheckpointMenu,
7362
8200
  ClientToolRegistry,
7363
8201
  ConnectionLostError,
7364
8202
  DEFAULT_UI_STRINGS,
@@ -7367,6 +8205,7 @@ export {
7367
8205
  MESSAGE_ROLE,
7368
8206
  PAGE_ACTIONS,
7369
8207
  RemoteConversationStore,
8208
+ RunIndex,
7370
8209
  SUBMIT_EVENT,
7371
8210
  SessionStorageStore,
7372
8211
  TOGGLE_EVENT,
@@ -7398,7 +8237,9 @@ export {
7398
8237
  pressThenClick,
7399
8238
  prettifyToolName,
7400
8239
  renderMarkdown,
8240
+ requestApproval,
7401
8241
  requestConfirmation,
8242
+ requestQuestion,
7402
8243
  scrollIntoCenterView,
7403
8244
  selectControl,
7404
8245
  selectOption,