@artooi/ag-ui-web-component 0.28.0 → 0.30.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 +615 -1
- package/README.md +564 -35
- package/dist/ag-ui-web-component.bundle.js +491 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +129 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +232 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +56 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2081 -98
- 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 +56 -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/subagent_panel.d.ts +92 -0
- package/dist/ui/subagent_panel.d.ts.map +1 -0
- package/dist/ui/subagent_update.d.ts +19 -0
- package/dist/ui/subagent_update.d.ts.map +1 -0
- 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 +81 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +50 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +138 -1
- package/src/core/ag_ui_chat.ts +1081 -73
- package/src/core/agui_client.ts +89 -2
- package/src/index.ts +43 -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 +170 -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 +410 -0
- package/src/ui/subagent_panel.ts +213 -0
- package/src/ui/subagent_update.ts +80 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +138 -3
- package/src/ui/ui_strings.ts +75 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,19 @@ 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";
|
|
14
|
+
var SUBAGENT_CUSTOM_NAME = "ag_ui.subagent";
|
|
15
|
+
var SUBAGENT_PHASE = {
|
|
16
|
+
STARTED: "started",
|
|
17
|
+
TOOL_CALL: "tool_call",
|
|
18
|
+
TOOL_RESULT: "tool_result",
|
|
19
|
+
FINISHED: "finished",
|
|
20
|
+
FAILED: "failed"
|
|
21
|
+
};
|
|
9
22
|
var MESSAGE_ROLE = {
|
|
10
23
|
USER: "user",
|
|
11
24
|
ASSISTANT: "assistant"
|
|
@@ -16,6 +29,11 @@ var X_SUMMARY_KEY = "x-summary";
|
|
|
16
29
|
var X_NAVIGATES_KEY = "x-navigates";
|
|
17
30
|
var READ_PAGE_TOOL = "read_page";
|
|
18
31
|
var MAX_TOOL_ROUNDS = 10;
|
|
32
|
+
var MESSAGE_ACTIONS = {
|
|
33
|
+
COPY: "copy",
|
|
34
|
+
RETRY: "retry",
|
|
35
|
+
FEEDBACK: "feedback"
|
|
36
|
+
};
|
|
19
37
|
var TOOL_CALL_STATUS = {
|
|
20
38
|
PENDING: "pending",
|
|
21
39
|
DEFERRED: "deferred",
|
|
@@ -47,6 +65,7 @@ var ICON_FILE_IMAGE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"
|
|
|
47
65
|
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
66
|
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
67
|
var CHART_ACTIVITY_TYPE = "chart";
|
|
68
|
+
var ANNOUNCE_CLEAR_MS = 150;
|
|
50
69
|
|
|
51
70
|
// src/core/ag_ui_chat.ts
|
|
52
71
|
import { randomUUID as randomUUID5 } from "@ag-ui/client";
|
|
@@ -537,6 +556,12 @@ var DEFAULT_UI_STRINGS = {
|
|
|
537
556
|
forkRun: "Fork",
|
|
538
557
|
forkedRun: "branched",
|
|
539
558
|
conversation: "Conversation",
|
|
559
|
+
jumpToLatest: "Jump to latest",
|
|
560
|
+
announceResponding: "Assistant is responding",
|
|
561
|
+
announceAnswerReady: "Assistant answered",
|
|
562
|
+
announceAwaitingDecision: "{count} action is waiting for your approval",
|
|
563
|
+
announceStopped: "Response stopped",
|
|
564
|
+
announceFailed: "The response failed",
|
|
540
565
|
thinking: "Assistant is thinking\u2026",
|
|
541
566
|
thoughts: "Thoughts",
|
|
542
567
|
stopped: "\u23F9 Stopped",
|
|
@@ -544,6 +569,8 @@ var DEFAULT_UI_STRINGS = {
|
|
|
544
569
|
noResult: "No result returned.",
|
|
545
570
|
declinedAction: "User declined the action.",
|
|
546
571
|
navigating: "Navigating\u2026",
|
|
572
|
+
historyReplaced: "The server replaced this conversation's history. Reload to see the updated transcript.",
|
|
573
|
+
chartUndrawable: "A chart could not be drawn from the data sent, so it was removed.",
|
|
547
574
|
historyCompacted: "Earlier turns condensed to fit the context window ({count} removed)",
|
|
548
575
|
usingSkill: "Using skill {name}",
|
|
549
576
|
runInterrupted: "The previous response didn\u2019t finish \u2014 the page changed before it arrived.",
|
|
@@ -573,7 +600,20 @@ var DEFAULT_UI_STRINGS = {
|
|
|
573
600
|
errorLabel: "Error",
|
|
574
601
|
declinedLabel: "Declined",
|
|
575
602
|
details: "Details",
|
|
603
|
+
subAgentWorking: "Working\u2026",
|
|
604
|
+
subAgentSteps: "Steps the sub-agent took",
|
|
605
|
+
approvalEditArgs: "Edit the arguments before approving",
|
|
606
|
+
approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
|
|
607
|
+
approvalArgsNotAnObject: "Arguments have to be a JSON object.",
|
|
608
|
+
suggestions: "Suggested follow-ups",
|
|
609
|
+
messageActions: "Message actions",
|
|
610
|
+
quoteSelection: "Quote",
|
|
611
|
+
copyMessage: "Copy message",
|
|
612
|
+
retryMessage: "Try again",
|
|
613
|
+
feedbackUp: "Good answer",
|
|
614
|
+
feedbackDown: "Poor answer",
|
|
576
615
|
confirmAction: "Confirm action",
|
|
616
|
+
confirmAlways: "Always allow",
|
|
577
617
|
confirmRun: "Run \u201C{tool}\u201D?",
|
|
578
618
|
confirm: "Confirm",
|
|
579
619
|
cancel: "Cancel",
|
|
@@ -640,6 +680,7 @@ function requestApproval(host, request, options = {}) {
|
|
|
640
680
|
body.className = "approval-body";
|
|
641
681
|
body.setAttribute("part", "approval-body");
|
|
642
682
|
body.textContent = request.message ?? strings.approvalPrompt;
|
|
683
|
+
const editor = buildEditor(request, options, strings);
|
|
643
684
|
const actions = document.createElement("div");
|
|
644
685
|
actions.className = "approval-actions";
|
|
645
686
|
actions.setAttribute("part", "approval-actions");
|
|
@@ -657,10 +698,15 @@ function requestApproval(host, request, options = {}) {
|
|
|
657
698
|
resolve(approved);
|
|
658
699
|
};
|
|
659
700
|
deny.addEventListener("click", () => close(false));
|
|
660
|
-
approve.addEventListener("click", () =>
|
|
701
|
+
approve.addEventListener("click", () => {
|
|
702
|
+
if (editor !== null && !editor.commit()) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
close(true);
|
|
706
|
+
});
|
|
661
707
|
options.signal?.addEventListener("abort", () => close(false), { once: true });
|
|
662
708
|
actions.append(deny, approve);
|
|
663
|
-
card.append(body, actions);
|
|
709
|
+
card.append(body, ...editor === null ? [] : [editor.root], actions);
|
|
664
710
|
host.appendChild(card);
|
|
665
711
|
if (options.signal?.aborted === true) {
|
|
666
712
|
close(false);
|
|
@@ -669,6 +715,54 @@ function requestApproval(host, request, options = {}) {
|
|
|
669
715
|
approve.focus();
|
|
670
716
|
});
|
|
671
717
|
}
|
|
718
|
+
function buildEditor(request, options, strings) {
|
|
719
|
+
const { onEdit } = options;
|
|
720
|
+
if (onEdit === void 0 || request.args === void 0) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const original = JSON.stringify(request.args, null, 2);
|
|
724
|
+
const root = document.createElement("div");
|
|
725
|
+
root.className = "approval-edit";
|
|
726
|
+
root.setAttribute("part", "approval-edit");
|
|
727
|
+
const field = document.createElement("textarea");
|
|
728
|
+
field.className = "approval-args";
|
|
729
|
+
field.setAttribute("part", "approval-args");
|
|
730
|
+
field.setAttribute("aria-label", strings.approvalEditArgs);
|
|
731
|
+
field.rows = Math.min(10, original.split("\n").length);
|
|
732
|
+
field.value = original;
|
|
733
|
+
const error = document.createElement("div");
|
|
734
|
+
error.className = "approval-error";
|
|
735
|
+
error.setAttribute("part", "approval-error");
|
|
736
|
+
error.setAttribute("role", "alert");
|
|
737
|
+
error.hidden = true;
|
|
738
|
+
root.append(field, error);
|
|
739
|
+
return {
|
|
740
|
+
root,
|
|
741
|
+
commit: () => {
|
|
742
|
+
if (field.value === original) {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
let parsed;
|
|
746
|
+
try {
|
|
747
|
+
parsed = JSON.parse(field.value);
|
|
748
|
+
} catch {
|
|
749
|
+
error.textContent = strings.approvalArgsInvalid;
|
|
750
|
+
error.hidden = false;
|
|
751
|
+
field.focus();
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
755
|
+
error.textContent = strings.approvalArgsNotAnObject;
|
|
756
|
+
error.hidden = false;
|
|
757
|
+
field.focus();
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
error.hidden = true;
|
|
761
|
+
onEdit(parsed);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
672
766
|
|
|
673
767
|
// src/ui/attach_copy_buttons.ts
|
|
674
768
|
var CONFIRM_MS = 1500;
|
|
@@ -1399,6 +1493,7 @@ var CheckpointMenu = class {
|
|
|
1399
1493
|
#heading;
|
|
1400
1494
|
/** What had focus before the panel opened, restored on close. */
|
|
1401
1495
|
#lastFocused = null;
|
|
1496
|
+
#formatRelativeTime = null;
|
|
1402
1497
|
#strings;
|
|
1403
1498
|
#runs = [];
|
|
1404
1499
|
constructor(onPick, strings = DEFAULT_UI_STRINGS) {
|
|
@@ -1431,6 +1526,21 @@ var CheckpointMenu = class {
|
|
|
1431
1526
|
this.#render();
|
|
1432
1527
|
}
|
|
1433
1528
|
/** Re-localize a panel built before the host's strings resolved. */
|
|
1529
|
+
/**
|
|
1530
|
+
* Replace the timestamp formatter, or restore the built-in with `null`.
|
|
1531
|
+
*
|
|
1532
|
+
* The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
|
|
1533
|
+
* in this component, so it never disagrees with a host's own formatting by
|
|
1534
|
+
* guessing a locale. That is a defensible default and a poor requirement, so
|
|
1535
|
+
* this is the way out.
|
|
1536
|
+
*/
|
|
1537
|
+
setRelativeTimeFormatter(format) {
|
|
1538
|
+
this.#formatRelativeTime = format;
|
|
1539
|
+
}
|
|
1540
|
+
/** This row's timestamp, through the host's formatter when it set one. */
|
|
1541
|
+
#formatTime(timestamp) {
|
|
1542
|
+
return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, Date.now(), this.#strings);
|
|
1543
|
+
}
|
|
1434
1544
|
setStrings(strings) {
|
|
1435
1545
|
this.#strings = strings;
|
|
1436
1546
|
this.element.setAttribute("aria-label", strings.checkpoints);
|
|
@@ -1532,7 +1642,7 @@ var CheckpointMenu = class {
|
|
|
1532
1642
|
row.className = "checkpoint-row";
|
|
1533
1643
|
row.setAttribute("part", "checkpoint-row");
|
|
1534
1644
|
const preview = previewOf(run);
|
|
1535
|
-
const time = run.started_at === null ? null :
|
|
1645
|
+
const time = run.started_at === null ? null : this.#formatTime(Date.parse(run.started_at));
|
|
1536
1646
|
const label = document.createElement("span");
|
|
1537
1647
|
label.className = "checkpoint-label";
|
|
1538
1648
|
label.setAttribute("part", "checkpoint-label");
|
|
@@ -1613,6 +1723,7 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1613
1723
|
actions.className = "confirm-actions";
|
|
1614
1724
|
actions.setAttribute("part", "confirm-actions");
|
|
1615
1725
|
const cancel = actionButton2("cancel", strings.cancel);
|
|
1726
|
+
const always = options.onAlwaysAllow === void 0 ? null : actionButton2("always", strings.confirmAlways.replace("{tool}", request.toolName));
|
|
1616
1727
|
const confirm2 = actionButton2("confirm", strings.confirm);
|
|
1617
1728
|
let settled = false;
|
|
1618
1729
|
const close = (accepted) => {
|
|
@@ -1625,8 +1736,12 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1625
1736
|
};
|
|
1626
1737
|
cancel.addEventListener("click", () => close(false));
|
|
1627
1738
|
confirm2.addEventListener("click", () => close(true));
|
|
1739
|
+
always?.addEventListener("click", () => {
|
|
1740
|
+
options.onAlwaysAllow?.();
|
|
1741
|
+
close(true);
|
|
1742
|
+
});
|
|
1628
1743
|
options.signal?.addEventListener("abort", () => close(false), { once: true });
|
|
1629
|
-
actions.append(cancel, confirm2);
|
|
1744
|
+
actions.append(cancel, ...always === null ? [] : [always], confirm2);
|
|
1630
1745
|
card.append(body, args, actions);
|
|
1631
1746
|
host.appendChild(card);
|
|
1632
1747
|
if (options.signal?.aborted === true) {
|
|
@@ -1637,6 +1752,349 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1637
1752
|
});
|
|
1638
1753
|
}
|
|
1639
1754
|
|
|
1755
|
+
// src/ui/message_actions.ts
|
|
1756
|
+
var CONFIRM_MS2 = 1500;
|
|
1757
|
+
function attachMessageActions(bubble, options) {
|
|
1758
|
+
if (existingBar(bubble) !== null) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const bar = messageActionBar(bubble, options.strings);
|
|
1762
|
+
const text3 = options.text;
|
|
1763
|
+
if (text3 !== void 0) {
|
|
1764
|
+
bar.appendChild(copyButton(options.strings, text3));
|
|
1765
|
+
}
|
|
1766
|
+
if (options.onFeedback !== void 0) {
|
|
1767
|
+
bar.append(
|
|
1768
|
+
feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
|
|
1769
|
+
feedbackButton("down", options.strings.feedbackDown, options.onFeedback)
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function messageActionBar(bubble, strings) {
|
|
1774
|
+
const existing = existingBar(bubble);
|
|
1775
|
+
if (existing !== null) {
|
|
1776
|
+
return existing;
|
|
1777
|
+
}
|
|
1778
|
+
const bar = document.createElement("div");
|
|
1779
|
+
bar.className = "message-actions";
|
|
1780
|
+
bar.setAttribute("part", "message-actions");
|
|
1781
|
+
bar.setAttribute("role", "group");
|
|
1782
|
+
bar.setAttribute("aria-label", strings.messageActions);
|
|
1783
|
+
bubble.after(bar);
|
|
1784
|
+
return bar;
|
|
1785
|
+
}
|
|
1786
|
+
function existingBar(bubble) {
|
|
1787
|
+
const next = bubble.nextElementSibling;
|
|
1788
|
+
return next?.classList.contains("message-actions") === true ? next : null;
|
|
1789
|
+
}
|
|
1790
|
+
function messageActionButton(modifier, label, glyph) {
|
|
1791
|
+
const button2 = document.createElement("button");
|
|
1792
|
+
button2.type = "button";
|
|
1793
|
+
button2.className = `message-action message-action--${modifier}`;
|
|
1794
|
+
button2.setAttribute("part", `message-action message-action-${modifier}`);
|
|
1795
|
+
button2.title = label;
|
|
1796
|
+
button2.setAttribute("aria-label", label);
|
|
1797
|
+
const icon = document.createElement("span");
|
|
1798
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1799
|
+
icon.textContent = glyph;
|
|
1800
|
+
button2.appendChild(icon);
|
|
1801
|
+
return button2;
|
|
1802
|
+
}
|
|
1803
|
+
function copyButton(strings, text3) {
|
|
1804
|
+
const button2 = messageActionButton("copy", strings.copyMessage, "\u2398");
|
|
1805
|
+
button2.addEventListener("click", () => {
|
|
1806
|
+
void navigator.clipboard.writeText(text3()).then(
|
|
1807
|
+
() => flash2(button2, strings.copied, strings.copyMessage),
|
|
1808
|
+
// A denied clipboard permission is the common case, not an exception:
|
|
1809
|
+
// say so on the button rather than throwing into an unhandled rejection.
|
|
1810
|
+
() => flash2(button2, strings.copyFailed, strings.copyMessage)
|
|
1811
|
+
);
|
|
1812
|
+
});
|
|
1813
|
+
return button2;
|
|
1814
|
+
}
|
|
1815
|
+
function feedbackButton(rating, label, report) {
|
|
1816
|
+
const button2 = messageActionButton(
|
|
1817
|
+
rating === "up" ? "up" : "down",
|
|
1818
|
+
label,
|
|
1819
|
+
rating === "up" ? "\u{1F44D}" : "\u{1F44E}"
|
|
1820
|
+
);
|
|
1821
|
+
button2.addEventListener("click", () => {
|
|
1822
|
+
const pressed = button2.getAttribute("aria-pressed") === "true";
|
|
1823
|
+
button2.setAttribute("aria-pressed", pressed ? "false" : "true");
|
|
1824
|
+
report(rating);
|
|
1825
|
+
});
|
|
1826
|
+
button2.setAttribute("aria-pressed", "false");
|
|
1827
|
+
return button2;
|
|
1828
|
+
}
|
|
1829
|
+
function flash2(button2, message, label) {
|
|
1830
|
+
button2.title = message;
|
|
1831
|
+
button2.setAttribute("aria-label", message);
|
|
1832
|
+
button2.classList.add("message-action--confirmed");
|
|
1833
|
+
setTimeout(() => {
|
|
1834
|
+
button2.title = label;
|
|
1835
|
+
button2.setAttribute("aria-label", label);
|
|
1836
|
+
button2.classList.remove("message-action--confirmed");
|
|
1837
|
+
}, CONFIRM_MS2);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/ui/quote_selection.ts
|
|
1841
|
+
var MAX_QUOTE_CHARS = 500;
|
|
1842
|
+
function quotableSelection(container, roots = [], near) {
|
|
1843
|
+
for (const endpoints of endpointCandidates(roots)) {
|
|
1844
|
+
if (!container.contains(endpoints.startContainer)) {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (!container.contains(endpoints.endContainer)) {
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const range = document.createRange();
|
|
1851
|
+
range.setStart(endpoints.startContainer, endpoints.startOffset);
|
|
1852
|
+
range.setEnd(endpoints.endContainer, endpoints.endOffset);
|
|
1853
|
+
const text3 = renderedText(range).trim();
|
|
1854
|
+
if (text3 === "") {
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
return { text: text3, rect: lineToHangFrom(range, near) };
|
|
1858
|
+
}
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
function asQuote(text3) {
|
|
1862
|
+
const lines = tidy(text3);
|
|
1863
|
+
if (lines.length === 0) {
|
|
1864
|
+
return "";
|
|
1865
|
+
}
|
|
1866
|
+
const capped = cap(lines.join("\n"));
|
|
1867
|
+
const quoted = capped.split("\n").map((line) => `> ${line}`.trimEnd()).join("\n");
|
|
1868
|
+
return `${quoted}
|
|
1869
|
+
|
|
1870
|
+
`;
|
|
1871
|
+
}
|
|
1872
|
+
function tidy(text3) {
|
|
1873
|
+
const lines = text3.split(/\r\n?|\n/).map((line) => line.trimEnd());
|
|
1874
|
+
const indents = lines.filter((line) => line !== "").map(indentOf);
|
|
1875
|
+
const shared = indents.length === 0 ? 0 : Math.min(...indents);
|
|
1876
|
+
const kept = [];
|
|
1877
|
+
for (const line of lines) {
|
|
1878
|
+
const dedented = line.slice(shared);
|
|
1879
|
+
if (dedented === "" && (kept.length === 0 || kept[kept.length - 1] === "")) {
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
kept.push(dedented);
|
|
1883
|
+
}
|
|
1884
|
+
while (kept[kept.length - 1] === "") {
|
|
1885
|
+
kept.pop();
|
|
1886
|
+
}
|
|
1887
|
+
return kept;
|
|
1888
|
+
}
|
|
1889
|
+
function indentOf(line) {
|
|
1890
|
+
return line.length - line.trimStart().length;
|
|
1891
|
+
}
|
|
1892
|
+
function cap(text3) {
|
|
1893
|
+
return text3.length > MAX_QUOTE_CHARS ? `${text3.slice(0, MAX_QUOTE_CHARS).trimEnd()}...` : text3;
|
|
1894
|
+
}
|
|
1895
|
+
function renderedText(range) {
|
|
1896
|
+
let text3 = "";
|
|
1897
|
+
for (const node of textNodesIn(range)) {
|
|
1898
|
+
const parent = node.parentElement;
|
|
1899
|
+
if (!isRendered(parent)) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
const from = node === range.startContainer ? range.startOffset : 0;
|
|
1903
|
+
const to = node === range.endContainer ? range.endOffset : node.data.length;
|
|
1904
|
+
text3 += collapse(node.data.slice(from, to), parent);
|
|
1905
|
+
}
|
|
1906
|
+
return text3;
|
|
1907
|
+
}
|
|
1908
|
+
function collapse(text3, parent) {
|
|
1909
|
+
if (PREFORMATTED.has(whiteSpaceOf(parent))) {
|
|
1910
|
+
return text3;
|
|
1911
|
+
}
|
|
1912
|
+
return text3.replace(/[^\S\n]*\n[^\S\n]*/g, "\n").replace(/[^\S\n]+/g, " ");
|
|
1913
|
+
}
|
|
1914
|
+
var PREFORMATTED = /* @__PURE__ */ new Set(["pre", "pre-wrap", "break-spaces"]);
|
|
1915
|
+
function whiteSpaceOf(element) {
|
|
1916
|
+
return window.getComputedStyle(element).whiteSpace;
|
|
1917
|
+
}
|
|
1918
|
+
function textNodesIn(range) {
|
|
1919
|
+
const root = range.commonAncestorContainer;
|
|
1920
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
1921
|
+
return [root];
|
|
1922
|
+
}
|
|
1923
|
+
const found = [];
|
|
1924
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1925
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
1926
|
+
if (range.intersectsNode(node)) {
|
|
1927
|
+
found.push(node);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return found;
|
|
1931
|
+
}
|
|
1932
|
+
function isRendered(element) {
|
|
1933
|
+
if (typeof element.checkVisibility !== "function") {
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
return element.checkVisibility({
|
|
1937
|
+
contentVisibilityAuto: true,
|
|
1938
|
+
opacityProperty: true,
|
|
1939
|
+
visibilityProperty: true
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
function lineToHangFrom(range, near) {
|
|
1943
|
+
const lines = [...range.getClientRects()];
|
|
1944
|
+
if (lines.length === 0) {
|
|
1945
|
+
return range.getBoundingClientRect();
|
|
1946
|
+
}
|
|
1947
|
+
if (near === void 0) {
|
|
1948
|
+
return lines[0];
|
|
1949
|
+
}
|
|
1950
|
+
let closest = lines[0];
|
|
1951
|
+
let shortest = distanceTo(closest, near);
|
|
1952
|
+
for (const line of lines.slice(1)) {
|
|
1953
|
+
const distance = distanceTo(line, near);
|
|
1954
|
+
if (distance < shortest) {
|
|
1955
|
+
shortest = distance;
|
|
1956
|
+
closest = line;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return closest;
|
|
1960
|
+
}
|
|
1961
|
+
function distanceTo(rect, point) {
|
|
1962
|
+
const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
|
|
1963
|
+
const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
|
|
1964
|
+
return Math.hypot(dx, dy);
|
|
1965
|
+
}
|
|
1966
|
+
function endpointCandidates(roots) {
|
|
1967
|
+
const selection = window.getSelection();
|
|
1968
|
+
if (selection === null) {
|
|
1969
|
+
return [];
|
|
1970
|
+
}
|
|
1971
|
+
const candidates = [...composedRanges(selection, roots)];
|
|
1972
|
+
if (selection.rangeCount > 0) {
|
|
1973
|
+
candidates.push(selection.getRangeAt(0));
|
|
1974
|
+
}
|
|
1975
|
+
return candidates;
|
|
1976
|
+
}
|
|
1977
|
+
function composedRanges(selection, roots) {
|
|
1978
|
+
const composed = selection.getComposedRanges;
|
|
1979
|
+
if (composed === void 0) {
|
|
1980
|
+
return [];
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
return composed.call(selection, { shadowRoots: roots });
|
|
1984
|
+
} catch {
|
|
1985
|
+
return composed.call(selection, ...roots);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// src/ui/page_quote_offer.ts
|
|
1990
|
+
var GAP = 6;
|
|
1991
|
+
var OFFER_CSS = `
|
|
1992
|
+
.ag-ui-quote-offer {
|
|
1993
|
+
position: fixed;
|
|
1994
|
+
z-index: 2147483000;
|
|
1995
|
+
transform: translate(-50%, -100%);
|
|
1996
|
+
margin: 0;
|
|
1997
|
+
padding: 0.25em 0.7em;
|
|
1998
|
+
border: 1px solid rgb(0 0 0 / 0.15);
|
|
1999
|
+
border-radius: 999px;
|
|
2000
|
+
background: Canvas;
|
|
2001
|
+
color: CanvasText;
|
|
2002
|
+
font: inherit;
|
|
2003
|
+
font-size: 0.8rem;
|
|
2004
|
+
line-height: 1.6;
|
|
2005
|
+
white-space: nowrap;
|
|
2006
|
+
cursor: pointer;
|
|
2007
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
.ag-ui-quote-offer[data-below="true"] {
|
|
2011
|
+
transform: translate(-50%, 0);
|
|
2012
|
+
}
|
|
2013
|
+
`;
|
|
2014
|
+
function attachQuoteOffer(options) {
|
|
2015
|
+
const { within, exclude, onQuote } = options;
|
|
2016
|
+
const sheet = new CSSStyleSheet();
|
|
2017
|
+
sheet.replaceSync(OFFER_CSS);
|
|
2018
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
2019
|
+
const button2 = document.createElement("button");
|
|
2020
|
+
button2.type = "button";
|
|
2021
|
+
button2.className = "ag-ui-quote-offer";
|
|
2022
|
+
button2.textContent = options.label;
|
|
2023
|
+
button2.hidden = true;
|
|
2024
|
+
document.body.append(button2);
|
|
2025
|
+
let quoting = "";
|
|
2026
|
+
const hide = () => {
|
|
2027
|
+
button2.hidden = true;
|
|
2028
|
+
quoting = "";
|
|
2029
|
+
};
|
|
2030
|
+
const settled = (event) => {
|
|
2031
|
+
if (event.composedPath().includes(exclude)) {
|
|
2032
|
+
hide();
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (fieldHasFocus()) {
|
|
2036
|
+
hide();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const near = event instanceof MouseEvent ? { x: event.clientX, y: event.clientY } : void 0;
|
|
2040
|
+
const selected = quotableSelection(within, [], near);
|
|
2041
|
+
if (selected === null) {
|
|
2042
|
+
hide();
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
quoting = selected.text;
|
|
2046
|
+
place(button2, selected.rect);
|
|
2047
|
+
};
|
|
2048
|
+
const onMouseDown = (event) => {
|
|
2049
|
+
if (!button2.contains(event.target)) {
|
|
2050
|
+
hide();
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
within.addEventListener("mouseup", settled);
|
|
2054
|
+
within.addEventListener("keyup", settled);
|
|
2055
|
+
within.addEventListener("mousedown", onMouseDown);
|
|
2056
|
+
document.addEventListener("scroll", hide, true);
|
|
2057
|
+
window.addEventListener("resize", hide);
|
|
2058
|
+
button2.addEventListener("mousedown", (event) => {
|
|
2059
|
+
event.preventDefault();
|
|
2060
|
+
});
|
|
2061
|
+
button2.addEventListener("click", () => {
|
|
2062
|
+
const text3 = quoting;
|
|
2063
|
+
window.getSelection()?.removeAllRanges();
|
|
2064
|
+
hide();
|
|
2065
|
+
onQuote(text3);
|
|
2066
|
+
});
|
|
2067
|
+
return {
|
|
2068
|
+
element: button2,
|
|
2069
|
+
detach() {
|
|
2070
|
+
within.removeEventListener("mouseup", settled);
|
|
2071
|
+
within.removeEventListener("keyup", settled);
|
|
2072
|
+
within.removeEventListener("mousedown", onMouseDown);
|
|
2073
|
+
document.removeEventListener("scroll", hide, true);
|
|
2074
|
+
window.removeEventListener("resize", hide);
|
|
2075
|
+
button2.remove();
|
|
2076
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((each) => each !== sheet);
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
function fieldHasFocus() {
|
|
2081
|
+
const active = document.activeElement;
|
|
2082
|
+
if (active === null) {
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
return active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable === true;
|
|
2086
|
+
}
|
|
2087
|
+
function place(button2, rect) {
|
|
2088
|
+
button2.hidden = false;
|
|
2089
|
+
const below = rect.top < GAP + button2.offsetHeight;
|
|
2090
|
+
button2.dataset["below"] = String(below);
|
|
2091
|
+
button2.style.top = `${below ? rect.bottom + GAP : rect.top - GAP}px`;
|
|
2092
|
+
const half = button2.offsetWidth / 2;
|
|
2093
|
+
const centre = rect.left + rect.width / 2;
|
|
2094
|
+
const width = document.documentElement.clientWidth;
|
|
2095
|
+
button2.style.left = `${Math.min(Math.max(centre, half), width - half)}px`;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1640
2098
|
// src/ui/prettify_tool_name.ts
|
|
1641
2099
|
function prettifyToolName(name) {
|
|
1642
2100
|
const spaced = name.replace(/[._-]+/g, " ").trim();
|
|
@@ -2126,7 +2584,7 @@ function createDOMPurify() {
|
|
|
2126
2584
|
const originalDocument = document2;
|
|
2127
2585
|
const currentScript = originalDocument.currentScript;
|
|
2128
2586
|
window2.DocumentFragment;
|
|
2129
|
-
const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element,
|
|
2587
|
+
const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element, NodeFilter2 = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
|
|
2130
2588
|
_window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap;
|
|
2131
2589
|
window2.HTMLFormElement;
|
|
2132
2590
|
const DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes;
|
|
@@ -2723,7 +3181,7 @@ function createDOMPurify() {
|
|
|
2723
3181
|
doc || root,
|
|
2724
3182
|
root,
|
|
2725
3183
|
// eslint-disable-next-line no-bitwise
|
|
2726
|
-
|
|
3184
|
+
NodeFilter2.SHOW_ELEMENT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_PROCESSING_INSTRUCTION | NodeFilter2.SHOW_CDATA_SECTION,
|
|
2727
3185
|
null
|
|
2728
3186
|
);
|
|
2729
3187
|
};
|
|
@@ -2741,7 +3199,7 @@ function createDOMPurify() {
|
|
|
2741
3199
|
doc || node,
|
|
2742
3200
|
node,
|
|
2743
3201
|
// eslint-disable-next-line no-bitwise
|
|
2744
|
-
|
|
3202
|
+
NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_CDATA_SECTION | NodeFilter2.SHOW_PROCESSING_INSTRUCTION,
|
|
2745
3203
|
null
|
|
2746
3204
|
);
|
|
2747
3205
|
let currentNode = walker.nextNode();
|
|
@@ -4936,6 +5394,60 @@ var SkillsMenu = class {
|
|
|
4936
5394
|
}
|
|
4937
5395
|
};
|
|
4938
5396
|
|
|
5397
|
+
// src/ui/stick_to_bottom.ts
|
|
5398
|
+
var BOTTOM_SLACK_PX = 4;
|
|
5399
|
+
function createStickToBottom({
|
|
5400
|
+
viewport,
|
|
5401
|
+
onMissedContent
|
|
5402
|
+
}) {
|
|
5403
|
+
let isFollowing = true;
|
|
5404
|
+
let missed = false;
|
|
5405
|
+
const atBottom = () => viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= BOTTOM_SLACK_PX;
|
|
5406
|
+
const setMissed = (next) => {
|
|
5407
|
+
if (next === missed) {
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5410
|
+
missed = next;
|
|
5411
|
+
onMissedContent(missed);
|
|
5412
|
+
};
|
|
5413
|
+
const toBottom = () => {
|
|
5414
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
5415
|
+
};
|
|
5416
|
+
const onScroll = () => {
|
|
5417
|
+
isFollowing = atBottom();
|
|
5418
|
+
if (isFollowing) {
|
|
5419
|
+
setMissed(false);
|
|
5420
|
+
}
|
|
5421
|
+
};
|
|
5422
|
+
const follow = () => {
|
|
5423
|
+
if (isFollowing) {
|
|
5424
|
+
toBottom();
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
setMissed(true);
|
|
5428
|
+
};
|
|
5429
|
+
viewport.addEventListener("scroll", onScroll, { passive: true });
|
|
5430
|
+
const observer = new ResizeObserver(() => {
|
|
5431
|
+
if (isFollowing) {
|
|
5432
|
+
toBottom();
|
|
5433
|
+
}
|
|
5434
|
+
});
|
|
5435
|
+
observer.observe(viewport);
|
|
5436
|
+
return {
|
|
5437
|
+
follow,
|
|
5438
|
+
jump: () => {
|
|
5439
|
+
isFollowing = true;
|
|
5440
|
+
setMissed(false);
|
|
5441
|
+
toBottom();
|
|
5442
|
+
},
|
|
5443
|
+
following: () => isFollowing,
|
|
5444
|
+
dispose: () => {
|
|
5445
|
+
viewport.removeEventListener("scroll", onScroll);
|
|
5446
|
+
observer.disconnect();
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
|
|
4939
5451
|
// src/ui/styles.ts
|
|
4940
5452
|
var STYLES = `
|
|
4941
5453
|
/* \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 +6018,7 @@ var STYLES = `
|
|
|
5506
6018
|
opacity: 0;
|
|
5507
6019
|
}
|
|
5508
6020
|
|
|
6021
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages-wrap,
|
|
5509
6022
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
|
|
5510
6023
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
|
|
5511
6024
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
|
|
@@ -5514,9 +6027,113 @@ var STYLES = `
|
|
|
5514
6027
|
display: none;
|
|
5515
6028
|
}
|
|
5516
6029
|
|
|
6030
|
+
/* Jump-to-latest: shown only once the reader has scrolled away *and* missed
|
|
6031
|
+
something. Anchored to the panel rather than the list so it does not scroll
|
|
6032
|
+
with the content it is offering to scroll to. */
|
|
6033
|
+
/* The transcript's own box, and the only one whose foot is the transcript's
|
|
6034
|
+
foot. The panel's foot is below the composer, the chips and the footer. */
|
|
6035
|
+
.messages-wrap {
|
|
6036
|
+
position: relative;
|
|
6037
|
+
flex: 1;
|
|
6038
|
+
min-height: 0;
|
|
6039
|
+
display: flex;
|
|
6040
|
+
flex-direction: column;
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
.jump-latest {
|
|
6044
|
+
position: absolute;
|
|
6045
|
+
left: 50%;
|
|
6046
|
+
transform: translateX(-50%);
|
|
6047
|
+
bottom: var(--_pad);
|
|
6048
|
+
z-index: 2;
|
|
6049
|
+
display: none;
|
|
6050
|
+
align-items: center;
|
|
6051
|
+
gap: 0.35em;
|
|
6052
|
+
padding: 0.4em 0.9em;
|
|
6053
|
+
border: 1px solid var(--_border);
|
|
6054
|
+
border-radius: 999px;
|
|
6055
|
+
/* A raised surface, not the panel's own background. Reusing --_bg made the
|
|
6056
|
+
pill the same colour as everything behind it, leaving a 1px border and a
|
|
6057
|
+
shadow to carry the whole affordance -- and a dark-on-dark shadow carries
|
|
6058
|
+
nothing. --_hover is the token that already means "lifted off the panel",
|
|
6059
|
+
and it separates in both themes without competing with the accent the send
|
|
6060
|
+
button owns. */
|
|
6061
|
+
background: var(--_hover);
|
|
6062
|
+
color: var(--_text);
|
|
6063
|
+
font: inherit;
|
|
6064
|
+
font-size: 0.85em;
|
|
6065
|
+
cursor: pointer;
|
|
6066
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
.jump-latest[data-missed="true"] {
|
|
6070
|
+
display: flex;
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
.jump-latest:hover {
|
|
6074
|
+
border-color: var(--_accent);
|
|
6075
|
+
}
|
|
6076
|
+
|
|
6077
|
+
/* The offer to quote a selection. Positioned in script against the transcript
|
|
6078
|
+
box, which is the only ancestor whose top and foot are the transcript's --
|
|
6079
|
+
the same reason .jump-latest lives here. The translate is the half the
|
|
6080
|
+
script does not do: script sets the point the offer hangs from, CSS decides
|
|
6081
|
+
which corner of the offer that point is. */
|
|
6082
|
+
.quote-selection {
|
|
6083
|
+
position: absolute;
|
|
6084
|
+
z-index: 2;
|
|
6085
|
+
transform: translate(-50%, -100%);
|
|
6086
|
+
padding: 0.25em 0.7em;
|
|
6087
|
+
border: 1px solid var(--_border);
|
|
6088
|
+
border-radius: 999px;
|
|
6089
|
+
background: var(--_hover);
|
|
6090
|
+
color: var(--_text);
|
|
6091
|
+
font: inherit;
|
|
6092
|
+
font-size: 0.8em;
|
|
6093
|
+
line-height: 1.6;
|
|
6094
|
+
white-space: nowrap;
|
|
6095
|
+
cursor: pointer;
|
|
6096
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
6097
|
+
}
|
|
6098
|
+
|
|
6099
|
+
/* Flipped under the selection when there was no room above it. Only the
|
|
6100
|
+
vertical half of the translate changes: it still hangs from its own centre
|
|
6101
|
+
horizontally. */
|
|
6102
|
+
.quote-selection[data-below="true"] {
|
|
6103
|
+
transform: translate(-50%, 0);
|
|
6104
|
+
}
|
|
6105
|
+
|
|
6106
|
+
.quote-selection:hover {
|
|
6107
|
+
border-color: var(--_accent);
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
/* Screen-reader-only status region. Off-screen rather than display:none or
|
|
6111
|
+
visibility:hidden, both of which take the element out of the accessibility
|
|
6112
|
+
tree entirely -- a hidden live region announces nothing at all, which is the
|
|
6113
|
+
classic way this pattern is written wrong.
|
|
6114
|
+
|
|
6115
|
+
The 1px box with clip-path, rather than width/height 0, is the shape that
|
|
6116
|
+
survives: a zero-sized element is dropped from the tree by some engines. */
|
|
6117
|
+
.sr-only {
|
|
6118
|
+
position: absolute;
|
|
6119
|
+
width: 1px;
|
|
6120
|
+
height: 1px;
|
|
6121
|
+
margin: -1px;
|
|
6122
|
+
padding: 0;
|
|
6123
|
+
border: 0;
|
|
6124
|
+
overflow: hidden;
|
|
6125
|
+
white-space: nowrap;
|
|
6126
|
+
clip-path: inset(50%);
|
|
6127
|
+
}
|
|
6128
|
+
|
|
5517
6129
|
.messages {
|
|
5518
6130
|
flex: 1;
|
|
5519
6131
|
overflow-y: auto;
|
|
6132
|
+
/* The browser's own scroll anchoring competes with the scroller for the same
|
|
6133
|
+
job and wins unpredictably -- it can hold the view still exactly when we
|
|
6134
|
+
want to follow. Turned off so following is decided in one place. Safari
|
|
6135
|
+
does not implement it, which is itself a reason not to depend on it. */
|
|
6136
|
+
overflow-anchor: none;
|
|
5520
6137
|
padding: var(--_pad);
|
|
5521
6138
|
display: flex;
|
|
5522
6139
|
flex-direction: column;
|
|
@@ -6025,6 +6642,21 @@ var STYLES = `
|
|
|
6025
6642
|
color: var(--_fg);
|
|
6026
6643
|
}
|
|
6027
6644
|
|
|
6645
|
+
/* A region a host formatter took over, marked by the card. Preformatted
|
|
6646
|
+
whitespace is what makes the built-in block read as written, and it is the one
|
|
6647
|
+
thing a host cannot want: a table inherits it as mangled cell spacing, and a
|
|
6648
|
+
sentence as line breaks nobody typed.
|
|
6649
|
+
|
|
6650
|
+
Whitespace only. The card's own face, frame, padding and scroll cap stay,
|
|
6651
|
+
because the card is one visual object -- the head row and the status pill are
|
|
6652
|
+
monospaced too -- and a region that dropped the family would be the only part
|
|
6653
|
+
of it wearing a different one. A host that wants that restyles the
|
|
6654
|
+
tool-card-result part, which does not need the formatter at all. */
|
|
6655
|
+
.tool-call-args[data-formatted],
|
|
6656
|
+
.tool-call-result[data-formatted] {
|
|
6657
|
+
white-space: normal;
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6028
6660
|
/* Display modes are pure visibility over one DOM shape, selected from the host
|
|
6029
6661
|
attribute rather than a value stamped on the card at build time, so flipping
|
|
6030
6662
|
data-tool-display re-styles cards already on screen. See ToolCallCard.
|
|
@@ -6089,32 +6721,200 @@ var STYLES = `
|
|
|
6089
6721
|
display: flex;
|
|
6090
6722
|
}
|
|
6091
6723
|
|
|
6092
|
-
|
|
6093
|
-
|
|
6724
|
+
/* A delegated sub-agent's progress, inside the card that delegated. Empty on
|
|
6725
|
+
every card that delegated nothing, so it collapses rather than adding a gap
|
|
6726
|
+
to each one -- the same shape the approval slot uses. */
|
|
6727
|
+
.tool-call-subagent:empty {
|
|
6728
|
+
display: none;
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6731
|
+
.tool-call-subagent {
|
|
6732
|
+
display: flex;
|
|
6733
|
+
flex-direction: column;
|
|
6734
|
+
gap: 4px;
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
.subagent {
|
|
6738
|
+
display: flex;
|
|
6739
|
+
flex-direction: column;
|
|
6740
|
+
gap: 4px;
|
|
6741
|
+
min-width: 0;
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
/* The collapsed row is the status and the expander at once, which is what keeps
|
|
6745
|
+
a ten-step child one row until somebody opens it. Full width and left-aligned,
|
|
6746
|
+
because it is a line of the card rather than a button on it. */
|
|
6747
|
+
.subagent-row {
|
|
6748
|
+
display: flex;
|
|
6749
|
+
align-items: center;
|
|
6750
|
+
gap: 6px;
|
|
6751
|
+
width: 100%;
|
|
6752
|
+
box-sizing: border-box;
|
|
6753
|
+
padding: 2px 0;
|
|
6094
6754
|
border: none;
|
|
6095
|
-
padding: 0;
|
|
6096
6755
|
background: none;
|
|
6097
6756
|
font: inherit;
|
|
6098
|
-
|
|
6099
|
-
color: var(--
|
|
6757
|
+
text-align: left;
|
|
6758
|
+
color: var(--_muted);
|
|
6100
6759
|
cursor: pointer;
|
|
6101
6760
|
}
|
|
6102
6761
|
|
|
6103
|
-
|
|
6104
|
-
|
|
6762
|
+
/* Nothing behind the row yet -- a delegation that failed before calling
|
|
6763
|
+
anything. Drop the affordances rather than offer a control that expands onto
|
|
6764
|
+
an empty region, which is the refusal the card's own toggle already makes. */
|
|
6765
|
+
.subagent-row:disabled {
|
|
6766
|
+
cursor: default;
|
|
6105
6767
|
}
|
|
6106
6768
|
|
|
6107
|
-
.
|
|
6108
|
-
content: "\
|
|
6769
|
+
.subagent-row::after {
|
|
6770
|
+
content: "\u25B8";
|
|
6771
|
+
flex: none;
|
|
6772
|
+
margin-left: auto;
|
|
6773
|
+
color: var(--_accent);
|
|
6109
6774
|
}
|
|
6110
6775
|
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6776
|
+
.subagent-row[aria-expanded="true"]::after {
|
|
6777
|
+
content: "\u25BE";
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
.subagent-row:disabled::after {
|
|
6781
|
+
display: none;
|
|
6782
|
+
}
|
|
6783
|
+
|
|
6784
|
+
/* Empty in the DOM; the glyph is drawn here from the panel's data-phase, so a
|
|
6785
|
+
host re-themes it through the same tool-icon custom properties the card uses. */
|
|
6786
|
+
.subagent-icon {
|
|
6787
|
+
flex: none;
|
|
6788
|
+
box-sizing: border-box;
|
|
6789
|
+
display: inline-flex;
|
|
6790
|
+
align-items: center;
|
|
6791
|
+
justify-content: center;
|
|
6792
|
+
width: 10px;
|
|
6793
|
+
height: 10px;
|
|
6794
|
+
font-size: 10px;
|
|
6795
|
+
line-height: 1;
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6798
|
+
/* Anything that is not a terminal phase is the child still working. Selected by
|
|
6799
|
+
what it is not, so a phase this client has not heard of still spins rather
|
|
6800
|
+
than rendering as a blank. */
|
|
6801
|
+
.subagent[data-phase]:not([data-phase="finished"]):not([data-phase="failed"]) .subagent-icon {
|
|
6802
|
+
border: 2px solid var(--_muted);
|
|
6803
|
+
border-top-color: transparent;
|
|
6804
|
+
border-radius: 50%;
|
|
6805
|
+
animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
.subagent[data-phase="finished"] .subagent-icon::before {
|
|
6809
|
+
content: var(--_tool-icon-done);
|
|
6810
|
+
color: var(--_success);
|
|
6811
|
+
}
|
|
6812
|
+
|
|
6813
|
+
.subagent[data-phase="failed"] .subagent-icon::before {
|
|
6814
|
+
content: var(--_tool-icon-error);
|
|
6815
|
+
color: var(--_danger);
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
@media (prefers-reduced-motion: reduce) {
|
|
6819
|
+
.subagent .subagent-icon {
|
|
6820
|
+
animation: none;
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
/* The server's own pre-rendered line. Shrinks and wraps rather than pushing the
|
|
6825
|
+
chevron out of the card, which is what a fixed-width sibling in a flex row
|
|
6826
|
+
does to a panel at sidebar width. */
|
|
6827
|
+
.subagent-status {
|
|
6828
|
+
flex: 1 1 auto;
|
|
6829
|
+
min-width: 0;
|
|
6830
|
+
overflow-wrap: anywhere;
|
|
6831
|
+
}
|
|
6832
|
+
|
|
6833
|
+
/* The child's own calls. Indented and ruled, so the nesting is visible without
|
|
6834
|
+
a second card frame around it. */
|
|
6835
|
+
.subagent-steps {
|
|
6836
|
+
display: flex;
|
|
6837
|
+
flex-direction: column;
|
|
6838
|
+
gap: 2px;
|
|
6839
|
+
margin-left: 4px;
|
|
6840
|
+
padding-left: 10px;
|
|
6841
|
+
border-left: 1px solid var(--_border);
|
|
6842
|
+
}
|
|
6843
|
+
|
|
6844
|
+
.subagent-steps[hidden] {
|
|
6845
|
+
display: none;
|
|
6846
|
+
}
|
|
6847
|
+
|
|
6848
|
+
.subagent-step {
|
|
6849
|
+
display: flex;
|
|
6850
|
+
align-items: center;
|
|
6851
|
+
gap: 6px;
|
|
6852
|
+
min-width: 0;
|
|
6853
|
+
color: var(--_muted);
|
|
6854
|
+
}
|
|
6855
|
+
|
|
6856
|
+
.subagent-step-icon {
|
|
6857
|
+
flex: none;
|
|
6858
|
+
box-sizing: border-box;
|
|
6859
|
+
display: inline-flex;
|
|
6860
|
+
align-items: center;
|
|
6861
|
+
justify-content: center;
|
|
6862
|
+
width: 8px;
|
|
6863
|
+
height: 8px;
|
|
6864
|
+
font-size: 9px;
|
|
6865
|
+
line-height: 1;
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6868
|
+
/* No outcome yet: the wire says null while the call is in flight, and the
|
|
6869
|
+
absence of the attribute is how that arrives here. A hollow ring, not a
|
|
6870
|
+
spinner -- several can be on screen at once and the row above already spins. */
|
|
6871
|
+
.subagent-step:not([data-ok]) .subagent-step-icon {
|
|
6872
|
+
border: 1px solid var(--_muted);
|
|
6873
|
+
border-radius: 50%;
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
.subagent-step[data-ok="true"] .subagent-step-icon::before {
|
|
6877
|
+
content: var(--_tool-icon-done);
|
|
6878
|
+
color: var(--_success);
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
.subagent-step[data-ok="false"] .subagent-step-icon::before {
|
|
6882
|
+
content: var(--_tool-icon-error);
|
|
6883
|
+
color: var(--_danger);
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
.subagent-step-name {
|
|
6887
|
+
flex: 1 1 auto;
|
|
6888
|
+
min-width: 0;
|
|
6889
|
+
overflow-wrap: anywhere;
|
|
6890
|
+
}
|
|
6891
|
+
|
|
6892
|
+
.tool-call-toggle {
|
|
6893
|
+
align-self: flex-start;
|
|
6894
|
+
border: none;
|
|
6895
|
+
padding: 0;
|
|
6896
|
+
background: none;
|
|
6897
|
+
font: inherit;
|
|
6898
|
+
font-weight: 600;
|
|
6899
|
+
color: var(--_accent);
|
|
6900
|
+
cursor: pointer;
|
|
6901
|
+
}
|
|
6902
|
+
|
|
6903
|
+
.tool-call-toggle::before {
|
|
6904
|
+
content: "\u25B8 ";
|
|
6905
|
+
}
|
|
6906
|
+
|
|
6907
|
+
.tool-call-toggle[aria-expanded="true"]::before {
|
|
6908
|
+
content: "\u25BE ";
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6911
|
+
/* Resize handle: a corner grip on a floating panel, an edge grip on a docked
|
|
6912
|
+
one. Absent entirely where the placement is full-bleed, since there is
|
|
6913
|
+
nothing to drag. */
|
|
6914
|
+
.resize-handle {
|
|
6915
|
+
position: absolute;
|
|
6916
|
+
z-index: 2;
|
|
6917
|
+
background: transparent;
|
|
6118
6918
|
touch-action: none;
|
|
6119
6919
|
}
|
|
6120
6920
|
|
|
@@ -6682,6 +7482,7 @@ var STYLES = `
|
|
|
6682
7482
|
|
|
6683
7483
|
.confirm-actions {
|
|
6684
7484
|
display: flex;
|
|
7485
|
+
flex-wrap: wrap;
|
|
6685
7486
|
gap: 8px;
|
|
6686
7487
|
justify-content: flex-end;
|
|
6687
7488
|
}
|
|
@@ -6708,6 +7509,99 @@ var STYLES = `
|
|
|
6708
7509
|
color: #ffffff;
|
|
6709
7510
|
}
|
|
6710
7511
|
|
|
7512
|
+
/* The session waiver. Deliberately the quietest of the three: it is the widest
|
|
7513
|
+
decision on the card, so it should be reachable without being the one the eye
|
|
7514
|
+
lands on when the user means to say yes once. */
|
|
7515
|
+
.confirm-btn--always {
|
|
7516
|
+
font-weight: 500;
|
|
7517
|
+
opacity: 0.85;
|
|
7518
|
+
}
|
|
7519
|
+
|
|
7520
|
+
.confirm-btn--always:hover,
|
|
7521
|
+
.confirm-btn--always:focus-visible {
|
|
7522
|
+
opacity: 1;
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
/* Editable arguments on an approval card. A plain field rather than a code
|
|
7526
|
+
editor: it holds the JSON a card already displays, and the only interaction
|
|
7527
|
+
is correcting a value before letting the call run. */
|
|
7528
|
+
.approval-edit {
|
|
7529
|
+
display: flex;
|
|
7530
|
+
flex-direction: column;
|
|
7531
|
+
gap: 6px;
|
|
7532
|
+
}
|
|
7533
|
+
|
|
7534
|
+
.approval-args {
|
|
7535
|
+
box-sizing: border-box;
|
|
7536
|
+
width: 100%;
|
|
7537
|
+
resize: vertical;
|
|
7538
|
+
border: 1px solid var(--_border);
|
|
7539
|
+
border-radius: 8px;
|
|
7540
|
+
padding: 8px;
|
|
7541
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
7542
|
+
font-size: 0.85em;
|
|
7543
|
+
background: var(--_bg);
|
|
7544
|
+
color: var(--_fg);
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
.approval-args:focus-visible {
|
|
7548
|
+
border-color: var(--_accent);
|
|
7549
|
+
outline: none;
|
|
7550
|
+
}
|
|
7551
|
+
|
|
7552
|
+
.approval-error {
|
|
7553
|
+
font-size: 0.85em;
|
|
7554
|
+
color: var(--_danger);
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
/* Message action row. Sits under a finished assistant bubble.
|
|
7558
|
+
|
|
7559
|
+
The wrap is insurance rather than a fix: these buttons are glyph-only, so at
|
|
7560
|
+
every width tested they fit on one line and removing the wrap changes
|
|
7561
|
+
nothing. It is here because the confirmation row one release earlier did
|
|
7562
|
+
overflow when it gained a third button, off the left edge and outside its own
|
|
7563
|
+
card, and the difference between the two rows is only that this one's labels
|
|
7564
|
+
are icons today. */
|
|
7565
|
+
.message-actions {
|
|
7566
|
+
display: flex;
|
|
7567
|
+
flex-wrap: wrap;
|
|
7568
|
+
gap: 4px;
|
|
7569
|
+
/* Negative, and that is the point. The answer group is a flex column with its
|
|
7570
|
+
own gap, so a positive margin here pushes the row further from the message
|
|
7571
|
+
it acts on than the next card is below it -- the buttons then read as
|
|
7572
|
+
belonging to whatever follows. Pulling back inside the gap is what makes
|
|
7573
|
+
them the message's own. */
|
|
7574
|
+
margin-top: -6px;
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
.message-action {
|
|
7578
|
+
border: none;
|
|
7579
|
+
border-radius: 6px;
|
|
7580
|
+
padding: 2px 6px;
|
|
7581
|
+
font: inherit;
|
|
7582
|
+
line-height: 1.2;
|
|
7583
|
+
cursor: pointer;
|
|
7584
|
+
background: transparent;
|
|
7585
|
+
color: var(--_muted);
|
|
7586
|
+
opacity: 0.75;
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
.message-action:hover,
|
|
7590
|
+
.message-action:focus-visible {
|
|
7591
|
+
opacity: 1;
|
|
7592
|
+
background: var(--_border);
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
.message-action[aria-pressed="true"] {
|
|
7596
|
+
opacity: 1;
|
|
7597
|
+
color: var(--_accent);
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
.message-action--confirmed {
|
|
7601
|
+
opacity: 1;
|
|
7602
|
+
color: var(--_accent);
|
|
7603
|
+
}
|
|
7604
|
+
|
|
6711
7605
|
/* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */
|
|
6712
7606
|
.approval {
|
|
6713
7607
|
align-self: stretch;
|
|
@@ -6852,6 +7746,34 @@ var STYLES = `
|
|
|
6852
7746
|
border-color: var(--_accent);
|
|
6853
7747
|
}
|
|
6854
7748
|
|
|
7749
|
+
/* Follow-up suggestion chips. Deliberately the skill chips' shape rather than a
|
|
7750
|
+
second chip vocabulary -- both are "a question you could ask", and the only
|
|
7751
|
+
difference is who chose it. Inside the transcript, so they scroll with the
|
|
7752
|
+
answer they follow instead of hovering above the composer. */
|
|
7753
|
+
.suggestions {
|
|
7754
|
+
display: flex;
|
|
7755
|
+
flex-wrap: wrap;
|
|
7756
|
+
gap: 6px;
|
|
7757
|
+
align-self: stretch;
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
.suggestion-chip {
|
|
7761
|
+
border: 1px solid var(--_border);
|
|
7762
|
+
border-radius: 999px;
|
|
7763
|
+
padding: 4px 12px;
|
|
7764
|
+
font: inherit;
|
|
7765
|
+
font-size: 0.9em;
|
|
7766
|
+
text-align: left;
|
|
7767
|
+
cursor: pointer;
|
|
7768
|
+
background: var(--_assistant-bg);
|
|
7769
|
+
color: var(--_fg);
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
.suggestion-chip:hover,
|
|
7773
|
+
.suggestion-chip:focus-visible {
|
|
7774
|
+
border-color: var(--_accent);
|
|
7775
|
+
}
|
|
7776
|
+
|
|
6855
7777
|
.skill-palette {
|
|
6856
7778
|
margin: 8px 12px 0;
|
|
6857
7779
|
display: flex;
|
|
@@ -7266,6 +8188,195 @@ var STYLES = `
|
|
|
7266
8188
|
}
|
|
7267
8189
|
`;
|
|
7268
8190
|
|
|
8191
|
+
// src/ui/subagent_panel.ts
|
|
8192
|
+
var SubAgentPanel = class {
|
|
8193
|
+
/** The panel's root; append this into the delegating card's slot. */
|
|
8194
|
+
element;
|
|
8195
|
+
/**
|
|
8196
|
+
* The collapsed row, which is the expander as well as the status.
|
|
8197
|
+
*
|
|
8198
|
+
* Disabled while the child has called nothing, so a delegation that failed
|
|
8199
|
+
* before it started offers no control that expands onto an empty region —
|
|
8200
|
+
* the same refusal the card's own Details toggle already makes.
|
|
8201
|
+
*/
|
|
8202
|
+
#row;
|
|
8203
|
+
#status;
|
|
8204
|
+
#steps;
|
|
8205
|
+
/** The child's tool calls, keyed by the child's own call id. */
|
|
8206
|
+
#stepRows = /* @__PURE__ */ new Map();
|
|
8207
|
+
constructor(strings = DEFAULT_UI_STRINGS) {
|
|
8208
|
+
this.element = document.createElement("div");
|
|
8209
|
+
this.element.className = "subagent";
|
|
8210
|
+
this.element.setAttribute("part", "subagent");
|
|
8211
|
+
this.#row = document.createElement("button");
|
|
8212
|
+
this.#row.type = "button";
|
|
8213
|
+
this.#row.className = "subagent-row";
|
|
8214
|
+
this.#row.setAttribute("part", "subagent-row");
|
|
8215
|
+
this.#row.setAttribute("aria-expanded", "false");
|
|
8216
|
+
this.#row.disabled = true;
|
|
8217
|
+
const icon = document.createElement("span");
|
|
8218
|
+
icon.className = "subagent-icon";
|
|
8219
|
+
icon.setAttribute("part", "subagent-icon");
|
|
8220
|
+
icon.setAttribute("aria-hidden", "true");
|
|
8221
|
+
this.#status = document.createElement("span");
|
|
8222
|
+
this.#status.className = "subagent-status";
|
|
8223
|
+
this.#status.setAttribute("part", "subagent-status");
|
|
8224
|
+
this.#status.textContent = strings.subAgentWorking;
|
|
8225
|
+
this.#row.append(icon, this.#status);
|
|
8226
|
+
this.#steps = document.createElement("div");
|
|
8227
|
+
this.#steps.className = "subagent-steps";
|
|
8228
|
+
this.#steps.setAttribute("part", "subagent-steps");
|
|
8229
|
+
this.#steps.setAttribute("role", "list");
|
|
8230
|
+
this.#steps.setAttribute("aria-label", strings.subAgentSteps);
|
|
8231
|
+
this.#steps.hidden = true;
|
|
8232
|
+
this.#row.addEventListener("click", () => {
|
|
8233
|
+
this.#setExpanded(this.#row.getAttribute("aria-expanded") !== "true");
|
|
8234
|
+
});
|
|
8235
|
+
this.element.append(this.#row, this.#steps);
|
|
8236
|
+
}
|
|
8237
|
+
/**
|
|
8238
|
+
* Fold one announcement in.
|
|
8239
|
+
*
|
|
8240
|
+
* Every field is applied only when the update actually carried it, so a phase
|
|
8241
|
+
* that says nothing about the agent or the status leaves both as they stand.
|
|
8242
|
+
* That is what lets `finished` be two keys wide on the wire without blanking
|
|
8243
|
+
* the row it closes.
|
|
8244
|
+
*/
|
|
8245
|
+
report(update) {
|
|
8246
|
+
this.element.setAttribute("data-phase", update.phase);
|
|
8247
|
+
if (update.agent !== null) {
|
|
8248
|
+
this.element.setAttribute("data-agent", update.agent);
|
|
8249
|
+
}
|
|
8250
|
+
if (update.status !== null) {
|
|
8251
|
+
this.#status.textContent = update.status;
|
|
8252
|
+
}
|
|
8253
|
+
if (update.tool !== null) {
|
|
8254
|
+
this.#recordStep(update.tool);
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
/**
|
|
8258
|
+
* Open or settle one of the child's calls, keyed by its own id.
|
|
8259
|
+
*
|
|
8260
|
+
* The absence of `data-ok` is what "still running" looks like, mirroring the
|
|
8261
|
+
* wire's `null` rather than inventing a third value for it — so the attribute
|
|
8262
|
+
* is removed on the way in and written on the way out.
|
|
8263
|
+
*/
|
|
8264
|
+
#recordStep(tool) {
|
|
8265
|
+
const row = this.#stepRows.get(tool.toolCallId) ?? this.#createStep(tool);
|
|
8266
|
+
if (tool.ok === null) {
|
|
8267
|
+
row.removeAttribute("data-ok");
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
row.setAttribute("data-ok", String(tool.ok));
|
|
8271
|
+
}
|
|
8272
|
+
#createStep(tool) {
|
|
8273
|
+
const row = document.createElement("div");
|
|
8274
|
+
row.className = "subagent-step";
|
|
8275
|
+
row.setAttribute("part", "subagent-step");
|
|
8276
|
+
row.setAttribute("role", "listitem");
|
|
8277
|
+
row.setAttribute("data-tool-call-id", tool.toolCallId);
|
|
8278
|
+
const icon = document.createElement("span");
|
|
8279
|
+
icon.className = "subagent-step-icon";
|
|
8280
|
+
icon.setAttribute("part", "subagent-step-icon");
|
|
8281
|
+
icon.setAttribute("aria-hidden", "true");
|
|
8282
|
+
const name = document.createElement("span");
|
|
8283
|
+
name.className = "subagent-step-name";
|
|
8284
|
+
name.setAttribute("part", "subagent-step-name");
|
|
8285
|
+
name.textContent = tool.name;
|
|
8286
|
+
row.append(icon, name);
|
|
8287
|
+
this.#steps.appendChild(row);
|
|
8288
|
+
this.#stepRows.set(tool.toolCallId, row);
|
|
8289
|
+
this.#row.disabled = false;
|
|
8290
|
+
return row;
|
|
8291
|
+
}
|
|
8292
|
+
#setExpanded(expanded) {
|
|
8293
|
+
this.#steps.hidden = !expanded;
|
|
8294
|
+
this.#row.setAttribute("aria-expanded", String(expanded));
|
|
8295
|
+
}
|
|
8296
|
+
};
|
|
8297
|
+
|
|
8298
|
+
// src/ui/subagent_update.ts
|
|
8299
|
+
var PHASES = Object.values(SUBAGENT_PHASE);
|
|
8300
|
+
function asRecord(value) {
|
|
8301
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
8302
|
+
return null;
|
|
8303
|
+
}
|
|
8304
|
+
return value;
|
|
8305
|
+
}
|
|
8306
|
+
function asText(value) {
|
|
8307
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
8308
|
+
}
|
|
8309
|
+
function asTool(value) {
|
|
8310
|
+
const record = asRecord(value);
|
|
8311
|
+
if (record === null) {
|
|
8312
|
+
return null;
|
|
8313
|
+
}
|
|
8314
|
+
const toolCallId = asText(record["toolCallId"]);
|
|
8315
|
+
const name = asText(record["name"]);
|
|
8316
|
+
const ok = record["ok"];
|
|
8317
|
+
if (toolCallId === null || name === null) {
|
|
8318
|
+
return null;
|
|
8319
|
+
}
|
|
8320
|
+
if (ok !== null && typeof ok !== "boolean") {
|
|
8321
|
+
return null;
|
|
8322
|
+
}
|
|
8323
|
+
return { toolCallId, name, ok };
|
|
8324
|
+
}
|
|
8325
|
+
function subAgentUpdate(value) {
|
|
8326
|
+
const record = asRecord(value);
|
|
8327
|
+
if (record === null) {
|
|
8328
|
+
return null;
|
|
8329
|
+
}
|
|
8330
|
+
const delegationId = asText(record["delegationId"]);
|
|
8331
|
+
const phase = record["phase"];
|
|
8332
|
+
if (delegationId === null || typeof phase !== "string" || !PHASES.includes(phase)) {
|
|
8333
|
+
return null;
|
|
8334
|
+
}
|
|
8335
|
+
return {
|
|
8336
|
+
delegationId,
|
|
8337
|
+
phase,
|
|
8338
|
+
agent: asText(record["agent"]),
|
|
8339
|
+
status: asText(record["status"]),
|
|
8340
|
+
tool: asTool(record["tool"])
|
|
8341
|
+
};
|
|
8342
|
+
}
|
|
8343
|
+
|
|
8344
|
+
// src/ui/suggestion_chips.ts
|
|
8345
|
+
var MAX_SUGGESTIONS = 4;
|
|
8346
|
+
var MAX_SUGGESTION_CHARS = 120;
|
|
8347
|
+
function suggestionPrompts(content) {
|
|
8348
|
+
if (typeof content !== "object" || content === null) {
|
|
8349
|
+
return null;
|
|
8350
|
+
}
|
|
8351
|
+
const raw = content.prompts;
|
|
8352
|
+
if (!Array.isArray(raw)) {
|
|
8353
|
+
return null;
|
|
8354
|
+
}
|
|
8355
|
+
const prompts = raw.filter((prompt) => typeof prompt === "string").map((prompt) => prompt.trim()).filter((prompt) => prompt !== "" && prompt.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
|
|
8356
|
+
return prompts.length === 0 ? null : prompts;
|
|
8357
|
+
}
|
|
8358
|
+
function renderSuggestionChips(content, strings, onPick) {
|
|
8359
|
+
const prompts = suggestionPrompts(content);
|
|
8360
|
+
if (prompts === null) {
|
|
8361
|
+
return null;
|
|
8362
|
+
}
|
|
8363
|
+
const row = document.createElement("div");
|
|
8364
|
+
row.className = "suggestions";
|
|
8365
|
+
row.setAttribute("part", "suggestions");
|
|
8366
|
+
row.setAttribute("role", "group");
|
|
8367
|
+
row.setAttribute("aria-label", strings.suggestions);
|
|
8368
|
+
for (const prompt of prompts) {
|
|
8369
|
+
const chip = document.createElement("button");
|
|
8370
|
+
chip.type = "button";
|
|
8371
|
+
chip.className = "suggestion-chip";
|
|
8372
|
+
chip.setAttribute("part", "suggestion-chip");
|
|
8373
|
+
chip.textContent = prompt;
|
|
8374
|
+
chip.addEventListener("click", () => onPick(prompt));
|
|
8375
|
+
row.appendChild(chip);
|
|
8376
|
+
}
|
|
8377
|
+
return row;
|
|
8378
|
+
}
|
|
8379
|
+
|
|
7269
8380
|
// src/ui/thoughts_block.ts
|
|
7270
8381
|
var ThoughtsBlock = class {
|
|
7271
8382
|
/** The block's root element; insert this at the top of the answer group. */
|
|
@@ -7332,6 +8443,7 @@ var ThreadDrawer = class {
|
|
|
7332
8443
|
#heading;
|
|
7333
8444
|
#newButton;
|
|
7334
8445
|
#list;
|
|
8446
|
+
#formatRelativeTime = null;
|
|
7335
8447
|
#strings;
|
|
7336
8448
|
#threads = [];
|
|
7337
8449
|
#activeId = "";
|
|
@@ -7379,6 +8491,21 @@ var ThreadDrawer = class {
|
|
|
7379
8491
|
this.element.append(backdrop, this.#panel);
|
|
7380
8492
|
}
|
|
7381
8493
|
/** Re-localize the drawer's chrome and rows (the host calls this on connect). */
|
|
8494
|
+
/**
|
|
8495
|
+
* Replace the timestamp formatter, or restore the built-in with `null`.
|
|
8496
|
+
*
|
|
8497
|
+
* The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
|
|
8498
|
+
* in this component, so it never disagrees with a host's own formatting by
|
|
8499
|
+
* guessing a locale. That is a defensible default and a poor requirement, so
|
|
8500
|
+
* this is the way out.
|
|
8501
|
+
*/
|
|
8502
|
+
setRelativeTimeFormatter(format) {
|
|
8503
|
+
this.#formatRelativeTime = format;
|
|
8504
|
+
}
|
|
8505
|
+
/** This row's timestamp, through the host's formatter when it set one. */
|
|
8506
|
+
#formatTime(timestamp) {
|
|
8507
|
+
return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, void 0, this.#strings);
|
|
8508
|
+
}
|
|
7382
8509
|
setStrings(strings) {
|
|
7383
8510
|
this.#strings = strings;
|
|
7384
8511
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
@@ -7478,7 +8605,7 @@ var ThreadDrawer = class {
|
|
|
7478
8605
|
const time = document.createElement("span");
|
|
7479
8606
|
time.className = "drawer-row-time";
|
|
7480
8607
|
time.setAttribute("part", "drawer-row-time");
|
|
7481
|
-
time.textContent =
|
|
8608
|
+
time.textContent = this.#formatTime(meta.updatedAt);
|
|
7482
8609
|
const preview = document.createElement("span");
|
|
7483
8610
|
preview.className = "drawer-row-preview";
|
|
7484
8611
|
preview.setAttribute("part", "drawer-row-preview");
|
|
@@ -7620,6 +8747,22 @@ var ToolCallCard = class {
|
|
|
7620
8747
|
* Empty until used, and hidden while empty by the shadow CSS.
|
|
7621
8748
|
*/
|
|
7622
8749
|
approvalSlot;
|
|
8750
|
+
/**
|
|
8751
|
+
* Where a *nested* run's progress renders — the sub-agent this call delegated
|
|
8752
|
+
* to, narrating itself while the card waits.
|
|
8753
|
+
*
|
|
8754
|
+
* A slot rather than a rendered thing, on the same reasoning as
|
|
8755
|
+
* {@link approvalSlot}: the card owns the position and something else owns the
|
|
8756
|
+
* content. What makes the position right is that the wire keys a delegation on
|
|
8757
|
+
* this card's own `toolCallId`, so the run being narrated is the one this card
|
|
8758
|
+
* already stands for.
|
|
8759
|
+
*
|
|
8760
|
+
* Placed above the Details toggle rather than inside the body, because the
|
|
8761
|
+
* body is what the display modes hide — and a progress line that only appears
|
|
8762
|
+
* in `full` mode would leave exactly the stall it exists to end. Empty on every
|
|
8763
|
+
* card that delegated nothing, and hidden while empty by the shadow CSS.
|
|
8764
|
+
*/
|
|
8765
|
+
subagentSlot;
|
|
7623
8766
|
#status;
|
|
7624
8767
|
#decision;
|
|
7625
8768
|
#toggle;
|
|
@@ -7627,9 +8770,23 @@ var ToolCallCard = class {
|
|
|
7627
8770
|
#resultLabel;
|
|
7628
8771
|
#resultBody;
|
|
7629
8772
|
#strings;
|
|
8773
|
+
/**
|
|
8774
|
+
* The arguments this call was made with.
|
|
8775
|
+
*
|
|
8776
|
+
* Retained rather than only rendered, because an approval interrupt names a
|
|
8777
|
+
* `toolCallId` and nothing else -- so this card is the only place the args
|
|
8778
|
+
* still exist when the user is asked to approve, edit or deny the call.
|
|
8779
|
+
*/
|
|
8780
|
+
args;
|
|
8781
|
+
/** The raw tool name, kept for the payloads handed to {@link ToolPayloadFormatter}. */
|
|
8782
|
+
#name;
|
|
8783
|
+
#formatPayload;
|
|
7630
8784
|
#settled = false;
|
|
7631
|
-
constructor(name, args, summary, strings = DEFAULT_UI_STRINGS) {
|
|
8785
|
+
constructor(name, args, summary, strings = DEFAULT_UI_STRINGS, options = {}) {
|
|
7632
8786
|
this.#strings = strings;
|
|
8787
|
+
this.args = args;
|
|
8788
|
+
this.#name = name;
|
|
8789
|
+
this.#formatPayload = options.formatPayload ?? null;
|
|
7633
8790
|
this.element = document.createElement("div");
|
|
7634
8791
|
this.element.className = "tool-call";
|
|
7635
8792
|
this.element.setAttribute("part", "tool-card");
|
|
@@ -7657,7 +8814,11 @@ var ToolCallCard = class {
|
|
|
7657
8814
|
this.#decision.hidden = true;
|
|
7658
8815
|
head.append(icon, label, this.#status, this.#decision);
|
|
7659
8816
|
const argsSection = this.#section("args", strings.argumentsLabel);
|
|
7660
|
-
|
|
8817
|
+
this.#renderPayload(
|
|
8818
|
+
argsSection.body,
|
|
8819
|
+
{ kind: "arguments", toolName: name, args },
|
|
8820
|
+
JSON.stringify(args, null, 2)
|
|
8821
|
+
);
|
|
7661
8822
|
argsSection.root.hidden = Object.keys(args).length === 0;
|
|
7662
8823
|
const resultSection = this.#section("result", strings.resultLabel);
|
|
7663
8824
|
this.#resultSection = resultSection.root;
|
|
@@ -7678,7 +8839,10 @@ var ToolCallCard = class {
|
|
|
7678
8839
|
this.approvalSlot = document.createElement("div");
|
|
7679
8840
|
this.approvalSlot.className = "tool-call-approval";
|
|
7680
8841
|
this.approvalSlot.setAttribute("part", "tool-card-approval");
|
|
7681
|
-
this.
|
|
8842
|
+
this.subagentSlot = document.createElement("div");
|
|
8843
|
+
this.subagentSlot.className = "tool-call-subagent";
|
|
8844
|
+
this.subagentSlot.setAttribute("part", "tool-card-subagent");
|
|
8845
|
+
this.element.append(head, this.subagentSlot, this.#toggle, body, this.approvalSlot);
|
|
7682
8846
|
}
|
|
7683
8847
|
/**
|
|
7684
8848
|
* Move between the two states that are not an outcome — `pending` (running)
|
|
@@ -7720,9 +8884,42 @@ var ToolCallCard = class {
|
|
|
7720
8884
|
this.element.setAttribute("data-status", status);
|
|
7721
8885
|
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
7722
8886
|
this.#resultLabel.textContent = resultLabels(this.#strings)[status];
|
|
7723
|
-
this.#
|
|
8887
|
+
this.#renderPayload(
|
|
8888
|
+
this.#resultBody,
|
|
8889
|
+
{ kind: "result", toolName: this.#name, status, text: text3 },
|
|
8890
|
+
formatPayload(text3)
|
|
8891
|
+
);
|
|
7724
8892
|
this.#resultSection.hidden = false;
|
|
7725
8893
|
}
|
|
8894
|
+
/**
|
|
8895
|
+
* Fill one body region, offering it to the host formatter first.
|
|
8896
|
+
*
|
|
8897
|
+
* `fallback` is computed by the caller rather than here because the two
|
|
8898
|
+
* regions build it differently -- arguments are already parsed, a result is a
|
|
8899
|
+
* string that may or may not be JSON -- and because a formatter that takes
|
|
8900
|
+
* the region over should not have paid for a pretty-print nobody sees. It is
|
|
8901
|
+
* cheap either way; the point is that the built-in rendering stays written
|
|
8902
|
+
* once, beside the payload it belongs to.
|
|
8903
|
+
*
|
|
8904
|
+
* The `data-formatted` marker is what the shadow CSS reads to relax the
|
|
8905
|
+
* preformatted whitespace on a region a host owns -- a table inherits it as
|
|
8906
|
+
* mangled cell spacing, a sentence as line breaks nobody typed. Marked for a
|
|
8907
|
+
* returned string too: one rule, and a summary line wants ordinary wrapping
|
|
8908
|
+
* as much as a table does.
|
|
8909
|
+
*/
|
|
8910
|
+
#renderPayload(body, payload, fallback) {
|
|
8911
|
+
const rendered = this.#formatPayload === null ? null : this.#formatPayload(payload);
|
|
8912
|
+
if (rendered === null) {
|
|
8913
|
+
body.textContent = fallback;
|
|
8914
|
+
return;
|
|
8915
|
+
}
|
|
8916
|
+
body.setAttribute("data-formatted", "true");
|
|
8917
|
+
if (typeof rendered === "string") {
|
|
8918
|
+
body.textContent = rendered;
|
|
8919
|
+
return;
|
|
8920
|
+
}
|
|
8921
|
+
body.replaceChildren(rendered);
|
|
8922
|
+
}
|
|
7726
8923
|
/** Build one labelled region of the body: a heading plus a payload block. */
|
|
7727
8924
|
#section(kind, labelText) {
|
|
7728
8925
|
const root = document.createElement("div");
|
|
@@ -7926,6 +9123,7 @@ var AgUiClient = class {
|
|
|
7926
9123
|
*/
|
|
7927
9124
|
#closedMessageIds = /* @__PURE__ */ new Set();
|
|
7928
9125
|
#connectionLostMessage;
|
|
9126
|
+
#maxToolRounds;
|
|
7929
9127
|
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
7930
9128
|
// a cancel between frontend-tool rounds doesn't start another round.
|
|
7931
9129
|
#cancelled = false;
|
|
@@ -7939,6 +9137,8 @@ var AgUiClient = class {
|
|
|
7939
9137
|
this.#onPersist = config.onPersist ?? (() => {
|
|
7940
9138
|
});
|
|
7941
9139
|
this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
|
|
9140
|
+
const rounds = config.maxToolRounds ?? MAX_TOOL_ROUNDS;
|
|
9141
|
+
this.#maxToolRounds = rounds >= 1 ? Math.floor(rounds) : MAX_TOOL_ROUNDS;
|
|
7942
9142
|
const onStateChanged = config.onStateChanged;
|
|
7943
9143
|
if (onStateChanged !== void 0) {
|
|
7944
9144
|
this.#agent.subscribe({
|
|
@@ -7969,7 +9169,8 @@ var AgUiClient = class {
|
|
|
7969
9169
|
*
|
|
7970
9170
|
* When the agent calls frontend tools, this executes them and re-runs the
|
|
7971
9171
|
* agent with the results, looping until the agent stops calling frontend
|
|
7972
|
-
* tools (bounded by {@link
|
|
9172
|
+
* tools (bounded by {@link AgUiClientConfig.maxToolRounds}, which defaults to
|
|
9173
|
+
* {@link MAX_TOOL_ROUNDS}).
|
|
7973
9174
|
*
|
|
7974
9175
|
* `attachments` ride on the user message as a non-standard field so the
|
|
7975
9176
|
* default store round-trips them for history replay; see
|
|
@@ -7984,6 +9185,41 @@ var AgUiClient = class {
|
|
|
7984
9185
|
this.#onPersist(this.#agent.messages);
|
|
7985
9186
|
await this.#run();
|
|
7986
9187
|
}
|
|
9188
|
+
/**
|
|
9189
|
+
* Drop everything after the most recent user message, so the same question
|
|
9190
|
+
* can be asked again.
|
|
9191
|
+
*
|
|
9192
|
+
* Returns the retained history, or `null` when there is nothing to retry (no
|
|
9193
|
+
* user message has been sent yet). **Truncates only** -- the caller re-renders
|
|
9194
|
+
* from the returned list and then calls {@link resume}, because the transcript
|
|
9195
|
+
* belongs to the element and a client that reached into it would own two
|
|
9196
|
+
* things. Running here instead would stream the new answer in underneath the
|
|
9197
|
+
* old one.
|
|
9198
|
+
*
|
|
9199
|
+
* Re-running answers the question the agent was last asked, rather than
|
|
9200
|
+
* telling it its answer was wrong, which is what makes the result a
|
|
9201
|
+
* *different* answer instead of a conversation about the previous one.
|
|
9202
|
+
*
|
|
9203
|
+
* **A retried turn re-runs its tools.** For a page-driving agent that is not
|
|
9204
|
+
* neutral: the previous attempt already clicked what it clicked, and this
|
|
9205
|
+
* does not undo it.
|
|
9206
|
+
*/
|
|
9207
|
+
truncateToLastUser() {
|
|
9208
|
+
const messages = [...this.#agent.messages];
|
|
9209
|
+
let lastUser = -1;
|
|
9210
|
+
for (const [index, message] of messages.entries()) {
|
|
9211
|
+
if (message.role === "user") {
|
|
9212
|
+
lastUser = index;
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
if (lastUser === -1) {
|
|
9216
|
+
return null;
|
|
9217
|
+
}
|
|
9218
|
+
const kept = messages.slice(0, lastUser + 1);
|
|
9219
|
+
this.#agent.setMessages(kept);
|
|
9220
|
+
this.#onPersist(this.#agent.messages);
|
|
9221
|
+
return kept;
|
|
9222
|
+
}
|
|
7987
9223
|
/**
|
|
7988
9224
|
* Resume the run loop after a navigating tool's result was supplied
|
|
7989
9225
|
* post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
|
|
@@ -8031,7 +9267,7 @@ var AgUiClient = class {
|
|
|
8031
9267
|
}
|
|
8032
9268
|
async #runLoop() {
|
|
8033
9269
|
let resume;
|
|
8034
|
-
for (let round = 0; round <
|
|
9270
|
+
for (let round = 0; round < this.#maxToolRounds; round += 1) {
|
|
8035
9271
|
if (this.#cancelled) {
|
|
8036
9272
|
return;
|
|
8037
9273
|
}
|
|
@@ -8150,6 +9386,12 @@ var AgUiClient = class {
|
|
|
8150
9386
|
// Emitted after the client has written the patched messages, which is the
|
|
8151
9387
|
// first moment the result exists. Only the ids marked above are looked at,
|
|
8152
9388
|
// so an ordinary text delta does not walk the transcript.
|
|
9389
|
+
onCustomEvent({ event }) {
|
|
9390
|
+
h.onCustomEvent(event.name, event.value);
|
|
9391
|
+
},
|
|
9392
|
+
onMessagesSnapshotEvent({ event }) {
|
|
9393
|
+
h.onMessagesSnapshot(event.messages);
|
|
9394
|
+
},
|
|
8153
9395
|
onMessagesChanged({ messages }) {
|
|
8154
9396
|
if (pendingDeltas.size === 0) {
|
|
8155
9397
|
return;
|
|
@@ -8853,6 +10095,7 @@ function isCredentialsMode(value) {
|
|
|
8853
10095
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
8854
10096
|
var SIZE_KEY = "ag-ui-chat:size";
|
|
8855
10097
|
var THEME_KEY = "ag-ui-chat:theme";
|
|
10098
|
+
var QUOTE_GAP = 6;
|
|
8856
10099
|
var CLAIMED_NAMESPACES = /* @__PURE__ */ new Set();
|
|
8857
10100
|
var AgUiChat = class extends HTMLElement {
|
|
8858
10101
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
@@ -8900,6 +10143,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8900
10143
|
* data. Enable only when the content source is trusted.
|
|
8901
10144
|
*/
|
|
8902
10145
|
allowImages = false;
|
|
10146
|
+
/**
|
|
10147
|
+
* Replace the relative timestamps in the thread drawer and the checkpoint
|
|
10148
|
+
* panel -- `"5m ago"`, `"2d ago"` -- with the host's own formatting.
|
|
10149
|
+
*
|
|
10150
|
+
* The built-in is locale-neutral on purpose: there is no `Intl` anywhere in
|
|
10151
|
+
* this component, so it never disagrees with the page it is embedded in by
|
|
10152
|
+
* guessing a locale. That is a good default and a bad requirement, which is
|
|
10153
|
+
* what this is for.
|
|
10154
|
+
*
|
|
10155
|
+
* ```js
|
|
10156
|
+
* const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
|
|
10157
|
+
* chat.formatRelativeTime = (ts) =>
|
|
10158
|
+
* rtf.format(Math.round((ts - Date.now()) / 60000), "minute");
|
|
10159
|
+
* ```
|
|
10160
|
+
*/
|
|
10161
|
+
formatRelativeTime = null;
|
|
8903
10162
|
/** When true, destructive tools execute without a confirmation modal. */
|
|
8904
10163
|
autoConfirm = false;
|
|
8905
10164
|
/**
|
|
@@ -8923,6 +10182,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8923
10182
|
* is enabled server-side; this only changes how the decision is collected.
|
|
8924
10183
|
*/
|
|
8925
10184
|
approvalRenderer = null;
|
|
10185
|
+
/**
|
|
10186
|
+
* Let the user edit a gated call's arguments before approving it.
|
|
10187
|
+
*
|
|
10188
|
+
* Off by default and **an assertion about your server**, not a negotiation:
|
|
10189
|
+
* AG-UI carries `editedArgs` in the resume payload and gates it on the
|
|
10190
|
+
* agent's own `approveWithEdits` capability, which this component never sees
|
|
10191
|
+
* -- capabilities are not on the wire it reads. So the host says whether its
|
|
10192
|
+
* agent honours them. Turned on against a server that does not, the user
|
|
10193
|
+
* would edit arguments it silently discards, which is worse than not
|
|
10194
|
+
* offering.
|
|
10195
|
+
*
|
|
10196
|
+
* Only affects calls whose arguments are known here: an interrupt names a
|
|
10197
|
+
* `toolCallId`, and the tool card for that call is where the arguments still
|
|
10198
|
+
* are. An interrupt naming no card gets the plain approve/deny.
|
|
10199
|
+
*/
|
|
10200
|
+
approveWithEdits = false;
|
|
8926
10201
|
/**
|
|
8927
10202
|
* Optional per-call confirmation predicate. When set it is authoritative,
|
|
8928
10203
|
* deciding from the tool name and args whether this particular call needs
|
|
@@ -9013,6 +10288,30 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9013
10288
|
* schema never reaches the browser. Client tools should prefer `x-summary`.
|
|
9014
10289
|
*/
|
|
9015
10290
|
toolSummaries = {};
|
|
10291
|
+
/**
|
|
10292
|
+
* Optional presentation hook for the two payload regions of a tool-call card
|
|
10293
|
+
* -- the arguments and the result. Unset (the default) leaves both
|
|
10294
|
+
* pretty-printed as JSON.
|
|
10295
|
+
*
|
|
10296
|
+
* The seam exists because a wide result has no good rendering as JSON: a
|
|
10297
|
+
* thirty-field row is a wall of text where the host wanted a table, or a
|
|
10298
|
+
* sentence. `ClientTool.render` cannot answer it -- it is handed the
|
|
10299
|
+
* *arguments* only, and a server-side tool has no `ClientTool` at all, so the
|
|
10300
|
+
* result region was the one part of the transcript a host could not reach.
|
|
10301
|
+
*
|
|
10302
|
+
* **Presentation, not translation.** The card and the model already read
|
|
10303
|
+
* separate copies of a tool result: the model's is maintained by
|
|
10304
|
+
* `@ag-ui/client` from the same event and persisted with the history, and the
|
|
10305
|
+
* card has always shown that string reformatted. So a formatter changes what
|
|
10306
|
+
* the person reads and nothing the agent reads -- which makes restyling safe
|
|
10307
|
+
* and *rewording* a way to make the card disagree with the prose beside it.
|
|
10308
|
+
* Rename a value on the server, where it reaches both.
|
|
10309
|
+
*
|
|
10310
|
+
* Read at render time rather than captured, so a host that sets it from a
|
|
10311
|
+
* framework effect after the first card still formats the results that settle
|
|
10312
|
+
* afterwards. See {@link ToolPayloadFormatter}.
|
|
10313
|
+
*/
|
|
10314
|
+
formatToolPayload = null;
|
|
9016
10315
|
/**
|
|
9017
10316
|
* Localizable UI strings — a partial override merged over the English
|
|
9018
10317
|
* {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
|
|
@@ -9062,13 +10361,32 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9062
10361
|
#toolRegistry = new ClientToolRegistry();
|
|
9063
10362
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
9064
10363
|
#toolCards = /* @__PURE__ */ new Map();
|
|
10364
|
+
/**
|
|
10365
|
+
* The live delegation panels, keyed by the **parent's** `delegate_task` call
|
|
10366
|
+
* id — which is what the wire keys a sub-agent's progress on, so this map and
|
|
10367
|
+
* {@link #toolCards} answer to the same key.
|
|
10368
|
+
*
|
|
10369
|
+
* Kept beside the cards rather than on them, so a card stays a card: the tool
|
|
10370
|
+
* card holds the slot and this holds what went into it, the same division the
|
|
10371
|
+
* approval prompt already uses.
|
|
10372
|
+
*/
|
|
10373
|
+
#subagentPanels = /* @__PURE__ */ new Map();
|
|
9065
10374
|
/**
|
|
9066
10375
|
* Call ids whose card was already settled from a streamed server-side result
|
|
9067
10376
|
* (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
|
|
9068
10377
|
* the real output with the generic "executed on the server" fallback.
|
|
9069
10378
|
*/
|
|
9070
10379
|
/** Whether a server-pushed chart activity is drawn. Off unless asked for. */
|
|
9071
|
-
|
|
10380
|
+
/**
|
|
10381
|
+
* Which `activity_type`s this element can draw, by name.
|
|
10382
|
+
*
|
|
10383
|
+
* A registry rather than a branch because `activity_type` is an open string
|
|
10384
|
+
* the protocol does not enumerate. The two built-ins go through it like any
|
|
10385
|
+
* host registration, which is the test that the seam is real.
|
|
10386
|
+
*/
|
|
10387
|
+
#activityRenderers = /* @__PURE__ */ new Map();
|
|
10388
|
+
/** Types that arrived with nobody registered to draw them. See {@link unhandledActivityTypes}. */
|
|
10389
|
+
#unhandledActivityTypes = /* @__PURE__ */ new Set();
|
|
9072
10390
|
/** Card elements by call id, so a rendering handler can find its own card. */
|
|
9073
10391
|
#cardElements = /* @__PURE__ */ new Map();
|
|
9074
10392
|
/** Chart blocks by activity message id, so an update redraws in place. */
|
|
@@ -9080,7 +10398,75 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9080
10398
|
* Spans tool rounds and an approval interrupt; cleared when the event fires.
|
|
9081
10399
|
*/
|
|
9082
10400
|
#runTools = [];
|
|
10401
|
+
/**
|
|
10402
|
+
* Keys announced during this interaction, de-duplicated in first-seen order.
|
|
10403
|
+
*
|
|
10404
|
+
* Per element, never module-level: a second mounted chat is a second run, and
|
|
10405
|
+
* sharing this would tell one page to refetch on the other's writes. Reset by
|
|
10406
|
+
* {@link AgUiChat.#dispatchRunFinished}, which is the one place that has read
|
|
10407
|
+
* it.
|
|
10408
|
+
*/
|
|
10409
|
+
/**
|
|
10410
|
+
* Tool names the user waived confirmation for, for the life of this element.
|
|
10411
|
+
*
|
|
10412
|
+
* Per instance and never persisted: a session decision that outlived the tab
|
|
10413
|
+
* would be a permanent grant made by one click, which is the thing
|
|
10414
|
+
* `autoConfirm` already exists to say deliberately. Cleared with the element.
|
|
10415
|
+
*/
|
|
10416
|
+
#sessionApproved = /* @__PURE__ */ new Set();
|
|
10417
|
+
/**
|
|
10418
|
+
* The one action row currently carrying Retry, if any.
|
|
10419
|
+
*
|
|
10420
|
+
* Retry belongs to the **last** turn only: re-running an older one is
|
|
10421
|
+
* branching, and for a page-driving agent editing a past turn is not neutral
|
|
10422
|
+
* -- those turns clicked buttons, and re-running turn 3 does not un-save what
|
|
10423
|
+
* turn 5 saved. Holding a single owner is what keeps exactly one offer on
|
|
10424
|
+
* screen without per-bubble bookkeeping.
|
|
10425
|
+
*/
|
|
10426
|
+
#retryOwner = null;
|
|
10427
|
+
#runInvalidated = /* @__PURE__ */ new Set();
|
|
9083
10428
|
#root;
|
|
10429
|
+
/** Screen-reader-only status region -- see {@link AgUiChat.#announce}. */
|
|
10430
|
+
#announcer = document.createElement("div");
|
|
10431
|
+
/** Return-to-foot affordance, shown only once something has been missed. */
|
|
10432
|
+
#jumpButton = document.createElement("button");
|
|
10433
|
+
/**
|
|
10434
|
+
* Offer to quote the current selection, floated beside it.
|
|
10435
|
+
*
|
|
10436
|
+
* Shares {@link AgUiChat.#messagesWrap} with the jump button for the same
|
|
10437
|
+
* reason: it is positioned against the transcript, and must not scroll away
|
|
10438
|
+
* with the words it is pointing at.
|
|
10439
|
+
*/
|
|
10440
|
+
#quoteButton = document.createElement("button");
|
|
10441
|
+
/** What {@link AgUiChat.#quoteButton} would quote, while it is showing. */
|
|
10442
|
+
#quoting = "";
|
|
10443
|
+
/** The host-page offer, while one is attached; see {@link AgUiChat.offerQuoteInPage}. */
|
|
10444
|
+
#pageQuote = null;
|
|
10445
|
+
/**
|
|
10446
|
+
* Positioning context for {@link AgUiChat.#jumpButton}.
|
|
10447
|
+
*
|
|
10448
|
+
* The button cannot live in the scrolling list -- it would scroll away with
|
|
10449
|
+
* the content it is offering to scroll to -- and it cannot be positioned
|
|
10450
|
+
* against the panel either: the panel's foot is below the composer, the skill
|
|
10451
|
+
* chips and the footer, so `bottom` measured from there lands the button on
|
|
10452
|
+
* top of the composer rather than over the transcript. This wrapper is the
|
|
10453
|
+
* only box whose foot *is* the transcript's foot.
|
|
10454
|
+
*/
|
|
10455
|
+
#messagesWrap = document.createElement("div");
|
|
10456
|
+
/** Follows the foot of the transcript, and stops when the reader scrolls away. */
|
|
10457
|
+
#scroller;
|
|
10458
|
+
/** Pending clear of {@link AgUiChat.#announcer}; see why it is cleared at all. */
|
|
10459
|
+
#announceTimer = null;
|
|
10460
|
+
/**
|
|
10461
|
+
* Whether this turn already announced how it ended.
|
|
10462
|
+
*
|
|
10463
|
+
* `onSettled` is the terminal guarantee and fires however the run ended, so
|
|
10464
|
+
* it is the only place that can promise the user hears *something*. But a
|
|
10465
|
+
* stopped or failed run has already said the truer thing from `onCancelled`
|
|
10466
|
+
* or `onError`, and "assistant answered" after "response stopped" is worse
|
|
10467
|
+
* than silence.
|
|
10468
|
+
*/
|
|
10469
|
+
#announcedOutcome = false;
|
|
9084
10470
|
#chat;
|
|
9085
10471
|
#messages;
|
|
9086
10472
|
#input;
|
|
@@ -9198,6 +10584,23 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9198
10584
|
this.#launcher = document.createElement("button");
|
|
9199
10585
|
this.#badge = document.createElement("span");
|
|
9200
10586
|
this.#emptyWrap = document.createElement("div");
|
|
10587
|
+
this.registerActivityRenderer({
|
|
10588
|
+
type: COMPACTION_ACTIVITY_TYPE,
|
|
10589
|
+
render: (content) => {
|
|
10590
|
+
const removed = compactionRemoved(content);
|
|
10591
|
+
return removed === null ? null : renderRunNotice(
|
|
10592
|
+
"\u{1F5DC}",
|
|
10593
|
+
this.#strings.historyCompacted.replace("{count}", String(removed)),
|
|
10594
|
+
"compaction"
|
|
10595
|
+
);
|
|
10596
|
+
}
|
|
10597
|
+
});
|
|
10598
|
+
this.registerActivityRenderer({
|
|
10599
|
+
type: SUGGESTIONS_ACTIVITY_TYPE,
|
|
10600
|
+
render: (content) => renderSuggestionChips(content, this.#strings, (prompt) => {
|
|
10601
|
+
void this.sendMessage(prompt);
|
|
10602
|
+
})
|
|
10603
|
+
});
|
|
9201
10604
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
9202
10605
|
this.#drawer = new ThreadDrawer({
|
|
9203
10606
|
onSelect: (threadId) => {
|
|
@@ -9282,6 +10685,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9282
10685
|
/** Load the checkpoint panel with the runs that can actually be continued. */
|
|
9283
10686
|
async #refreshCheckpoints() {
|
|
9284
10687
|
const index = this.#runs();
|
|
10688
|
+
this.#checkpoints.setRelativeTimeFormatter(this.formatRelativeTime);
|
|
9285
10689
|
this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
|
|
9286
10690
|
}
|
|
9287
10691
|
/** Attributes the element reacts to after it has been connected. */
|
|
@@ -9478,7 +10882,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9478
10882
|
});
|
|
9479
10883
|
this.#confirmAbort = null;
|
|
9480
10884
|
this.#updateEmptyState();
|
|
9481
|
-
this.#
|
|
10885
|
+
this.#scroller.follow();
|
|
9482
10886
|
return answer;
|
|
9483
10887
|
}
|
|
9484
10888
|
/**
|
|
@@ -9734,8 +11138,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9734
11138
|
this.#claimedNs = null;
|
|
9735
11139
|
}
|
|
9736
11140
|
this.#cancelRun();
|
|
11141
|
+
this.#pageQuote?.detach();
|
|
11142
|
+
this.#pageQuote = null;
|
|
9737
11143
|
this.#attachTray?.dispose();
|
|
9738
11144
|
this.#voice?.dispose();
|
|
11145
|
+
this.#scroller.dispose();
|
|
11146
|
+
if (this.#announceTimer !== null) {
|
|
11147
|
+
clearTimeout(this.#announceTimer);
|
|
11148
|
+
this.#announceTimer = null;
|
|
11149
|
+
}
|
|
9739
11150
|
}
|
|
9740
11151
|
/**
|
|
9741
11152
|
* Read an opt-in flag attribute the way HTML reads a boolean attribute.
|
|
@@ -9846,6 +11257,142 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9846
11257
|
this.#onInput();
|
|
9847
11258
|
this.#input.focus();
|
|
9848
11259
|
}
|
|
11260
|
+
/**
|
|
11261
|
+
* Put `text` into the composer as a markdown quotation, and focus it.
|
|
11262
|
+
*
|
|
11263
|
+
* Deliberately **not** a send. Quoting is how a question narrows to one part
|
|
11264
|
+
* of an answer, so the quotation is the preamble and the question is what
|
|
11265
|
+
* comes next -- the caret is left after it, on its own line.
|
|
11266
|
+
*
|
|
11267
|
+
* This is also the seam for the half of this feature the component cannot
|
|
11268
|
+
* build: selection in the **host page**. A widget mounted beside a table can
|
|
11269
|
+
* be asked about a row, and nothing in a chat's own transcript can offer
|
|
11270
|
+
* that. A host reads its own selection, however it likes, and calls this.
|
|
11271
|
+
*
|
|
11272
|
+
* No-ops on text that is empty or only whitespace.
|
|
11273
|
+
*/
|
|
11274
|
+
quote(text3) {
|
|
11275
|
+
const quoted = asQuote(text3);
|
|
11276
|
+
if (quoted === "") {
|
|
11277
|
+
return;
|
|
11278
|
+
}
|
|
11279
|
+
const current = this.#input.value.replace(/\s+$/, "");
|
|
11280
|
+
this.#input.value = current === "" ? quoted : `${current}
|
|
11281
|
+
|
|
11282
|
+
${quoted}`;
|
|
11283
|
+
this.#autoGrow();
|
|
11284
|
+
this.#input.focus();
|
|
11285
|
+
const end = this.#input.value.length;
|
|
11286
|
+
this.#input.setSelectionRange(end, end);
|
|
11287
|
+
}
|
|
11288
|
+
/**
|
|
11289
|
+
* Offer to quote what the user selects in the **host page**, not just in the
|
|
11290
|
+
* transcript. Returns a function that stops offering.
|
|
11291
|
+
*
|
|
11292
|
+
* The same select-then-offer gesture, over a table, a diff, a report -- the
|
|
11293
|
+
* surface the user actually works in, which is the half of quoting no hosted
|
|
11294
|
+
* chat can reach. Opt-in, because it listens on the host's document and that
|
|
11295
|
+
* is theirs to grant.
|
|
11296
|
+
*
|
|
11297
|
+
* Deliberately **not** a four-line recipe, which is how this shipped first
|
|
11298
|
+
* and was wrong: a page listener that quotes every settled selection appends
|
|
11299
|
+
* to the composer on every drag made to read, to copy or to fix a typo -- and
|
|
11300
|
+
* it cannot tell a selection in the page's prose from one inside the user's
|
|
11301
|
+
* own half-typed `<input>`, because Chrome reports a field's internal
|
|
11302
|
+
* selection as an ordinary range over the field's *wrapper*. See
|
|
11303
|
+
* {@link attachQuoteOffer} for the guards.
|
|
11304
|
+
*
|
|
11305
|
+
* Detached automatically when the element leaves the document; a host that
|
|
11306
|
+
* re-mounts it calls this again.
|
|
11307
|
+
*/
|
|
11308
|
+
offerQuoteInPage(within = document.body) {
|
|
11309
|
+
this.#pageQuote?.detach();
|
|
11310
|
+
const offer = attachQuoteOffer({
|
|
11311
|
+
within,
|
|
11312
|
+
label: this.#strings.quoteSelection,
|
|
11313
|
+
exclude: this,
|
|
11314
|
+
onQuote: (text3) => this.quote(text3)
|
|
11315
|
+
});
|
|
11316
|
+
this.#pageQuote = offer;
|
|
11317
|
+
return () => {
|
|
11318
|
+
offer.detach();
|
|
11319
|
+
if (this.#pageQuote === offer) {
|
|
11320
|
+
this.#pageQuote = null;
|
|
11321
|
+
}
|
|
11322
|
+
};
|
|
11323
|
+
}
|
|
11324
|
+
/** Whether the transcript offers to quote what the user selects. */
|
|
11325
|
+
#quoteEnabled() {
|
|
11326
|
+
return this.getAttribute("data-quote-selection") !== "false";
|
|
11327
|
+
}
|
|
11328
|
+
/**
|
|
11329
|
+
* Offer to quote the settled selection, or retire the offer.
|
|
11330
|
+
*
|
|
11331
|
+
* `event` is passed for its coordinates and only those: they say which line
|
|
11332
|
+
* of a selection spanning several messages the offer should hang from. A
|
|
11333
|
+
* keyboard selection has none, and the first line is used instead.
|
|
11334
|
+
*/
|
|
11335
|
+
#onSelectionSettled(event) {
|
|
11336
|
+
if (!this.#quoteEnabled()) {
|
|
11337
|
+
return;
|
|
11338
|
+
}
|
|
11339
|
+
const near = event === void 0 ? void 0 : { x: event.clientX, y: event.clientY };
|
|
11340
|
+
const selected = quotableSelection(this.#messages, [this.#root], near);
|
|
11341
|
+
if (selected === null) {
|
|
11342
|
+
this.#hideQuote();
|
|
11343
|
+
return;
|
|
11344
|
+
}
|
|
11345
|
+
this.#quoting = selected.text;
|
|
11346
|
+
this.#placeQuote(selected.rect);
|
|
11347
|
+
}
|
|
11348
|
+
/** Float the offer beside `rect`, kept inside the transcript's own box. */
|
|
11349
|
+
#placeQuote(rect) {
|
|
11350
|
+
this.#quoteButton.hidden = false;
|
|
11351
|
+
const wrap = this.#messagesWrap.getBoundingClientRect();
|
|
11352
|
+
const top = rect.top - wrap.top;
|
|
11353
|
+
const below = top < QUOTE_GAP + this.#quoteButton.offsetHeight;
|
|
11354
|
+
this.#quoteButton.dataset["below"] = String(below);
|
|
11355
|
+
this.#quoteButton.style.top = `${below ? rect.bottom - wrap.top + QUOTE_GAP : top - QUOTE_GAP}px`;
|
|
11356
|
+
const half = this.#quoteButton.offsetWidth / 2;
|
|
11357
|
+
const centre = rect.left + rect.width / 2 - wrap.left;
|
|
11358
|
+
this.#quoteButton.style.left = `${Math.min(Math.max(centre, half), wrap.width - half)}px`;
|
|
11359
|
+
}
|
|
11360
|
+
/** Retire the offer, and forget what it was pointing at. */
|
|
11361
|
+
#hideQuote() {
|
|
11362
|
+
this.#quoteButton.hidden = true;
|
|
11363
|
+
this.#quoting = "";
|
|
11364
|
+
}
|
|
11365
|
+
/**
|
|
11366
|
+
* The tool-round budget from `data-max-tool-rounds`, for one send.
|
|
11367
|
+
*
|
|
11368
|
+
* Anything unparseable becomes `NaN`, which {@link AgUiClient} rejects along
|
|
11369
|
+
* with a bound below one -- so the two ways of setting this are validated in
|
|
11370
|
+
* one place rather than agreeing by coincidence.
|
|
11371
|
+
*/
|
|
11372
|
+
#maxToolRounds() {
|
|
11373
|
+
const attr = this.getAttribute("data-max-tool-rounds");
|
|
11374
|
+
return attr === null ? MAX_TOOL_ROUNDS : Number.parseInt(attr, 10);
|
|
11375
|
+
}
|
|
11376
|
+
/**
|
|
11377
|
+
* Which message actions a finished bubble offers, from
|
|
11378
|
+
* `data-message-actions`.
|
|
11379
|
+
*
|
|
11380
|
+
* Absent means all of them, so the attribute only ever subtracts: the row
|
|
11381
|
+
* shipped without an off switch and a host that never sets this must keep
|
|
11382
|
+
* exactly what it had. A value names the survivors, which makes
|
|
11383
|
+
* `data-message-actions="false"` -- the spelling its sibling
|
|
11384
|
+
* `data-quote-selection` uses -- an empty set by falling out of the same rule
|
|
11385
|
+
* rather than by a case of its own.
|
|
11386
|
+
*/
|
|
11387
|
+
#messageActions() {
|
|
11388
|
+
const attr = this.getAttribute("data-message-actions");
|
|
11389
|
+
if (attr === null) {
|
|
11390
|
+
return new Set(Object.values(MESSAGE_ACTIONS));
|
|
11391
|
+
}
|
|
11392
|
+
return new Set(
|
|
11393
|
+
attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
|
|
11394
|
+
);
|
|
11395
|
+
}
|
|
9849
11396
|
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
9850
11397
|
#attachmentMaxBytes() {
|
|
9851
11398
|
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
@@ -10375,19 +11922,63 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10375
11922
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
10376
11923
|
#resetState() {
|
|
10377
11924
|
this.#client = null;
|
|
11925
|
+
this.#clearTranscript();
|
|
11926
|
+
this.#initialMessages = [];
|
|
11927
|
+
}
|
|
11928
|
+
/**
|
|
11929
|
+
* Wipe the rendered transcript and everything that indexes into it.
|
|
11930
|
+
*
|
|
11931
|
+
* Split from {@link #resetState} because a retry re-renders the transcript
|
|
11932
|
+
* while keeping the *client*: dropping the client there would take the
|
|
11933
|
+
* agent's message list with it, which is the thing being truncated.
|
|
11934
|
+
*/
|
|
11935
|
+
#clearTranscript() {
|
|
10378
11936
|
this.#endStream();
|
|
10379
11937
|
this.#currentGroup = null;
|
|
10380
11938
|
this.#thoughts = null;
|
|
10381
11939
|
this.#hidePending();
|
|
10382
11940
|
this.#toolCards.clear();
|
|
11941
|
+
this.#subagentPanels.clear();
|
|
10383
11942
|
this.#serverSettled.clear();
|
|
10384
11943
|
this.#cardElements.clear();
|
|
10385
11944
|
this.#activityBlocks.clear();
|
|
10386
|
-
this.#
|
|
11945
|
+
this.#retryOwner = null;
|
|
10387
11946
|
this.#attachTray?.clear();
|
|
10388
11947
|
this.#messages.replaceChildren(this.#emptyWrap);
|
|
10389
11948
|
this.#updateEmptyState();
|
|
10390
11949
|
}
|
|
11950
|
+
/**
|
|
11951
|
+
* Ask the same question again and replace the answer.
|
|
11952
|
+
*
|
|
11953
|
+
* History is truncated to the most recent user message inclusive and the run
|
|
11954
|
+
* repeats, so the agent answers what it was asked rather than being told its
|
|
11955
|
+
* last answer was wrong. Returns `false` when there is nothing to retry or a
|
|
11956
|
+
* run is already in flight.
|
|
11957
|
+
*
|
|
11958
|
+
* Public because a host with its own message UI wants the same button, and
|
|
11959
|
+
* because the failed-run notice reaches it from outside the action row.
|
|
11960
|
+
*
|
|
11961
|
+
* **A retried turn re-runs its tools**, which for a page-driving agent is not
|
|
11962
|
+
* neutral: the previous attempt already clicked what it clicked, and this
|
|
11963
|
+
* does not undo it. Confirmation still applies, so a destructive tool asks
|
|
11964
|
+
* again -- unless the user waived it for this session.
|
|
11965
|
+
*/
|
|
11966
|
+
async retryLastTurn() {
|
|
11967
|
+
if (this.#running) {
|
|
11968
|
+
return false;
|
|
11969
|
+
}
|
|
11970
|
+
const client = this.#ensureClient();
|
|
11971
|
+
const kept = client.truncateToLastUser();
|
|
11972
|
+
if (kept === null) {
|
|
11973
|
+
return false;
|
|
11974
|
+
}
|
|
11975
|
+
this.#clearTranscript();
|
|
11976
|
+
for (const message of kept) {
|
|
11977
|
+
this.#renderHistoricMessage(message);
|
|
11978
|
+
}
|
|
11979
|
+
await client.resume();
|
|
11980
|
+
return true;
|
|
11981
|
+
}
|
|
10391
11982
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
10392
11983
|
async #switchThread(threadId) {
|
|
10393
11984
|
if (threadId === this.#threadId) {
|
|
@@ -10416,6 +12007,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10416
12007
|
}
|
|
10417
12008
|
/** Reload the drawer's thread list, marking the active thread. */
|
|
10418
12009
|
async #refreshDrawer() {
|
|
12010
|
+
this.#drawer.setRelativeTimeFormatter(this.formatRelativeTime);
|
|
10419
12011
|
this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
|
|
10420
12012
|
}
|
|
10421
12013
|
/**
|
|
@@ -10507,7 +12099,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10507
12099
|
}
|
|
10508
12100
|
if (message.role === MESSAGE_ROLE.ASSISTANT) {
|
|
10509
12101
|
if (text3 !== "") {
|
|
10510
|
-
this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3)
|
|
12102
|
+
const restoredBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3);
|
|
12103
|
+
restoredBubble.classList.add("message--restored");
|
|
12104
|
+
this.#attachActions(restoredBubble);
|
|
10511
12105
|
}
|
|
10512
12106
|
for (const call of restoredToolCalls(message.toolCalls)) {
|
|
10513
12107
|
const restored = {
|
|
@@ -10528,8 +12122,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10528
12122
|
}
|
|
10529
12123
|
if (message.role === "activity") {
|
|
10530
12124
|
const activity = message;
|
|
10531
|
-
if (activity.activityType ===
|
|
10532
|
-
this.#
|
|
12125
|
+
if (typeof activity.activityType === "string") {
|
|
12126
|
+
this.#drawActivity(message.id, activity.activityType, activity.content);
|
|
10533
12127
|
}
|
|
10534
12128
|
return;
|
|
10535
12129
|
}
|
|
@@ -10594,7 +12188,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10594
12188
|
this.#messages.appendChild(bubble);
|
|
10595
12189
|
}
|
|
10596
12190
|
this.#updateEmptyState();
|
|
10597
|
-
|
|
12191
|
+
if (role === MESSAGE_ROLE.USER) {
|
|
12192
|
+
this.#scroller.jump();
|
|
12193
|
+
} else {
|
|
12194
|
+
this.#scroller.follow();
|
|
12195
|
+
}
|
|
10598
12196
|
return bubble;
|
|
10599
12197
|
}
|
|
10600
12198
|
/**
|
|
@@ -10616,8 +12214,6 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10616
12214
|
return this.#currentGroup;
|
|
10617
12215
|
}
|
|
10618
12216
|
#render() {
|
|
10619
|
-
const style = document.createElement("style");
|
|
10620
|
-
style.textContent = STYLES;
|
|
10621
12217
|
this.#chat.className = "chat";
|
|
10622
12218
|
this.#chat.setAttribute("part", "panel");
|
|
10623
12219
|
const header = document.createElement("div");
|
|
@@ -10641,8 +12237,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10641
12237
|
checkpoints.addEventListener("click", () => this.toggleCheckpoints());
|
|
10642
12238
|
const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
|
|
10643
12239
|
newChat.addEventListener("click", () => this.newChat());
|
|
10644
|
-
const
|
|
10645
|
-
|
|
12240
|
+
const collapse2 = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
12241
|
+
collapse2.addEventListener("click", () => this.toggleCollapsed());
|
|
10646
12242
|
if (this.#runs() !== null) {
|
|
10647
12243
|
controls.append(history, checkpoints, newChat);
|
|
10648
12244
|
} else {
|
|
@@ -10658,13 +12254,46 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10658
12254
|
this.#syncThemeGlyph();
|
|
10659
12255
|
controls.append(this.#themeToggle);
|
|
10660
12256
|
}
|
|
10661
|
-
controls.append(
|
|
12257
|
+
controls.append(collapse2);
|
|
10662
12258
|
header.append(title, headerActions, controls);
|
|
10663
12259
|
this.#messages.className = "messages";
|
|
10664
12260
|
this.#messages.setAttribute("part", "messages");
|
|
10665
12261
|
this.#messages.setAttribute("role", "log");
|
|
10666
|
-
this.#messages.setAttribute("aria-live", "
|
|
12262
|
+
this.#messages.setAttribute("aria-live", "off");
|
|
10667
12263
|
this.#messages.setAttribute("aria-label", this.#strings.conversation);
|
|
12264
|
+
this.#jumpButton.className = "jump-latest";
|
|
12265
|
+
this.#jumpButton.type = "button";
|
|
12266
|
+
this.#jumpButton.setAttribute("part", "jump-latest");
|
|
12267
|
+
this.#jumpButton.textContent = this.#strings.jumpToLatest;
|
|
12268
|
+
this.#jumpButton.addEventListener("click", () => {
|
|
12269
|
+
this.#scroller.jump();
|
|
12270
|
+
});
|
|
12271
|
+
this.#quoteButton.className = "quote-selection";
|
|
12272
|
+
this.#quoteButton.type = "button";
|
|
12273
|
+
this.#quoteButton.setAttribute("part", "quote-selection");
|
|
12274
|
+
this.#quoteButton.textContent = this.#strings.quoteSelection;
|
|
12275
|
+
this.#quoteButton.hidden = true;
|
|
12276
|
+
this.#quoteButton.addEventListener("mousedown", (event) => {
|
|
12277
|
+
event.preventDefault();
|
|
12278
|
+
});
|
|
12279
|
+
this.#quoteButton.addEventListener("click", () => {
|
|
12280
|
+
this.quote(this.#quoting);
|
|
12281
|
+
window.getSelection()?.removeAllRanges();
|
|
12282
|
+
this.#hideQuote();
|
|
12283
|
+
});
|
|
12284
|
+
this.#messages.addEventListener("mouseup", (event) => this.#onSelectionSettled(event));
|
|
12285
|
+
this.#messages.addEventListener("keyup", () => this.#onSelectionSettled());
|
|
12286
|
+
this.#messages.addEventListener("mousedown", () => this.#hideQuote());
|
|
12287
|
+
this.#scroller = createStickToBottom({
|
|
12288
|
+
viewport: this.#messages,
|
|
12289
|
+
onMissedContent: (missed) => {
|
|
12290
|
+
this.#jumpButton.dataset["missed"] = String(missed);
|
|
12291
|
+
}
|
|
12292
|
+
});
|
|
12293
|
+
this.#announcer.className = "sr-only";
|
|
12294
|
+
this.#announcer.setAttribute("role", "status");
|
|
12295
|
+
this.#announcer.setAttribute("aria-live", "polite");
|
|
12296
|
+
this.#announcer.setAttribute("aria-atomic", "true");
|
|
10668
12297
|
this.#emptyWrap.className = "empty";
|
|
10669
12298
|
this.#emptyWrap.setAttribute("part", "empty");
|
|
10670
12299
|
const emptySlot = document.createElement("slot");
|
|
@@ -10727,9 +12356,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10727
12356
|
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
10728
12357
|
composer.append(this.#input, tools);
|
|
10729
12358
|
inputRow.append(composer, this.#fileInput);
|
|
12359
|
+
this.#messagesWrap.className = "messages-wrap";
|
|
12360
|
+
this.#messagesWrap.append(this.#messages, this.#jumpButton, this.#quoteButton);
|
|
10730
12361
|
this.#chat.append(
|
|
10731
12362
|
header,
|
|
10732
|
-
this.#
|
|
12363
|
+
this.#messagesWrap,
|
|
10733
12364
|
this.#skillsMenu.palette,
|
|
10734
12365
|
this.#skillsMenu.chips,
|
|
10735
12366
|
this.#skillHint,
|
|
@@ -10775,7 +12406,66 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10775
12406
|
label: this.#strings.resizePanel
|
|
10776
12407
|
})
|
|
10777
12408
|
);
|
|
10778
|
-
this.#
|
|
12409
|
+
this.#adoptStyles();
|
|
12410
|
+
this.#root.append(this.#announcer, this.#chat, this.#launcher);
|
|
12411
|
+
}
|
|
12412
|
+
/**
|
|
12413
|
+
* Attach the stylesheet without an inline `<style>` element.
|
|
12414
|
+
*
|
|
12415
|
+
* A host with a strict `style-src` and no `'unsafe-inline'` drops an injected
|
|
12416
|
+
* `<style>` silently: the component mounts, functions, and renders completely
|
|
12417
|
+
* unstyled, with nothing in the console to point at. `adoptedStyleSheets`
|
|
12418
|
+
* carries no inline-style origin, so it is unaffected by that policy.
|
|
12419
|
+
*
|
|
12420
|
+
* The sheet is constructed **per instance** rather than shared at module
|
|
12421
|
+
* scope. A shared sheet would additionally avoid re-parsing the stylesheet
|
|
12422
|
+
* once per mounted element, which is what `adoptedStyleSheets` is usually
|
|
12423
|
+
* reached for -- but a module-level singleton is exactly what this package
|
|
12424
|
+
* forbids, and the CSP defect is fixed either way. Per instance is no worse
|
|
12425
|
+
* than the `<style>` element it replaces, which also parsed once per mount.
|
|
12426
|
+
*
|
|
12427
|
+
* No fallback: constructible `CSSStyleSheet` is Chrome 73, Firefox 101 and
|
|
12428
|
+
* Safari 16.4, all below this package's declared Safari 17 runtime target. A
|
|
12429
|
+
* guard here would be code no supported browser can reach, and the only way
|
|
12430
|
+
* to keep it would be to exempt it from the coverage gate.
|
|
12431
|
+
*/
|
|
12432
|
+
/**
|
|
12433
|
+
* Say one short thing to a screen reader, without touching the transcript.
|
|
12434
|
+
*
|
|
12435
|
+
* The transcript cannot do this job. It is rewritten on every animation
|
|
12436
|
+
* frame while an answer streams, so as a live region it re-announced the
|
|
12437
|
+
* whole answer tens of times per turn -- not merely unhelpful but actively
|
|
12438
|
+
* hostile. The published fix for this exact bug (Microsoft's Bot Framework
|
|
12439
|
+
* WebChat #3236) is architectural rather than a matter of tuning attributes:
|
|
12440
|
+
* demote the visible transcript out of live-region duty and put one
|
|
12441
|
+
* synthesised status per event into a separate invisible region. MDN and
|
|
12442
|
+
* Scott O'Hara prescribe the same empty-region-then-inject shape.
|
|
12443
|
+
*
|
|
12444
|
+
* Roughly four calls land per turn -- responding, answered, a card is waiting,
|
|
12445
|
+
* stopped or failed -- so the user is told what happened and reads the answer
|
|
12446
|
+
* itself by navigating the log, at their own pace, rather than having it
|
|
12447
|
+
* shouted at them a token at a time.
|
|
12448
|
+
*
|
|
12449
|
+
* **The clear is load-bearing, twice.** A reader announces a live region when
|
|
12450
|
+
* its content *changes*, so setting the same string twice running -- two turns
|
|
12451
|
+
* in a row both starting -- is not a change and is silently not announced.
|
|
12452
|
+
* Emptying first makes the next set a change again. It also stops a stale
|
|
12453
|
+
* status being read out when a reader later lands on the region.
|
|
12454
|
+
*/
|
|
12455
|
+
#announce(message) {
|
|
12456
|
+
if (this.#announceTimer !== null) {
|
|
12457
|
+
clearTimeout(this.#announceTimer);
|
|
12458
|
+
}
|
|
12459
|
+
this.#announcer.textContent = message;
|
|
12460
|
+
this.#announceTimer = setTimeout(() => {
|
|
12461
|
+
this.#announceTimer = null;
|
|
12462
|
+
this.#announcer.textContent = "";
|
|
12463
|
+
}, ANNOUNCE_CLEAR_MS);
|
|
12464
|
+
}
|
|
12465
|
+
#adoptStyles() {
|
|
12466
|
+
const sheet = new CSSStyleSheet();
|
|
12467
|
+
sheet.replaceSync(STYLES);
|
|
12468
|
+
this.#root.adoptedStyleSheets = [sheet];
|
|
10779
12469
|
}
|
|
10780
12470
|
/**
|
|
10781
12471
|
* Build a header control button: a named slot a host can project markup into,
|
|
@@ -11071,7 +12761,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11071
12761
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
11072
12762
|
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
11073
12763
|
onStateChanged: (state) => this.#onSharedStateChanged(state),
|
|
11074
|
-
connectionLostMessage: this.#strings.connectionLost
|
|
12764
|
+
connectionLostMessage: this.#strings.connectionLost,
|
|
12765
|
+
maxToolRounds: this.#maxToolRounds()
|
|
11075
12766
|
});
|
|
11076
12767
|
}
|
|
11077
12768
|
return this.#client;
|
|
@@ -11087,15 +12778,78 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11087
12778
|
})
|
|
11088
12779
|
);
|
|
11089
12780
|
}
|
|
11090
|
-
/**
|
|
11091
|
-
|
|
12781
|
+
/**
|
|
12782
|
+
* Give a finished assistant bubble its action row, and hand it Retry.
|
|
12783
|
+
*
|
|
12784
|
+
* Every finished bubble gets copy and feedback -- both are safe on a message
|
|
12785
|
+
* of any age. Retry moves to the newest, because it is the only one where
|
|
12786
|
+
* re-running answers the same question rather than rewriting history.
|
|
12787
|
+
*
|
|
12788
|
+
* `data-message-actions` subtracts from that. The row is built only when
|
|
12789
|
+
* something survives to go in it: an empty row still takes its margin, still
|
|
12790
|
+
* answers to the `message-actions` part, and still reads to a screen reader
|
|
12791
|
+
* as a group of actions with none in it.
|
|
12792
|
+
*/
|
|
12793
|
+
#attachActions(bubble, options = {}) {
|
|
12794
|
+
const enabled = this.#messageActions();
|
|
12795
|
+
const copyable = enabled.has(MESSAGE_ACTIONS.COPY);
|
|
12796
|
+
const rateable = options.rateable !== false && enabled.has(MESSAGE_ACTIONS.FEEDBACK);
|
|
12797
|
+
if (copyable || rateable) {
|
|
12798
|
+
attachMessageActions(bubble, {
|
|
12799
|
+
strings: this.#strings,
|
|
12800
|
+
// Read at click time, not captured: a bubble rendered from markdown
|
|
12801
|
+
// holds its text in the DOM, and that is what the user sees and means
|
|
12802
|
+
// to copy.
|
|
12803
|
+
...copyable ? { text: () => bubble.textContent } : {},
|
|
12804
|
+
...rateable ? {
|
|
12805
|
+
onFeedback: (rating) => {
|
|
12806
|
+
this.dispatchEvent(
|
|
12807
|
+
new CustomEvent(FEEDBACK_EVENT, {
|
|
12808
|
+
detail: { content: bubble.textContent, rating },
|
|
12809
|
+
bubbles: true,
|
|
12810
|
+
composed: true
|
|
12811
|
+
})
|
|
12812
|
+
);
|
|
12813
|
+
}
|
|
12814
|
+
} : {}
|
|
12815
|
+
});
|
|
12816
|
+
}
|
|
12817
|
+
if (enabled.has(MESSAGE_ACTIONS.RETRY)) {
|
|
12818
|
+
this.#moveRetryTo(messageActionBar(bubble, this.#strings));
|
|
12819
|
+
}
|
|
12820
|
+
}
|
|
12821
|
+
/** Move the Retry button onto `bar`, taking it off whoever held it. */
|
|
12822
|
+
#moveRetryTo(bar) {
|
|
12823
|
+
this.#retryOwner?.querySelector(".message-action--retry")?.remove();
|
|
12824
|
+
const retry = messageActionButton("retry", this.#strings.retryMessage, "\u21BB");
|
|
12825
|
+
retry.addEventListener("click", () => {
|
|
12826
|
+
void this.retryLastTurn();
|
|
12827
|
+
});
|
|
12828
|
+
bar.prepend(retry);
|
|
12829
|
+
this.#retryOwner = bar;
|
|
12830
|
+
}
|
|
12831
|
+
/**
|
|
12832
|
+
* Which rule gates `call`, or `null` when it runs straight through.
|
|
12833
|
+
*
|
|
12834
|
+
* The rule, rather than a bare boolean, because it decides whether the user
|
|
12835
|
+
* may *waive* the prompt for the rest of the session. Only the default
|
|
12836
|
+
* `x-destructive` gate is waivable: `confirmPredicate` is documented as
|
|
12837
|
+
* authoritative, so letting one click retire it would silently defeat a host
|
|
12838
|
+
* policy — and the session allowlist is consulted on the same path it can
|
|
12839
|
+
* be added from, so the button is never offered where honouring it would be
|
|
12840
|
+
* refused.
|
|
12841
|
+
*/
|
|
12842
|
+
async #confirmationRule(call, tool) {
|
|
11092
12843
|
if (this.autoConfirm) {
|
|
11093
|
-
return
|
|
12844
|
+
return null;
|
|
11094
12845
|
}
|
|
11095
12846
|
if (this.confirmPredicate !== null) {
|
|
11096
|
-
return await this.confirmPredicate(call.name, call.args) === true;
|
|
12847
|
+
return await this.confirmPredicate(call.name, call.args) === true ? "predicate" : null;
|
|
12848
|
+
}
|
|
12849
|
+
if (this.#sessionApproved.has(call.name)) {
|
|
12850
|
+
return null;
|
|
11097
12851
|
}
|
|
11098
|
-
return isDestructive(tool.parameters);
|
|
12852
|
+
return isDestructive(tool.parameters) ? "destructive" : null;
|
|
11099
12853
|
}
|
|
11100
12854
|
async #executeTool(call) {
|
|
11101
12855
|
if (skillNameFrom(call) !== null) {
|
|
@@ -11117,7 +12871,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11117
12871
|
this.#showPending();
|
|
11118
12872
|
return { content: `Error: ${message}`, error: message };
|
|
11119
12873
|
}
|
|
11120
|
-
|
|
12874
|
+
const rule = await this.#confirmationRule(call, tool);
|
|
12875
|
+
if (rule !== null) {
|
|
11121
12876
|
const request = { toolName: call.name, args: call.args };
|
|
11122
12877
|
const confirmText = tool.parameters[X_CONFIRM_KEY];
|
|
11123
12878
|
if (typeof confirmText === "string") {
|
|
@@ -11126,10 +12881,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11126
12881
|
this.#confirmAbort = new AbortController();
|
|
11127
12882
|
const decision = requestConfirmation(this.#ensureGroup(), request, {
|
|
11128
12883
|
signal: this.#confirmAbort.signal,
|
|
11129
|
-
strings: this.#strings
|
|
12884
|
+
strings: this.#strings,
|
|
12885
|
+
// Offered only where it can be honoured -- see `#confirmationRule`.
|
|
12886
|
+
...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
|
|
11130
12887
|
});
|
|
11131
12888
|
this.#updateEmptyState();
|
|
11132
|
-
this.#
|
|
12889
|
+
this.#scroller.follow();
|
|
11133
12890
|
const accepted = await decision;
|
|
11134
12891
|
this.#confirmAbort = null;
|
|
11135
12892
|
card.recordDecision(accepted ? "approved" : "declined");
|
|
@@ -11191,6 +12948,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11191
12948
|
*/
|
|
11192
12949
|
async #resolveInterrupts(interrupts) {
|
|
11193
12950
|
this.#confirmAbort = new AbortController();
|
|
12951
|
+
this.#announce(
|
|
12952
|
+
this.#strings.announceAwaitingDecision.replace("{count}", String(interrupts.length))
|
|
12953
|
+
);
|
|
11194
12954
|
this.#hidePending();
|
|
11195
12955
|
const signal = this.#confirmAbort.signal;
|
|
11196
12956
|
const answered = await Promise.all(
|
|
@@ -11205,10 +12965,20 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11205
12965
|
if (toolName !== null && toolName !== void 0) {
|
|
11206
12966
|
request.toolName = toolName;
|
|
11207
12967
|
}
|
|
12968
|
+
let editedArgs;
|
|
12969
|
+
const editable = this.approveWithEdits && card !== void 0;
|
|
12970
|
+
if (editable) {
|
|
12971
|
+
request.args = card.args;
|
|
12972
|
+
}
|
|
11208
12973
|
card?.mark(TOOL_CALL_STATUS.DEFERRED);
|
|
11209
12974
|
const approved = this.approvalRenderer !== null ? await this.approvalRenderer(request, { signal }) : await requestApproval(card?.approvalSlot ?? this.#ensureGroup(), request, {
|
|
11210
12975
|
signal,
|
|
11211
|
-
strings: this.#strings
|
|
12976
|
+
strings: this.#strings,
|
|
12977
|
+
...editable ? {
|
|
12978
|
+
onEdit: (args) => {
|
|
12979
|
+
editedArgs = args;
|
|
12980
|
+
}
|
|
12981
|
+
} : {}
|
|
11212
12982
|
});
|
|
11213
12983
|
card?.recordDecision(approved ? "approved" : "declined");
|
|
11214
12984
|
if (approved) {
|
|
@@ -11216,21 +12986,28 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11216
12986
|
} else {
|
|
11217
12987
|
card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
|
|
11218
12988
|
}
|
|
11219
|
-
return { id: interrupt.id, approved };
|
|
12989
|
+
return { id: interrupt.id, approved, editedArgs };
|
|
11220
12990
|
})
|
|
11221
12991
|
);
|
|
11222
12992
|
this.#updateEmptyState();
|
|
11223
|
-
this.#
|
|
12993
|
+
this.#scroller.follow();
|
|
11224
12994
|
this.#confirmAbort = null;
|
|
11225
12995
|
const responses = {};
|
|
11226
|
-
for (const { id, approved } of answered) {
|
|
11227
|
-
responses[id] = approved ? {
|
|
12996
|
+
for (const { id, approved, editedArgs } of answered) {
|
|
12997
|
+
responses[id] = approved ? {
|
|
12998
|
+
status: "resolved",
|
|
12999
|
+
payload: editedArgs === void 0 ? { approved: true } : { approved: true, editedArgs }
|
|
13000
|
+
} : { status: "cancelled" };
|
|
11228
13001
|
}
|
|
11229
13002
|
return responses;
|
|
11230
13003
|
}
|
|
11231
13004
|
#handlers() {
|
|
11232
13005
|
return {
|
|
11233
13006
|
onRunStart: () => {
|
|
13007
|
+
if (!this.#running) {
|
|
13008
|
+
this.#announcedOutcome = false;
|
|
13009
|
+
this.#announce(this.#strings.announceResponding);
|
|
13010
|
+
}
|
|
11234
13011
|
this.#setRunning(true);
|
|
11235
13012
|
this.#ensureGroup();
|
|
11236
13013
|
this.#showPending();
|
|
@@ -11256,6 +13033,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11256
13033
|
this.#revealWords(bubble);
|
|
11257
13034
|
}
|
|
11258
13035
|
attachCopyButtons(bubble, this.#strings);
|
|
13036
|
+
this.#attachActions(bubble);
|
|
11259
13037
|
this.#endStream();
|
|
11260
13038
|
this.#noteUnread();
|
|
11261
13039
|
},
|
|
@@ -11268,25 +13046,28 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11268
13046
|
this.#cardFor(call);
|
|
11269
13047
|
},
|
|
11270
13048
|
onActivity: (activityType, content, messageId) => {
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
}
|
|
11277
|
-
if (activityType !== COMPACTION_ACTIVITY_TYPE) {
|
|
13049
|
+
this.#drawActivity(messageId, activityType, content);
|
|
13050
|
+
},
|
|
13051
|
+
onCustomEvent: (name, value) => {
|
|
13052
|
+
if (name === INVALIDATE_CUSTOM_NAME) {
|
|
13053
|
+
this.#dispatchInvalidation(value);
|
|
11278
13054
|
return;
|
|
11279
13055
|
}
|
|
11280
|
-
|
|
11281
|
-
|
|
13056
|
+
if (name === SUBAGENT_CUSTOM_NAME) {
|
|
13057
|
+
this.#reportSubAgent(value);
|
|
11282
13058
|
return;
|
|
11283
13059
|
}
|
|
11284
|
-
this
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
13060
|
+
this.dispatchEvent(
|
|
13061
|
+
new CustomEvent(CUSTOM_AGENT_EVENT, {
|
|
13062
|
+
detail: { name, value },
|
|
13063
|
+
bubbles: true,
|
|
13064
|
+
composed: true
|
|
13065
|
+
})
|
|
11288
13066
|
);
|
|
11289
13067
|
},
|
|
13068
|
+
onMessagesSnapshot: () => {
|
|
13069
|
+
this.#appendNotice("\u{1F504}", this.#strings.historyReplaced, "history-replaced");
|
|
13070
|
+
},
|
|
11290
13071
|
onToolResult: (toolCallId, content) => {
|
|
11291
13072
|
const card = this.#toolCards.get(toolCallId);
|
|
11292
13073
|
if (card === void 0) {
|
|
@@ -11297,25 +13078,33 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11297
13078
|
this.#showPending();
|
|
11298
13079
|
},
|
|
11299
13080
|
onActivityChanged: (messageId, activityType, content) => {
|
|
11300
|
-
|
|
11301
|
-
this.#drawActivityChart(messageId, content);
|
|
11302
|
-
}
|
|
13081
|
+
this.#drawActivity(messageId, activityType, content);
|
|
11303
13082
|
},
|
|
11304
13083
|
onRunEnd: () => {
|
|
11305
13084
|
this.#hidePending();
|
|
11306
13085
|
this.#endStream();
|
|
11307
13086
|
},
|
|
11308
13087
|
onError: (message) => {
|
|
13088
|
+
this.#announcedOutcome = true;
|
|
13089
|
+
this.#announce(this.#strings.announceFailed);
|
|
11309
13090
|
this.#hidePending();
|
|
11310
|
-
this
|
|
13091
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`);
|
|
13092
|
+
bubble.classList.add("message--failed");
|
|
13093
|
+
this.#attachActions(bubble, { rateable: false });
|
|
13094
|
+
this.#revealWords(bubble);
|
|
11311
13095
|
this.#endStream();
|
|
11312
13096
|
},
|
|
11313
13097
|
onCancelled: () => {
|
|
13098
|
+
this.#announcedOutcome = true;
|
|
13099
|
+
this.#announce(this.#strings.announceStopped);
|
|
11314
13100
|
this.#hidePending();
|
|
11315
13101
|
this.#appendStoppedNote();
|
|
11316
13102
|
this.#endStream();
|
|
11317
13103
|
},
|
|
11318
13104
|
onSettled: () => {
|
|
13105
|
+
if (!this.#announcedOutcome) {
|
|
13106
|
+
this.#announce(this.#strings.announceAnswerReady);
|
|
13107
|
+
}
|
|
11319
13108
|
this.#hidePending();
|
|
11320
13109
|
this.#setRunning(false);
|
|
11321
13110
|
this.#endStream();
|
|
@@ -11349,14 +13138,86 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11349
13138
|
side: this.#serverSettled.has(id) ? "server" : "client"
|
|
11350
13139
|
}));
|
|
11351
13140
|
this.#runTools = [];
|
|
13141
|
+
const invalidated = [...this.#runInvalidated];
|
|
13142
|
+
this.#runInvalidated = /* @__PURE__ */ new Set();
|
|
11352
13143
|
this.dispatchEvent(
|
|
11353
13144
|
new CustomEvent(RUN_FINISHED_EVENT, {
|
|
11354
|
-
detail: { tools },
|
|
13145
|
+
detail: { tools, invalidated },
|
|
13146
|
+
bubbles: true,
|
|
13147
|
+
composed: true
|
|
13148
|
+
})
|
|
13149
|
+
);
|
|
13150
|
+
}
|
|
13151
|
+
/**
|
|
13152
|
+
* Route one invalidation to the host, and remember it for the run summary.
|
|
13153
|
+
*
|
|
13154
|
+
* Dispatched immediately rather than only at the end, because that is what
|
|
13155
|
+
* makes a long multi-step run feel live -- the list refreshes as the third of
|
|
13156
|
+
* eight writes lands. The accumulated set rides
|
|
13157
|
+
* {@link RUN_FINISHED_EVENT} as well, so a host that would rather refetch once
|
|
13158
|
+
* upgrades by reading one extra field instead of adding a listener.
|
|
13159
|
+
*
|
|
13160
|
+
* Nothing is rendered, persisted or replayed. An invalidation is an
|
|
13161
|
+
* imperative: it has no place in the transcript and no meaning once acted on,
|
|
13162
|
+
* and replaying one on every thread load would be a refetch storm. That is the
|
|
13163
|
+
* whole reason the server sends it as `CUSTOM` rather than as an activity.
|
|
13164
|
+
*/
|
|
13165
|
+
#dispatchInvalidation(value) {
|
|
13166
|
+
const payload = value ?? {};
|
|
13167
|
+
const keys = Array.isArray(payload.keys) ? payload.keys.filter((key) => typeof key === "string") : [];
|
|
13168
|
+
if (keys.length === 0) {
|
|
13169
|
+
return;
|
|
13170
|
+
}
|
|
13171
|
+
for (const key of keys) {
|
|
13172
|
+
this.#runInvalidated.add(key);
|
|
13173
|
+
}
|
|
13174
|
+
this.dispatchEvent(
|
|
13175
|
+
new CustomEvent(INVALIDATE_EVENT, {
|
|
13176
|
+
detail: { keys, reason: typeof payload.reason === "string" ? payload.reason : null },
|
|
11355
13177
|
bubbles: true,
|
|
11356
13178
|
composed: true
|
|
11357
13179
|
})
|
|
11358
13180
|
);
|
|
11359
13181
|
}
|
|
13182
|
+
/**
|
|
13183
|
+
* Draw one step of a delegated sub-agent's progress, on the card that
|
|
13184
|
+
* delegated.
|
|
13185
|
+
*
|
|
13186
|
+
* `delegationId` is the parent's own `delegate_task` tool-call id, so the
|
|
13187
|
+
* attachment point is a card this element already drew on `TOOL_CALL_START`.
|
|
13188
|
+
* That is the whole design: a run that hands work to a sub-agent used to read
|
|
13189
|
+
* as a stall -- the card sat at "running…" for the child's entire duration --
|
|
13190
|
+
* and the fix is to narrate *into* the thing that was already standing there,
|
|
13191
|
+
* rather than to float a second element with the same identity.
|
|
13192
|
+
*
|
|
13193
|
+
* A progress event for a call this client never drew is dropped. It has no
|
|
13194
|
+
* card to attach to, and inventing a floating one is precisely the alternative
|
|
13195
|
+
* that was rejected: parent and child interleave in the transcript with
|
|
13196
|
+
* nothing marking whose is whose, and the persisted transcript -- which never
|
|
13197
|
+
* held the progress at all -- would not match what was on screen.
|
|
13198
|
+
*
|
|
13199
|
+
* Nothing here writes to the conversation store. `CUSTOM` never enters
|
|
13200
|
+
* `agent.messages`, so a reload mid-run leaves the tool card and loses the
|
|
13201
|
+
* nested detail, which is the intended behaviour rather than a gap.
|
|
13202
|
+
*/
|
|
13203
|
+
#reportSubAgent(value) {
|
|
13204
|
+
const update = subAgentUpdate(value);
|
|
13205
|
+
if (update === null) {
|
|
13206
|
+
return;
|
|
13207
|
+
}
|
|
13208
|
+
const card = this.#toolCards.get(update.delegationId);
|
|
13209
|
+
if (card === void 0) {
|
|
13210
|
+
return;
|
|
13211
|
+
}
|
|
13212
|
+
let panel = this.#subagentPanels.get(update.delegationId);
|
|
13213
|
+
if (panel === void 0) {
|
|
13214
|
+
panel = new SubAgentPanel(this.#strings);
|
|
13215
|
+
this.#subagentPanels.set(update.delegationId, panel);
|
|
13216
|
+
card.subagentSlot.appendChild(panel.element);
|
|
13217
|
+
}
|
|
13218
|
+
panel.report(update);
|
|
13219
|
+
this.#scroller.follow();
|
|
13220
|
+
}
|
|
11360
13221
|
/** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
|
|
11361
13222
|
#appendStoppedNote() {
|
|
11362
13223
|
const note = document.createElement("div");
|
|
@@ -11366,7 +13227,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11366
13227
|
note.textContent = this.#strings.stopped;
|
|
11367
13228
|
this.#ensureGroup().appendChild(note);
|
|
11368
13229
|
this.#updateEmptyState();
|
|
11369
|
-
this.#
|
|
13230
|
+
this.#scroller.follow();
|
|
11370
13231
|
}
|
|
11371
13232
|
/**
|
|
11372
13233
|
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
@@ -11390,7 +13251,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11390
13251
|
this.#pending = pending;
|
|
11391
13252
|
this.#ensureGroup().appendChild(pending);
|
|
11392
13253
|
this.#updateEmptyState();
|
|
11393
|
-
this.#
|
|
13254
|
+
this.#scroller.follow();
|
|
11394
13255
|
}
|
|
11395
13256
|
/** Remove the pending indicator if shown. */
|
|
11396
13257
|
#hidePending() {
|
|
@@ -11408,7 +13269,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11408
13269
|
const group = this.#ensureGroup();
|
|
11409
13270
|
group.insertBefore(this.#thoughts.element, group.firstChild);
|
|
11410
13271
|
this.#updateEmptyState();
|
|
11411
|
-
this.#
|
|
13272
|
+
this.#scroller.follow();
|
|
11412
13273
|
}
|
|
11413
13274
|
return this.#thoughts;
|
|
11414
13275
|
}
|
|
@@ -11460,7 +13321,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11460
13321
|
this.#streamBuffer = buffer;
|
|
11461
13322
|
const bubble = this.#openStream();
|
|
11462
13323
|
bubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
11463
|
-
this.#
|
|
13324
|
+
this.#scroller.follow();
|
|
11464
13325
|
return bubble;
|
|
11465
13326
|
}
|
|
11466
13327
|
/**
|
|
@@ -11508,7 +13369,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11508
13369
|
#appendNotice(icon, text3, kind) {
|
|
11509
13370
|
this.#ensureGroup().appendChild(renderRunNotice(icon, text3, kind));
|
|
11510
13371
|
this.#updateEmptyState();
|
|
11511
|
-
this.#
|
|
13372
|
+
this.#scroller.follow();
|
|
11512
13373
|
}
|
|
11513
13374
|
/**
|
|
11514
13375
|
* Turn on chart rendering, by whichever route this consumer wants.
|
|
@@ -11529,9 +13390,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11529
13390
|
* arrives is not something to switch on for everybody.
|
|
11530
13391
|
*/
|
|
11531
13392
|
enableCharts(routes = ["tool", "activity"]) {
|
|
11532
|
-
const first = !this.#
|
|
13393
|
+
const first = !this.#activityRenderers.has(CHART_ACTIVITY_TYPE) && !this.#toolRegistry.has(CHART_TOOL_NAME);
|
|
11533
13394
|
if (routes.includes("activity")) {
|
|
11534
|
-
this
|
|
13395
|
+
this.registerActivityRenderer({
|
|
13396
|
+
type: CHART_ACTIVITY_TYPE,
|
|
13397
|
+
render: (content) => {
|
|
13398
|
+
const spec = chartSpecFrom(content);
|
|
13399
|
+
return spec === null ? null : renderChart(spec);
|
|
13400
|
+
},
|
|
13401
|
+
removedNotice: this.#strings.chartUndrawable
|
|
13402
|
+
});
|
|
11535
13403
|
}
|
|
11536
13404
|
if (routes.includes("tool")) {
|
|
11537
13405
|
this.registerTool(createChartTool());
|
|
@@ -11564,27 +13432,120 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11564
13432
|
this.#cardElements.get(call.id)?.after(node);
|
|
11565
13433
|
this.#afterTranscriptGrew();
|
|
11566
13434
|
}
|
|
11567
|
-
/**
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
13435
|
+
/**
|
|
13436
|
+
* Teach this element to draw one kind of AG-UI activity.
|
|
13437
|
+
*
|
|
13438
|
+
* `activity_type` is one of exactly two fields the protocol leaves an open
|
|
13439
|
+
* string, and it is the **content** one: an activity is materialised into a
|
|
13440
|
+
* message, persisted with the thread, and replayed on every restore. Its
|
|
13441
|
+
* sibling `CUSTOM` carries an imperative and is dispatched to the page
|
|
13442
|
+
* instead ({@link CUSTOM_AGENT_EVENT}).
|
|
13443
|
+
*
|
|
13444
|
+
* That asymmetry decides which carrier a server should use. Content has a
|
|
13445
|
+
* place in the conversation and should come back; an imperative has no place
|
|
13446
|
+
* and no meaning once acted on.
|
|
13447
|
+
*
|
|
13448
|
+
* ```js
|
|
13449
|
+
* chat.registerActivityRenderer({
|
|
13450
|
+
* type: "build_status",
|
|
13451
|
+
* render: (content) => {
|
|
13452
|
+
* const el = document.createElement("div");
|
|
13453
|
+
* el.textContent = `Build ${content.status}`;
|
|
13454
|
+
* return el;
|
|
13455
|
+
* },
|
|
13456
|
+
* });
|
|
13457
|
+
* ```
|
|
13458
|
+
*
|
|
13459
|
+
* Registering a type twice replaces the earlier renderer, so a host can
|
|
13460
|
+
* override a built-in -- `chart` and `compaction` are registrations like any
|
|
13461
|
+
* other, not privileged branches.
|
|
13462
|
+
*
|
|
13463
|
+
* ⚠ `render` runs again on every thread load. See {@link ActivityRenderer}
|
|
13464
|
+
* for what that requires of it.
|
|
13465
|
+
*/
|
|
13466
|
+
registerActivityRenderer(registration) {
|
|
13467
|
+
this.#activityRenderers.set(registration.type, registration);
|
|
13468
|
+
this.#unhandledActivityTypes.delete(registration.type);
|
|
13469
|
+
}
|
|
13470
|
+
/**
|
|
13471
|
+
* Activity types that arrived with nobody registered to draw them.
|
|
13472
|
+
*
|
|
13473
|
+
* Deliberately the only trace an unhandled activity leaves. Ignoring an
|
|
13474
|
+
* unknown name is the protocol's own answer and the whole point of an open
|
|
13475
|
+
* field, so warning would fire on every forward-compatible server -- but
|
|
13476
|
+
* "nothing happened and nothing was said" is impossible to debug, so the set
|
|
13477
|
+
* is readable. Accumulates for the element's lifetime, across threads.
|
|
13478
|
+
*/
|
|
13479
|
+
get unhandledActivityTypes() {
|
|
13480
|
+
return [...this.#unhandledActivityTypes];
|
|
13481
|
+
}
|
|
13482
|
+
/**
|
|
13483
|
+
* Draw, replace or remove one activity, whatever kind it is.
|
|
13484
|
+
*
|
|
13485
|
+
* The single path for all three routes an activity arrives by -- pushed
|
|
13486
|
+
* (`onActivity`), patched (`onActivityChanged`) and replayed from history --
|
|
13487
|
+
* which is why the renderer contract has to be pure: the same content is
|
|
13488
|
+
* drawn again on every thread load.
|
|
13489
|
+
*
|
|
13490
|
+
* An unregistered type draws nothing and says nothing. That is the protocol's
|
|
13491
|
+
* own answer -- a client that does not know a name ignores the event -- and a
|
|
13492
|
+
* warning here would fire on every well-behaved forward-compatible server,
|
|
13493
|
+
* while a placeholder would put the protocol's growth in the user's face.
|
|
13494
|
+
* {@link unhandledActivityTypes} is the way to find out what arrived.
|
|
13495
|
+
*/
|
|
13496
|
+
#drawActivity(messageId, activityType, content) {
|
|
13497
|
+
const registration = this.#activityRenderers.get(activityType);
|
|
13498
|
+
if (registration === void 0) {
|
|
13499
|
+
this.#unhandledActivityTypes.add(activityType);
|
|
13500
|
+
return;
|
|
13501
|
+
}
|
|
13502
|
+
let node;
|
|
13503
|
+
try {
|
|
13504
|
+
node = registration.render(content);
|
|
13505
|
+
} catch (error) {
|
|
13506
|
+
console.warn(`ag-ui-chat: render failed for activity ${activityType}`, error);
|
|
13507
|
+
node = null;
|
|
13508
|
+
}
|
|
13509
|
+
if (node === null) {
|
|
13510
|
+
this.#removeActivity(messageId, activityType, registration.removedNotice, content);
|
|
11574
13511
|
return;
|
|
11575
13512
|
}
|
|
11576
13513
|
const existing = this.#activityBlocks.get(messageId);
|
|
11577
13514
|
if (existing === void 0) {
|
|
11578
|
-
this.#ensureGroup().appendChild(
|
|
13515
|
+
this.#ensureGroup().appendChild(node);
|
|
11579
13516
|
} else {
|
|
11580
|
-
existing.replaceWith(
|
|
13517
|
+
existing.replaceWith(node);
|
|
11581
13518
|
}
|
|
11582
|
-
this.#activityBlocks.set(messageId,
|
|
13519
|
+
this.#activityBlocks.set(messageId, node);
|
|
11583
13520
|
this.#afterTranscriptGrew();
|
|
11584
13521
|
}
|
|
13522
|
+
/**
|
|
13523
|
+
* Take away an activity whose content stopped being drawable.
|
|
13524
|
+
*
|
|
13525
|
+
* Leaving the old one up is the worst available answer: it shows values that
|
|
13526
|
+
* have been retracted, reading as current, and a reload drops it anyway
|
|
13527
|
+
* because the *stored* content is the version that could not be drawn. Live
|
|
13528
|
+
* and reload should agree, and both should say "gone".
|
|
13529
|
+
*
|
|
13530
|
+
* Removing is right; doing it in silence was not. A chart that had been drawn
|
|
13531
|
+
* simply disappeared, with no `console` call anywhere on the path -- which
|
|
13532
|
+
* nobody reports as a bug, they report as "the charts are flaky".
|
|
13533
|
+
*/
|
|
13534
|
+
#removeActivity(messageId, activityType, notice, content) {
|
|
13535
|
+
const had = this.#activityBlocks.has(messageId);
|
|
13536
|
+
this.#activityBlocks.get(messageId)?.remove();
|
|
13537
|
+
this.#activityBlocks.delete(messageId);
|
|
13538
|
+
console.warn(
|
|
13539
|
+
`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.`,
|
|
13540
|
+
content
|
|
13541
|
+
);
|
|
13542
|
+
if (had && notice !== void 0) {
|
|
13543
|
+
this.#appendNotice("\u{1F4C9}", notice, "chart-undrawable");
|
|
13544
|
+
}
|
|
13545
|
+
}
|
|
11585
13546
|
#afterTranscriptGrew() {
|
|
11586
13547
|
this.#updateEmptyState();
|
|
11587
|
-
this.#
|
|
13548
|
+
this.#scroller.follow();
|
|
11588
13549
|
}
|
|
11589
13550
|
#cardFor(call) {
|
|
11590
13551
|
const existing = this.#toolCards.get(call.id);
|
|
@@ -11593,11 +13554,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11593
13554
|
}
|
|
11594
13555
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
11595
13556
|
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name]?.summary ?? prettifyToolName(call.name);
|
|
11596
|
-
const card = new ToolCallCard(call.name, call.args, summary, this.#strings
|
|
13557
|
+
const card = new ToolCallCard(call.name, call.args, summary, this.#strings, {
|
|
13558
|
+
// A thunk over the live property, not the property itself: the card keeps
|
|
13559
|
+
// this for the life of the call, and the result region is filled when the
|
|
13560
|
+
// tool settles -- which can be long after a host set the hook.
|
|
13561
|
+
formatPayload: (payload) => this.formatToolPayload?.(payload) ?? null
|
|
13562
|
+
});
|
|
11597
13563
|
this.#toolCards.set(call.id, card);
|
|
11598
13564
|
this.#ensureGroup().appendChild(card.element);
|
|
11599
13565
|
this.#updateEmptyState();
|
|
11600
|
-
this.#
|
|
13566
|
+
this.#scroller.follow();
|
|
11601
13567
|
return card;
|
|
11602
13568
|
}
|
|
11603
13569
|
};
|
|
@@ -11667,7 +13633,7 @@ function setControlValue(el2, value) {
|
|
|
11667
13633
|
}
|
|
11668
13634
|
|
|
11669
13635
|
// src/version.ts
|
|
11670
|
-
var VERSION = "0.
|
|
13636
|
+
var VERSION = "0.30.0";
|
|
11671
13637
|
export {
|
|
11672
13638
|
ATTACHMENT_EVENT,
|
|
11673
13639
|
AgUiChat,
|
|
@@ -11675,13 +13641,21 @@ export {
|
|
|
11675
13641
|
CHART_ACTIVITY_TYPE,
|
|
11676
13642
|
CHART_TOOL_NAME,
|
|
11677
13643
|
COMPACTION_ACTIVITY_TYPE,
|
|
13644
|
+
CUSTOM_AGENT_EVENT,
|
|
11678
13645
|
CheckpointMenu,
|
|
11679
13646
|
ClientToolRegistry,
|
|
11680
13647
|
ConnectionLostError,
|
|
11681
13648
|
DEFAULT_UI_STRINGS,
|
|
11682
13649
|
ELEMENT_TAG,
|
|
13650
|
+
FEEDBACK_EVENT,
|
|
13651
|
+
INVALIDATE_CUSTOM_NAME,
|
|
13652
|
+
INVALIDATE_EVENT,
|
|
11683
13653
|
LOAD_CAPABILITY_TOOL,
|
|
13654
|
+
MAX_QUOTE_CHARS,
|
|
13655
|
+
MAX_SUGGESTIONS,
|
|
13656
|
+
MAX_SUGGESTION_CHARS,
|
|
11684
13657
|
MAX_TOOL_ROUNDS,
|
|
13658
|
+
MESSAGE_ACTIONS,
|
|
11685
13659
|
MESSAGE_ROLE,
|
|
11686
13660
|
PAGE_ACTIONS,
|
|
11687
13661
|
RUN_FINISHED_EVENT,
|
|
@@ -11689,6 +13663,7 @@ export {
|
|
|
11689
13663
|
RunIndex,
|
|
11690
13664
|
STATE_EVENT,
|
|
11691
13665
|
SUBMIT_EVENT,
|
|
13666
|
+
SUGGESTIONS_ACTIVITY_TYPE,
|
|
11692
13667
|
SessionStorageStore,
|
|
11693
13668
|
TOGGLE_EVENT,
|
|
11694
13669
|
TOOL_CALL_STATUS,
|
|
@@ -11700,6 +13675,9 @@ export {
|
|
|
11700
13675
|
X_DESTRUCTIVE_KEY,
|
|
11701
13676
|
X_NAVIGATES_KEY,
|
|
11702
13677
|
X_SUMMARY_KEY,
|
|
13678
|
+
asQuote,
|
|
13679
|
+
attachMessageActions,
|
|
13680
|
+
attachQuoteOffer,
|
|
11703
13681
|
chartSpecFrom,
|
|
11704
13682
|
clickElement,
|
|
11705
13683
|
createHttpAgent,
|
|
@@ -11716,14 +13694,18 @@ export {
|
|
|
11716
13694
|
isDestructive,
|
|
11717
13695
|
isNavigates,
|
|
11718
13696
|
mergeUiStrings,
|
|
13697
|
+
messageActionBar,
|
|
11719
13698
|
messageAttachments,
|
|
11720
13699
|
parseToolCatalog,
|
|
11721
13700
|
prefersReducedMotion,
|
|
11722
13701
|
pressButton,
|
|
11723
13702
|
pressThenClick,
|
|
11724
13703
|
prettifyToolName,
|
|
13704
|
+
quotableSelection,
|
|
13705
|
+
relativeTime,
|
|
11725
13706
|
renderChart,
|
|
11726
13707
|
renderMarkdown,
|
|
13708
|
+
renderSuggestionChips,
|
|
11727
13709
|
requestApproval,
|
|
11728
13710
|
requestConfirmation,
|
|
11729
13711
|
requestQuestion,
|
|
@@ -11733,6 +13715,7 @@ export {
|
|
|
11733
13715
|
setControlValue,
|
|
11734
13716
|
setNativeChecked,
|
|
11735
13717
|
setNativeValue,
|
|
13718
|
+
suggestionPrompts,
|
|
11736
13719
|
toggleCheckbox,
|
|
11737
13720
|
toggleControl,
|
|
11738
13721
|
transcribeAudio,
|