@artooi/ag-ui-web-component 0.28.0 → 0.29.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 (59) hide show
  1. package/CHANGELOG.md +417 -1
  2. package/README.md +371 -5
  3. package/dist/ag-ui-web-component.bundle.js +308 -50
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +69 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +207 -0
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +38 -0
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +7 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1516 -76
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/approval_card.d.ts +18 -0
  16. package/dist/ui/approval_card.d.ts.map +1 -1
  17. package/dist/ui/checkpoint_menu.d.ts +10 -0
  18. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  19. package/dist/ui/confirmation_card.d.ts +16 -0
  20. package/dist/ui/confirmation_card.d.ts.map +1 -1
  21. package/dist/ui/message_actions.d.ts +46 -0
  22. package/dist/ui/message_actions.d.ts.map +1 -0
  23. package/dist/ui/page_quote_offer.d.ts +33 -0
  24. package/dist/ui/page_quote_offer.d.ts.map +1 -0
  25. package/dist/ui/quote_selection.d.ts +66 -0
  26. package/dist/ui/quote_selection.d.ts.map +1 -0
  27. package/dist/ui/relative_time.d.ts +10 -0
  28. package/dist/ui/relative_time.d.ts.map +1 -1
  29. package/dist/ui/stick_to_bottom.d.ts +55 -0
  30. package/dist/ui/stick_to_bottom.d.ts.map +1 -0
  31. package/dist/ui/styles.d.ts +1 -1
  32. package/dist/ui/styles.d.ts.map +1 -1
  33. package/dist/ui/suggestion_chips.d.ts +29 -0
  34. package/dist/ui/suggestion_chips.d.ts.map +1 -0
  35. package/dist/ui/thread_drawer.d.ts +10 -0
  36. package/dist/ui/thread_drawer.d.ts.map +1 -1
  37. package/dist/ui/tool_call_card.d.ts +8 -0
  38. package/dist/ui/tool_call_card.d.ts.map +1 -1
  39. package/dist/ui/ui_strings.d.ts +40 -0
  40. package/dist/ui/ui_strings.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/constants.ts +75 -0
  43. package/src/core/ag_ui_chat.ts +927 -73
  44. package/src/core/agui_client.ts +63 -0
  45. package/src/index.ts +39 -0
  46. package/src/ui/approval_card.ts +90 -2
  47. package/src/ui/checkpoint_menu.ts +22 -5
  48. package/src/ui/confirmation_card.ts +29 -1
  49. package/src/ui/message_actions.ts +158 -0
  50. package/src/ui/page_quote_offer.ts +215 -0
  51. package/src/ui/quote_selection.ts +345 -0
  52. package/src/ui/relative_time.ts +11 -0
  53. package/src/ui/stick_to_bottom.ts +126 -0
  54. package/src/ui/styles.ts +227 -0
  55. package/src/ui/suggestion_chips.ts +73 -0
  56. package/src/ui/thread_drawer.ts +22 -2
  57. package/src/ui/tool_call_card.ts +9 -0
  58. package/src/ui/ui_strings.ts +60 -0
  59. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -6,6 +6,11 @@ var UNREAD_EVENT = "ag-ui-unread";
6
6
  var STATE_EVENT = "ag-ui-state";
7
7
  var ATTACHMENT_EVENT = "ag-ui-attachments";
8
8
  var RUN_FINISHED_EVENT = "ag-ui-run-finished";
