@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.
- package/CHANGELOG.md +417 -1
- package/README.md +371 -5
- package/dist/ag-ui-web-component.bundle.js +308 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +69 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +207 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +38 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1516 -76
- package/dist/index.js.map +4 -4
- package/dist/ui/approval_card.d.ts +18 -0
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts +10 -0
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +16 -0
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/message_actions.d.ts +46 -0
- package/dist/ui/message_actions.d.ts.map +1 -0
- package/dist/ui/page_quote_offer.d.ts +33 -0
- package/dist/ui/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/quote_selection.d.ts +66 -0
- package/dist/ui/quote_selection.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +10 -0
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/stick_to_bottom.d.ts +55 -0
- package/dist/ui/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/suggestion_chips.d.ts +29 -0
- package/dist/ui/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/thread_drawer.d.ts +10 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +8 -0
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +40 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +75 -0
- package/src/core/ag_ui_chat.ts +927 -73
- package/src/core/agui_client.ts +63 -0
- package/src/index.ts +39 -0
- package/src/ui/approval_card.ts +90 -2
- package/src/ui/checkpoint_menu.ts +22 -5
- package/src/ui/confirmation_card.ts +29 -1
- package/src/ui/message_actions.ts +158 -0
- package/src/ui/page_quote_offer.ts +215 -0
- package/src/ui/quote_selection.ts +345 -0
- package/src/ui/relative_time.ts +11 -0
- package/src/ui/stick_to_bottom.ts +126 -0
- package/src/ui/styles.ts +227 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +9 -0
- package/src/ui/ui_strings.ts +60 -0
- 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", () =>
|
|
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 :
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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)
|
|
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 ===
|
|
10532
|
-
this.#
|
|
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
|
-
|
|
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
|
|
10645
|
-
|
|
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(
|
|
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", "
|
|
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.#
|
|
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.#
|
|
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
|
-
/**
|
|
11091
|
-
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
12499
|
+
this.#scroller.follow();
|
|
11224
12500
|
this.#confirmAbort = null;
|
|
11225
12501
|
const responses = {};
|
|
11226
|
-
for (const { id, approved } of answered) {
|
|
11227
|
-
responses[id] = approved ? {
|
|
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
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
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
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
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
|
-
|
|
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
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
12856
|
+
const first = !this.#activityRenderers.has(CHART_ACTIVITY_TYPE) && !this.#toolRegistry.has(CHART_TOOL_NAME);
|
|
11533
12857
|
if (routes.includes("activity")) {
|
|
11534
|
-
this
|
|
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
|
-
/**
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
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(
|
|
12978
|
+
this.#ensureGroup().appendChild(node);
|
|
11579
12979
|
} else {
|
|
11580
|
-
existing.replaceWith(
|
|
12980
|
+
existing.replaceWith(node);
|
|
11581
12981
|
}
|
|
11582
|
-
this.#activityBlocks.set(messageId,
|
|
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.#
|
|
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.#
|
|
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.
|
|
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,
|