9
+ var CUSTOM_AGENT_EVENT = "ag-ui-custom";
10
+ var INVALIDATE_EVENT = "ag-ui-invalidate";
11
+ var FEEDBACK_EVENT = "ag-ui-feedback";
12
+ var SUGGESTIONS_ACTIVITY_TYPE = "suggestions";
13
+ var INVALIDATE_CUSTOM_NAME = "ag_ui.invalidate";
9
14
  var MESSAGE_ROLE = {
10
15
  USER: "user",
11
16
  ASSISTANT: "assistant"
@@ -47,6 +52,7 @@ var ICON_FILE_IMAGE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"
47
52
  var ICON_FILE_PDF = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><rect class="glyph--solid" x="7.5" y="13.5" width="9" height="4.5" rx="1"/></svg>`;
48
53
  var ICON_FILE_TEXT = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M8.5 13.5h7M8.5 17h4.5"/></svg>`;
49
54
  var CHART_ACTIVITY_TYPE = "chart";
55
+ var ANNOUNCE_CLEAR_MS = 150;
50
56
 
51
57
  // src/core/ag_ui_chat.ts
52
58
  import { randomUUID as randomUUID5 } from "@ag-ui/client";
@@ -537,6 +543,12 @@ var DEFAULT_UI_STRINGS = {
537
543
  forkRun: "Fork",
538
544
  forkedRun: "branched",
539
545
  conversation: "Conversation",
546
+ jumpToLatest: "Jump to latest",
547
+ announceResponding: "Assistant is responding",
548
+ announceAnswerReady: "Assistant answered",
549
+ announceAwaitingDecision: "{count} action is waiting for your approval",
550
+ announceStopped: "Response stopped",
551
+ announceFailed: "The response failed",
540
552
  thinking: "Assistant is thinking\u2026",
541
553
  thoughts: "Thoughts",
542
554
  stopped: "\u23F9 Stopped",
@@ -544,6 +556,8 @@ var DEFAULT_UI_STRINGS = {
544
556
  noResult: "No result returned.",
545
557
  declinedAction: "User declined the action.",
546
558
  navigating: "Navigating\u2026",
559
+ historyReplaced: "The server replaced this conversation's history. Reload to see the updated transcript.",
560
+ chartUndrawable: "A chart could not be drawn from the data sent, so it was removed.",
547
561
  historyCompacted: "Earlier turns condensed to fit the context window ({count} removed)",
548
562
  usingSkill: "Using skill {name}",
549
563
  runInterrupted: "The previous response didn\u2019t finish \u2014 the page changed before it arrived.",
@@ -573,7 +587,18 @@ var DEFAULT_UI_STRINGS = {
573
587
  errorLabel: "Error",
574
588
  declinedLabel: "Declined",
575
589
  details: "Details",
590
+ approvalEditArgs: "Edit the arguments before approving",
591
+ approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
592
+ approvalArgsNotAnObject: "Arguments have to be a JSON object.",
593
+ suggestions: "Suggested follow-ups",
594
+ messageActions: "Message actions",
595
+ quoteSelection: "Quote",
596
+ copyMessage: "Copy message",
597
+ retryMessage: "Try again",
598
+ feedbackUp: "Good answer",
599
+ feedbackDown: "Poor answer",
576
600
  confirmAction: "Confirm action",
601
+ confirmAlways: "Always allow",
577
602
  confirmRun: "Run \u201C{tool}\u201D?",
578
603
  confirm: "Confirm",
579
604
  cancel: "Cancel",
@@ -640,6 +665,7 @@ function requestApproval(host, request, options = {}) {
640
665
  body.className = "approval-body";
641
666
  body.setAttribute("part", "approval-body");
642
667
  body.textContent = request.message ?? strings.approvalPrompt;
668
+ const editor = buildEditor(request, options, strings);
643
669
  const actions = document.createElement("div");
644
670
  actions.className = "approval-actions";
645
671
  actions.setAttribute("part", "approval-actions");
@@ -657,10 +683,15 @@ function requestApproval(host, request, options = {}) {
657
683
  resolve(approved);
658
684
  };
659
685
  deny.addEventListener("click", () => close(false));
660
- approve.addEventListener("click", () => close(true));
686
+ approve.addEventListener("click", () => {
687
+ if (editor !== null && !editor.commit()) {
688
+ return;
689
+ }
690
+ close(true);
691
+ });
661
692
  options.signal?.addEventListener("abort", () => close(false), { once: true });
662
693
  actions.append(deny, approve);
663
- card.append(body, actions);
694
+ card.append(body, ...editor === null ? [] : [editor.root], actions);
664
695
  host.appendChild(card);
665
696
  if (options.signal?.aborted === true) {
666
697
  close(false);
@@ -669,6 +700,54 @@ function requestApproval(host, request, options = {}) {
669
700
  approve.focus();
670
701
  });
671
702
  }
703
+ function buildEditor(request, options, strings) {
704
+ const { onEdit } = options;
705
+ if (onEdit === void 0 || request.args === void 0) {
706
+ return null;
707
+ }
708
+ const original = JSON.stringify(request.args, null, 2);
709
+ const root = document.createElement("div");
710
+ root.className = "approval-edit";
711
+ root.setAttribute("part", "approval-edit");
712
+ const field = document.createElement("textarea");
713
+ field.className = "approval-args";
714
+ field.setAttribute("part", "approval-args");
715
+ field.setAttribute("aria-label", strings.approvalEditArgs);
716
+ field.rows = Math.min(10, original.split("\n").length);
717
+ field.value = original;
718
+ const error = document.createElement("div");
719
+ error.className = "approval-error";
720
+ error.setAttribute("part", "approval-error");
721
+ error.setAttribute("role", "alert");
722
+ error.hidden = true;
723
+ root.append(field, error);
724
+ return {
725
+ root,
726
+ commit: () => {
727
+ if (field.value === original) {
728
+ return true;
729
+ }
730
+ let parsed;
731
+ try {
732
+ parsed = JSON.parse(field.value);
733
+ } catch {
734
+ error.textContent = strings.approvalArgsInvalid;
735
+ error.hidden = false;
736
+ field.focus();
737
+ return false;
738
+ }
739
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
740
+ error.textContent = strings.approvalArgsNotAnObject;
741
+ error.hidden = false;
742
+ field.focus();
743
+ return false;
744
+ }
745
+ error.hidden = true;
746
+ onEdit(parsed);
747
+ return true;
748
+ }
749
+ };
750
+ }
672
751
 
673
752
  // src/ui/attach_copy_buttons.ts
674
753
  var CONFIRM_MS = 1500;
@@ -1399,6 +1478,7 @@ var CheckpointMenu = class {
1399
1478
  #heading;
1400
1479
  /** What had focus before the panel opened, restored on close. */
1401
1480
  #lastFocused = null;
1481
+ #formatRelativeTime = null;
1402
1482
  #strings;
1403
1483
  #runs = [];
1404
1484
  constructor(onPick, strings = DEFAULT_UI_STRINGS) {
@@ -1431,6 +1511,21 @@ var CheckpointMenu = class {
1431
1511
  this.#render();
1432
1512
  }
1433
1513
  /** Re-localize a panel built before the host's strings resolved. */
1514
+ /**
1515
+ * Replace the timestamp formatter, or restore the built-in with `null`.
1516
+ *
1517
+ * The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
1518
+ * in this component, so it never disagrees with a host's own formatting by
1519
+ * guessing a locale. That is a defensible default and a poor requirement, so
1520
+ * this is the way out.
1521
+ */
1522
+ setRelativeTimeFormatter(format) {
1523
+ this.#formatRelativeTime = format;
1524
+ }
1525
+ /** This row's timestamp, through the host's formatter when it set one. */
1526
+ #formatTime(timestamp) {
1527
+ return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, Date.now(), this.#strings);
1528
+ }
1434
1529
  setStrings(strings) {
1435
1530
  this.#strings = strings;
1436
1531
  this.element.setAttribute("aria-label", strings.checkpoints);
@@ -1532,7 +1627,7 @@ var CheckpointMenu = class {
1532
1627
  row.className = "checkpoint-row";
1533
1628
  row.setAttribute("part", "checkpoint-row");
1534
1629
  const preview = previewOf(run);
1535
- const time = run.started_at === null ? null : relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
1630
+ const time = run.started_at === null ? null : this.#formatTime(Date.parse(run.started_at));
1536
1631
  const label = document.createElement("span");
1537
1632
  label.className = "checkpoint-label";
1538
1633
  label.setAttribute("part", "checkpoint-label");
@@ -1613,6 +1708,7 @@ function requestConfirmation(host, request, options = {}) {
1613
1708
  actions.className = "confirm-actions";
1614
1709
  actions.setAttribute("part", "confirm-actions");
1615
1710
  const cancel = actionButton2("cancel", strings.cancel);
1711
+ const always = options.onAlwaysAllow === void 0 ? null : actionButton2("always", strings.confirmAlways.replace("{tool}", request.toolName));
1616
1712
  const confirm2 = actionButton2("confirm", strings.confirm);
1617
1713
  let settled = false;
1618
1714
  const close = (accepted) => {
@@ -1625,8 +1721,12 @@ function requestConfirmation(host, request, options = {}) {
1625
1721
  };
1626
1722
  cancel.addEventListener("click", () => close(false));
1627
1723
  confirm2.addEventListener("click", () => close(true));
1724
+ always?.addEventListener("click", () => {
1725
+ options.onAlwaysAllow?.();
1726
+ close(true);
1727
+ });
1628
1728
  options.signal?.addEventListener("abort", () => close(false), { once: true });
1629
- actions.append(cancel, confirm2);
1729
+ actions.append(cancel, ...always === null ? [] : [always], confirm2);
1630
1730
  card.append(body, args, actions);
1631
1731
  host.appendChild(card);
1632
1732
  if (options.signal?.aborted === true) {
@@ -1637,6 +1737,347 @@ function requestConfirmation(host, request, options = {}) {
1637
1737
  });
1638
1738
  }
1639
1739
 
1740
+ // src/ui/message_actions.ts
1741
+ var CONFIRM_MS2 = 1500;
1742
+ function attachMessageActions(bubble, options) {
1743
+ if (existingBar(bubble) !== null) {
1744
+ return;
1745
+ }
1746
+ const bar = messageActionBar(bubble, options.strings);
1747
+ bar.appendChild(copyButton(options));
1748
+ if (options.onFeedback !== void 0) {
1749
+ bar.append(
1750
+ feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
1751
+ feedbackButton("down", options.strings.feedbackDown, options.onFeedback)
1752
+ );
1753
+ }
1754
+ }
1755
+ function messageActionBar(bubble, strings) {
1756
+ const existing = existingBar(bubble);
1757
+ if (existing !== null) {
1758
+ return existing;
1759
+ }
1760
+ const bar = document.createElement("div");
1761
+ bar.className = "message-actions";
1762
+ bar.setAttribute("part", "message-actions");
1763
+ bar.setAttribute("role", "group");
1764
+ bar.setAttribute("aria-label", strings.messageActions);
1765
+ bubble.after(bar);
1766
+ return bar;
1767
+ }
1768
+ function existingBar(bubble) {
1769
+ const next = bubble.nextElementSibling;
1770
+ return next?.classList.contains("message-actions") === true ? next : null;
1771
+ }
1772
+ function messageActionButton(modifier, label, glyph) {
1773
+ const button2 = document.createElement("button");
1774
+ button2.type = "button";
1775
+ button2.className = `message-action message-action--${modifier}`;
1776
+ button2.setAttribute("part", `message-action message-action-${modifier}`);
1777
+ button2.title = label;
1778
+ button2.setAttribute("aria-label", label);
1779
+ const icon = document.createElement("span");
1780
+ icon.setAttribute("aria-hidden", "true");
1781
+ icon.textContent = glyph;
1782
+ button2.appendChild(icon);
1783
+ return button2;
1784
+ }
1785
+ function copyButton(options) {
1786
+ const { strings } = options;
1787
+ const button2 = messageActionButton("copy", strings.copyMessage, "\u2398");
1788
+ button2.addEventListener("click", () => {
1789
+ void navigator.clipboard.writeText(options.text()).then(
1790
+ () => flash2(button2, strings.copied, strings.copyMessage),
1791
+ // A denied clipboard permission is the common case, not an exception:
1792
+ // say so on the button rather than throwing into an unhandled rejection.
1793
+ () => flash2(button2, strings.copyFailed, strings.copyMessage)
1794
+ );
1795
+ });
1796
+ return button2;
1797
+ }
1798
+ function feedbackButton(rating, label, report) {
1799
+ const button2 = messageActionButton(
1800
+ rating === "up" ? "up" : "down",
1801
+ label,
1802
+ rating === "up" ? "\u{1F44D}" : "\u{1F44E}"
1803
+ );
1804
+ button2.addEventListener("click", () => {
1805
+ const pressed = button2.getAttribute("aria-pressed") === "true";
1806
+ button2.setAttribute("aria-pressed", pressed ? "false" : "true");
1807
+ report(rating);
1808
+ });
1809
+ button2.setAttribute("aria-pressed", "false");
1810
+ return button2;
1811
+ }
1812
+ function flash2(button2, message, label) {
1813
+ button2.title = message;
1814
+ button2.setAttribute("aria-label", message);
1815
+ button2.classList.add("message-action--confirmed");
1816
+ setTimeout(() => {
1817
+ button2.title = label;
1818
+ button2.setAttribute("aria-label", label);
1819
+ button2.classList.remove("message-action--confirmed");
1820
+ }, CONFIRM_MS2);
1821
+ }
1822
+
1823
+ // src/ui/quote_selection.ts
1824
+ var MAX_QUOTE_CHARS = 500;
1825
+ function quotableSelection(container, roots = [], near) {
1826
+ for (const endpoints of endpointCandidates(roots)) {
1827
+ if (!container.contains(endpoints.startContainer)) {
1828
+ continue;
1829
+ }
1830
+ if (!container.contains(endpoints.endContainer)) {
1831
+ continue;
1832
+ }
1833
+ const range = document.createRange();
1834
+ range.setStart(endpoints.startContainer, endpoints.startOffset);
1835
+ range.setEnd(endpoints.endContainer, endpoints.endOffset);
1836
+ const text3 = renderedText(range).trim();
1837
+ if (text3 === "") {
1838
+ continue;
1839
+ }
1840
+ return { text: text3, rect: lineToHangFrom(range, near) };
1841
+ }
1842
+ return null;
1843
+ }
1844
+ function asQuote(text3) {
1845
+ const lines = tidy(text3);
1846
+ if (lines.length === 0) {
1847
+ return "";
1848
+ }
1849
+ const capped = cap(lines.join("\n"));
1850
+ const quoted = capped.split("\n").map((line) => `> ${line}`.trimEnd()).join("\n");
1851
+ return `${quoted}
1852
+
1853
+ `;
1854
+ }
1855
+ function tidy(text3) {
1856
+ const lines = text3.split(/\r\n?|\n/).map((line) => line.trimEnd());
1857
+ const indents = lines.filter((line) => line !== "").map(indentOf);
1858
+ const shared = indents.length === 0 ? 0 : Math.min(...indents);
1859
+ const kept = [];
1860
+ for (const line of lines) {
1861
+ const dedented = line.slice(shared);
1862
+ if (dedented === "" && (kept.length === 0 || kept[kept.length - 1] === "")) {
1863
+ continue;
1864
+ }
1865
+ kept.push(dedented);
1866
+ }
1867
+ while (kept[kept.length - 1] === "") {
1868
+ kept.pop();
1869
+ }
1870
+ return kept;
1871
+ }
1872
+ function indentOf(line) {
1873
+ return line.length - line.trimStart().length;
1874
+ }
1875
+ function cap(text3) {
1876
+ return text3.length > MAX_QUOTE_CHARS ? `${text3.slice(0, MAX_QUOTE_CHARS).trimEnd()}...` : text3;
1877
+ }
1878
+ function renderedText(range) {
1879
+ let text3 = "";
1880
+ for (const node of textNodesIn(range)) {
1881
+ const parent = node.parentElement;
1882
+ if (!isRendered(parent)) {
1883
+ continue;
1884
+ }
1885
+ const from = node === range.startContainer ? range.startOffset : 0;
1886
+ const to = node === range.endContainer ? range.endOffset : node.data.length;
1887
+ text3 += collapse(node.data.slice(from, to), parent);
1888
+ }
1889
+ return text3;
1890
+ }
1891
+ function collapse(text3, parent) {
1892
+ if (PREFORMATTED.has(whiteSpaceOf(parent))) {
1893
+ return text3;
1894
+ }
1895
+ return text3.replace(/[^\S\n]*\n[^\S\n]*/g, "\n").replace(/[^\S\n]+/g, " ");
1896
+ }
1897
+ var PREFORMATTED = /* @__PURE__ */ new Set(["pre", "pre-wrap", "break-spaces"]);
1898
+ function whiteSpaceOf(element) {
1899
+ return window.getComputedStyle(element).whiteSpace;
1900
+ }
1901
+ function textNodesIn(range) {
1902
+ const root = range.commonAncestorContainer;
1903
+ if (root.nodeType === Node.TEXT_NODE) {
1904
+ return [root];
1905
+ }
1906
+ const found = [];
1907
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1908
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
1909
+ if (range.intersectsNode(node)) {
1910
+ found.push(node);
1911
+ }
1912
+ }
1913
+ return found;
1914
+ }
1915
+ function isRendered(element) {
1916
+ if (typeof element.checkVisibility !== "function") {
1917
+ return true;
1918
+ }
1919
+ return element.checkVisibility({
1920
+ contentVisibilityAuto: true,
1921
+ opacityProperty: true,
1922
+ visibilityProperty: true
1923
+ });
1924
+ }
1925
+ function lineToHangFrom(range, near) {
1926
+ const lines = [...range.getClientRects()];
1927
+ if (lines.length === 0) {
1928
+ return range.getBoundingClientRect();
1929
+ }
1930
+ if (near === void 0) {
1931
+ return lines[0];
1932
+ }
1933
+ let closest = lines[0];
1934
+ let shortest = distanceTo(closest, near);
1935
+ for (const line of lines.slice(1)) {
1936
+ const distance = distanceTo(line, near);
1937
+ if (distance < shortest) {
1938
+ shortest = distance;
1939
+ closest = line;
1940
+ }
1941
+ }
1942
+ return closest;
1943
+ }
1944
+ function distanceTo(rect, point) {
1945
+ const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
1946
+ const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
1947
+ return Math.hypot(dx, dy);
1948
+ }
1949
+ function endpointCandidates(roots) {
1950
+ const selection = window.getSelection();
1951
+ if (selection === null) {
1952
+ return [];
1953
+ }
1954
+ const candidates = [...composedRanges(selection, roots)];
1955
+ if (selection.rangeCount > 0) {
1956
+ candidates.push(selection.getRangeAt(0));
1957
+ }
1958
+ return candidates;
1959
+ }
1960
+ function composedRanges(selection, roots) {
1961
+ const composed = selection.getComposedRanges;
1962
+ if (composed === void 0) {
1963
+ return [];
1964
+ }
1965
+ try {
1966
+ return composed.call(selection, { shadowRoots: roots });
1967
+ } catch {
1968
+ return composed.call(selection, ...roots);
1969
+ }
1970
+ }
1971
+
1972
+ // src/ui/page_quote_offer.ts
1973
+ var GAP = 6;
1974
+ var OFFER_CSS = `
1975
+ .ag-ui-quote-offer {
1976
+ position: fixed;
1977
+ z-index: 2147483000;
1978
+ transform: translate(-50%, -100%);
1979
+ margin: 0;
1980
+ padding: 0.25em 0.7em;
1981
+ border: 1px solid rgb(0 0 0 / 0.15);
1982
+ border-radius: 999px;
1983
+ background: Canvas;
1984
+ color: CanvasText;
1985
+ font: inherit;
1986
+ font-size: 0.8rem;
1987
+ line-height: 1.6;
1988
+ white-space: nowrap;
1989
+ cursor: pointer;
1990
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
1991
+ }
1992
+
1993
+ .ag-ui-quote-offer[data-below="true"] {
1994
+ transform: translate(-50%, 0);
1995
+ }
1996
+ `;
1997
+ function attachQuoteOffer(options) {
1998
+ const { within, exclude, onQuote } = options;
1999
+ const sheet = new CSSStyleSheet();
2000
+ sheet.replaceSync(OFFER_CSS);
2001
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
2002
+ const button2 = document.createElement("button");
2003
+ button2.type = "button";
2004
+ button2.className = "ag-ui-quote-offer";
2005
+ button2.textContent = options.label;
2006
+ button2.hidden = true;
2007
+ document.body.append(button2);
2008
+ let quoting = "";
2009
+ const hide = () => {
2010
+ button2.hidden = true;
2011
+ quoting = "";
2012
+ };
2013
+ const settled = (event) => {
2014
+ if (event.composedPath().includes(exclude)) {
2015
+ hide();
2016
+ return;
2017
+ }
2018
+ if (fieldHasFocus()) {
2019
+ hide();
2020
+ return;
2021
+ }
2022
+ const near = event instanceof MouseEvent ? { x: event.clientX, y: event.clientY } : void 0;
2023
+ const selected = quotableSelection(within, [], near);
2024
+ if (selected === null) {
2025
+ hide();
2026
+ return;
2027
+ }
2028
+ quoting = selected.text;
2029
+ place(button2, selected.rect);
2030
+ };
2031
+ const onMouseDown = (event) => {
2032
+ if (!button2.contains(event.target)) {
2033
+ hide();
2034
+ }
2035
+ };
2036
+ within.addEventListener("mouseup", settled);
2037
+ within.addEventListener("keyup", settled);
2038
+ within.addEventListener("mousedown", onMouseDown);
2039
+ document.addEventListener("scroll", hide, true);
2040
+ window.addEventListener("resize", hide);
2041
+ button2.addEventListener("mousedown", (event) => {
2042
+ event.preventDefault();
2043
+ });
2044
+ button2.addEventListener("click", () => {
2045
+ const text3 = quoting;
2046
+ window.getSelection()?.removeAllRanges();
2047
+ hide();
2048
+ onQuote(text3);
2049
+ });
2050
+ return {
2051
+ element: button2,
2052
+ detach() {
2053
+ within.removeEventListener("mouseup", settled);
2054
+ within.removeEventListener("keyup", settled);
2055
+ within.removeEventListener("mousedown", onMouseDown);
2056
+ document.removeEventListener("scroll", hide, true);
2057
+ window.removeEventListener("resize", hide);
2058
+ button2.remove();
2059
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter((each) => each !== sheet);
2060
+ }
2061
+ };
2062
+ }
2063
+ function fieldHasFocus() {
2064
+ const active = document.activeElement;
2065
+ if (active === null) {
2066
+ return false;
2067
+ }
2068
+ return active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable === true;
2069
+ }
2070
+ function place(button2, rect) {
2071
+ button2.hidden = false;
2072
+ const below = rect.top < GAP + button2.offsetHeight;
2073
+ button2.dataset["below"] = String(below);
2074
+ button2.style.top = `${below ? rect.bottom + GAP : rect.top - GAP}px`;
2075
+ const half = button2.offsetWidth / 2;
2076
+ const centre = rect.left + rect.width / 2;
2077
+ const width = document.documentElement.clientWidth;
2078
+ button2.style.left = `${Math.min(Math.max(centre, half), width - half)}px`;
2079
+ }
2080
+
1640
2081
  // src/ui/prettify_tool_name.ts
1641
2082
  function prettifyToolName(name) {
1642
2083
  const spaced = name.replace(/[._-]+/g, " ").trim();
@@ -2126,7 +2567,7 @@ function createDOMPurify() {
2126
2567
  const originalDocument = document2;
2127
2568
  const currentScript = originalDocument.currentScript;
2128
2569
  window2.DocumentFragment;
2129
- const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
2570
+ const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element, NodeFilter2 = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
2130
2571
  _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap;
2131
2572
  window2.HTMLFormElement;
2132
2573
  const DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes;
@@ -2723,7 +3164,7 @@ function createDOMPurify() {
2723
3164
  doc || root,
2724
3165
  root,
2725
3166
  // eslint-disable-next-line no-bitwise
2726
- NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION,
3167
+ NodeFilter2.SHOW_ELEMENT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_PROCESSING_INSTRUCTION | NodeFilter2.SHOW_CDATA_SECTION,
2727
3168
  null
2728
3169
  );
2729
3170
  };
@@ -2741,7 +3182,7 @@ function createDOMPurify() {
2741
3182
  doc || node,
2742
3183
  node,
2743
3184
  // eslint-disable-next-line no-bitwise
2744
- NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION,
3185
+ NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_CDATA_SECTION | NodeFilter2.SHOW_PROCESSING_INSTRUCTION,
2745
3186
  null
2746
3187
  );
2747
3188
  let currentNode = walker.nextNode();
@@ -4936,6 +5377,60 @@ var SkillsMenu = class {
4936
5377
  }
4937
5378
  };
4938
5379
 
5380
+ // src/ui/stick_to_bottom.ts
5381
+ var BOTTOM_SLACK_PX = 4;
5382
+ function createStickToBottom({
5383
+ viewport,
5384
+ onMissedContent
5385
+ }) {
5386
+ let isFollowing = true;
5387
+ let missed = false;
5388
+ const atBottom = () => viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= BOTTOM_SLACK_PX;
5389
+ const setMissed = (next) => {
5390
+ if (next === missed) {
5391
+ return;
5392
+ }
5393
+ missed = next;
5394
+ onMissedContent(missed);
5395
+ };
5396
+ const toBottom = () => {
5397
+ viewport.scrollTop = viewport.scrollHeight;
5398
+ };
5399
+ const onScroll = () => {
5400
+ isFollowing = atBottom();
5401
+ if (isFollowing) {
5402
+ setMissed(false);
5403
+ }
5404
+ };
5405
+ const follow = () => {
5406
+ if (isFollowing) {
5407
+ toBottom();
5408
+ return;
5409
+ }
5410
+ setMissed(true);
5411
+ };
5412
+ viewport.addEventListener("scroll", onScroll, { passive: true });
5413
+ const observer = new ResizeObserver(() => {
5414
+ if (isFollowing) {
5415
+ toBottom();
5416
+ }
5417
+ });
5418
+ observer.observe(viewport);
5419
+ return {
5420
+ follow,
5421
+ jump: () => {
5422
+ isFollowing = true;
5423
+ setMissed(false);
5424
+ toBottom();
5425
+ },
5426
+ following: () => isFollowing,
5427
+ dispose: () => {
5428
+ viewport.removeEventListener("scroll", onScroll);
5429
+ observer.disconnect();
5430
+ }
5431
+ };
5432
+ }
5433
+
4939
5434
  // src/ui/styles.ts
4940
5435
  var STYLES = `
4941
5436
  /* \u2500\u2500 Token defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -5506,6 +6001,7 @@ var STYLES = `
5506
6001
  opacity: 0;
5507
6002
  }
5508
6003
 
6004
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .messages-wrap,
5509
6005
  :host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
5510
6006
  :host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
5511
6007
  :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
@@ -5514,9 +6010,113 @@ var STYLES = `
5514
6010
  display: none;
5515
6011
  }
5516
6012
 
6013
+ /* Jump-to-latest: shown only once the reader has scrolled away *and* missed
6014
+ something. Anchored to the panel rather than the list so it does not scroll
6015
+ with the content it is offering to scroll to. */
6016
+ /* The transcript's own box, and the only one whose foot is the transcript's
6017
+ foot. The panel's foot is below the composer, the chips and the footer. */
6018
+ .messages-wrap {
6019
+ position: relative;
6020
+ flex: 1;
6021
+ min-height: 0;
6022
+ display: flex;
6023
+ flex-direction: column;
6024
+ }
6025
+
6026
+ .jump-latest {
6027
+ position: absolute;
6028
+ left: 50%;
6029
+ transform: translateX(-50%);
6030
+ bottom: var(--_pad);
6031
+ z-index: 2;
6032
+ display: none;
6033
+ align-items: center;
6034
+ gap: 0.35em;
6035
+ padding: 0.4em 0.9em;
6036
+ border: 1px solid var(--_border);
6037
+ border-radius: 999px;
6038
+ /* A raised surface, not the panel's own background. Reusing --_bg made the
6039
+ pill the same colour as everything behind it, leaving a 1px border and a
6040
+ shadow to carry the whole affordance -- and a dark-on-dark shadow carries
6041
+ nothing. --_hover is the token that already means "lifted off the panel",
6042
+ and it separates in both themes without competing with the accent the send
6043
+ button owns. */
6044
+ background: var(--_hover);
6045
+ color: var(--_text);
6046
+ font: inherit;
6047
+ font-size: 0.85em;
6048
+ cursor: pointer;
6049
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
6050
+ }
6051
+
6052
+ .jump-latest[data-missed="true"] {
6053
+ display: flex;
6054
+ }
6055
+
6056
+ .jump-latest:hover {
6057
+ border-color: var(--_accent);
6058
+ }
6059
+
6060
+ /* The offer to quote a selection. Positioned in script against the transcript
6061
+ box, which is the only ancestor whose top and foot are the transcript's --
6062
+ the same reason .jump-latest lives here. The translate is the half the
6063
+ script does not do: script sets the point the offer hangs from, CSS decides
6064
+ which corner of the offer that point is. */
6065
+ .quote-selection {
6066
+ position: absolute;
6067
+ z-index: 2;
6068
+ transform: translate(-50%, -100%);
6069
+ padding: 0.25em 0.7em;
6070
+ border: 1px solid var(--_border);
6071
+ border-radius: 999px;
6072
+ background: var(--_hover);
6073
+ color: var(--_text);
6074
+ font: inherit;
6075
+ font-size: 0.8em;
6076
+ line-height: 1.6;
6077
+ white-space: nowrap;
6078
+ cursor: pointer;
6079
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
6080
+ }
6081
+
6082
+ /* Flipped under the selection when there was no room above it. Only the
6083
+ vertical half of the translate changes: it still hangs from its own centre
6084
+ horizontally. */
6085
+ .quote-selection[data-below="true"] {
6086
+ transform: translate(-50%, 0);
6087
+ }
6088
+
6089
+ .quote-selection:hover {
6090
+ border-color: var(--_accent);
6091
+ }
6092
+
6093
+ /* Screen-reader-only status region. Off-screen rather than display:none or
6094
+ visibility:hidden, both of which take the element out of the accessibility
6095
+ tree entirely -- a hidden live region announces nothing at all, which is the
6096
+ classic way this pattern is written wrong.
6097
+
6098
+ The 1px box with clip-path, rather than width/height 0, is the shape that
6099
+ survives: a zero-sized element is dropped from the tree by some engines. */
6100
+ .sr-only {
6101
+ position: absolute;
6102
+ width: 1px;
6103
+ height: 1px;
6104
+ margin: -1px;
6105
+ padding: 0;
6106
+ border: 0;
6107
+ overflow: hidden;
6108
+ white-space: nowrap;
6109
+ clip-path: inset(50%);
6110
+ }
6111
+
5517
6112
  .messages {
5518
6113
  flex: 1;
5519
6114
  overflow-y: auto;
6115
+ /* The browser's own scroll anchoring competes with the scroller for the same
6116
+ job and wins unpredictably -- it can hold the view still exactly when we
6117
+ want to follow. Turned off so following is decided in one place. Safari
6118
+ does not implement it, which is itself a reason not to depend on it. */
6119
+ overflow-anchor: none;
5520
6120
  padding: var(--_pad);
5521
6121
  display: flex;
5522
6122
  flex-direction: column;
@@ -6682,6 +7282,7 @@ var STYLES = `
6682
7282
 
6683
7283
  .confirm-actions {
6684
7284
  display: flex;
7285
+ flex-wrap: wrap;
6685
7286
  gap: 8px;
6686
7287
  justify-content: flex-end;
6687
7288
  }
@@ -6708,6 +7309,99 @@ var STYLES = `
6708
7309
  color: #ffffff;
6709
7310
  }
6710
7311
 
7312
+ /* The session waiver. Deliberately the quietest of the three: it is the widest
7313
+ decision on the card, so it should be reachable without being the one the eye
7314
+ lands on when the user means to say yes once. */
7315
+ .confirm-btn--always {
7316
+ font-weight: 500;
7317
+ opacity: 0.85;
7318
+ }
7319
+
7320
+ .confirm-btn--always:hover,
7321
+ .confirm-btn--always:focus-visible {
7322
+ opacity: 1;
7323
+ }
7324
+
7325
+ /* Editable arguments on an approval card. A plain field rather than a code
7326
+ editor: it holds the JSON a card already displays, and the only interaction
7327
+ is correcting a value before letting the call run. */
7328
+ .approval-edit {
7329
+ display: flex;
7330
+ flex-direction: column;
7331
+ gap: 6px;
7332
+ }
7333
+
7334
+ .approval-args {
7335
+ box-sizing: border-box;
7336
+ width: 100%;
7337
+ resize: vertical;
7338
+ border: 1px solid var(--_border);
7339
+ border-radius: 8px;
7340
+ padding: 8px;
7341
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
7342
+ font-size: 0.85em;
7343
+ background: var(--_bg);
7344
+ color: var(--_fg);
7345
+ }
7346
+
7347
+ .approval-args:focus-visible {
7348
+ border-color: var(--_accent);
7349
+ outline: none;
7350
+ }
7351
+
7352
+ .approval-error {
7353
+ font-size: 0.85em;
7354
+ color: var(--_danger);
7355
+ }
7356
+
7357
+ /* Message action row. Sits under a finished assistant bubble.
7358
+
7359
+ The wrap is insurance rather than a fix: these buttons are glyph-only, so at
7360
+ every width tested they fit on one line and removing the wrap changes
7361
+ nothing. It is here because the confirmation row one release earlier did
7362
+ overflow when it gained a third button, off the left edge and outside its own
7363
+ card, and the difference between the two rows is only that this one's labels
7364
+ are icons today. */
7365
+ .message-actions {
7366
+ display: flex;
7367
+ flex-wrap: wrap;
7368
+ gap: 4px;
7369
+ /* Negative, and that is the point. The answer group is a flex column with its
7370
+ own gap, so a positive margin here pushes the row further from the message
7371
+ it acts on than the next card is below it -- the buttons then read as
7372
+ belonging to whatever follows. Pulling back inside the gap is what makes
7373
+ them the message's own. */
7374
+ margin-top: -6px;
7375
+ }
7376
+
7377
+ .message-action {
7378
+ border: none;
7379
+ border-radius: 6px;
7380
+ padding: 2px 6px;
7381
+ font: inherit;
7382
+ line-height: 1.2;
7383
+ cursor: pointer;
7384
+ background: transparent;
7385
+ color: var(--_muted);
7386
+ opacity: 0.75;
7387
+ }
7388
+
7389
+ .message-action:hover,
7390
+ .message-action:focus-visible {
7391
+ opacity: 1;
7392
+ background: var(--_border);
7393
+ }
7394
+
7395
+ .message-action[aria-pressed="true"] {
7396
+ opacity: 1;
7397
+ color: var(--_accent);
7398
+ }
7399
+
7400
+ .message-action--confirmed {
7401
+ opacity: 1;
7402
+ color: var(--_accent);
7403
+ }
7404
+
6711
7405
  /* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */
6712
7406
  .approval {
6713
7407
  align-self: stretch;
@@ -6852,6 +7546,34 @@ var STYLES = `
6852
7546
  border-color: var(--_accent);
6853
7547
  }
6854
7548
 
7549
+ /* Follow-up suggestion chips. Deliberately the skill chips' shape rather than a
7550
+ second chip vocabulary -- both are "a question you could ask", and the only
7551
+ difference is who chose it. Inside the transcript, so they scroll with the
7552
+ answer they follow instead of hovering above the composer. */
7553
+ .suggestions {
7554
+ display: flex;
7555
+ flex-wrap: wrap;
7556
+ gap: 6px;
7557
+ align-self: stretch;
7558
+ }
7559
+
7560
+ .suggestion-chip {
7561
+ border: 1px solid var(--_border);
7562
+ border-radius: 999px;
7563
+ padding: 4px 12px;
7564
+ font: inherit;
7565
+ font-size: 0.9em;
7566
+ text-align: left;
7567
+ cursor: pointer;
7568
+ background: var(--_assistant-bg);
7569
+ color: var(--_fg);
7570
+ }
7571
+
7572
+ .suggestion-chip:hover,
7573
+ .suggestion-chip:focus-visible {
7574
+ border-color: var(--_accent);
7575
+ }
7576
+
6855
7577
  .skill-palette {
6856
7578
  margin: 8px 12px 0;
6857
7579
  display: flex;
@@ -7266,6 +7988,42 @@ var STYLES = `
7266
7988
  }
7267
7989
  `;
7268
7990
 
7991
+ // src/ui/suggestion_chips.ts
7992
+ var MAX_SUGGESTIONS = 4;
7993
+ var MAX_SUGGESTION_CHARS = 120;
7994
+ function suggestionPrompts(content) {
7995
+ if (typeof content !== "object" || content === null) {
7996
+ return null;
7997
+ }
7998
+ const raw = content.prompts;
7999
+ if (!Array.isArray(raw)) {
8000
+ return null;
8001
+ }
8002
+ const prompts = raw.filter((prompt) => typeof prompt === "string").map((prompt) => prompt.trim()).filter((prompt) => prompt !== "" && prompt.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
8003
+ return prompts.length === 0 ? null : prompts;
8004
+ }
8005
+ function renderSuggestionChips(content, strings, onPick) {
8006
+ const prompts = suggestionPrompts(content);
8007
+ if (prompts === null) {
8008
+ return null;
8009
+ }
8010
+ const row = document.createElement("div");
8011
+ row.className = "suggestions";
8012
+ row.setAttribute("part", "suggestions");
8013
+ row.setAttribute("role", "group");
8014
+ row.setAttribute("aria-label", strings.suggestions);
8015
+ for (const prompt of prompts) {
8016
+ const chip = document.createElement("button");
8017
+ chip.type = "button";
8018
+ chip.className = "suggestion-chip";
8019
+ chip.setAttribute("part", "suggestion-chip");
8020
+ chip.textContent = prompt;
8021
+ chip.addEventListener("click", () => onPick(prompt));
8022
+ row.appendChild(chip);
8023
+ }
8024
+ return row;
8025
+ }
8026
+
7269
8027
  // src/ui/thoughts_block.ts
7270
8028
  var ThoughtsBlock = class {
7271
8029
  /** The block's root element; insert this at the top of the answer group. */
@@ -7332,6 +8090,7 @@ var ThreadDrawer = class {
7332
8090
  #heading;
7333
8091
  #newButton;
7334
8092
  #list;
8093
+ #formatRelativeTime = null;
7335
8094
  #strings;
7336
8095
  #threads = [];
7337
8096
  #activeId = "";
@@ -7379,6 +8138,21 @@ var ThreadDrawer = class {
7379
8138
  this.element.append(backdrop, this.#panel);
7380
8139
  }
7381
8140
  /** Re-localize the drawer's chrome and rows (the host calls this on connect). */
8141
+ /**
8142
+ * Replace the timestamp formatter, or restore the built-in with `null`.
8143
+ *
8144
+ * The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
8145
+ * in this component, so it never disagrees with a host's own formatting by
8146
+ * guessing a locale. That is a defensible default and a poor requirement, so
8147
+ * this is the way out.
8148
+ */
8149
+ setRelativeTimeFormatter(format) {
8150
+ this.#formatRelativeTime = format;
8151
+ }
8152
+ /** This row's timestamp, through the host's formatter when it set one. */
8153
+ #formatTime(timestamp) {
8154
+ return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, void 0, this.#strings);
8155
+ }
7382
8156
  setStrings(strings) {
7383
8157
  this.#strings = strings;
7384
8158
  this.#panel.setAttribute("aria-label", strings.chatHistory);
@@ -7478,7 +8252,7 @@ var ThreadDrawer = class {
7478
8252
  const time = document.createElement("span");
7479
8253
  time.className = "drawer-row-time";
7480
8254
  time.setAttribute("part", "drawer-row-time");
7481
- time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
8255
+ time.textContent = this.#formatTime(meta.updatedAt);
7482
8256
  const preview = document.createElement("span");
7483
8257
  preview.className = "drawer-row-preview";
7484
8258
  preview.setAttribute("part", "drawer-row-preview");
@@ -7627,9 +8401,18 @@ var ToolCallCard = class {
7627
8401
  #resultLabel;
7628
8402
  #resultBody;
7629
8403
  #strings;
8404
+ /**
8405
+ * The arguments this call was made with.
8406
+ *
8407
+ * Retained rather than only rendered, because an approval interrupt names a
8408
+ * `toolCallId` and nothing else -- so this card is the only place the args
8409
+ * still exist when the user is asked to approve, edit or deny the call.
8410
+ */
8411
+ args;
7630
8412
  #settled = false;
7631
8413
  constructor(name, args, summary, strings = DEFAULT_UI_STRINGS) {
7632
8414
  this.#strings = strings;
8415
+ this.args = args;
7633
8416
  this.element = document.createElement("div");
7634
8417
  this.element.className = "tool-call";
7635
8418
  this.element.setAttribute("part", "tool-card");
@@ -7984,6 +8767,41 @@ var AgUiClient = class {
7984
8767
  this.#onPersist(this.#agent.messages);
7985
8768
  await this.#run();
7986
8769
  }
8770
+ /**
8771
+ * Drop everything after the most recent user message, so the same question
8772
+ * can be asked again.
8773
+ *
8774
+ * Returns the retained history, or `null` when there is nothing to retry (no
8775
+ * user message has been sent yet). **Truncates only** -- the caller re-renders
8776
+ * from the returned list and then calls {@link resume}, because the transcript
8777
+ * belongs to the element and a client that reached into it would own two
8778
+ * things. Running here instead would stream the new answer in underneath the
8779
+ * old one.
8780
+ *
8781
+ * Re-running answers the question the agent was last asked, rather than
8782
+ * telling it its answer was wrong, which is what makes the result a
8783
+ * *different* answer instead of a conversation about the previous one.
8784
+ *
8785
+ * **A retried turn re-runs its tools.** For a page-driving agent that is not
8786
+ * neutral: the previous attempt already clicked what it clicked, and this
8787
+ * does not undo it.
8788
+ */
8789
+ truncateToLastUser() {
8790
+ const messages = [...this.#agent.messages];
8791
+ let lastUser = -1;
8792
+ for (const [index, message] of messages.entries()) {
8793
+ if (message.role === "user") {
8794
+ lastUser = index;
8795
+ }
8796
+ }
8797
+ if (lastUser === -1) {
8798
+ return null;
8799
+ }
8800
+ const kept = messages.slice(0, lastUser + 1);
8801
+ this.#agent.setMessages(kept);
8802
+ this.#onPersist(this.#agent.messages);
8803
+ return kept;
8804
+ }
7987
8805
  /**
7988
8806
  * Resume the run loop after a navigating tool's result was supplied
7989
8807
  * post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
@@ -8150,6 +8968,12 @@ var AgUiClient = class {
8150
8968
  // Emitted after the client has written the patched messages, which is the
8151
8969
  // first moment the result exists. Only the ids marked above are looked at,
8152
8970
  // so an ordinary text delta does not walk the transcript.
8971
+ onCustomEvent({ event }) {
8972
+ h.onCustomEvent(event.name, event.value);
8973
+ },
8974
+ onMessagesSnapshotEvent({ event }) {
8975
+ h.onMessagesSnapshot(event.messages);
8976
+ },
8153
8977
  onMessagesChanged({ messages }) {
8154
8978
  if (pendingDeltas.size === 0) {
8155
8979
  return;
@@ -8853,6 +9677,7 @@ function isCredentialsMode(value) {
8853
9677
  var COLLAPSED_KEY = "ag-ui-chat:collapsed";
8854
9678
  var SIZE_KEY = "ag-ui-chat:size";
8855
9679
  var THEME_KEY = "ag-ui-chat:theme";
9680
+ var QUOTE_GAP = 6;
8856
9681
  var CLAIMED_NAMESPACES = /* @__PURE__ */ new Set();
8857
9682
  var AgUiChat = class extends HTMLElement {
8858
9683
  /** Agent factory; override to inject a custom or fake agent (tests). */
@@ -8900,6 +9725,22 @@ var AgUiChat = class extends HTMLElement {
8900
9725
  * data. Enable only when the content source is trusted.
8901
9726
  */
8902
9727
  allowImages = false;
9728
+ /**
9729
+ * Replace the relative timestamps in the thread drawer and the checkpoint
9730
+ * panel -- `"5m ago"`, `"2d ago"` -- with the host's own formatting.
9731
+ *
9732
+ * The built-in is locale-neutral on purpose: there is no `Intl` anywhere in
9733
+ * this component, so it never disagrees with the page it is embedded in by
9734
+ * guessing a locale. That is a good default and a bad requirement, which is
9735
+ * what this is for.
9736
+ *
9737
+ * ```js
9738
+ * const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
9739
+ * chat.formatRelativeTime = (ts) =>
9740
+ * rtf.format(Math.round((ts - Date.now()) / 60000), "minute");
9741
+ * ```
9742
+ */
9743
+ formatRelativeTime = null;
8903
9744
  /** When true, destructive tools execute without a confirmation modal. */
8904
9745
  autoConfirm = false;
8905
9746
  /**
@@ -8923,6 +9764,22 @@ var AgUiChat = class extends HTMLElement {
8923
9764
  * is enabled server-side; this only changes how the decision is collected.
8924
9765
  */
8925
9766
  approvalRenderer = null;
9767
+ /**
9768
+ * Let the user edit a gated call's arguments before approving it.
9769
+ *
9770
+ * Off by default and **an assertion about your server**, not a negotiation:
9771
+ * AG-UI carries `editedArgs` in the resume payload and gates it on the
9772
+ * agent's own `approveWithEdits` capability, which this component never sees
9773
+ * -- capabilities are not on the wire it reads. So the host says whether its
9774
+ * agent honours them. Turned on against a server that does not, the user
9775
+ * would edit arguments it silently discards, which is worse than not
9776
+ * offering.
9777
+ *
9778
+ * Only affects calls whose arguments are known here: an interrupt names a
9779
+ * `toolCallId`, and the tool card for that call is where the arguments still
9780
+ * are. An interrupt naming no card gets the plain approve/deny.
9781
+ */
9782
+ approveWithEdits = false;
8926
9783
  /**
8927
9784
  * Optional per-call confirmation predicate. When set it is authoritative,
8928
9785
  * deciding from the tool name and args whether this particular call needs
@@ -9068,7 +9925,16 @@ var AgUiChat = class extends HTMLElement {
9068
9925
  * the real output with the generic "executed on the server" fallback.
9069
9926
  */
9070
9927
  /** Whether a server-pushed chart activity is drawn. Off unless asked for. */
9071
- #chartActivity = false;
9928
+ /**
9929
+ * Which `activity_type`s this element can draw, by name.
9930
+ *
9931
+ * A registry rather than a branch because `activity_type` is an open string
9932
+ * the protocol does not enumerate. The two built-ins go through it like any
9933
+ * host registration, which is the test that the seam is real.
9934
+ */
9935
+ #activityRenderers = /* @__PURE__ */ new Map();
9936
+ /** Types that arrived with nobody registered to draw them. See {@link unhandledActivityTypes}. */
9937
+ #unhandledActivityTypes = /* @__PURE__ */ new Set();
9072
9938
  /** Card elements by call id, so a rendering handler can find its own card. */
9073
9939
  #cardElements = /* @__PURE__ */ new Map();
9074
9940
  /** Chart blocks by activity message id, so an update redraws in place. */
@@ -9080,7 +9946,75 @@ var AgUiChat = class extends HTMLElement {
9080
9946
  * Spans tool rounds and an approval interrupt; cleared when the event fires.
9081
9947
  */
9082
9948
  #runTools = [];
9949
+ /**
9950
+ * Keys announced during this interaction, de-duplicated in first-seen order.
9951
+ *
9952
+ * Per element, never module-level: a second mounted chat is a second run, and
9953
+ * sharing this would tell one page to refetch on the other's writes. Reset by
9954
+ * {@link AgUiChat.#dispatchRunFinished}, which is the one place that has read
9955
+ * it.
9956
+ */
9957
+ /**
9958
+ * Tool names the user waived confirmation for, for the life of this element.
9959
+ *
9960
+ * Per instance and never persisted: a session decision that outlived the tab
9961
+ * would be a permanent grant made by one click, which is the thing
9962
+ * `autoConfirm` already exists to say deliberately. Cleared with the element.
9963
+ */
9964
+ #sessionApproved = /* @__PURE__ */ new Set();
9965
+ /**
9966
+ * The one action row currently carrying Retry, if any.
9967
+ *
9968
+ * Retry belongs to the **last** turn only: re-running an older one is
9969
+ * branching, and for a page-driving agent editing a past turn is not neutral
9970
+ * -- those turns clicked buttons, and re-running turn 3 does not un-save what
9971
+ * turn 5 saved. Holding a single owner is what keeps exactly one offer on
9972
+ * screen without per-bubble bookkeeping.
9973
+ */
9974
+ #retryOwner = null;
9975
+ #runInvalidated = /* @__PURE__ */ new Set();
9083
9976
  #root;
9977
+ /** Screen-reader-only status region -- see {@link AgUiChat.#announce}. */
9978
+ #announcer = document.createElement("div");
9979
+ /** Return-to-foot affordance, shown only once something has been missed. */
9980
+ #jumpButton = document.createElement("button");
9981
+ /**
9982
+ * Offer to quote the current selection, floated beside it.
9983
+ *
9984
+ * Shares {@link AgUiChat.#messagesWrap} with the jump button for the same
9985
+ * reason: it is positioned against the transcript, and must not scroll away
9986
+ * with the words it is pointing at.
9987
+ */
9988
+ #quoteButton = document.createElement("button");
9989
+ /** What {@link AgUiChat.#quoteButton} would quote, while it is showing. */
9990
+ #quoting = "";
9991
+ /** The host-page offer, while one is attached; see {@link AgUiChat.offerQuoteInPage}. */
9992
+ #pageQuote = null;
9993
+ /**
9994
+ * Positioning context for {@link AgUiChat.#jumpButton}.
9995
+ *
9996
+ * The button cannot live in the scrolling list -- it would scroll away with
9997
+ * the content it is offering to scroll to -- and it cannot be positioned
9998
+ * against the panel either: the panel's foot is below the composer, the skill
9999
+ * chips and the footer, so `bottom` measured from there lands the button on
10000
+ * top of the composer rather than over the transcript. This wrapper is the
10001
+ * only box whose foot *is* the transcript's foot.
10002
+ */
10003
+ #messagesWrap = document.createElement("div");
10004
+ /** Follows the foot of the transcript, and stops when the reader scrolls away. */
10005
+ #scroller;
10006
+ /** Pending clear of {@link AgUiChat.#announcer}; see why it is cleared at all. */
10007
+ #announceTimer = null;
10008
+ /**
10009
+ * Whether this turn already announced how it ended.
10010
+ *
10011
+ * `onSettled` is the terminal guarantee and fires however the run ended, so
10012
+ * it is the only place that can promise the user hears *something*. But a
10013
+ * stopped or failed run has already said the truer thing from `onCancelled`
10014
+ * or `onError`, and "assistant answered" after "response stopped" is worse
10015
+ * than silence.
10016
+ */
10017
+ #announcedOutcome = false;
9084
10018
  #chat;
9085
10019
  #messages;
9086
10020
  #input;
@@ -9198,6 +10132,23 @@ var AgUiChat = class extends HTMLElement {
9198
10132
  this.#launcher = document.createElement("button");
9199
10133
  this.#badge = document.createElement("span");
9200
10134
  this.#emptyWrap = document.createElement("div");
10135
+ this.registerActivityRenderer({
10136
+ type: COMPACTION_ACTIVITY_TYPE,
10137
+ render: (content) => {
10138
+ const removed = compactionRemoved(content);
10139
+ return removed === null ? null : renderRunNotice(
10140
+ "\u{1F5DC}",
10141
+ this.#strings.historyCompacted.replace("{count}", String(removed)),
10142
+ "compaction"
10143
+ );
10144
+ }
10145
+ });
10146
+ this.registerActivityRenderer({
10147
+ type: SUGGESTIONS_ACTIVITY_TYPE,
10148
+ render: (content) => renderSuggestionChips(content, this.#strings, (prompt) => {
10149
+ void this.sendMessage(prompt);
10150
+ })
10151
+ });
9201
10152
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
9202
10153
  this.#drawer = new ThreadDrawer({
9203
10154
  onSelect: (threadId) => {
@@ -9282,6 +10233,7 @@ var AgUiChat = class extends HTMLElement {
9282
10233
  /** Load the checkpoint panel with the runs that can actually be continued. */
9283
10234
  async #refreshCheckpoints() {
9284
10235
  const index = this.#runs();
10236
+ this.#checkpoints.setRelativeTimeFormatter(this.formatRelativeTime);
9285
10237
  this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
9286
10238
  }
9287
10239
  /** Attributes the element reacts to after it has been connected. */
@@ -9478,7 +10430,7 @@ var AgUiChat = class extends HTMLElement {
9478
10430
  });
9479
10431
  this.#confirmAbort = null;
9480
10432
  this.#updateEmptyState();
9481
- this.#messages.scrollTop = this.#messages.scrollHeight;
10433
+ this.#scroller.follow();
9482
10434
  return answer;
9483
10435
  }
9484
10436
  /**
@@ -9734,8 +10686,15 @@ var AgUiChat = class extends HTMLElement {
9734
10686
  this.#claimedNs = null;
9735
10687
  }
9736
10688
  this.#cancelRun();
10689
+ this.#pageQuote?.detach();
10690
+ this.#pageQuote = null;
9737
10691
  this.#attachTray?.dispose();
9738
10692
  this.#voice?.dispose();
10693
+ this.#scroller.dispose();
10694
+ if (this.#announceTimer !== null) {
10695
+ clearTimeout(this.#announceTimer);
10696
+ this.#announceTimer = null;
10697
+ }
9739
10698
  }
9740
10699
  /**
9741
10700
  * Read an opt-in flag attribute the way HTML reads a boolean attribute.
@@ -9846,6 +10805,111 @@ var AgUiChat = class extends HTMLElement {
9846
10805
  this.#onInput();
9847
10806
  this.#input.focus();
9848
10807
  }
10808
+ /**
10809
+ * Put `text` into the composer as a markdown quotation, and focus it.
10810
+ *
10811
+ * Deliberately **not** a send. Quoting is how a question narrows to one part
10812
+ * of an answer, so the quotation is the preamble and the question is what
10813
+ * comes next -- the caret is left after it, on its own line.
10814
+ *
10815
+ * This is also the seam for the half of this feature the component cannot
10816
+ * build: selection in the **host page**. A widget mounted beside a table can
10817
+ * be asked about a row, and nothing in a chat's own transcript can offer
10818
+ * that. A host reads its own selection, however it likes, and calls this.
10819
+ *
10820
+ * No-ops on text that is empty or only whitespace.
10821
+ */
10822
+ quote(text3) {
10823
+ const quoted = asQuote(text3);
10824
+ if (quoted === "") {
10825
+ return;
10826
+ }
10827
+ const current = this.#input.value.replace(/\s+$/, "");
10828
+ this.#input.value = current === "" ? quoted : `${current}
10829
+
10830
+ ${quoted}`;
10831
+ this.#autoGrow();
10832
+ this.#input.focus();
10833
+ const end = this.#input.value.length;
10834
+ this.#input.setSelectionRange(end, end);
10835
+ }
10836
+ /**
10837
+ * Offer to quote what the user selects in the **host page**, not just in the
10838
+ * transcript. Returns a function that stops offering.
10839
+ *
10840
+ * The same select-then-offer gesture, over a table, a diff, a report -- the
10841
+ * surface the user actually works in, which is the half of quoting no hosted
10842
+ * chat can reach. Opt-in, because it listens on the host's document and that
10843
+ * is theirs to grant.
10844
+ *
10845
+ * Deliberately **not** a four-line recipe, which is how this shipped first
10846
+ * and was wrong: a page listener that quotes every settled selection appends
10847
+ * to the composer on every drag made to read, to copy or to fix a typo -- and
10848
+ * it cannot tell a selection in the page's prose from one inside the user's
10849
+ * own half-typed `<input>`, because Chrome reports a field's internal
10850
+ * selection as an ordinary range over the field's *wrapper*. See
10851
+ * {@link attachQuoteOffer} for the guards.
10852
+ *
10853
+ * Detached automatically when the element leaves the document; a host that
10854
+ * re-mounts it calls this again.
10855
+ */
10856
+ offerQuoteInPage(within = document.body) {
10857
+ this.#pageQuote?.detach();
10858
+ const offer = attachQuoteOffer({
10859
+ within,
10860
+ label: this.#strings.quoteSelection,
10861
+ exclude: this,
10862
+ onQuote: (text3) => this.quote(text3)
10863
+ });
10864
+ this.#pageQuote = offer;
10865
+ return () => {
10866
+ offer.detach();
10867
+ if (this.#pageQuote === offer) {
10868
+ this.#pageQuote = null;
10869
+ }
10870
+ };
10871
+ }
10872
+ /** Whether the transcript offers to quote what the user selects. */
10873
+ #quoteEnabled() {
10874
+ return this.getAttribute("data-quote-selection") !== "false";
10875
+ }
10876
+ /**
10877
+ * Offer to quote the settled selection, or retire the offer.
10878
+ *
10879
+ * `event` is passed for its coordinates and only those: they say which line
10880
+ * of a selection spanning several messages the offer should hang from. A
10881
+ * keyboard selection has none, and the first line is used instead.
10882
+ */
10883
+ #onSelectionSettled(event) {
10884
+ if (!this.#quoteEnabled()) {
10885
+ return;
10886
+ }
10887
+ const near = event === void 0 ? void 0 : { x: event.clientX, y: event.clientY };
10888
+ const selected = quotableSelection(this.#messages, [this.#root], near);
10889
+ if (selected === null) {
10890
+ this.#hideQuote();
10891
+ return;
10892
+ }
10893
+ this.#quoting = selected.text;
10894
+ this.#placeQuote(selected.rect);
10895
+ }
10896
+ /** Float the offer beside `rect`, kept inside the transcript's own box. */
10897
+ #placeQuote(rect) {
10898
+ this.#quoteButton.hidden = false;
10899
+ const wrap = this.#messagesWrap.getBoundingClientRect();
10900
+ const top = rect.top - wrap.top;
10901
+ const below = top < QUOTE_GAP + this.#quoteButton.offsetHeight;
10902
+ this.#quoteButton.dataset["below"] = String(below);
10903
+ this.#quoteButton.style.top = `${below ? rect.bottom - wrap.top + QUOTE_GAP : top - QUOTE_GAP}px`;
10904
+ const half = this.#quoteButton.offsetWidth / 2;
10905
+ const centre = rect.left + rect.width / 2 - wrap.left;
10906
+ this.#quoteButton.style.left = `${Math.min(Math.max(centre, half), wrap.width - half)}px`;
10907
+ }
10908
+ /** Retire the offer, and forget what it was pointing at. */
10909
+ #hideQuote() {
10910
+ this.#quoteButton.hidden = true;
10911
+ this.#quoting = "";
10912
+ }
9849
10913
  /** The client-side upload size cap from `data-attachment-max-bytes`. */
9850
10914
  #attachmentMaxBytes() {
9851
10915
  const attr = this.getAttribute("data-attachment-max-bytes");
@@ -10375,6 +11439,17 @@ var AgUiChat = class extends HTMLElement {
10375
11439
  /** Drop the in-memory run + transcript, leaving the thread id untouched. */
10376
11440
  #resetState() {
10377
11441
  this.#client = null;
11442
+ this.#clearTranscript();
11443
+ this.#initialMessages = [];
11444
+ }
11445
+ /**
11446
+ * Wipe the rendered transcript and everything that indexes into it.
11447
+ *
11448
+ * Split from {@link #resetState} because a retry re-renders the transcript
11449
+ * while keeping the *client*: dropping the client there would take the
11450
+ * agent's message list with it, which is the thing being truncated.
11451
+ */
11452
+ #clearTranscript() {
10378
11453
  this.#endStream();
10379
11454
  this.#currentGroup = null;
10380
11455
  this.#thoughts = null;
@@ -10383,11 +11458,43 @@ var AgUiChat = class extends HTMLElement {
10383
11458
  this.#serverSettled.clear();
10384
11459
  this.#cardElements.clear();
10385
11460
  this.#activityBlocks.clear();
10386
- this.#initialMessages = [];
11461
+ this.#retryOwner = null;
10387
11462
  this.#attachTray?.clear();
10388
11463
  this.#messages.replaceChildren(this.#emptyWrap);
10389
11464
  this.#updateEmptyState();
10390
11465
  }
11466
+ /**
11467
+ * Ask the same question again and replace the answer.
11468
+ *
11469
+ * History is truncated to the most recent user message inclusive and the run
11470
+ * repeats, so the agent answers what it was asked rather than being told its
11471
+ * last answer was wrong. Returns `false` when there is nothing to retry or a
11472
+ * run is already in flight.
11473
+ *
11474
+ * Public because a host with its own message UI wants the same button, and
11475
+ * because the failed-run notice reaches it from outside the action row.
11476
+ *
11477
+ * **A retried turn re-runs its tools**, which for a page-driving agent is not
11478
+ * neutral: the previous attempt already clicked what it clicked, and this
11479
+ * does not undo it. Confirmation still applies, so a destructive tool asks
11480
+ * again -- unless the user waived it for this session.
11481
+ */
11482
+ async retryLastTurn() {
11483
+ if (this.#running) {
11484
+ return false;
11485
+ }
11486
+ const client = this.#ensureClient();
11487
+ const kept = client.truncateToLastUser();
11488
+ if (kept === null) {
11489
+ return false;
11490
+ }
11491
+ this.#clearTranscript();
11492
+ for (const message of kept) {
11493
+ this.#renderHistoricMessage(message);
11494
+ }
11495
+ await client.resume();
11496
+ return true;
11497
+ }
10391
11498
  /** Switch the active conversation to an existing thread and replay it. */
10392
11499
  async #switchThread(threadId) {
10393
11500
  if (threadId === this.#threadId) {
@@ -10416,6 +11523,7 @@ var AgUiChat = class extends HTMLElement {
10416
11523
  }
10417
11524
  /** Reload the drawer's thread list, marking the active thread. */
10418
11525
  async #refreshDrawer() {
11526
+ this.#drawer.setRelativeTimeFormatter(this.formatRelativeTime);
10419
11527
  this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
10420
11528
  }
10421
11529
  /**
@@ -10507,7 +11615,9 @@ var AgUiChat = class extends HTMLElement {
10507
11615
  }
10508
11616
  if (message.role === MESSAGE_ROLE.ASSISTANT) {
10509
11617
  if (text3 !== "") {
10510
- this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3).classList.add("message--restored");
11618
+ const restoredBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3);
11619
+ restoredBubble.classList.add("message--restored");
11620
+ this.#attachActions(restoredBubble);
10511
11621
  }
10512
11622
  for (const call of restoredToolCalls(message.toolCalls)) {
10513
11623
  const restored = {
@@ -10528,8 +11638,8 @@ var AgUiChat = class extends HTMLElement {
10528
11638
  }
10529
11639
  if (message.role === "activity") {
10530
11640
  const activity = message;
10531
- if (activity.activityType === CHART_ACTIVITY_TYPE && this.#chartActivity) {
10532
- this.#drawActivityChart(message.id, activity.content);
11641
+ if (typeof activity.activityType === "string") {
11642
+ this.#drawActivity(message.id, activity.activityType, activity.content);
10533
11643
  }
10534
11644
  return;
10535
11645
  }
@@ -10594,7 +11704,11 @@ var AgUiChat = class extends HTMLElement {
10594
11704
  this.#messages.appendChild(bubble);
10595
11705
  }
10596
11706
  this.#updateEmptyState();
10597
- this.#messages.scrollTop = this.#messages.scrollHeight;
11707
+ if (role === MESSAGE_ROLE.USER) {
11708
+ this.#scroller.jump();
11709
+ } else {
11710
+ this.#scroller.follow();
11711
+ }
10598
11712
  return bubble;
10599
11713
  }
10600
11714
  /**
@@ -10616,8 +11730,6 @@ var AgUiChat = class extends HTMLElement {
10616
11730
  return this.#currentGroup;
10617
11731
  }
10618
11732
  #render() {
10619
- const style = document.createElement("style");
10620
- style.textContent = STYLES;
10621
11733
  this.#chat.className = "chat";
10622
11734
  this.#chat.setAttribute("part", "panel");
10623
11735
  const header = document.createElement("div");
@@ -10641,8 +11753,8 @@ var AgUiChat = class extends HTMLElement {
10641
11753
  checkpoints.addEventListener("click", () => this.toggleCheckpoints());
10642
11754
  const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
10643
11755
  newChat.addEventListener("click", () => this.newChat());
10644
- const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
10645
- collapse.addEventListener("click", () => this.toggleCollapsed());
11756
+ const collapse2 = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
11757
+ collapse2.addEventListener("click", () => this.toggleCollapsed());
10646
11758
  if (this.#runs() !== null) {
10647
11759
  controls.append(history, checkpoints, newChat);
10648
11760
  } else {
@@ -10658,13 +11770,46 @@ var AgUiChat = class extends HTMLElement {
10658
11770
  this.#syncThemeGlyph();
10659
11771
  controls.append(this.#themeToggle);
10660
11772
  }
10661
- controls.append(collapse);
11773
+ controls.append(collapse2);
10662
11774
  header.append(title, headerActions, controls);
10663
11775
  this.#messages.className = "messages";
10664
11776
  this.#messages.setAttribute("part", "messages");
10665
11777
  this.#messages.setAttribute("role", "log");
10666
- this.#messages.setAttribute("aria-live", "polite");
11778
+ this.#messages.setAttribute("aria-live", "off");
10667
11779
  this.#messages.setAttribute("aria-label", this.#strings.conversation);
11780
+ this.#jumpButton.className = "jump-latest";
11781
+ this.#jumpButton.type = "button";
11782
+ this.#jumpButton.setAttribute("part", "jump-latest");
11783
+ this.#jumpButton.textContent = this.#strings.jumpToLatest;
11784
+ this.#jumpButton.addEventListener("click", () => {
11785
+ this.#scroller.jump();
11786
+ });
11787
+ this.#quoteButton.className = "quote-selection";
11788
+ this.#quoteButton.type = "button";
11789
+ this.#quoteButton.setAttribute("part", "quote-selection");
11790
+ this.#quoteButton.textContent = this.#strings.quoteSelection;
11791
+ this.#quoteButton.hidden = true;
11792
+ this.#quoteButton.addEventListener("mousedown", (event) => {
11793
+ event.preventDefault();
11794
+ });
11795
+ this.#quoteButton.addEventListener("click", () => {
11796
+ this.quote(this.#quoting);
11797
+ window.getSelection()?.removeAllRanges();
11798
+ this.#hideQuote();
11799
+ });
11800
+ this.#messages.addEventListener("mouseup", (event) => this.#onSelectionSettled(event));
11801
+ this.#messages.addEventListener("keyup", () => this.#onSelectionSettled());
11802
+ this.#messages.addEventListener("mousedown", () => this.#hideQuote());
11803
+ this.#scroller = createStickToBottom({
11804
+ viewport: this.#messages,
11805
+ onMissedContent: (missed) => {
11806
+ this.#jumpButton.dataset["missed"] = String(missed);
11807
+ }
11808
+ });
11809
+ this.#announcer.className = "sr-only";
11810
+ this.#announcer.setAttribute("role", "status");
11811
+ this.#announcer.setAttribute("aria-live", "polite");
11812
+ this.#announcer.setAttribute("aria-atomic", "true");
10668
11813
  this.#emptyWrap.className = "empty";
10669
11814
  this.#emptyWrap.setAttribute("part", "empty");
10670
11815
  const emptySlot = document.createElement("slot");
@@ -10727,9 +11872,11 @@ var AgUiChat = class extends HTMLElement {
10727
11872
  tools.append(this.#attachButton, this.#voiceSlot, this.#send);
10728
11873
  composer.append(this.#input, tools);
10729
11874
  inputRow.append(composer, this.#fileInput);
11875
+ this.#messagesWrap.className = "messages-wrap";
11876
+ this.#messagesWrap.append(this.#messages, this.#jumpButton, this.#quoteButton);
10730
11877
  this.#chat.append(
10731
11878
  header,
10732
- this.#messages,
11879
+ this.#messagesWrap,
10733
11880
  this.#skillsMenu.palette,
10734
11881
  this.#skillsMenu.chips,
10735
11882
  this.#skillHint,
@@ -10775,7 +11922,66 @@ var AgUiChat = class extends HTMLElement {
10775
11922
  label: this.#strings.resizePanel
10776
11923
  })
10777
11924
  );
10778
- this.#root.append(style, this.#chat, this.#launcher);
11925
+ this.#adoptStyles();
11926
+ this.#root.append(this.#announcer, this.#chat, this.#launcher);
11927
+ }
11928
+ /**
11929
+ * Attach the stylesheet without an inline `<style>` element.
11930
+ *
11931
+ * A host with a strict `style-src` and no `'unsafe-inline'` drops an injected
11932
+ * `<style>` silently: the component mounts, functions, and renders completely
11933
+ * unstyled, with nothing in the console to point at. `adoptedStyleSheets`
11934
+ * carries no inline-style origin, so it is unaffected by that policy.
11935
+ *
11936
+ * The sheet is constructed **per instance** rather than shared at module
11937
+ * scope. A shared sheet would additionally avoid re-parsing the stylesheet
11938
+ * once per mounted element, which is what `adoptedStyleSheets` is usually
11939
+ * reached for -- but a module-level singleton is exactly what this package
11940
+ * forbids, and the CSP defect is fixed either way. Per instance is no worse
11941
+ * than the `<style>` element it replaces, which also parsed once per mount.
11942
+ *
11943
+ * No fallback: constructible `CSSStyleSheet` is Chrome 73, Firefox 101 and
11944
+ * Safari 16.4, all below this package's declared Safari 17 runtime target. A
11945
+ * guard here would be code no supported browser can reach, and the only way
11946
+ * to keep it would be to exempt it from the coverage gate.
11947
+ */
11948
+ /**
11949
+ * Say one short thing to a screen reader, without touching the transcript.
11950
+ *
11951
+ * The transcript cannot do this job. It is rewritten on every animation
11952
+ * frame while an answer streams, so as a live region it re-announced the
11953
+ * whole answer tens of times per turn -- not merely unhelpful but actively
11954
+ * hostile. The published fix for this exact bug (Microsoft's Bot Framework
11955
+ * WebChat #3236) is architectural rather than a matter of tuning attributes:
11956
+ * demote the visible transcript out of live-region duty and put one
11957
+ * synthesised status per event into a separate invisible region. MDN and
11958
+ * Scott O'Hara prescribe the same empty-region-then-inject shape.
11959
+ *
11960
+ * Roughly four calls land per turn -- responding, answered, a card is waiting,
11961
+ * stopped or failed -- so the user is told what happened and reads the answer
11962
+ * itself by navigating the log, at their own pace, rather than having it
11963
+ * shouted at them a token at a time.
11964
+ *
11965
+ * **The clear is load-bearing, twice.** A reader announces a live region when
11966
+ * its content *changes*, so setting the same string twice running -- two turns
11967
+ * in a row both starting -- is not a change and is silently not announced.
11968
+ * Emptying first makes the next set a change again. It also stops a stale
11969
+ * status being read out when a reader later lands on the region.
11970
+ */
11971
+ #announce(message) {
11972
+ if (this.#announceTimer !== null) {
11973
+ clearTimeout(this.#announceTimer);
11974
+ }
11975
+ this.#announcer.textContent = message;
11976
+ this.#announceTimer = setTimeout(() => {
11977
+ this.#announceTimer = null;
11978
+ this.#announcer.textContent = "";
11979
+ }, ANNOUNCE_CLEAR_MS);
11980
+ }
11981
+ #adoptStyles() {
11982
+ const sheet = new CSSStyleSheet();
11983
+ sheet.replaceSync(STYLES);
11984
+ this.#root.adoptedStyleSheets = [sheet];
10779
11985
  }
10780
11986
  /**
10781
11987
  * Build a header control button: a named slot a host can project markup into,
@@ -11087,15 +12293,69 @@ var AgUiChat = class extends HTMLElement {
11087
12293
  })
11088
12294
  );
11089
12295
  }
11090
- /** Whether ``call`` should be gated behind the confirmation card. */
11091
- async #needsConfirmation(call, tool) {
12296
+ /**
12297
+ * Give a finished assistant bubble its action row, and hand it Retry.
12298
+ *
12299
+ * Every finished bubble gets copy and feedback -- both are safe on a message
12300
+ * of any age. Retry moves to the newest, because it is the only one where
12301
+ * re-running answers the same question rather than rewriting history.
12302
+ */
12303
+ #attachActions(bubble, options = {}) {
12304
+ attachMessageActions(bubble, {
12305
+ strings: this.#strings,
12306
+ // Read at click time, not captured: a bubble rendered from markdown holds
12307
+ // its text in the DOM, and that is what the user sees and means to copy.
12308
+ text: () => bubble.textContent,
12309
+ // A failed run is copyable -- error text is what people paste into a bug
12310
+ // report -- but not rateable: a rating is a statement about an *answer*,
12311
+ // and mixing "the connection dropped" into that signal makes the host's
12312
+ // feedback data say less than it did before.
12313
+ ...options.rateable === false ? {} : {
12314
+ onFeedback: (rating) => {
12315
+ this.dispatchEvent(
12316
+ new CustomEvent(FEEDBACK_EVENT, {
12317
+ detail: { content: bubble.textContent, rating },
12318
+ bubbles: true,
12319
+ composed: true
12320
+ })
12321
+ );
12322
+ }
12323
+ }
12324
+ });
12325
+ this.#moveRetryTo(messageActionBar(bubble, this.#strings));
12326
+ }
12327
+ /** Move the Retry button onto `bar`, taking it off whoever held it. */
12328
+ #moveRetryTo(bar) {
12329
+ this.#retryOwner?.querySelector(".message-action--retry")?.remove();
12330
+ const retry = messageActionButton("retry", this.#strings.retryMessage, "\u21BB");
12331
+ retry.addEventListener("click", () => {
12332
+ void this.retryLastTurn();
12333
+ });
12334
+ bar.prepend(retry);
12335
+ this.#retryOwner = bar;
12336
+ }
12337
+ /**
12338
+ * Which rule gates `call`, or `null` when it runs straight through.
12339
+ *
12340
+ * The rule, rather than a bare boolean, because it decides whether the user
12341
+ * may *waive* the prompt for the rest of the session. Only the default
12342
+ * `x-destructive` gate is waivable: `confirmPredicate` is documented as
12343
+ * authoritative, so letting one click retire it would silently defeat a host
12344
+ * policy — and the session allowlist is consulted on the same path it can
12345
+ * be added from, so the button is never offered where honouring it would be
12346
+ * refused.
12347
+ */
12348
+ async #confirmationRule(call, tool) {
11092
12349
  if (this.autoConfirm) {
11093
- return false;
12350
+ return null;
11094
12351
  }
11095
12352
  if (this.confirmPredicate !== null) {
11096
- return await this.confirmPredicate(call.name, call.args) === true;
12353
+ return await this.confirmPredicate(call.name, call.args) === true ? "predicate" : null;
12354
+ }
12355
+ if (this.#sessionApproved.has(call.name)) {
12356
+ return null;
11097
12357
  }
11098
- return isDestructive(tool.parameters);
12358
+ return isDestructive(tool.parameters) ? "destructive" : null;
11099
12359
  }
11100
12360
  async #executeTool(call) {
11101
12361
  if (skillNameFrom(call) !== null) {
@@ -11117,7 +12377,8 @@ var AgUiChat = class extends HTMLElement {
11117
12377
  this.#showPending();
11118
12378
  return { content: `Error: ${message}`, error: message };
11119
12379
  }
11120
- if (await this.#needsConfirmation(call, tool)) {
12380
+ const rule = await this.#confirmationRule(call, tool);
12381
+ if (rule !== null) {
11121
12382
  const request = { toolName: call.name, args: call.args };
11122
12383
  const confirmText = tool.parameters[X_CONFIRM_KEY];
11123
12384
  if (typeof confirmText === "string") {
@@ -11126,10 +12387,12 @@ var AgUiChat = class extends HTMLElement {
11126
12387
  this.#confirmAbort = new AbortController();
11127
12388
  const decision = requestConfirmation(this.#ensureGroup(), request, {
11128
12389
  signal: this.#confirmAbort.signal,
11129
- strings: this.#strings
12390
+ strings: this.#strings,
12391
+ // Offered only where it can be honoured -- see `#confirmationRule`.
12392
+ ...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
11130
12393
  });
11131
12394
  this.#updateEmptyState();
11132
- this.#messages.scrollTop = this.#messages.scrollHeight;
12395
+ this.#scroller.follow();
11133
12396
  const accepted = await decision;
11134
12397
  this.#confirmAbort = null;
11135
12398
  card.recordDecision(accepted ? "approved" : "declined");
@@ -11191,6 +12454,9 @@ var AgUiChat = class extends HTMLElement {
11191
12454
  */
11192
12455
  async #resolveInterrupts(interrupts) {
11193
12456
  this.#confirmAbort = new AbortController();
12457
+ this.#announce(
12458
+ this.#strings.announceAwaitingDecision.replace("{count}", String(interrupts.length))
12459
+ );
11194
12460
  this.#hidePending();
11195
12461
  const signal = this.#confirmAbort.signal;
11196
12462
  const answered = await Promise.all(
@@ -11205,10 +12471,20 @@ var AgUiChat = class extends HTMLElement {
11205
12471
  if (toolName !== null && toolName !== void 0) {
11206
12472
  request.toolName = toolName;
11207
12473
  }
12474
+ let editedArgs;
12475
+ const editable = this.approveWithEdits && card !== void 0;
12476
+ if (editable) {
12477
+ request.args = card.args;
12478
+ }
11208
12479
  card?.mark(TOOL_CALL_STATUS.DEFERRED);
11209
12480
  const approved = this.approvalRenderer !== null ? await this.approvalRenderer(request, { signal }) : await requestApproval(card?.approvalSlot ?? this.#ensureGroup(), request, {
11210
12481
  signal,
11211
- strings: this.#strings
12482
+ strings: this.#strings,
12483
+ ...editable ? {
12484
+ onEdit: (args) => {
12485
+ editedArgs = args;
12486
+ }
12487
+ } : {}
11212
12488
  });
11213
12489
  card?.recordDecision(approved ? "approved" : "declined");
11214
12490
  if (approved) {
@@ -11216,21 +12492,28 @@ var AgUiChat = class extends HTMLElement {
11216
12492
  } else {
11217
12493
  card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
11218
12494
  }
11219
- return { id: interrupt.id, approved };
12495
+ return { id: interrupt.id, approved, editedArgs };
11220
12496
  })
11221
12497
  );
11222
12498
  this.#updateEmptyState();
11223
- this.#messages.scrollTop = this.#messages.scrollHeight;
12499
+ this.#scroller.follow();
11224
12500
  this.#confirmAbort = null;
11225
12501
  const responses = {};
11226
- for (const { id, approved } of answered) {
11227
- responses[id] = approved ? { status: "resolved", payload: { approved: true } } : { status: "cancelled" };
12502
+ for (const { id, approved, editedArgs } of answered) {
12503
+ responses[id] = approved ? {
12504
+ status: "resolved",
12505
+ payload: editedArgs === void 0 ? { approved: true } : { approved: true, editedArgs }
12506
+ } : { status: "cancelled" };
11228
12507
  }
11229
12508
  return responses;
11230
12509
  }
11231
12510
  #handlers() {
11232
12511
  return {
11233
12512
  onRunStart: () => {
12513
+ if (!this.#running) {
12514
+ this.#announcedOutcome = false;
12515
+ this.#announce(this.#strings.announceResponding);
12516
+ }
11234
12517
  this.#setRunning(true);
11235
12518
  this.#ensureGroup();
11236
12519
  this.#showPending();
@@ -11256,6 +12539,7 @@ var AgUiChat = class extends HTMLElement {
11256
12539
  this.#revealWords(bubble);
11257
12540
  }
11258
12541
  attachCopyButtons(bubble, this.#strings);
12542
+ this.#attachActions(bubble);
11259
12543
  this.#endStream();
11260
12544
  this.#noteUnread();
11261
12545
  },
@@ -11268,25 +12552,24 @@ var AgUiChat = class extends HTMLElement {
11268
12552
  this.#cardFor(call);
11269
12553
  },
11270
12554
  onActivity: (activityType, content, messageId) => {
11271
- if (activityType === CHART_ACTIVITY_TYPE) {
11272
- if (this.#chartActivity) {
11273
- this.#drawActivityChart(messageId, content);
11274
- }
11275
- return;
11276
- }
11277
- if (activityType !== COMPACTION_ACTIVITY_TYPE) {
11278
- return;
11279
- }
11280
- const removed = compactionRemoved(content);
11281
- if (removed === null) {
12555
+ this.#drawActivity(messageId, activityType, content);
12556
+ },
12557
+ onCustomEvent: (name, value) => {
12558
+ if (name === INVALIDATE_CUSTOM_NAME) {
12559
+ this.#dispatchInvalidation(value);
11282
12560
  return;
11283
12561
  }
11284
- this.#appendNotice(
11285
- "\u{1F5DC}",
11286
- this.#strings.historyCompacted.replace("{count}", String(removed)),
11287
- "compaction"
12562
+ this.dispatchEvent(
12563
+ new CustomEvent(CUSTOM_AGENT_EVENT, {
12564
+ detail: { name, value },
12565
+ bubbles: true,
12566
+ composed: true
12567
+ })
11288
12568
  );
11289
12569
  },
12570
+ onMessagesSnapshot: () => {
12571
+ this.#appendNotice("\u{1F504}", this.#strings.historyReplaced, "history-replaced");
12572
+ },
11290
12573
  onToolResult: (toolCallId, content) => {
11291
12574
  const card = this.#toolCards.get(toolCallId);
11292
12575
  if (card === void 0) {
@@ -11297,25 +12580,33 @@ var AgUiChat = class extends HTMLElement {
11297
12580
  this.#showPending();
11298
12581
  },
11299
12582
  onActivityChanged: (messageId, activityType, content) => {
11300
- if (activityType === CHART_ACTIVITY_TYPE && this.#chartActivity) {
11301
- this.#drawActivityChart(messageId, content);
11302
- }
12583
+ this.#drawActivity(messageId, activityType, content);
11303
12584
  },
11304
12585
  onRunEnd: () => {
11305
12586
  this.#hidePending();
11306
12587
  this.#endStream();
11307
12588
  },
11308
12589
  onError: (message) => {
12590
+ this.#announcedOutcome = true;
12591
+ this.#announce(this.#strings.announceFailed);
11309
12592
  this.#hidePending();
11310
- this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`));
12593
+ const bubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`);
12594
+ bubble.classList.add("message--failed");
12595
+ this.#attachActions(bubble, { rateable: false });
12596
+ this.#revealWords(bubble);
11311
12597
  this.#endStream();
11312
12598
  },
11313
12599
  onCancelled: () => {
12600
+ this.#announcedOutcome = true;
12601
+ this.#announce(this.#strings.announceStopped);
11314
12602
  this.#hidePending();
11315
12603
  this.#appendStoppedNote();
11316
12604
  this.#endStream();
11317
12605
  },
11318
12606
  onSettled: () => {
12607
+ if (!this.#announcedOutcome) {
12608
+ this.#announce(this.#strings.announceAnswerReady);
12609
+ }
11319
12610
  this.#hidePending();
11320
12611
  this.#setRunning(false);
11321
12612
  this.#endStream();
@@ -11349,9 +12640,42 @@ var AgUiChat = class extends HTMLElement {
11349
12640
  side: this.#serverSettled.has(id) ? "server" : "client"
11350
12641
  }));
11351
12642
  this.#runTools = [];
12643
+ const invalidated = [...this.#runInvalidated];
12644
+ this.#runInvalidated = /* @__PURE__ */ new Set();
11352
12645
  this.dispatchEvent(
11353
12646
  new CustomEvent(RUN_FINISHED_EVENT, {
11354
- detail: { tools },
12647
+ detail: { tools, invalidated },
12648
+ bubbles: true,
12649
+ composed: true
12650
+ })
12651
+ );
12652
+ }
12653
+ /**
12654
+ * Route one invalidation to the host, and remember it for the run summary.
12655
+ *
12656
+ * Dispatched immediately rather than only at the end, because that is what
12657
+ * makes a long multi-step run feel live -- the list refreshes as the third of
12658
+ * eight writes lands. The accumulated set rides
12659
+ * {@link RUN_FINISHED_EVENT} as well, so a host that would rather refetch once
12660
+ * upgrades by reading one extra field instead of adding a listener.
12661
+ *
12662
+ * Nothing is rendered, persisted or replayed. An invalidation is an
12663
+ * imperative: it has no place in the transcript and no meaning once acted on,
12664
+ * and replaying one on every thread load would be a refetch storm. That is the
12665
+ * whole reason the server sends it as `CUSTOM` rather than as an activity.
12666
+ */
12667
+ #dispatchInvalidation(value) {
12668
+ const payload = value ?? {};
12669
+ const keys = Array.isArray(payload.keys) ? payload.keys.filter((key) => typeof key === "string") : [];
12670
+ if (keys.length === 0) {
12671
+ return;
12672
+ }
12673
+ for (const key of keys) {
12674
+ this.#runInvalidated.add(key);
12675
+ }
12676
+ this.dispatchEvent(
12677
+ new CustomEvent(INVALIDATE_EVENT, {
12678
+ detail: { keys, reason: typeof payload.reason === "string" ? payload.reason : null },
11355
12679
  bubbles: true,
11356
12680
  composed: true
11357
12681
  })
@@ -11366,7 +12690,7 @@ var AgUiChat = class extends HTMLElement {
11366
12690
  note.textContent = this.#strings.stopped;
11367
12691
  this.#ensureGroup().appendChild(note);
11368
12692
  this.#updateEmptyState();
11369
- this.#messages.scrollTop = this.#messages.scrollHeight;
12693
+ this.#scroller.follow();
11370
12694
  }
11371
12695
  /**
11372
12696
  * Show a "thinking" indicator while the agent is being awaited — both the
@@ -11390,7 +12714,7 @@ var AgUiChat = class extends HTMLElement {
11390
12714
  this.#pending = pending;
11391
12715
  this.#ensureGroup().appendChild(pending);
11392
12716
  this.#updateEmptyState();
11393
- this.#messages.scrollTop = this.#messages.scrollHeight;
12717
+ this.#scroller.follow();
11394
12718
  }
11395
12719
  /** Remove the pending indicator if shown. */
11396
12720
  #hidePending() {
@@ -11408,7 +12732,7 @@ var AgUiChat = class extends HTMLElement {
11408
12732
  const group = this.#ensureGroup();
11409
12733
  group.insertBefore(this.#thoughts.element, group.firstChild);
11410
12734
  this.#updateEmptyState();
11411
- this.#messages.scrollTop = this.#messages.scrollHeight;
12735
+ this.#scroller.follow();
11412
12736
  }
11413
12737
  return this.#thoughts;
11414
12738
  }
@@ -11460,7 +12784,7 @@ var AgUiChat = class extends HTMLElement {
11460
12784
  this.#streamBuffer = buffer;
11461
12785
  const bubble = this.#openStream();
11462
12786
  bubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
11463
- this.#messages.scrollTop = this.#messages.scrollHeight;
12787
+ this.#scroller.follow();
11464
12788
  return bubble;
11465
12789
  }
11466
12790
  /**
@@ -11508,7 +12832,7 @@ var AgUiChat = class extends HTMLElement {
11508
12832
  #appendNotice(icon, text3, kind) {
11509
12833
  this.#ensureGroup().appendChild(renderRunNotice(icon, text3, kind));
11510
12834
  this.#updateEmptyState();
11511
- this.#messages.scrollTop = this.#messages.scrollHeight;
12835
+ this.#scroller.follow();
11512
12836
  }
11513
12837
  /**
11514
12838
  * Turn on chart rendering, by whichever route this consumer wants.
@@ -11529,9 +12853,16 @@ var AgUiChat = class extends HTMLElement {
11529
12853
  * arrives is not something to switch on for everybody.
11530
12854
  */
11531
12855
  enableCharts(routes = ["tool", "activity"]) {
11532
- const first = !this.#chartActivity && !this.#toolRegistry.has(CHART_TOOL_NAME);
12856
+ const first = !this.#activityRenderers.has(CHART_ACTIVITY_TYPE) && !this.#toolRegistry.has(CHART_TOOL_NAME);
11533
12857
  if (routes.includes("activity")) {
11534
- this.#chartActivity = true;
12858
+ this.registerActivityRenderer({
12859
+ type: CHART_ACTIVITY_TYPE,
12860
+ render: (content) => {
12861
+ const spec = chartSpecFrom(content);
12862
+ return spec === null ? null : renderChart(spec);
12863
+ },
12864
+ removedNotice: this.#strings.chartUndrawable
12865
+ });
11535
12866
  }
11536
12867
  if (routes.includes("tool")) {
11537
12868
  this.registerTool(createChartTool());
@@ -11564,27 +12895,120 @@ var AgUiChat = class extends HTMLElement {
11564
12895
  this.#cardElements.get(call.id)?.after(node);
11565
12896
  this.#afterTranscriptGrew();
11566
12897
  }
11567
- /** Draw, or redraw in place, the chart for one activity message. */
11568
- #drawActivityChart(messageId, content) {
11569
- const spec = chartSpecFrom(content);
11570
- const block = spec === null ? null : renderChart(spec);
11571
- if (block === null) {
11572
- this.#activityBlocks.get(messageId)?.remove();
11573
- this.#activityBlocks.delete(messageId);
12898
+ /**
12899
+ * Teach this element to draw one kind of AG-UI activity.
12900
+ *
12901
+ * `activity_type` is one of exactly two fields the protocol leaves an open
12902
+ * string, and it is the **content** one: an activity is materialised into a
12903
+ * message, persisted with the thread, and replayed on every restore. Its
12904
+ * sibling `CUSTOM` carries an imperative and is dispatched to the page
12905
+ * instead ({@link CUSTOM_AGENT_EVENT}).
12906
+ *
12907
+ * That asymmetry decides which carrier a server should use. Content has a
12908
+ * place in the conversation and should come back; an imperative has no place
12909
+ * and no meaning once acted on.
12910
+ *
12911
+ * ```js
12912
+ * chat.registerActivityRenderer({
12913
+ * type: "build_status",
12914
+ * render: (content) => {
12915
+ * const el = document.createElement("div");
12916
+ * el.textContent = `Build ${content.status}`;
12917
+ * return el;
12918
+ * },
12919
+ * });
12920
+ * ```
12921
+ *
12922
+ * Registering a type twice replaces the earlier renderer, so a host can
12923
+ * override a built-in -- `chart` and `compaction` are registrations like any
12924
+ * other, not privileged branches.
12925
+ *
12926
+ * ⚠ `render` runs again on every thread load. See {@link ActivityRenderer}
12927
+ * for what that requires of it.
12928
+ */
12929
+ registerActivityRenderer(registration) {
12930
+ this.#activityRenderers.set(registration.type, registration);
12931
+ this.#unhandledActivityTypes.delete(registration.type);
12932
+ }
12933
+ /**
12934
+ * Activity types that arrived with nobody registered to draw them.
12935
+ *
12936
+ * Deliberately the only trace an unhandled activity leaves. Ignoring an
12937
+ * unknown name is the protocol's own answer and the whole point of an open
12938
+ * field, so warning would fire on every forward-compatible server -- but
12939
+ * "nothing happened and nothing was said" is impossible to debug, so the set
12940
+ * is readable. Accumulates for the element's lifetime, across threads.
12941
+ */
12942
+ get unhandledActivityTypes() {
12943
+ return [...this.#unhandledActivityTypes];
12944
+ }
12945
+ /**
12946
+ * Draw, replace or remove one activity, whatever kind it is.
12947
+ *
12948
+ * The single path for all three routes an activity arrives by -- pushed
12949
+ * (`onActivity`), patched (`onActivityChanged`) and replayed from history --
12950
+ * which is why the renderer contract has to be pure: the same content is
12951
+ * drawn again on every thread load.
12952
+ *
12953
+ * An unregistered type draws nothing and says nothing. That is the protocol's
12954
+ * own answer -- a client that does not know a name ignores the event -- and a
12955
+ * warning here would fire on every well-behaved forward-compatible server,
12956
+ * while a placeholder would put the protocol's growth in the user's face.
12957
+ * {@link unhandledActivityTypes} is the way to find out what arrived.
12958
+ */
12959
+ #drawActivity(messageId, activityType, content) {
12960
+ const registration = this.#activityRenderers.get(activityType);
12961
+ if (registration === void 0) {
12962
+ this.#unhandledActivityTypes.add(activityType);
12963
+ return;
12964
+ }
12965
+ let node;
12966
+ try {
12967
+ node = registration.render(content);
12968
+ } catch (error) {
12969
+ console.warn(`ag-ui-chat: render failed for activity ${activityType}`, error);
12970
+ node = null;
12971
+ }
12972
+ if (node === null) {
12973
+ this.#removeActivity(messageId, activityType, registration.removedNotice, content);
11574
12974
  return;
11575
12975
  }
11576
12976
  const existing = this.#activityBlocks.get(messageId);
11577
12977
  if (existing === void 0) {
11578
- this.#ensureGroup().appendChild(block);
12978
+ this.#ensureGroup().appendChild(node);
11579
12979
  } else {
11580
- existing.replaceWith(block);
12980
+ existing.replaceWith(node);
11581
12981
  }
11582
- this.#activityBlocks.set(messageId, block);
12982
+ this.#activityBlocks.set(messageId, node);
11583
12983
  this.#afterTranscriptGrew();
11584
12984
  }
12985
+ /**
12986
+ * Take away an activity whose content stopped being drawable.
12987
+ *
12988
+ * Leaving the old one up is the worst available answer: it shows values that
12989
+ * have been retracted, reading as current, and a reload drops it anyway
12990
+ * because the *stored* content is the version that could not be drawn. Live
12991
+ * and reload should agree, and both should say "gone".
12992
+ *
12993
+ * Removing is right; doing it in silence was not. A chart that had been drawn
12994
+ * simply disappeared, with no `console` call anywhere on the path -- which
12995
+ * nobody reports as a bug, they report as "the charts are flaky".
12996
+ */
12997
+ #removeActivity(messageId, activityType, notice, content) {
12998
+ const had = this.#activityBlocks.has(messageId);
12999
+ this.#activityBlocks.get(messageId)?.remove();
13000
+ this.#activityBlocks.delete(messageId);
13001
+ console.warn(
13002
+ `ag-ui-chat: activity ${messageId} (${activityType}) was not drawable and has been removed. A chart's points must each be a finite JSON number; a numeric column serialised as a string (a Decimal, typically) is rejected rather than coerced.`,
13003
+ content
13004
+ );
13005
+ if (had && notice !== void 0) {
13006
+ this.#appendNotice("\u{1F4C9}", notice, "chart-undrawable");
13007
+ }
13008
+ }
11585
13009
  #afterTranscriptGrew() {
11586
13010
  this.#updateEmptyState();
11587
- this.#messages.scrollTop = this.#messages.scrollHeight;
13011
+ this.#scroller.follow();
11588
13012
  }
11589
13013
  #cardFor(call) {
11590
13014
  const existing = this.#toolCards.get(call.id);
@@ -11597,7 +13021,7 @@ var AgUiChat = class extends HTMLElement {
11597
13021
  this.#toolCards.set(call.id, card);
11598
13022
  this.#ensureGroup().appendChild(card.element);
11599
13023
  this.#updateEmptyState();
11600
- this.#messages.scrollTop = this.#messages.scrollHeight;
13024
+ this.#scroller.follow();
11601
13025
  return card;
11602
13026
  }
11603
13027
  };
@@ -11667,7 +13091,7 @@ function setControlValue(el2, value) {
11667
13091
  }
11668
13092
 
11669
13093
  // src/version.ts
11670
- var VERSION = "0.28.0";
13094
+ var VERSION = "0.29.0";
11671
13095
  export {
11672
13096
  ATTACHMENT_EVENT,
11673
13097
  AgUiChat,
@@ -11675,12 +13099,19 @@ export {
11675
13099
  CHART_ACTIVITY_TYPE,
11676
13100
  CHART_TOOL_NAME,
11677
13101
  COMPACTION_ACTIVITY_TYPE,
13102
+ CUSTOM_AGENT_EVENT,
11678
13103
  CheckpointMenu,
11679
13104
  ClientToolRegistry,
11680
13105
  ConnectionLostError,
11681
13106
  DEFAULT_UI_STRINGS,
11682
13107
  ELEMENT_TAG,
13108
+ FEEDBACK_EVENT,
13109
+ INVALIDATE_CUSTOM_NAME,
13110
+ INVALIDATE_EVENT,
11683
13111
  LOAD_CAPABILITY_TOOL,
13112
+ MAX_QUOTE_CHARS,
13113
+ MAX_SUGGESTIONS,
13114
+ MAX_SUGGESTION_CHARS,
11684
13115
  MAX_TOOL_ROUNDS,
11685
13116
  MESSAGE_ROLE,
11686
13117
  PAGE_ACTIONS,
@@ -11689,6 +13120,7 @@ export {
11689
13120
  RunIndex,
11690
13121
  STATE_EVENT,
11691
13122
  SUBMIT_EVENT,
13123
+ SUGGESTIONS_ACTIVITY_TYPE,
11692
13124
  SessionStorageStore,
11693
13125
  TOGGLE_EVENT,
11694
13126
  TOOL_CALL_STATUS,
@@ -11700,6 +13132,9 @@ export {
11700
13132
  X_DESTRUCTIVE_KEY,
11701
13133
  X_NAVIGATES_KEY,
11702
13134
  X_SUMMARY_KEY,
13135
+ asQuote,
13136
+ attachMessageActions,
13137
+ attachQuoteOffer,
11703
13138
  chartSpecFrom,
11704
13139
  clickElement,
11705
13140
  createHttpAgent,
@@ -11716,14 +13151,18 @@ export {
11716
13151
  isDestructive,
11717
13152
  isNavigates,
11718
13153
  mergeUiStrings,
13154
+ messageActionBar,
11719
13155
  messageAttachments,
11720
13156
  parseToolCatalog,
11721
13157
  prefersReducedMotion,
11722
13158
  pressButton,
11723
13159
  pressThenClick,
11724
13160
  prettifyToolName,
13161
+ quotableSelection,
13162
+ relativeTime,
11725
13163
  renderChart,
11726
13164
  renderMarkdown,
13165
+ renderSuggestionChips,
11727
13166
  requestApproval,
11728
13167
  requestConfirmation,
11729
13168
  requestQuestion,
@@ -11733,6 +13172,7 @@ export {
11733
13172
  setControlValue,
11734
13173
  setNativeChecked,
11735
13174
  setNativeValue,
13175
+ suggestionPrompts,
11736
13176
  toggleCheckbox,
11737
13177
  toggleControl,
11738
13178
  transcribeAudio,