@artooi/ag-ui-web-component 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +663 -1
- package/README.md +557 -11
- package/dist/ag-ui-web-component.bundle.js +294 -36
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +69 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +262 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +46 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +43 -1
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +23 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +28 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2104 -194
- package/dist/index.js.map +4 -4
- package/dist/tools/is_destructive.d.ts +8 -2
- package/dist/tools/is_destructive.d.ts.map +1 -1
- package/dist/tools/parse_tool_catalog.d.ts +11 -4
- package/dist/tools/parse_tool_catalog.d.ts.map +1 -1
- package/dist/ui/approval_card.d.ts +18 -0
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts +10 -0
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +16 -0
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/message_actions.d.ts +46 -0
- package/dist/ui/message_actions.d.ts.map +1 -0
- package/dist/ui/page_quote_offer.d.ts +33 -0
- package/dist/ui/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/quote_selection.d.ts +66 -0
- package/dist/ui/quote_selection.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +10 -0
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +23 -5
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +5 -1
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/stick_to_bottom.d.ts +55 -0
- package/dist/ui/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/suggestion_chips.d.ts +29 -0
- package/dist/ui/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/thread_drawer.d.ts +10 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +8 -0
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +53 -7
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +75 -0
- package/src/core/ag_ui_chat.ts +1357 -113
- package/src/core/agui_client.ts +81 -1
- package/src/core/conversation_store.ts +128 -42
- package/src/core/create_http_agent.ts +24 -2
- package/src/core/remote_conversation_store.ts +35 -2
- package/src/core/utils.ts +58 -0
- package/src/index.ts +39 -0
- package/src/tools/is_destructive.ts +8 -2
- package/src/tools/parse_tool_catalog.ts +18 -6
- package/src/ui/approval_card.ts +90 -2
- package/src/ui/checkpoint_menu.ts +22 -5
- package/src/ui/confirmation_card.ts +29 -1
- package/src/ui/message_actions.ts +158 -0
- package/src/ui/page_quote_offer.ts +215 -0
- package/src/ui/quote_selection.ts +345 -0
- package/src/ui/relative_time.ts +11 -0
- package/src/ui/render_markdown.ts +111 -21
- package/src/ui/resize_handle.ts +32 -2
- package/src/ui/stick_to_bottom.ts +126 -0
- package/src/ui/styles.ts +227 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +9 -0
- package/src/ui/ui_strings.ts +79 -8
- package/src/ui/voice_input.ts +43 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,11 @@ var UNREAD_EVENT = "ag-ui-unread";
|
|
|
6
6
|
var STATE_EVENT = "ag-ui-state";
|
|
7
7
|
var ATTACHMENT_EVENT = "ag-ui-attachments";
|
|
8
8
|
var RUN_FINISHED_EVENT = "ag-ui-run-finished";
|
|
9
|
+
var CUSTOM_AGENT_EVENT = "ag-ui-custom";
|
|
10
|
+
var INVALIDATE_EVENT = "ag-ui-invalidate";
|
|
11
|
+
var FEEDBACK_EVENT = "ag-ui-feedback";
|
|
12
|
+
var SUGGESTIONS_ACTIVITY_TYPE = "suggestions";
|
|
13
|
+
var INVALIDATE_CUSTOM_NAME = "ag_ui.invalidate";
|
|
9
14
|
var MESSAGE_ROLE = {
|
|
10
15
|
USER: "user",
|
|
11
16
|
ASSISTANT: "assistant"
|
|
@@ -47,6 +52,10 @@ var ICON_FILE_IMAGE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"
|
|
|
47
52
|
var ICON_FILE_PDF = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><rect class="glyph--solid" x="7.5" y="13.5" width="9" height="4.5" rx="1"/></svg>`;
|
|
48
53
|
var ICON_FILE_TEXT = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M8.5 13.5h7M8.5 17h4.5"/></svg>`;
|
|
49
54
|
var CHART_ACTIVITY_TYPE = "chart";
|
|
55
|
+
var ANNOUNCE_CLEAR_MS = 150;
|
|
56
|
+
|
|
57
|
+
// src/core/ag_ui_chat.ts
|
|
58
|
+
import { randomUUID as randomUUID5 } from "@ag-ui/client";
|
|
50
59
|
|
|
51
60
|
// src/skills/fill_template.ts
|
|
52
61
|
var PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
@@ -441,8 +450,9 @@ function parseToolCatalog(data) {
|
|
|
441
450
|
const record = entry;
|
|
442
451
|
const name = record["name"];
|
|
443
452
|
const summary = record["summary"];
|
|
453
|
+
const description = record["description"];
|
|
444
454
|
if (typeof name === "string" && typeof summary === "string") {
|
|
445
|
-
out[name] = summary;
|
|
455
|
+
out[name] = typeof description === "string" ? { name, summary, description } : { name, summary };
|
|
446
456
|
}
|
|
447
457
|
}
|
|
448
458
|
return out;
|
|
@@ -533,6 +543,12 @@ var DEFAULT_UI_STRINGS = {
|
|
|
533
543
|
forkRun: "Fork",
|
|
534
544
|
forkedRun: "branched",
|
|
535
545
|
conversation: "Conversation",
|
|
546
|
+
jumpToLatest: "Jump to latest",
|
|
547
|
+
announceResponding: "Assistant is responding",
|
|
548
|
+
announceAnswerReady: "Assistant answered",
|
|
549
|
+
announceAwaitingDecision: "{count} action is waiting for your approval",
|
|
550
|
+
announceStopped: "Response stopped",
|
|
551
|
+
announceFailed: "The response failed",
|
|
536
552
|
thinking: "Assistant is thinking\u2026",
|
|
537
553
|
thoughts: "Thoughts",
|
|
538
554
|
stopped: "\u23F9 Stopped",
|
|
@@ -540,6 +556,8 @@ var DEFAULT_UI_STRINGS = {
|
|
|
540
556
|
noResult: "No result returned.",
|
|
541
557
|
declinedAction: "User declined the action.",
|
|
542
558
|
navigating: "Navigating\u2026",
|
|
559
|
+
historyReplaced: "The server replaced this conversation's history. Reload to see the updated transcript.",
|
|
560
|
+
chartUndrawable: "A chart could not be drawn from the data sent, so it was removed.",
|
|
543
561
|
historyCompacted: "Earlier turns condensed to fit the context window ({count} removed)",
|
|
544
562
|
usingSkill: "Using skill {name}",
|
|
545
563
|
runInterrupted: "The previous response didn\u2019t finish \u2014 the page changed before it arrived.",
|
|
@@ -555,6 +573,7 @@ var DEFAULT_UI_STRINGS = {
|
|
|
555
573
|
stopRecording: "Stop recording",
|
|
556
574
|
transcribing: "Transcribing\u2026",
|
|
557
575
|
transcriptionFailed: "Transcription failed",
|
|
576
|
+
recordingLimit: "Stopped at the {n}-minute limit \u2014 transcribing what was recorded.",
|
|
558
577
|
toolRunning: "running\u2026",
|
|
559
578
|
toolDeferred: "waiting for you",
|
|
560
579
|
toolDone: "\u2713 done",
|
|
@@ -568,7 +587,18 @@ var DEFAULT_UI_STRINGS = {
|
|
|
568
587
|
errorLabel: "Error",
|
|
569
588
|
declinedLabel: "Declined",
|
|
570
589
|
details: "Details",
|
|
590
|
+
approvalEditArgs: "Edit the arguments before approving",
|
|
591
|
+
approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
|
|
592
|
+
approvalArgsNotAnObject: "Arguments have to be a JSON object.",
|
|
593
|
+
suggestions: "Suggested follow-ups",
|
|
594
|
+
messageActions: "Message actions",
|
|
595
|
+
quoteSelection: "Quote",
|
|
596
|
+
copyMessage: "Copy message",
|
|
597
|
+
retryMessage: "Try again",
|
|
598
|
+
feedbackUp: "Good answer",
|
|
599
|
+
feedbackDown: "Poor answer",
|
|
571
600
|
confirmAction: "Confirm action",
|
|
601
|
+
confirmAlways: "Always allow",
|
|
572
602
|
confirmRun: "Run \u201C{tool}\u201D?",
|
|
573
603
|
confirm: "Confirm",
|
|
574
604
|
cancel: "Cancel",
|
|
@@ -635,6 +665,7 @@ function requestApproval(host, request, options = {}) {
|
|
|
635
665
|
body.className = "approval-body";
|
|
636
666
|
body.setAttribute("part", "approval-body");
|
|
637
667
|
body.textContent = request.message ?? strings.approvalPrompt;
|
|
668
|
+
const editor = buildEditor(request, options, strings);
|
|
638
669
|
const actions = document.createElement("div");
|
|
639
670
|
actions.className = "approval-actions";
|
|
640
671
|
actions.setAttribute("part", "approval-actions");
|
|
@@ -652,10 +683,15 @@ function requestApproval(host, request, options = {}) {
|
|
|
652
683
|
resolve(approved);
|
|
653
684
|
};
|
|
654
685
|
deny.addEventListener("click", () => close(false));
|
|
655
|
-
approve.addEventListener("click", () =>
|
|
686
|
+
approve.addEventListener("click", () => {
|
|
687
|
+
if (editor !== null && !editor.commit()) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
close(true);
|
|
691
|
+
});
|
|
656
692
|
options.signal?.addEventListener("abort", () => close(false), { once: true });
|
|
657
693
|
actions.append(deny, approve);
|
|
658
|
-
card.append(body, actions);
|
|
694
|
+
card.append(body, ...editor === null ? [] : [editor.root], actions);
|
|
659
695
|
host.appendChild(card);
|
|
660
696
|
if (options.signal?.aborted === true) {
|
|
661
697
|
close(false);
|
|
@@ -664,6 +700,54 @@ function requestApproval(host, request, options = {}) {
|
|
|
664
700
|
approve.focus();
|
|
665
701
|
});
|
|
666
702
|
}
|
|
703
|
+
function buildEditor(request, options, strings) {
|
|
704
|
+
const { onEdit } = options;
|
|
705
|
+
if (onEdit === void 0 || request.args === void 0) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const original = JSON.stringify(request.args, null, 2);
|
|
709
|
+
const root = document.createElement("div");
|
|
710
|
+
root.className = "approval-edit";
|
|
711
|
+
root.setAttribute("part", "approval-edit");
|
|
712
|
+
const field = document.createElement("textarea");
|
|
713
|
+
field.className = "approval-args";
|
|
714
|
+
field.setAttribute("part", "approval-args");
|
|
715
|
+
field.setAttribute("aria-label", strings.approvalEditArgs);
|
|
716
|
+
field.rows = Math.min(10, original.split("\n").length);
|
|
717
|
+
field.value = original;
|
|
718
|
+
const error = document.createElement("div");
|
|
719
|
+
error.className = "approval-error";
|
|
720
|
+
error.setAttribute("part", "approval-error");
|
|
721
|
+
error.setAttribute("role", "alert");
|
|
722
|
+
error.hidden = true;
|
|
723
|
+
root.append(field, error);
|
|
724
|
+
return {
|
|
725
|
+
root,
|
|
726
|
+
commit: () => {
|
|
727
|
+
if (field.value === original) {
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
let parsed;
|
|
731
|
+
try {
|
|
732
|
+
parsed = JSON.parse(field.value);
|
|
733
|
+
} catch {
|
|
734
|
+
error.textContent = strings.approvalArgsInvalid;
|
|
735
|
+
error.hidden = false;
|
|
736
|
+
field.focus();
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
740
|
+
error.textContent = strings.approvalArgsNotAnObject;
|
|
741
|
+
error.hidden = false;
|
|
742
|
+
field.focus();
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
error.hidden = true;
|
|
746
|
+
onEdit(parsed);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
667
751
|
|
|
668
752
|
// src/ui/attach_copy_buttons.ts
|
|
669
753
|
var CONFIRM_MS = 1500;
|
|
@@ -1394,6 +1478,7 @@ var CheckpointMenu = class {
|
|
|
1394
1478
|
#heading;
|
|
1395
1479
|
/** What had focus before the panel opened, restored on close. */
|
|
1396
1480
|
#lastFocused = null;
|
|
1481
|
+
#formatRelativeTime = null;
|
|
1397
1482
|
#strings;
|
|
1398
1483
|
#runs = [];
|
|
1399
1484
|
constructor(onPick, strings = DEFAULT_UI_STRINGS) {
|
|
@@ -1426,6 +1511,21 @@ var CheckpointMenu = class {
|
|
|
1426
1511
|
this.#render();
|
|
1427
1512
|
}
|
|
1428
1513
|
/** Re-localize a panel built before the host's strings resolved. */
|
|
1514
|
+
/**
|
|
1515
|
+
* Replace the timestamp formatter, or restore the built-in with `null`.
|
|
1516
|
+
*
|
|
1517
|
+
* The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
|
|
1518
|
+
* in this component, so it never disagrees with a host's own formatting by
|
|
1519
|
+
* guessing a locale. That is a defensible default and a poor requirement, so
|
|
1520
|
+
* this is the way out.
|
|
1521
|
+
*/
|
|
1522
|
+
setRelativeTimeFormatter(format) {
|
|
1523
|
+
this.#formatRelativeTime = format;
|
|
1524
|
+
}
|
|
1525
|
+
/** This row's timestamp, through the host's formatter when it set one. */
|
|
1526
|
+
#formatTime(timestamp) {
|
|
1527
|
+
return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, Date.now(), this.#strings);
|
|
1528
|
+
}
|
|
1429
1529
|
setStrings(strings) {
|
|
1430
1530
|
this.#strings = strings;
|
|
1431
1531
|
this.element.setAttribute("aria-label", strings.checkpoints);
|
|
@@ -1527,7 +1627,7 @@ var CheckpointMenu = class {
|
|
|
1527
1627
|
row.className = "checkpoint-row";
|
|
1528
1628
|
row.setAttribute("part", "checkpoint-row");
|
|
1529
1629
|
const preview = previewOf(run);
|
|
1530
|
-
const time = run.started_at === null ? null :
|
|
1630
|
+
const time = run.started_at === null ? null : this.#formatTime(Date.parse(run.started_at));
|
|
1531
1631
|
const label = document.createElement("span");
|
|
1532
1632
|
label.className = "checkpoint-label";
|
|
1533
1633
|
label.setAttribute("part", "checkpoint-label");
|
|
@@ -1608,6 +1708,7 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1608
1708
|
actions.className = "confirm-actions";
|
|
1609
1709
|
actions.setAttribute("part", "confirm-actions");
|
|
1610
1710
|
const cancel = actionButton2("cancel", strings.cancel);
|
|
1711
|
+
const always = options.onAlwaysAllow === void 0 ? null : actionButton2("always", strings.confirmAlways.replace("{tool}", request.toolName));
|
|
1611
1712
|
const confirm2 = actionButton2("confirm", strings.confirm);
|
|
1612
1713
|
let settled = false;
|
|
1613
1714
|
const close = (accepted) => {
|
|
@@ -1620,8 +1721,12 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1620
1721
|
};
|
|
1621
1722
|
cancel.addEventListener("click", () => close(false));
|
|
1622
1723
|
confirm2.addEventListener("click", () => close(true));
|
|
1724
|
+
always?.addEventListener("click", () => {
|
|
1725
|
+
options.onAlwaysAllow?.();
|
|
1726
|
+
close(true);
|
|
1727
|
+
});
|
|
1623
1728
|
options.signal?.addEventListener("abort", () => close(false), { once: true });
|
|
1624
|
-
actions.append(cancel, confirm2);
|
|
1729
|
+
actions.append(cancel, ...always === null ? [] : [always], confirm2);
|
|
1625
1730
|
card.append(body, args, actions);
|
|
1626
1731
|
host.appendChild(card);
|
|
1627
1732
|
if (options.signal?.aborted === true) {
|
|
@@ -1632,6 +1737,347 @@ function requestConfirmation(host, request, options = {}) {
|
|
|
1632
1737
|
});
|
|
1633
1738
|
}
|
|
1634
1739
|
|
|
1740
|
+
// src/ui/message_actions.ts
|
|
1741
|
+
var CONFIRM_MS2 = 1500;
|
|
1742
|
+
function attachMessageActions(bubble, options) {
|
|
1743
|
+
if (existingBar(bubble) !== null) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const bar = messageActionBar(bubble, options.strings);
|
|
1747
|
+
bar.appendChild(copyButton(options));
|
|
1748
|
+
if (options.onFeedback !== void 0) {
|
|
1749
|
+
bar.append(
|
|
1750
|
+
feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
|
|
1751
|
+
feedbackButton("down", options.strings.feedbackDown, options.onFeedback)
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function messageActionBar(bubble, strings) {
|
|
1756
|
+
const existing = existingBar(bubble);
|
|
1757
|
+
if (existing !== null) {
|
|
1758
|
+
return existing;
|
|
1759
|
+
}
|
|
1760
|
+
const bar = document.createElement("div");
|
|
1761
|
+
bar.className = "message-actions";
|
|
1762
|
+
bar.setAttribute("part", "message-actions");
|
|
1763
|
+
bar.setAttribute("role", "group");
|
|
1764
|
+
bar.setAttribute("aria-label", strings.messageActions);
|
|
1765
|
+
bubble.after(bar);
|
|
1766
|
+
return bar;
|
|
1767
|
+
}
|
|
1768
|
+
function existingBar(bubble) {
|
|
1769
|
+
const next = bubble.nextElementSibling;
|
|
1770
|
+
return next?.classList.contains("message-actions") === true ? next : null;
|
|
1771
|
+
}
|
|
1772
|
+
function messageActionButton(modifier, label, glyph) {
|
|
1773
|
+
const button2 = document.createElement("button");
|
|
1774
|
+
button2.type = "button";
|
|
1775
|
+
button2.className = `message-action message-action--${modifier}`;
|
|
1776
|
+
button2.setAttribute("part", `message-action message-action-${modifier}`);
|
|
1777
|
+
button2.title = label;
|
|
1778
|
+
button2.setAttribute("aria-label", label);
|
|
1779
|
+
const icon = document.createElement("span");
|
|
1780
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1781
|
+
icon.textContent = glyph;
|
|
1782
|
+
button2.appendChild(icon);
|
|
1783
|
+
return button2;
|
|
1784
|
+
}
|
|
1785
|
+
function copyButton(options) {
|
|
1786
|
+
const { strings } = options;
|
|
1787
|
+
const button2 = messageActionButton("copy", strings.copyMessage, "\u2398");
|
|
1788
|
+
button2.addEventListener("click", () => {
|
|
1789
|
+
void navigator.clipboard.writeText(options.text()).then(
|
|
1790
|
+
() => flash2(button2, strings.copied, strings.copyMessage),
|
|
1791
|
+
// A denied clipboard permission is the common case, not an exception:
|
|
1792
|
+
// say so on the button rather than throwing into an unhandled rejection.
|
|
1793
|
+
() => flash2(button2, strings.copyFailed, strings.copyMessage)
|
|
1794
|
+
);
|
|
1795
|
+
});
|
|
1796
|
+
return button2;
|
|
1797
|
+
}
|
|
1798
|
+
function feedbackButton(rating, label, report) {
|
|
1799
|
+
const button2 = messageActionButton(
|
|
1800
|
+
rating === "up" ? "up" : "down",
|
|
1801
|
+
label,
|
|
1802
|
+
rating === "up" ? "\u{1F44D}" : "\u{1F44E}"
|
|
1803
|
+
);
|
|
1804
|
+
button2.addEventListener("click", () => {
|
|
1805
|
+
const pressed = button2.getAttribute("aria-pressed") === "true";
|
|
1806
|
+
button2.setAttribute("aria-pressed", pressed ? "false" : "true");
|
|
1807
|
+
report(rating);
|
|
1808
|
+
});
|
|
1809
|
+
button2.setAttribute("aria-pressed", "false");
|
|
1810
|
+
return button2;
|
|
1811
|
+
}
|
|
1812
|
+
function flash2(button2, message, label) {
|
|
1813
|
+
button2.title = message;
|
|
1814
|
+
button2.setAttribute("aria-label", message);
|
|
1815
|
+
button2.classList.add("message-action--confirmed");
|
|
1816
|
+
setTimeout(() => {
|
|
1817
|
+
button2.title = label;
|
|
1818
|
+
button2.setAttribute("aria-label", label);
|
|
1819
|
+
button2.classList.remove("message-action--confirmed");
|
|
1820
|
+
}, CONFIRM_MS2);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/ui/quote_selection.ts
|
|
1824
|
+
var MAX_QUOTE_CHARS = 500;
|
|
1825
|
+
function quotableSelection(container, roots = [], near) {
|
|
1826
|
+
for (const endpoints of endpointCandidates(roots)) {
|
|
1827
|
+
if (!container.contains(endpoints.startContainer)) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (!container.contains(endpoints.endContainer)) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const range = document.createRange();
|
|
1834
|
+
range.setStart(endpoints.startContainer, endpoints.startOffset);
|
|
1835
|
+
range.setEnd(endpoints.endContainer, endpoints.endOffset);
|
|
1836
|
+
const text3 = renderedText(range).trim();
|
|
1837
|
+
if (text3 === "") {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
return { text: text3, rect: lineToHangFrom(range, near) };
|
|
1841
|
+
}
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
function asQuote(text3) {
|
|
1845
|
+
const lines = tidy(text3);
|
|
1846
|
+
if (lines.length === 0) {
|
|
1847
|
+
return "";
|
|
1848
|
+
}
|
|
1849
|
+
const capped = cap(lines.join("\n"));
|
|
1850
|
+
const quoted = capped.split("\n").map((line) => `> ${line}`.trimEnd()).join("\n");
|
|
1851
|
+
return `${quoted}
|
|
1852
|
+
|
|
1853
|
+
`;
|
|
1854
|
+
}
|
|
1855
|
+
function tidy(text3) {
|
|
1856
|
+
const lines = text3.split(/\r\n?|\n/).map((line) => line.trimEnd());
|
|
1857
|
+
const indents = lines.filter((line) => line !== "").map(indentOf);
|
|
1858
|
+
const shared = indents.length === 0 ? 0 : Math.min(...indents);
|
|
1859
|
+
const kept = [];
|
|
1860
|
+
for (const line of lines) {
|
|
1861
|
+
const dedented = line.slice(shared);
|
|
1862
|
+
if (dedented === "" && (kept.length === 0 || kept[kept.length - 1] === "")) {
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
kept.push(dedented);
|
|
1866
|
+
}
|
|
1867
|
+
while (kept[kept.length - 1] === "") {
|
|
1868
|
+
kept.pop();
|
|
1869
|
+
}
|
|
1870
|
+
return kept;
|
|
1871
|
+
}
|
|
1872
|
+
function indentOf(line) {
|
|
1873
|
+
return line.length - line.trimStart().length;
|
|
1874
|
+
}
|
|
1875
|
+
function cap(text3) {
|
|
1876
|
+
return text3.length > MAX_QUOTE_CHARS ? `${text3.slice(0, MAX_QUOTE_CHARS).trimEnd()}...` : text3;
|
|
1877
|
+
}
|
|
1878
|
+
function renderedText(range) {
|
|
1879
|
+
let text3 = "";
|
|
1880
|
+
for (const node of textNodesIn(range)) {
|
|
1881
|
+
const parent = node.parentElement;
|
|
1882
|
+
if (!isRendered(parent)) {
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
const from = node === range.startContainer ? range.startOffset : 0;
|
|
1886
|
+
const to = node === range.endContainer ? range.endOffset : node.data.length;
|
|
1887
|
+
text3 += collapse(node.data.slice(from, to), parent);
|
|
1888
|
+
}
|
|
1889
|
+
return text3;
|
|
1890
|
+
}
|
|
1891
|
+
function collapse(text3, parent) {
|
|
1892
|
+
if (PREFORMATTED.has(whiteSpaceOf(parent))) {
|
|
1893
|
+
return text3;
|
|
1894
|
+
}
|
|
1895
|
+
return text3.replace(/[^\S\n]*\n[^\S\n]*/g, "\n").replace(/[^\S\n]+/g, " ");
|
|
1896
|
+
}
|
|
1897
|
+
var PREFORMATTED = /* @__PURE__ */ new Set(["pre", "pre-wrap", "break-spaces"]);
|
|
1898
|
+
function whiteSpaceOf(element) {
|
|
1899
|
+
return window.getComputedStyle(element).whiteSpace;
|
|
1900
|
+
}
|
|
1901
|
+
function textNodesIn(range) {
|
|
1902
|
+
const root = range.commonAncestorContainer;
|
|
1903
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
1904
|
+
return [root];
|
|
1905
|
+
}
|
|
1906
|
+
const found = [];
|
|
1907
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1908
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
1909
|
+
if (range.intersectsNode(node)) {
|
|
1910
|
+
found.push(node);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return found;
|
|
1914
|
+
}
|
|
1915
|
+
function isRendered(element) {
|
|
1916
|
+
if (typeof element.checkVisibility !== "function") {
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
return element.checkVisibility({
|
|
1920
|
+
contentVisibilityAuto: true,
|
|
1921
|
+
opacityProperty: true,
|
|
1922
|
+
visibilityProperty: true
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function lineToHangFrom(range, near) {
|
|
1926
|
+
const lines = [...range.getClientRects()];
|
|
1927
|
+
if (lines.length === 0) {
|
|
1928
|
+
return range.getBoundingClientRect();
|
|
1929
|
+
}
|
|
1930
|
+
if (near === void 0) {
|
|
1931
|
+
return lines[0];
|
|
1932
|
+
}
|
|
1933
|
+
let closest = lines[0];
|
|
1934
|
+
let shortest = distanceTo(closest, near);
|
|
1935
|
+
for (const line of lines.slice(1)) {
|
|
1936
|
+
const distance = distanceTo(line, near);
|
|
1937
|
+
if (distance < shortest) {
|
|
1938
|
+
shortest = distance;
|
|
1939
|
+
closest = line;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
return closest;
|
|
1943
|
+
}
|
|
1944
|
+
function distanceTo(rect, point) {
|
|
1945
|
+
const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
|
|
1946
|
+
const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
|
|
1947
|
+
return Math.hypot(dx, dy);
|
|
1948
|
+
}
|
|
1949
|
+
function endpointCandidates(roots) {
|
|
1950
|
+
const selection = window.getSelection();
|
|
1951
|
+
if (selection === null) {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
const candidates = [...composedRanges(selection, roots)];
|
|
1955
|
+
if (selection.rangeCount > 0) {
|
|
1956
|
+
candidates.push(selection.getRangeAt(0));
|
|
1957
|
+
}
|
|
1958
|
+
return candidates;
|
|
1959
|
+
}
|
|
1960
|
+
function composedRanges(selection, roots) {
|
|
1961
|
+
const composed = selection.getComposedRanges;
|
|
1962
|
+
if (composed === void 0) {
|
|
1963
|
+
return [];
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
return composed.call(selection, { shadowRoots: roots });
|
|
1967
|
+
} catch {
|
|
1968
|
+
return composed.call(selection, ...roots);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/ui/page_quote_offer.ts
|
|
1973
|
+
var GAP = 6;
|
|
1974
|
+
var OFFER_CSS = `
|
|
1975
|
+
.ag-ui-quote-offer {
|
|
1976
|
+
position: fixed;
|
|
1977
|
+
z-index: 2147483000;
|
|
1978
|
+
transform: translate(-50%, -100%);
|
|
1979
|
+
margin: 0;
|
|
1980
|
+
padding: 0.25em 0.7em;
|
|
1981
|
+
border: 1px solid rgb(0 0 0 / 0.15);
|
|
1982
|
+
border-radius: 999px;
|
|
1983
|
+
background: Canvas;
|
|
1984
|
+
color: CanvasText;
|
|
1985
|
+
font: inherit;
|
|
1986
|
+
font-size: 0.8rem;
|
|
1987
|
+
line-height: 1.6;
|
|
1988
|
+
white-space: nowrap;
|
|
1989
|
+
cursor: pointer;
|
|
1990
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
.ag-ui-quote-offer[data-below="true"] {
|
|
1994
|
+
transform: translate(-50%, 0);
|
|
1995
|
+
}
|
|
1996
|
+
`;
|
|
1997
|
+
function attachQuoteOffer(options) {
|
|
1998
|
+
const { within, exclude, onQuote } = options;
|
|
1999
|
+
const sheet = new CSSStyleSheet();
|
|
2000
|
+
sheet.replaceSync(OFFER_CSS);
|
|
2001
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
2002
|
+
const button2 = document.createElement("button");
|
|
2003
|
+
button2.type = "button";
|
|
2004
|
+
button2.className = "ag-ui-quote-offer";
|
|
2005
|
+
button2.textContent = options.label;
|
|
2006
|
+
button2.hidden = true;
|
|
2007
|
+
document.body.append(button2);
|
|
2008
|
+
let quoting = "";
|
|
2009
|
+
const hide = () => {
|
|
2010
|
+
button2.hidden = true;
|
|
2011
|
+
quoting = "";
|
|
2012
|
+
};
|
|
2013
|
+
const settled = (event) => {
|
|
2014
|
+
if (event.composedPath().includes(exclude)) {
|
|
2015
|
+
hide();
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
if (fieldHasFocus()) {
|
|
2019
|
+
hide();
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
const near = event instanceof MouseEvent ? { x: event.clientX, y: event.clientY } : void 0;
|
|
2023
|
+
const selected = quotableSelection(within, [], near);
|
|
2024
|
+
if (selected === null) {
|
|
2025
|
+
hide();
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
quoting = selected.text;
|
|
2029
|
+
place(button2, selected.rect);
|
|
2030
|
+
};
|
|
2031
|
+
const onMouseDown = (event) => {
|
|
2032
|
+
if (!button2.contains(event.target)) {
|
|
2033
|
+
hide();
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
within.addEventListener("mouseup", settled);
|
|
2037
|
+
within.addEventListener("keyup", settled);
|
|
2038
|
+
within.addEventListener("mousedown", onMouseDown);
|
|
2039
|
+
document.addEventListener("scroll", hide, true);
|
|
2040
|
+
window.addEventListener("resize", hide);
|
|
2041
|
+
button2.addEventListener("mousedown", (event) => {
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
});
|
|
2044
|
+
button2.addEventListener("click", () => {
|
|
2045
|
+
const text3 = quoting;
|
|
2046
|
+
window.getSelection()?.removeAllRanges();
|
|
2047
|
+
hide();
|
|
2048
|
+
onQuote(text3);
|
|
2049
|
+
});
|
|
2050
|
+
return {
|
|
2051
|
+
element: button2,
|
|
2052
|
+
detach() {
|
|
2053
|
+
within.removeEventListener("mouseup", settled);
|
|
2054
|
+
within.removeEventListener("keyup", settled);
|
|
2055
|
+
within.removeEventListener("mousedown", onMouseDown);
|
|
2056
|
+
document.removeEventListener("scroll", hide, true);
|
|
2057
|
+
window.removeEventListener("resize", hide);
|
|
2058
|
+
button2.remove();
|
|
2059
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((each) => each !== sheet);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function fieldHasFocus() {
|
|
2064
|
+
const active = document.activeElement;
|
|
2065
|
+
if (active === null) {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
return active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable === true;
|
|
2069
|
+
}
|
|
2070
|
+
function place(button2, rect) {
|
|
2071
|
+
button2.hidden = false;
|
|
2072
|
+
const below = rect.top < GAP + button2.offsetHeight;
|
|
2073
|
+
button2.dataset["below"] = String(below);
|
|
2074
|
+
button2.style.top = `${below ? rect.bottom + GAP : rect.top - GAP}px`;
|
|
2075
|
+
const half = button2.offsetWidth / 2;
|
|
2076
|
+
const centre = rect.left + rect.width / 2;
|
|
2077
|
+
const width = document.documentElement.clientWidth;
|
|
2078
|
+
button2.style.left = `${Math.min(Math.max(centre, half), width - half)}px`;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1635
2081
|
// src/ui/prettify_tool_name.ts
|
|
1636
2082
|
function prettifyToolName(name) {
|
|
1637
2083
|
const spaced = name.replace(/[._-]+/g, " ").trim();
|
|
@@ -2121,7 +2567,7 @@ function createDOMPurify() {
|
|
|
2121
2567
|
const originalDocument = document2;
|
|
2122
2568
|
const currentScript = originalDocument.currentScript;
|
|
2123
2569
|
window2.DocumentFragment;
|
|
2124
|
-
const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element,
|
|
2570
|
+
const HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element = window2.Element, NodeFilter2 = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
|
|
2125
2571
|
_window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap;
|
|
2126
2572
|
window2.HTMLFormElement;
|
|
2127
2573
|
const DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes;
|
|
@@ -2718,7 +3164,7 @@ function createDOMPurify() {
|
|
|
2718
3164
|
doc || root,
|
|
2719
3165
|
root,
|
|
2720
3166
|
// eslint-disable-next-line no-bitwise
|
|
2721
|
-
|
|
3167
|
+
NodeFilter2.SHOW_ELEMENT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_PROCESSING_INSTRUCTION | NodeFilter2.SHOW_CDATA_SECTION,
|
|
2722
3168
|
null
|
|
2723
3169
|
);
|
|
2724
3170
|
};
|
|
@@ -2736,7 +3182,7 @@ function createDOMPurify() {
|
|
|
2736
3182
|
doc || node,
|
|
2737
3183
|
node,
|
|
2738
3184
|
// eslint-disable-next-line no-bitwise
|
|
2739
|
-
|
|
3185
|
+
NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_CDATA_SECTION | NodeFilter2.SHOW_PROCESSING_INSTRUCTION,
|
|
2740
3186
|
null
|
|
2741
3187
|
);
|
|
2742
3188
|
let currentNode = walker.nextNode();
|
|
@@ -4596,23 +5042,51 @@ var ALLOWED_TAGS = [
|
|
|
4596
5042
|
"th",
|
|
4597
5043
|
"td"
|
|
4598
5044
|
];
|
|
4599
|
-
var ALLOWED_ATTR = ["href", "title", "class"];
|
|
4600
|
-
var
|
|
4601
|
-
|
|
5045
|
+
var ALLOWED_ATTR = ["href", "title", "class", "target", "rel"];
|
|
5046
|
+
var SANITIZE_CONFIG = {
|
|
5047
|
+
ALLOWED_TAGS,
|
|
5048
|
+
ALLOWED_ATTR,
|
|
5049
|
+
ALLOW_DATA_ATTR: false,
|
|
5050
|
+
ALLOW_ARIA_ATTR: false
|
|
5051
|
+
};
|
|
5052
|
+
var SANITIZE_CONFIG_WITH_IMAGES = {
|
|
5053
|
+
...SANITIZE_CONFIG,
|
|
5054
|
+
ALLOWED_TAGS: [...ALLOWED_TAGS, "img"],
|
|
5055
|
+
ALLOWED_ATTR: [...ALLOWED_ATTR, "src", "alt", "width", "height"]
|
|
5056
|
+
};
|
|
5057
|
+
var LANGUAGE_CLASS = /^language-[A-Za-z0-9_+#.-]+$/;
|
|
5058
|
+
var LANGUAGE_HOSTS = /* @__PURE__ */ new Set(["CODE", "PRE"]);
|
|
5059
|
+
function harden(node) {
|
|
5060
|
+
if (node.nodeName === "A" && node.hasAttribute("href")) {
|
|
5061
|
+
node.setAttribute("target", "_blank");
|
|
5062
|
+
node.setAttribute("rel", "noopener noreferrer");
|
|
5063
|
+
} else {
|
|
5064
|
+
node.removeAttribute("target");
|
|
5065
|
+
node.removeAttribute("rel");
|
|
5066
|
+
}
|
|
5067
|
+
const classes = node.getAttribute("class");
|
|
5068
|
+
if (classes === null) {
|
|
5069
|
+
return;
|
|
5070
|
+
}
|
|
5071
|
+
const kept = LANGUAGE_HOSTS.has(node.nodeName) ? classes.split(/\s+/).filter((token) => LANGUAGE_CLASS.test(token)) : [];
|
|
5072
|
+
if (kept.length === 0) {
|
|
5073
|
+
node.removeAttribute("class");
|
|
5074
|
+
return;
|
|
5075
|
+
}
|
|
5076
|
+
node.setAttribute("class", kept.join(" "));
|
|
5077
|
+
}
|
|
5078
|
+
var purifier = null;
|
|
5079
|
+
function sanitizer() {
|
|
5080
|
+
if (purifier === null) {
|
|
5081
|
+
purifier = purify();
|
|
5082
|
+
purifier.addHook("afterSanitizeAttributes", harden);
|
|
5083
|
+
}
|
|
5084
|
+
return purifier;
|
|
5085
|
+
}
|
|
4602
5086
|
function renderMarkdown(text3, options) {
|
|
4603
5087
|
const allowImages = options?.allowImages === true;
|
|
4604
5088
|
const rendered = parser.parse(text3, { async: false });
|
|
4605
|
-
|
|
4606
|
-
ALLOWED_TAGS: allowImages ? ALLOWED_TAGS_WITH_IMAGES : ALLOWED_TAGS,
|
|
4607
|
-
ALLOWED_ATTR: allowImages ? ALLOWED_ATTR_WITH_IMAGES : ALLOWED_ATTR
|
|
4608
|
-
});
|
|
4609
|
-
const template = document.createElement("template");
|
|
4610
|
-
template.innerHTML = clean;
|
|
4611
|
-
for (const anchor of template.content.querySelectorAll("a[href]")) {
|
|
4612
|
-
anchor.setAttribute("target", "_blank");
|
|
4613
|
-
anchor.setAttribute("rel", "noopener noreferrer");
|
|
4614
|
-
}
|
|
4615
|
-
return template.innerHTML.trim();
|
|
5089
|
+
return sanitizer().sanitize(rendered, allowImages ? SANITIZE_CONFIG_WITH_IMAGES : SANITIZE_CONFIG).trim();
|
|
4616
5090
|
}
|
|
4617
5091
|
|
|
4618
5092
|
// src/ui/resize_handle.ts
|
|
@@ -4655,6 +5129,15 @@ function createResizeHandle(options) {
|
|
|
4655
5129
|
window.addEventListener("pointerup", onUp);
|
|
4656
5130
|
event.preventDefault();
|
|
4657
5131
|
});
|
|
5132
|
+
let pending = null;
|
|
5133
|
+
const settle = () => {
|
|
5134
|
+
if (pending === null) {
|
|
5135
|
+
return;
|
|
5136
|
+
}
|
|
5137
|
+
const size = pending;
|
|
5138
|
+
pending = null;
|
|
5139
|
+
options.commit(size);
|
|
5140
|
+
};
|
|
4658
5141
|
handle.addEventListener("keydown", (event) => {
|
|
4659
5142
|
const axis = options.axis();
|
|
4660
5143
|
if (axis === "none") {
|
|
@@ -4680,8 +5163,10 @@ function createResizeHandle(options) {
|
|
|
4680
5163
|
}
|
|
4681
5164
|
event.preventDefault();
|
|
4682
5165
|
options.apply(next);
|
|
4683
|
-
|
|
5166
|
+
pending = next;
|
|
4684
5167
|
});
|
|
5168
|
+
handle.addEventListener("keyup", settle);
|
|
5169
|
+
handle.addEventListener("blur", settle);
|
|
4685
5170
|
return handle;
|
|
4686
5171
|
}
|
|
4687
5172
|
|
|
@@ -4892,6 +5377,60 @@ var SkillsMenu = class {
|
|
|
4892
5377
|
}
|
|
4893
5378
|
};
|
|
4894
5379
|
|
|
5380
|
+
// src/ui/stick_to_bottom.ts
|
|
5381
|
+
var BOTTOM_SLACK_PX = 4;
|
|
5382
|
+
function createStickToBottom({
|
|
5383
|
+
viewport,
|
|
5384
|
+
onMissedContent
|
|
5385
|
+
}) {
|
|
5386
|
+
let isFollowing = true;
|
|
5387
|
+
let missed = false;
|
|
5388
|
+
const atBottom = () => viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= BOTTOM_SLACK_PX;
|
|
5389
|
+
const setMissed = (next) => {
|
|
5390
|
+
if (next === missed) {
|
|
5391
|
+
return;
|
|
5392
|
+
}
|
|
5393
|
+
missed = next;
|
|
5394
|
+
onMissedContent(missed);
|
|
5395
|
+
};
|
|
5396
|
+
const toBottom = () => {
|
|
5397
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
5398
|
+
};
|
|
5399
|
+
const onScroll = () => {
|
|
5400
|
+
isFollowing = atBottom();
|
|
5401
|
+
if (isFollowing) {
|
|
5402
|
+
setMissed(false);
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
const follow = () => {
|
|
5406
|
+
if (isFollowing) {
|
|
5407
|
+
toBottom();
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5410
|
+
setMissed(true);
|
|
5411
|
+
};
|
|
5412
|
+
viewport.addEventListener("scroll", onScroll, { passive: true });
|
|
5413
|
+
const observer = new ResizeObserver(() => {
|
|
5414
|
+
if (isFollowing) {
|
|
5415
|
+
toBottom();
|
|
5416
|
+
}
|
|
5417
|
+
});
|
|
5418
|
+
observer.observe(viewport);
|
|
5419
|
+
return {
|
|
5420
|
+
follow,
|
|
5421
|
+
jump: () => {
|
|
5422
|
+
isFollowing = true;
|
|
5423
|
+
setMissed(false);
|
|
5424
|
+
toBottom();
|
|
5425
|
+
},
|
|
5426
|
+
following: () => isFollowing,
|
|
5427
|
+
dispose: () => {
|
|
5428
|
+
viewport.removeEventListener("scroll", onScroll);
|
|
5429
|
+
observer.disconnect();
|
|
5430
|
+
}
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
|
|
4895
5434
|
// src/ui/styles.ts
|
|
4896
5435
|
var STYLES = `
|
|
4897
5436
|
/* \u2500\u2500 Token defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -5462,6 +6001,7 @@ var STYLES = `
|
|
|
5462
6001
|
opacity: 0;
|
|
5463
6002
|
}
|
|
5464
6003
|
|
|
6004
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages-wrap,
|
|
5465
6005
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
|
|
5466
6006
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
|
|
5467
6007
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
|
|
@@ -5470,9 +6010,113 @@ var STYLES = `
|
|
|
5470
6010
|
display: none;
|
|
5471
6011
|
}
|
|
5472
6012
|
|
|
6013
|
+
/* Jump-to-latest: shown only once the reader has scrolled away *and* missed
|
|
6014
|
+
something. Anchored to the panel rather than the list so it does not scroll
|
|
6015
|
+
with the content it is offering to scroll to. */
|
|
6016
|
+
/* The transcript's own box, and the only one whose foot is the transcript's
|
|
6017
|
+
foot. The panel's foot is below the composer, the chips and the footer. */
|
|
6018
|
+
.messages-wrap {
|
|
6019
|
+
position: relative;
|
|
6020
|
+
flex: 1;
|
|
6021
|
+
min-height: 0;
|
|
6022
|
+
display: flex;
|
|
6023
|
+
flex-direction: column;
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
.jump-latest {
|
|
6027
|
+
position: absolute;
|
|
6028
|
+
left: 50%;
|
|
6029
|
+
transform: translateX(-50%);
|
|
6030
|
+
bottom: var(--_pad);
|
|
6031
|
+
z-index: 2;
|
|
6032
|
+
display: none;
|
|
6033
|
+
align-items: center;
|
|
6034
|
+
gap: 0.35em;
|
|
6035
|
+
padding: 0.4em 0.9em;
|
|
6036
|
+
border: 1px solid var(--_border);
|
|
6037
|
+
border-radius: 999px;
|
|
6038
|
+
/* A raised surface, not the panel's own background. Reusing --_bg made the
|
|
6039
|
+
pill the same colour as everything behind it, leaving a 1px border and a
|
|
6040
|
+
shadow to carry the whole affordance -- and a dark-on-dark shadow carries
|
|
6041
|
+
nothing. --_hover is the token that already means "lifted off the panel",
|
|
6042
|
+
and it separates in both themes without competing with the accent the send
|
|
6043
|
+
button owns. */
|
|
6044
|
+
background: var(--_hover);
|
|
6045
|
+
color: var(--_text);
|
|
6046
|
+
font: inherit;
|
|
6047
|
+
font-size: 0.85em;
|
|
6048
|
+
cursor: pointer;
|
|
6049
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
6050
|
+
}
|
|
6051
|
+
|
|
6052
|
+
.jump-latest[data-missed="true"] {
|
|
6053
|
+
display: flex;
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
.jump-latest:hover {
|
|
6057
|
+
border-color: var(--_accent);
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
/* The offer to quote a selection. Positioned in script against the transcript
|
|
6061
|
+
box, which is the only ancestor whose top and foot are the transcript's --
|
|
6062
|
+
the same reason .jump-latest lives here. The translate is the half the
|
|
6063
|
+
script does not do: script sets the point the offer hangs from, CSS decides
|
|
6064
|
+
which corner of the offer that point is. */
|
|
6065
|
+
.quote-selection {
|
|
6066
|
+
position: absolute;
|
|
6067
|
+
z-index: 2;
|
|
6068
|
+
transform: translate(-50%, -100%);
|
|
6069
|
+
padding: 0.25em 0.7em;
|
|
6070
|
+
border: 1px solid var(--_border);
|
|
6071
|
+
border-radius: 999px;
|
|
6072
|
+
background: var(--_hover);
|
|
6073
|
+
color: var(--_text);
|
|
6074
|
+
font: inherit;
|
|
6075
|
+
font-size: 0.8em;
|
|
6076
|
+
line-height: 1.6;
|
|
6077
|
+
white-space: nowrap;
|
|
6078
|
+
cursor: pointer;
|
|
6079
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6082
|
+
/* Flipped under the selection when there was no room above it. Only the
|
|
6083
|
+
vertical half of the translate changes: it still hangs from its own centre
|
|
6084
|
+
horizontally. */
|
|
6085
|
+
.quote-selection[data-below="true"] {
|
|
6086
|
+
transform: translate(-50%, 0);
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6089
|
+
.quote-selection:hover {
|
|
6090
|
+
border-color: var(--_accent);
|
|
6091
|
+
}
|
|
6092
|
+
|
|
6093
|
+
/* Screen-reader-only status region. Off-screen rather than display:none or
|
|
6094
|
+
visibility:hidden, both of which take the element out of the accessibility
|
|
6095
|
+
tree entirely -- a hidden live region announces nothing at all, which is the
|
|
6096
|
+
classic way this pattern is written wrong.
|
|
6097
|
+
|
|
6098
|
+
The 1px box with clip-path, rather than width/height 0, is the shape that
|
|
6099
|
+
survives: a zero-sized element is dropped from the tree by some engines. */
|
|
6100
|
+
.sr-only {
|
|
6101
|
+
position: absolute;
|
|
6102
|
+
width: 1px;
|
|
6103
|
+
height: 1px;
|
|
6104
|
+
margin: -1px;
|
|
6105
|
+
padding: 0;
|
|
6106
|
+
border: 0;
|
|
6107
|
+
overflow: hidden;
|
|
6108
|
+
white-space: nowrap;
|
|
6109
|
+
clip-path: inset(50%);
|
|
6110
|
+
}
|
|
6111
|
+
|
|
5473
6112
|
.messages {
|
|
5474
6113
|
flex: 1;
|
|
5475
6114
|
overflow-y: auto;
|
|
6115
|
+
/* The browser's own scroll anchoring competes with the scroller for the same
|
|
6116
|
+
job and wins unpredictably -- it can hold the view still exactly when we
|
|
6117
|
+
want to follow. Turned off so following is decided in one place. Safari
|
|
6118
|
+
does not implement it, which is itself a reason not to depend on it. */
|
|
6119
|
+
overflow-anchor: none;
|
|
5476
6120
|
padding: var(--_pad);
|
|
5477
6121
|
display: flex;
|
|
5478
6122
|
flex-direction: column;
|
|
@@ -6638,6 +7282,7 @@ var STYLES = `
|
|
|
6638
7282
|
|
|
6639
7283
|
.confirm-actions {
|
|
6640
7284
|
display: flex;
|
|
7285
|
+
flex-wrap: wrap;
|
|
6641
7286
|
gap: 8px;
|
|
6642
7287
|
justify-content: flex-end;
|
|
6643
7288
|
}
|
|
@@ -6664,41 +7309,134 @@ var STYLES = `
|
|
|
6664
7309
|
color: #ffffff;
|
|
6665
7310
|
}
|
|
6666
7311
|
|
|
6667
|
-
/*
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
gap: 8px;
|
|
6674
|
-
padding: 12px;
|
|
6675
|
-
background: var(--_bg);
|
|
6676
|
-
border: 1px solid var(--_accent);
|
|
6677
|
-
border-radius: 10px;
|
|
6678
|
-
}
|
|
6679
|
-
|
|
6680
|
-
.approval[data-resolved] {
|
|
6681
|
-
opacity: 0.7;
|
|
6682
|
-
border-color: var(--_border);
|
|
7312
|
+
/* The session waiver. Deliberately the quietest of the three: it is the widest
|
|
7313
|
+
decision on the card, so it should be reachable without being the one the eye
|
|
7314
|
+
lands on when the user means to say yes once. */
|
|
7315
|
+
.confirm-btn--always {
|
|
7316
|
+
font-weight: 500;
|
|
7317
|
+
opacity: 0.85;
|
|
6683
7318
|
}
|
|
6684
7319
|
|
|
6685
|
-
.
|
|
6686
|
-
|
|
7320
|
+
.confirm-btn--always:hover,
|
|
7321
|
+
.confirm-btn--always:focus-visible {
|
|
7322
|
+
opacity: 1;
|
|
6687
7323
|
}
|
|
6688
7324
|
|
|
6689
|
-
|
|
7325
|
+
/* Editable arguments on an approval card. A plain field rather than a code
|
|
7326
|
+
editor: it holds the JSON a card already displays, and the only interaction
|
|
7327
|
+
is correcting a value before letting the call run. */
|
|
7328
|
+
.approval-edit {
|
|
6690
7329
|
display: flex;
|
|
6691
|
-
|
|
6692
|
-
|
|
7330
|
+
flex-direction: column;
|
|
7331
|
+
gap: 6px;
|
|
6693
7332
|
}
|
|
6694
7333
|
|
|
6695
|
-
.approval-
|
|
7334
|
+
.approval-args {
|
|
7335
|
+
box-sizing: border-box;
|
|
7336
|
+
width: 100%;
|
|
7337
|
+
resize: vertical;
|
|
6696
7338
|
border: 1px solid var(--_border);
|
|
6697
7339
|
border-radius: 8px;
|
|
6698
|
-
padding: 8px
|
|
6699
|
-
font:
|
|
6700
|
-
font-
|
|
6701
|
-
|
|
7340
|
+
padding: 8px;
|
|
7341
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
7342
|
+
font-size: 0.85em;
|
|
7343
|
+
background: var(--_bg);
|
|
7344
|
+
color: var(--_fg);
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
.approval-args:focus-visible {
|
|
7348
|
+
border-color: var(--_accent);
|
|
7349
|
+
outline: none;
|
|
7350
|
+
}
|
|
7351
|
+
|
|
7352
|
+
.approval-error {
|
|
7353
|
+
font-size: 0.85em;
|
|
7354
|
+
color: var(--_danger);
|
|
7355
|
+
}
|
|
7356
|
+
|
|
7357
|
+
/* Message action row. Sits under a finished assistant bubble.
|
|
7358
|
+
|
|
7359
|
+
The wrap is insurance rather than a fix: these buttons are glyph-only, so at
|
|
7360
|
+
every width tested they fit on one line and removing the wrap changes
|
|
7361
|
+
nothing. It is here because the confirmation row one release earlier did
|
|
7362
|
+
overflow when it gained a third button, off the left edge and outside its own
|
|
7363
|
+
card, and the difference between the two rows is only that this one's labels
|
|
7364
|
+
are icons today. */
|
|
7365
|
+
.message-actions {
|
|
7366
|
+
display: flex;
|
|
7367
|
+
flex-wrap: wrap;
|
|
7368
|
+
gap: 4px;
|
|
7369
|
+
/* Negative, and that is the point. The answer group is a flex column with its
|
|
7370
|
+
own gap, so a positive margin here pushes the row further from the message
|
|
7371
|
+
it acts on than the next card is below it -- the buttons then read as
|
|
7372
|
+
belonging to whatever follows. Pulling back inside the gap is what makes
|
|
7373
|
+
them the message's own. */
|
|
7374
|
+
margin-top: -6px;
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7377
|
+
.message-action {
|
|
7378
|
+
border: none;
|
|
7379
|
+
border-radius: 6px;
|
|
7380
|
+
padding: 2px 6px;
|
|
7381
|
+
font: inherit;
|
|
7382
|
+
line-height: 1.2;
|
|
7383
|
+
cursor: pointer;
|
|
7384
|
+
background: transparent;
|
|
7385
|
+
color: var(--_muted);
|
|
7386
|
+
opacity: 0.75;
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7389
|
+
.message-action:hover,
|
|
7390
|
+
.message-action:focus-visible {
|
|
7391
|
+
opacity: 1;
|
|
7392
|
+
background: var(--_border);
|
|
7393
|
+
}
|
|
7394
|
+
|
|
7395
|
+
.message-action[aria-pressed="true"] {
|
|
7396
|
+
opacity: 1;
|
|
7397
|
+
color: var(--_accent);
|
|
7398
|
+
}
|
|
7399
|
+
|
|
7400
|
+
.message-action--confirmed {
|
|
7401
|
+
opacity: 1;
|
|
7402
|
+
color: var(--_accent);
|
|
7403
|
+
}
|
|
7404
|
+
|
|
7405
|
+
/* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */
|
|
7406
|
+
.approval {
|
|
7407
|
+
align-self: stretch;
|
|
7408
|
+
box-sizing: border-box;
|
|
7409
|
+
display: flex;
|
|
7410
|
+
flex-direction: column;
|
|
7411
|
+
gap: 8px;
|
|
7412
|
+
padding: 12px;
|
|
7413
|
+
background: var(--_bg);
|
|
7414
|
+
border: 1px solid var(--_accent);
|
|
7415
|
+
border-radius: 10px;
|
|
7416
|
+
}
|
|
7417
|
+
|
|
7418
|
+
.approval[data-resolved] {
|
|
7419
|
+
opacity: 0.7;
|
|
7420
|
+
border-color: var(--_border);
|
|
7421
|
+
}
|
|
7422
|
+
|
|
7423
|
+
.approval-body {
|
|
7424
|
+
font-weight: 600;
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7427
|
+
.approval-actions {
|
|
7428
|
+
display: flex;
|
|
7429
|
+
gap: 8px;
|
|
7430
|
+
justify-content: flex-end;
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7433
|
+
.approval-btn {
|
|
7434
|
+
border: 1px solid var(--_border);
|
|
7435
|
+
border-radius: 8px;
|
|
7436
|
+
padding: 8px 14px;
|
|
7437
|
+
font: inherit;
|
|
7438
|
+
font-weight: 600;
|
|
7439
|
+
cursor: pointer;
|
|
6702
7440
|
background: var(--_bg);
|
|
6703
7441
|
color: var(--_fg);
|
|
6704
7442
|
}
|
|
@@ -6808,6 +7546,34 @@ var STYLES = `
|
|
|
6808
7546
|
border-color: var(--_accent);
|
|
6809
7547
|
}
|
|
6810
7548
|
|
|
7549
|
+
/* Follow-up suggestion chips. Deliberately the skill chips' shape rather than a
|
|
7550
|
+
second chip vocabulary -- both are "a question you could ask", and the only
|
|
7551
|
+
difference is who chose it. Inside the transcript, so they scroll with the
|
|
7552
|
+
answer they follow instead of hovering above the composer. */
|
|
7553
|
+
.suggestions {
|
|
7554
|
+
display: flex;
|
|
7555
|
+
flex-wrap: wrap;
|
|
7556
|
+
gap: 6px;
|
|
7557
|
+
align-self: stretch;
|
|
7558
|
+
}
|
|
7559
|
+
|
|
7560
|
+
.suggestion-chip {
|
|
7561
|
+
border: 1px solid var(--_border);
|
|
7562
|
+
border-radius: 999px;
|
|
7563
|
+
padding: 4px 12px;
|
|
7564
|
+
font: inherit;
|
|
7565
|
+
font-size: 0.9em;
|
|
7566
|
+
text-align: left;
|
|
7567
|
+
cursor: pointer;
|
|
7568
|
+
background: var(--_assistant-bg);
|
|
7569
|
+
color: var(--_fg);
|
|
7570
|
+
}
|
|
7571
|
+
|
|
7572
|
+
.suggestion-chip:hover,
|
|
7573
|
+
.suggestion-chip:focus-visible {
|
|
7574
|
+
border-color: var(--_accent);
|
|
7575
|
+
}
|
|
7576
|
+
|
|
6811
7577
|
.skill-palette {
|
|
6812
7578
|
margin: 8px 12px 0;
|
|
6813
7579
|
display: flex;
|
|
@@ -7222,6 +7988,42 @@ var STYLES = `
|
|
|
7222
7988
|
}
|
|
7223
7989
|
`;
|
|
7224
7990
|
|
|
7991
|
+
// src/ui/suggestion_chips.ts
|
|
7992
|
+
var MAX_SUGGESTIONS = 4;
|
|
7993
|
+
var MAX_SUGGESTION_CHARS = 120;
|
|
7994
|
+
function suggestionPrompts(content) {
|
|
7995
|
+
if (typeof content !== "object" || content === null) {
|
|
7996
|
+
return null;
|
|
7997
|
+
}
|
|
7998
|
+
const raw = content.prompts;
|
|
7999
|
+
if (!Array.isArray(raw)) {
|
|
8000
|
+
return null;
|
|
8001
|
+
}
|
|
8002
|
+
const prompts = raw.filter((prompt) => typeof prompt === "string").map((prompt) => prompt.trim()).filter((prompt) => prompt !== "" && prompt.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
|
|
8003
|
+
return prompts.length === 0 ? null : prompts;
|
|
8004
|
+
}
|
|
8005
|
+
function renderSuggestionChips(content, strings, onPick) {
|
|
8006
|
+
const prompts = suggestionPrompts(content);
|
|
8007
|
+
if (prompts === null) {
|
|
8008
|
+
return null;
|
|
8009
|
+
}
|
|
8010
|
+
const row = document.createElement("div");
|
|
8011
|
+
row.className = "suggestions";
|
|
8012
|
+
row.setAttribute("part", "suggestions");
|
|
8013
|
+
row.setAttribute("role", "group");
|
|
8014
|
+
row.setAttribute("aria-label", strings.suggestions);
|
|
8015
|
+
for (const prompt of prompts) {
|
|
8016
|
+
const chip = document.createElement("button");
|
|
8017
|
+
chip.type = "button";
|
|
8018
|
+
chip.className = "suggestion-chip";
|
|
8019
|
+
chip.setAttribute("part", "suggestion-chip");
|
|
8020
|
+
chip.textContent = prompt;
|
|
8021
|
+
chip.addEventListener("click", () => onPick(prompt));
|
|
8022
|
+
row.appendChild(chip);
|
|
8023
|
+
}
|
|
8024
|
+
return row;
|
|
8025
|
+
}
|
|
8026
|
+
|
|
7225
8027
|
// src/ui/thoughts_block.ts
|
|
7226
8028
|
var ThoughtsBlock = class {
|
|
7227
8029
|
/** The block's root element; insert this at the top of the answer group. */
|
|
@@ -7288,6 +8090,7 @@ var ThreadDrawer = class {
|
|
|
7288
8090
|
#heading;
|
|
7289
8091
|
#newButton;
|
|
7290
8092
|
#list;
|
|
8093
|
+
#formatRelativeTime = null;
|
|
7291
8094
|
#strings;
|
|
7292
8095
|
#threads = [];
|
|
7293
8096
|
#activeId = "";
|
|
@@ -7335,6 +8138,21 @@ var ThreadDrawer = class {
|
|
|
7335
8138
|
this.element.append(backdrop, this.#panel);
|
|
7336
8139
|
}
|
|
7337
8140
|
/** Re-localize the drawer's chrome and rows (the host calls this on connect). */
|
|
8141
|
+
/**
|
|
8142
|
+
* Replace the timestamp formatter, or restore the built-in with `null`.
|
|
8143
|
+
*
|
|
8144
|
+
* The built-in is deliberately locale-neutral -- there is no `Intl` anywhere
|
|
8145
|
+
* in this component, so it never disagrees with a host's own formatting by
|
|
8146
|
+
* guessing a locale. That is a defensible default and a poor requirement, so
|
|
8147
|
+
* this is the way out.
|
|
8148
|
+
*/
|
|
8149
|
+
setRelativeTimeFormatter(format) {
|
|
8150
|
+
this.#formatRelativeTime = format;
|
|
8151
|
+
}
|
|
8152
|
+
/** This row's timestamp, through the host's formatter when it set one. */
|
|
8153
|
+
#formatTime(timestamp) {
|
|
8154
|
+
return this.#formatRelativeTime !== null ? this.#formatRelativeTime(timestamp) : relativeTime(timestamp, void 0, this.#strings);
|
|
8155
|
+
}
|
|
7338
8156
|
setStrings(strings) {
|
|
7339
8157
|
this.#strings = strings;
|
|
7340
8158
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
@@ -7434,7 +8252,7 @@ var ThreadDrawer = class {
|
|
|
7434
8252
|
const time = document.createElement("span");
|
|
7435
8253
|
time.className = "drawer-row-time";
|
|
7436
8254
|
time.setAttribute("part", "drawer-row-time");
|
|
7437
|
-
time.textContent =
|
|
8255
|
+
time.textContent = this.#formatTime(meta.updatedAt);
|
|
7438
8256
|
const preview = document.createElement("span");
|
|
7439
8257
|
preview.className = "drawer-row-preview";
|
|
7440
8258
|
preview.setAttribute("part", "drawer-row-preview");
|
|
@@ -7583,9 +8401,18 @@ var ToolCallCard = class {
|
|
|
7583
8401
|
#resultLabel;
|
|
7584
8402
|
#resultBody;
|
|
7585
8403
|
#strings;
|
|
8404
|
+
/**
|
|
8405
|
+
* The arguments this call was made with.
|
|
8406
|
+
*
|
|
8407
|
+
* Retained rather than only rendered, because an approval interrupt names a
|
|
8408
|
+
* `toolCallId` and nothing else -- so this card is the only place the args
|
|
8409
|
+
* still exist when the user is asked to approve, edit or deny the call.
|
|
8410
|
+
*/
|
|
8411
|
+
args;
|
|
7586
8412
|
#settled = false;
|
|
7587
8413
|
constructor(name, args, summary, strings = DEFAULT_UI_STRINGS) {
|
|
7588
8414
|
this.#strings = strings;
|
|
8415
|
+
this.args = args;
|
|
7589
8416
|
this.element = document.createElement("div");
|
|
7590
8417
|
this.element.className = "tool-call";
|
|
7591
8418
|
this.element.setAttribute("part", "tool-card");
|
|
@@ -7704,6 +8531,7 @@ var ToolCallCard = class {
|
|
|
7704
8531
|
};
|
|
7705
8532
|
|
|
7706
8533
|
// src/ui/voice_input.ts
|
|
8534
|
+
var MAX_RECORDING_MS = 12e4;
|
|
7707
8535
|
var VoiceInput = class {
|
|
7708
8536
|
/** The mic button; mount this in the composer. */
|
|
7709
8537
|
element;
|
|
@@ -7714,6 +8542,8 @@ var VoiceInput = class {
|
|
|
7714
8542
|
#recorder = null;
|
|
7715
8543
|
#stream = null;
|
|
7716
8544
|
#chunks = [];
|
|
8545
|
+
#capTimer = null;
|
|
8546
|
+
#hitCap = false;
|
|
7717
8547
|
#disposed = false;
|
|
7718
8548
|
constructor(options) {
|
|
7719
8549
|
this.#transcribe = options.transcribe;
|
|
@@ -7753,6 +8583,7 @@ var VoiceInput = class {
|
|
|
7753
8583
|
}
|
|
7754
8584
|
this.#stream = stream;
|
|
7755
8585
|
this.#chunks = [];
|
|
8586
|
+
this.#hitCap = false;
|
|
7756
8587
|
const recorder = new MediaRecorder(stream);
|
|
7757
8588
|
recorder.addEventListener("dataavailable", (event) => {
|
|
7758
8589
|
this.#chunks.push(event.data);
|
|
@@ -7762,11 +8593,23 @@ var VoiceInput = class {
|
|
|
7762
8593
|
});
|
|
7763
8594
|
this.#recorder = recorder;
|
|
7764
8595
|
recorder.start();
|
|
8596
|
+
this.#capTimer = setTimeout(() => {
|
|
8597
|
+
this.#hitCap = true;
|
|
8598
|
+
this.#stop();
|
|
8599
|
+
}, MAX_RECORDING_MS);
|
|
7765
8600
|
this.#setState("recording");
|
|
7766
8601
|
}
|
|
7767
8602
|
#stop() {
|
|
8603
|
+
this.#clearCap();
|
|
7768
8604
|
this.#recorder?.stop();
|
|
7769
8605
|
}
|
|
8606
|
+
/** Drop the cap timer; recording is over, by whichever route. */
|
|
8607
|
+
#clearCap() {
|
|
8608
|
+
if (this.#capTimer !== null) {
|
|
8609
|
+
clearTimeout(this.#capTimer);
|
|
8610
|
+
this.#capTimer = null;
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
7770
8613
|
/**
|
|
7771
8614
|
* Tear the control down, for a host element removed mid-recording. Stops any
|
|
7772
8615
|
* live `MediaRecorder`, releases the mic tracks so the browser's recording
|
|
@@ -7775,6 +8618,7 @@ var VoiceInput = class {
|
|
|
7775
8618
|
*/
|
|
7776
8619
|
dispose() {
|
|
7777
8620
|
this.#disposed = true;
|
|
8621
|
+
this.#clearCap();
|
|
7778
8622
|
if (this.#recorder !== null && this.#recorder.state !== "inactive") {
|
|
7779
8623
|
this.#recorder.stop();
|
|
7780
8624
|
}
|
|
@@ -7791,6 +8635,12 @@ var VoiceInput = class {
|
|
|
7791
8635
|
try {
|
|
7792
8636
|
const text3 = await this.#transcribe(audio);
|
|
7793
8637
|
this.#setState("idle");
|
|
8638
|
+
if (this.#hitCap) {
|
|
8639
|
+
this.element.title = this.#strings.recordingLimit.replace(
|
|
8640
|
+
"{n}",
|
|
8641
|
+
String(MAX_RECORDING_MS / 6e4)
|
|
8642
|
+
);
|
|
8643
|
+
}
|
|
7794
8644
|
if (text3 !== "") {
|
|
7795
8645
|
this.#onText(text3);
|
|
7796
8646
|
}
|
|
@@ -7917,6 +8767,41 @@ var AgUiClient = class {
|
|
|
7917
8767
|
this.#onPersist(this.#agent.messages);
|
|
7918
8768
|
await this.#run();
|
|
7919
8769
|
}
|
|
8770
|
+
/**
|
|
8771
|
+
* Drop everything after the most recent user message, so the same question
|
|
8772
|
+
* can be asked again.
|
|
8773
|
+
*
|
|
8774
|
+
* Returns the retained history, or `null` when there is nothing to retry (no
|
|
8775
|
+
* user message has been sent yet). **Truncates only** -- the caller re-renders
|
|
8776
|
+
* from the returned list and then calls {@link resume}, because the transcript
|
|
8777
|
+
* belongs to the element and a client that reached into it would own two
|
|
8778
|
+
* things. Running here instead would stream the new answer in underneath the
|
|
8779
|
+
* old one.
|
|
8780
|
+
*
|
|
8781
|
+
* Re-running answers the question the agent was last asked, rather than
|
|
8782
|
+
* telling it its answer was wrong, which is what makes the result a
|
|
8783
|
+
* *different* answer instead of a conversation about the previous one.
|
|
8784
|
+
*
|
|
8785
|
+
* **A retried turn re-runs its tools.** For a page-driving agent that is not
|
|
8786
|
+
* neutral: the previous attempt already clicked what it clicked, and this
|
|
8787
|
+
* does not undo it.
|
|
8788
|
+
*/
|
|
8789
|
+
truncateToLastUser() {
|
|
8790
|
+
const messages = [...this.#agent.messages];
|
|
8791
|
+
let lastUser = -1;
|
|
8792
|
+
for (const [index, message] of messages.entries()) {
|
|
8793
|
+
if (message.role === "user") {
|
|
8794
|
+
lastUser = index;
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
if (lastUser === -1) {
|
|
8798
|
+
return null;
|
|
8799
|
+
}
|
|
8800
|
+
const kept = messages.slice(0, lastUser + 1);
|
|
8801
|
+
this.#agent.setMessages(kept);
|
|
8802
|
+
this.#onPersist(this.#agent.messages);
|
|
8803
|
+
return kept;
|
|
8804
|
+
}
|
|
7920
8805
|
/**
|
|
7921
8806
|
* Resume the run loop after a navigating tool's result was supplied
|
|
7922
8807
|
* post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
|
|
@@ -8083,6 +8968,12 @@ var AgUiClient = class {
|
|
|
8083
8968
|
// Emitted after the client has written the patched messages, which is the
|
|
8084
8969
|
// first moment the result exists. Only the ids marked above are looked at,
|
|
8085
8970
|
// so an ordinary text delta does not walk the transcript.
|
|
8971
|
+
onCustomEvent({ event }) {
|
|
8972
|
+
h.onCustomEvent(event.name, event.value);
|
|
8973
|
+
},
|
|
8974
|
+
onMessagesSnapshotEvent({ event }) {
|
|
8975
|
+
h.onMessagesSnapshot(event.messages);
|
|
8976
|
+
},
|
|
8086
8977
|
onMessagesChanged({ messages }) {
|
|
8087
8978
|
if (pendingDeltas.size === 0) {
|
|
8088
8979
|
return;
|
|
@@ -8104,6 +8995,16 @@ var AgUiClient = class {
|
|
|
8104
8995
|
onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
|
|
8105
8996
|
h.onReasoningDelta(reasoningMessageBuffer);
|
|
8106
8997
|
},
|
|
8998
|
+
// The delta callback reports the buffer as it stood *before* the announced
|
|
8999
|
+
// delta was appended, so on its own it always trails the stream by one and
|
|
9000
|
+
// renders nothing at all for a block that arrives as a single delta. The
|
|
9001
|
+
// answer text is spared that because its own end event carries the whole
|
|
9002
|
+
// message; this is the reasoning counterpart, and it has to be
|
|
9003
|
+
// REASONING_MESSAGE_END rather than REASONING_END, because only the former
|
|
9004
|
+
// carries a buffer.
|
|
9005
|
+
onReasoningMessageEndEvent({ reasoningMessageBuffer }) {
|
|
9006
|
+
h.onReasoningDelta(reasoningMessageBuffer);
|
|
9007
|
+
},
|
|
8107
9008
|
onReasoningEndEvent() {
|
|
8108
9009
|
h.onReasoningEnd();
|
|
8109
9010
|
},
|
|
@@ -8159,12 +9060,68 @@ var MINTED_SUFFIX = "minted:";
|
|
|
8159
9060
|
var TITLE_LIMIT = 60;
|
|
8160
9061
|
var PREVIEW_LIMIT = 100;
|
|
8161
9062
|
var DEFAULT_TITLE = "New conversation";
|
|
8162
|
-
var
|
|
9063
|
+
var writeFailureReported = false;
|
|
9064
|
+
function writeStoredItem(key, value) {
|
|
9065
|
+
try {
|
|
9066
|
+
sessionStorage.setItem(key, value);
|
|
9067
|
+
} catch {
|
|
9068
|
+
if (writeFailureReported) {
|
|
9069
|
+
return;
|
|
9070
|
+
}
|
|
9071
|
+
writeFailureReported = true;
|
|
9072
|
+
console.warn(
|
|
9073
|
+
"<ag-ui-chat>: the browser refused a sessionStorage write \u2014 the quota is full, or storage is disabled for this context. The conversation continues, but it will not survive a page reload. Deleting a long conversation from the history drawer frees the quota."
|
|
9074
|
+
);
|
|
9075
|
+
}
|
|
9076
|
+
}
|
|
9077
|
+
function rootFor(namespace) {
|
|
9078
|
+
return namespace === "" ? KEY_ROOT : `${KEY_ROOT}@${namespace}`;
|
|
9079
|
+
}
|
|
9080
|
+
var SessionStorageStore = class _SessionStorageStore {
|
|
8163
9081
|
#root;
|
|
8164
9082
|
constructor(namespace = "") {
|
|
8165
|
-
this.#root = namespace
|
|
9083
|
+
this.#root = rootFor(namespace);
|
|
8166
9084
|
if (namespace !== "") {
|
|
8167
|
-
|
|
9085
|
+
_SessionStorageStore.adopt("", namespace);
|
|
9086
|
+
}
|
|
9087
|
+
}
|
|
9088
|
+
/**
|
|
9089
|
+
* Move every key a store owns out of `from`'s namespace and into `to`'s.
|
|
9090
|
+
*
|
|
9091
|
+
* Two callers, one move. The constructor adopts the pre-namespacing global
|
|
9092
|
+
* keys (`from` = `""`); `<ag-ui-chat>` adopts an element-scoped conversation
|
|
9093
|
+
* into a principal-scoped one the first time a `user-key` arrives, which is a
|
|
9094
|
+
* host naming the user who was already there rather than a handover.
|
|
9095
|
+
*
|
|
9096
|
+
* Only this store's own suffixes move — the element's `collapsed` / `size` /
|
|
9097
|
+
* `theme` keys share the global root and are deliberately left where they
|
|
9098
|
+
* are. A value already present at the destination wins: the destination is
|
|
9099
|
+
* the durable record and the source is the stray this move exists to clear.
|
|
9100
|
+
*/
|
|
9101
|
+
static adopt(from, to) {
|
|
9102
|
+
const fromRoot = `${rootFor(from)}:`;
|
|
9103
|
+
const toRoot = `${rootFor(to)}:`;
|
|
9104
|
+
for (const [key, suffix] of ownedKeys(fromRoot)) {
|
|
9105
|
+
const value = sessionStorage.getItem(key);
|
|
9106
|
+
const destination = toRoot + suffix;
|
|
9107
|
+
if (value !== null && sessionStorage.getItem(destination) === null) {
|
|
9108
|
+
writeStoredItem(destination, value);
|
|
9109
|
+
}
|
|
9110
|
+
sessionStorage.removeItem(key);
|
|
9111
|
+
}
|
|
9112
|
+
}
|
|
9113
|
+
/**
|
|
9114
|
+
* Forget everything a store holds for `namespace`.
|
|
9115
|
+
*
|
|
9116
|
+
* The logout primitive: `<ag-ui-chat>` calls it when its `user-key` changes,
|
|
9117
|
+
* and a host driving its own store can call it from its own sign-out path.
|
|
9118
|
+
* Deliberately narrow — it removes only keys under this exact namespace whose
|
|
9119
|
+
* suffix parses as one this store writes, so it can never reach another
|
|
9120
|
+
* element's conversation or the host's own `sessionStorage` entries.
|
|
9121
|
+
*/
|
|
9122
|
+
static purge(namespace) {
|
|
9123
|
+
for (const [key] of ownedKeys(`${rootFor(namespace)}:`)) {
|
|
9124
|
+
sessionStorage.removeItem(key);
|
|
8168
9125
|
}
|
|
8169
9126
|
}
|
|
8170
9127
|
threadId() {
|
|
@@ -8172,8 +9129,8 @@ var SessionStorageStore = class {
|
|
|
8172
9129
|
}
|
|
8173
9130
|
newThread() {
|
|
8174
9131
|
const id = randomUUID3();
|
|
8175
|
-
|
|
8176
|
-
|
|
9132
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), id);
|
|
9133
|
+
writeStoredItem(this.#key(MINTED_SUFFIX + id), "1");
|
|
8177
9134
|
return id;
|
|
8178
9135
|
}
|
|
8179
9136
|
isUnsent(threadId) {
|
|
@@ -8183,7 +9140,7 @@ var SessionStorageStore = class {
|
|
|
8183
9140
|
return Promise.resolve(this.#readJson(this.#key(MESSAGES_SUFFIX + threadId)));
|
|
8184
9141
|
}
|
|
8185
9142
|
saveMessages(threadId, messages) {
|
|
8186
|
-
|
|
9143
|
+
writeStoredItem(this.#key(MESSAGES_SUFFIX + threadId), JSON.stringify(messages));
|
|
8187
9144
|
sessionStorage.removeItem(this.#key(MINTED_SUFFIX + threadId));
|
|
8188
9145
|
this.#touchThread(threadId, messages);
|
|
8189
9146
|
}
|
|
@@ -8196,7 +9153,7 @@ var SessionStorageStore = class {
|
|
|
8196
9153
|
sessionStorage.removeItem(key);
|
|
8197
9154
|
return;
|
|
8198
9155
|
}
|
|
8199
|
-
|
|
9156
|
+
writeStoredItem(key, JSON.stringify(checkpoint));
|
|
8200
9157
|
}
|
|
8201
9158
|
clear(threadId) {
|
|
8202
9159
|
sessionStorage.removeItem(this.#key(MESSAGES_SUFFIX + threadId));
|
|
@@ -8212,7 +9169,7 @@ var SessionStorageStore = class {
|
|
|
8212
9169
|
return Promise.resolve(metas);
|
|
8213
9170
|
}
|
|
8214
9171
|
setActiveThread(threadId) {
|
|
8215
|
-
|
|
9172
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), threadId);
|
|
8216
9173
|
}
|
|
8217
9174
|
renameThread(threadId, title) {
|
|
8218
9175
|
const threads = this.#readThreads();
|
|
@@ -8256,40 +9213,12 @@ var SessionStorageStore = class {
|
|
|
8256
9213
|
sessionStorage.removeItem(key);
|
|
8257
9214
|
return;
|
|
8258
9215
|
}
|
|
8259
|
-
|
|
9216
|
+
writeStoredItem(key, JSON.stringify(threads));
|
|
8260
9217
|
}
|
|
8261
9218
|
/** This store's fully-qualified key for a suffix (namespaced when set). */
|
|
8262
9219
|
#key(suffix) {
|
|
8263
9220
|
return `${this.#root}:${suffix}`;
|
|
8264
9221
|
}
|
|
8265
|
-
/**
|
|
8266
|
-
* One-time move of un-namespaced `ag-ui-chat:*` keys into this instance's
|
|
8267
|
-
* namespace, so an existing conversation isn't orphaned. Only this store's own
|
|
8268
|
-
* keys move — the element's `collapsed` / `theme` keys are left alone. The
|
|
8269
|
-
* first namespaced instance to mount adopts the data; a second namespace
|
|
8270
|
-
* finds it gone and starts fresh.
|
|
8271
|
-
*/
|
|
8272
|
-
#migrateLegacyKeys() {
|
|
8273
|
-
const legacyRoot = `${KEY_ROOT}:`;
|
|
8274
|
-
const moves = [];
|
|
8275
|
-
for (let i = 0; i < sessionStorage.length; i += 1) {
|
|
8276
|
-
const key = sessionStorage.key(i);
|
|
8277
|
-
if (key === null || !key.startsWith(legacyRoot)) {
|
|
8278
|
-
continue;
|
|
8279
|
-
}
|
|
8280
|
-
const suffix = key.slice(legacyRoot.length);
|
|
8281
|
-
if (isOwnedSuffix(suffix)) {
|
|
8282
|
-
moves.push([key, this.#key(suffix)]);
|
|
8283
|
-
}
|
|
8284
|
-
}
|
|
8285
|
-
for (const [from, to] of moves) {
|
|
8286
|
-
const value = sessionStorage.getItem(from);
|
|
8287
|
-
if (value !== null && sessionStorage.getItem(to) === null) {
|
|
8288
|
-
sessionStorage.setItem(to, value);
|
|
8289
|
-
}
|
|
8290
|
-
sessionStorage.removeItem(from);
|
|
8291
|
-
}
|
|
8292
|
-
}
|
|
8293
9222
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
8294
9223
|
#readJson(key) {
|
|
8295
9224
|
const raw = sessionStorage.getItem(key);
|
|
@@ -8303,8 +9232,22 @@ var SessionStorageStore = class {
|
|
|
8303
9232
|
}
|
|
8304
9233
|
}
|
|
8305
9234
|
};
|
|
9235
|
+
function ownedKeys(root) {
|
|
9236
|
+
const found = [];
|
|
9237
|
+
for (let index = 0; index < sessionStorage.length; index += 1) {
|
|
9238
|
+
const key = sessionStorage.key(index);
|
|
9239
|
+
if (key === null || !key.startsWith(root)) {
|
|
9240
|
+
continue;
|
|
9241
|
+
}
|
|
9242
|
+
const suffix = key.slice(root.length);
|
|
9243
|
+
if (isOwnedSuffix(suffix)) {
|
|
9244
|
+
found.push([key, suffix]);
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
return found;
|
|
9248
|
+
}
|
|
8306
9249
|
function isOwnedSuffix(suffix) {
|
|
8307
|
-
return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX);
|
|
9250
|
+
return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX) || suffix.startsWith(MINTED_SUFFIX);
|
|
8308
9251
|
}
|
|
8309
9252
|
function deriveTitle(messages) {
|
|
8310
9253
|
for (const message of messages) {
|
|
@@ -8349,12 +9292,27 @@ function mintThread(store) {
|
|
|
8349
9292
|
store.setActiveThread(id);
|
|
8350
9293
|
return id;
|
|
8351
9294
|
}
|
|
9295
|
+
function warnOnCrossOriginCredentials(url, credentialNames, trustedOrigins, warned) {
|
|
9296
|
+
if (credentialNames.length === 0) {
|
|
9297
|
+
return;
|
|
9298
|
+
}
|
|
9299
|
+
const destination = new URL(String(url), location.href).origin;
|
|
9300
|
+
if (destination === location.origin || trustedOrigins.includes(destination) || warned.has(destination)) {
|
|
9301
|
+
return;
|
|
9302
|
+
}
|
|
9303
|
+
warned.add(destination);
|
|
9304
|
+
console.warn(
|
|
9305
|
+
`<ag-ui-chat>: sending host credentials (${credentialNames.join(", ")}) to ${destination}, which is not this page's origin (${location.origin}). Those headers are the page's own authentication, and whichever server answers the browser's preflight receives them \u2014 so a URL attribute built from a query parameter or from tenant-authored configuration is a channel for the token to leave on. If this destination is deliberate, name it in \`trustedOrigins\` to confirm it and silence this notice. Reported once per origin.`
|
|
9306
|
+
);
|
|
9307
|
+
}
|
|
8352
9308
|
|
|
8353
9309
|
// src/core/create_http_agent.ts
|
|
8354
9310
|
function createHttpAgent(options) {
|
|
9311
|
+
const staticHeaders = options.headers ?? {};
|
|
9312
|
+
const warned = /* @__PURE__ */ new Set();
|
|
8355
9313
|
return new HttpAgent({
|
|
8356
9314
|
url: options.endpoint,
|
|
8357
|
-
headers:
|
|
9315
|
+
headers: staticHeaders,
|
|
8358
9316
|
initialState: { ...options.initialState ?? {} },
|
|
8359
9317
|
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
8360
9318
|
// rebinding the global `fetch` to the agent instance — "Illegal invocation"
|
|
@@ -8363,6 +9321,10 @@ function createHttpAgent(options) {
|
|
|
8363
9321
|
// own config having no seam for either.
|
|
8364
9322
|
fetch: (url, init) => {
|
|
8365
9323
|
const fresh = options.getHeaders?.();
|
|
9324
|
+
const credentialNames = [
|
|
9325
|
+
.../* @__PURE__ */ new Set([...Object.keys(staticHeaders), ...Object.keys(fresh ?? {})])
|
|
9326
|
+
].sort();
|
|
9327
|
+
warnOnCrossOriginCredentials(url, credentialNames, options.trustedOrigins ?? [], warned);
|
|
8366
9328
|
if (fresh === void 0) {
|
|
8367
9329
|
return fetch(url, withCredentials(init, options.credentials));
|
|
8368
9330
|
}
|
|
@@ -8385,13 +9347,34 @@ var RemoteConversationStore = class {
|
|
|
8385
9347
|
#headers;
|
|
8386
9348
|
#local;
|
|
8387
9349
|
#credentials;
|
|
9350
|
+
#cacheMessages;
|
|
8388
9351
|
#dropped = /* @__PURE__ */ new Set();
|
|
8389
9352
|
#renamed = /* @__PURE__ */ new Map();
|
|
8390
|
-
|
|
9353
|
+
/**
|
|
9354
|
+
* @param cacheMessages Whether to mirror message bodies into the local store.
|
|
9355
|
+
*
|
|
9356
|
+
* `true` (the default, and the behaviour this class has always had) keeps a
|
|
9357
|
+
* local copy of every turn, so the transcript still replays when the thread
|
|
9358
|
+
* endpoint is unreachable. `false` is for the deployment that chose a
|
|
9359
|
+
* server-backed store precisely so transcripts do not sit in the browser:
|
|
9360
|
+
* regulated content, a shared workstation, an operator who has to be able to
|
|
9361
|
+
* say where the conversation lives. It is not the same as passing a local
|
|
9362
|
+
* store that does nothing — the local store also owns the active thread id,
|
|
9363
|
+
* the navigation checkpoint and the "nothing sent here yet" marker, all of
|
|
9364
|
+
* which must keep working — so the opt-out is scoped to the bodies alone.
|
|
9365
|
+
*
|
|
9366
|
+
* The cost is deliberate and worth stating: with no local copy there is
|
|
9367
|
+
* nothing to fall back to, so a failed request shows an empty transcript
|
|
9368
|
+
* rather than a stale one, and the drawer's offline list loses its previews
|
|
9369
|
+
* (a preview is an excerpt of a message, which is the very thing being kept
|
|
9370
|
+
* off the client).
|
|
9371
|
+
*/
|
|
9372
|
+
constructor(url, headers = () => ({}), local = new SessionStorageStore(), credentials = () => void 0, cacheMessages = true) {
|
|
8391
9373
|
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
8392
9374
|
this.#headers = headers;
|
|
8393
9375
|
this.#local = local;
|
|
8394
9376
|
this.#credentials = credentials;
|
|
9377
|
+
this.#cacheMessages = cacheMessages;
|
|
8395
9378
|
}
|
|
8396
9379
|
threadId() {
|
|
8397
9380
|
return this.#local.threadId();
|
|
@@ -8412,7 +9395,7 @@ var RemoteConversationStore = class {
|
|
|
8412
9395
|
return this.#local.isUnsent?.(threadId) === true;
|
|
8413
9396
|
}
|
|
8414
9397
|
saveMessages(threadId, messages) {
|
|
8415
|
-
this.#local.saveMessages(threadId, messages);
|
|
9398
|
+
this.#local.saveMessages(threadId, this.#cacheMessages ? messages : []);
|
|
8416
9399
|
}
|
|
8417
9400
|
loadCheckpoint(threadId) {
|
|
8418
9401
|
return this.#local.loadCheckpoint(threadId);
|
|
@@ -8677,6 +9660,7 @@ var CONNECT_TIME_ATTRIBUTES = [
|
|
|
8677
9660
|
"data-attachment-max-bytes",
|
|
8678
9661
|
"data-transcribe-url",
|
|
8679
9662
|
"data-threads-url",
|
|
9663
|
+
"data-threads-cache",
|
|
8680
9664
|
"data-tools-url",
|
|
8681
9665
|
"data-skills-url",
|
|
8682
9666
|
"data-skills",
|
|
@@ -8693,6 +9677,8 @@ function isCredentialsMode(value) {
|
|
|
8693
9677
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
8694
9678
|
var SIZE_KEY = "ag-ui-chat:size";
|
|
8695
9679
|
var THEME_KEY = "ag-ui-chat:theme";
|
|
9680
|
+
var QUOTE_GAP = 6;
|
|
9681
|
+
var CLAIMED_NAMESPACES = /* @__PURE__ */ new Set();
|
|
8696
9682
|
var AgUiChat = class extends HTMLElement {
|
|
8697
9683
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
8698
9684
|
agentFactory = createHttpAgent;
|
|
@@ -8716,6 +9702,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8716
9702
|
* `Authorization` are configured independently and neither drops the other.
|
|
8717
9703
|
*/
|
|
8718
9704
|
getHeaders = null;
|
|
9705
|
+
/**
|
|
9706
|
+
* Origins, besides the page's own, that this element may send {@link headers}
|
|
9707
|
+
* and {@link getHeaders} credentials to without saying so on the console.
|
|
9708
|
+
*
|
|
9709
|
+
* Seven attributes name a URL, and every one of them carries these headers.
|
|
9710
|
+
* They are plain HTML, so a page that builds one from a query parameter or
|
|
9711
|
+
* from tenant-authored configuration has handed an attacker the destination,
|
|
9712
|
+
* and the token leaves on the element's first request. Naming the origins you
|
|
9713
|
+
* expect turns that from silent into either confirmed or reported.
|
|
9714
|
+
*
|
|
9715
|
+
* A notice rather than a refusal: a cross-origin agent is a documented
|
|
9716
|
+
* deployment, so refusing would break working installations to defend against
|
|
9717
|
+
* a page that is already interpolating untrusted data into its own markup.
|
|
9718
|
+
* Leaving this empty costs nothing but one console line per foreign origin.
|
|
9719
|
+
*/
|
|
9720
|
+
trustedOrigins = [];
|
|
8719
9721
|
/**
|
|
8720
9722
|
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
8721
9723
|
* model-controlled image URL is fetched with no user interaction, which
|
|
@@ -8723,6 +9725,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8723
9725
|
* data. Enable only when the content source is trusted.
|
|
8724
9726
|
*/
|
|
8725
9727
|
allowImages = false;
|
|
9728
|
+
/**
|
|
9729
|
+
* Replace the relative timestamps in the thread drawer and the checkpoint
|
|
9730
|
+
* panel -- `"5m ago"`, `"2d ago"` -- with the host's own formatting.
|
|
9731
|
+
*
|
|
9732
|
+
* The built-in is locale-neutral on purpose: there is no `Intl` anywhere in
|
|
9733
|
+
* this component, so it never disagrees with the page it is embedded in by
|
|
9734
|
+
* guessing a locale. That is a good default and a bad requirement, which is
|
|
9735
|
+
* what this is for.
|
|
9736
|
+
*
|
|
9737
|
+
* ```js
|
|
9738
|
+
* const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
|
|
9739
|
+
* chat.formatRelativeTime = (ts) =>
|
|
9740
|
+
* rtf.format(Math.round((ts - Date.now()) / 60000), "minute");
|
|
9741
|
+
* ```
|
|
9742
|
+
*/
|
|
9743
|
+
formatRelativeTime = null;
|
|
8726
9744
|
/** When true, destructive tools execute without a confirmation modal. */
|
|
8727
9745
|
autoConfirm = false;
|
|
8728
9746
|
/**
|
|
@@ -8746,6 +9764,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8746
9764
|
* is enabled server-side; this only changes how the decision is collected.
|
|
8747
9765
|
*/
|
|
8748
9766
|
approvalRenderer = null;
|
|
9767
|
+
/**
|
|
9768
|
+
* Let the user edit a gated call's arguments before approving it.
|
|
9769
|
+
*
|
|
9770
|
+
* Off by default and **an assertion about your server**, not a negotiation:
|
|
9771
|
+
* AG-UI carries `editedArgs` in the resume payload and gates it on the
|
|
9772
|
+
* agent's own `approveWithEdits` capability, which this component never sees
|
|
9773
|
+
* -- capabilities are not on the wire it reads. So the host says whether its
|
|
9774
|
+
* agent honours them. Turned on against a server that does not, the user
|
|
9775
|
+
* would edit arguments it silently discards, which is worse than not
|
|
9776
|
+
* offering.
|
|
9777
|
+
*
|
|
9778
|
+
* Only affects calls whose arguments are known here: an interrupt names a
|
|
9779
|
+
* `toolCallId`, and the tool card for that call is where the arguments still
|
|
9780
|
+
* are. An interrupt naming no card gets the plain approve/deny.
|
|
9781
|
+
*/
|
|
9782
|
+
approveWithEdits = false;
|
|
8749
9783
|
/**
|
|
8750
9784
|
* Optional per-call confirmation predicate. When set it is authoritative,
|
|
8751
9785
|
* deciding from the tool name and args whether this particular call needs
|
|
@@ -8851,13 +9885,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8851
9885
|
*/
|
|
8852
9886
|
resolvePageTarget = (target) => document.querySelector(target);
|
|
8853
9887
|
/**
|
|
8854
|
-
*
|
|
8855
|
-
*
|
|
8856
|
-
*
|
|
9888
|
+
* The server tool catalog fetched from `data-tools-url`, keyed by tool
|
|
9889
|
+
* name. Cards label themselves from each entry's `summary`, the base
|
|
9890
|
+
* layer behind {@link toolSummaries}: an explicit entry in `toolSummaries`
|
|
9891
|
+
* wins, this fills the rest. Held as whole entries rather than labels so a
|
|
9892
|
+
* field the server sent is not lost on the way in. Populated once on connect.
|
|
8857
9893
|
*/
|
|
8858
9894
|
#toolCatalog = {};
|
|
9895
|
+
/**
|
|
9896
|
+
* Foreign origins already reported, so the notice is once per origin per
|
|
9897
|
+
* element rather than once per request. Per-element rather than module-level,
|
|
9898
|
+
* because two elements on one page are two separate configurations.
|
|
9899
|
+
*/
|
|
9900
|
+
#warnedOrigins = /* @__PURE__ */ new Set();
|
|
8859
9901
|
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
8860
9902
|
#strings = DEFAULT_UI_STRINGS;
|
|
9903
|
+
/**
|
|
9904
|
+
* The tool names the current round handed the agent, captured as the catalog
|
|
9905
|
+
* went out.
|
|
9906
|
+
*
|
|
9907
|
+
* The registry is mount-wide but {@link getTools} is per-run, so a host is
|
|
9908
|
+
* free to scope what a given page offers — and a call naming a tool this run
|
|
9909
|
+
* withheld must not reach the handler that is merely still registered.
|
|
9910
|
+
* Snapshotted rather than re-asked at dispatch: a provider is a function, and
|
|
9911
|
+
* calling it again asks a question the run already answered, which is exactly
|
|
9912
|
+
* the window a scoped catalog exists to close.
|
|
9913
|
+
*
|
|
9914
|
+
* Empty until the first round advertises, which cannot precede a call: the
|
|
9915
|
+
* client builds `RunAgentInput.tools` at the top of every round, before the
|
|
9916
|
+
* calls that round produces are executed.
|
|
9917
|
+
*/
|
|
9918
|
+
#advertisedTools = /* @__PURE__ */ new Set();
|
|
8861
9919
|
#toolRegistry = new ClientToolRegistry();
|
|
8862
9920
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
8863
9921
|
#toolCards = /* @__PURE__ */ new Map();
|
|
@@ -8867,7 +9925,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8867
9925
|
* the real output with the generic "executed on the server" fallback.
|
|
8868
9926
|
*/
|
|
8869
9927
|
/** Whether a server-pushed chart activity is drawn. Off unless asked for. */
|
|
8870
|
-
|
|
9928
|
+
/**
|
|
9929
|
+
* Which `activity_type`s this element can draw, by name.
|
|
9930
|
+
*
|
|
9931
|
+
* A registry rather than a branch because `activity_type` is an open string
|
|
9932
|
+
* the protocol does not enumerate. The two built-ins go through it like any
|
|
9933
|
+
* host registration, which is the test that the seam is real.
|
|
9934
|
+
*/
|
|
9935
|
+
#activityRenderers = /* @__PURE__ */ new Map();
|
|
9936
|
+
/** Types that arrived with nobody registered to draw them. See {@link unhandledActivityTypes}. */
|
|
9937
|
+
#unhandledActivityTypes = /* @__PURE__ */ new Set();
|
|
8871
9938
|
/** Card elements by call id, so a rendering handler can find its own card. */
|
|
8872
9939
|
#cardElements = /* @__PURE__ */ new Map();
|
|
8873
9940
|
/** Chart blocks by activity message id, so an update redraws in place. */
|
|
@@ -8879,7 +9946,75 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8879
9946
|
* Spans tool rounds and an approval interrupt; cleared when the event fires.
|
|
8880
9947
|
*/
|
|
8881
9948
|
#runTools = [];
|
|
9949
|
+
/**
|
|
9950
|
+
* Keys announced during this interaction, de-duplicated in first-seen order.
|
|
9951
|
+
*
|
|
9952
|
+
* Per element, never module-level: a second mounted chat is a second run, and
|
|
9953
|
+
* sharing this would tell one page to refetch on the other's writes. Reset by
|
|
9954
|
+
* {@link AgUiChat.#dispatchRunFinished}, which is the one place that has read
|
|
9955
|
+
* it.
|
|
9956
|
+
*/
|
|
9957
|
+
/**
|
|
9958
|
+
* Tool names the user waived confirmation for, for the life of this element.
|
|
9959
|
+
*
|
|
9960
|
+
* Per instance and never persisted: a session decision that outlived the tab
|
|
9961
|
+
* would be a permanent grant made by one click, which is the thing
|
|
9962
|
+
* `autoConfirm` already exists to say deliberately. Cleared with the element.
|
|
9963
|
+
*/
|
|
9964
|
+
#sessionApproved = /* @__PURE__ */ new Set();
|
|
9965
|
+
/**
|
|
9966
|
+
* The one action row currently carrying Retry, if any.
|
|
9967
|
+
*
|
|
9968
|
+
* Retry belongs to the **last** turn only: re-running an older one is
|
|
9969
|
+
* branching, and for a page-driving agent editing a past turn is not neutral
|
|
9970
|
+
* -- those turns clicked buttons, and re-running turn 3 does not un-save what
|
|
9971
|
+
* turn 5 saved. Holding a single owner is what keeps exactly one offer on
|
|
9972
|
+
* screen without per-bubble bookkeeping.
|
|
9973
|
+
*/
|
|
9974
|
+
#retryOwner = null;
|
|
9975
|
+
#runInvalidated = /* @__PURE__ */ new Set();
|
|
8882
9976
|
#root;
|
|
9977
|
+
/** Screen-reader-only status region -- see {@link AgUiChat.#announce}. */
|
|
9978
|
+
#announcer = document.createElement("div");
|
|
9979
|
+
/** Return-to-foot affordance, shown only once something has been missed. */
|
|
9980
|
+
#jumpButton = document.createElement("button");
|
|
9981
|
+
/**
|
|
9982
|
+
* Offer to quote the current selection, floated beside it.
|
|
9983
|
+
*
|
|
9984
|
+
* Shares {@link AgUiChat.#messagesWrap} with the jump button for the same
|
|
9985
|
+
* reason: it is positioned against the transcript, and must not scroll away
|
|
9986
|
+
* with the words it is pointing at.
|
|
9987
|
+
*/
|
|
9988
|
+
#quoteButton = document.createElement("button");
|
|
9989
|
+
/** What {@link AgUiChat.#quoteButton} would quote, while it is showing. */
|
|
9990
|
+
#quoting = "";
|
|
9991
|
+
/** The host-page offer, while one is attached; see {@link AgUiChat.offerQuoteInPage}. */
|
|
9992
|
+
#pageQuote = null;
|
|
9993
|
+
/**
|
|
9994
|
+
* Positioning context for {@link AgUiChat.#jumpButton}.
|
|
9995
|
+
*
|
|
9996
|
+
* The button cannot live in the scrolling list -- it would scroll away with
|
|
9997
|
+
* the content it is offering to scroll to -- and it cannot be positioned
|
|
9998
|
+
* against the panel either: the panel's foot is below the composer, the skill
|
|
9999
|
+
* chips and the footer, so `bottom` measured from there lands the button on
|
|
10000
|
+
* top of the composer rather than over the transcript. This wrapper is the
|
|
10001
|
+
* only box whose foot *is* the transcript's foot.
|
|
10002
|
+
*/
|
|
10003
|
+
#messagesWrap = document.createElement("div");
|
|
10004
|
+
/** Follows the foot of the transcript, and stops when the reader scrolls away. */
|
|
10005
|
+
#scroller;
|
|
10006
|
+
/** Pending clear of {@link AgUiChat.#announcer}; see why it is cleared at all. */
|
|
10007
|
+
#announceTimer = null;
|
|
10008
|
+
/**
|
|
10009
|
+
* Whether this turn already announced how it ended.
|
|
10010
|
+
*
|
|
10011
|
+
* `onSettled` is the terminal guarantee and fires however the run ended, so
|
|
10012
|
+
* it is the only place that can promise the user hears *something*. But a
|
|
10013
|
+
* stopped or failed run has already said the truer thing from `onCancelled`
|
|
10014
|
+
* or `onError`, and "assistant answered" after "response stopped" is worse
|
|
10015
|
+
* than silence.
|
|
10016
|
+
*/
|
|
10017
|
+
#announcedOutcome = false;
|
|
8883
10018
|
#chat;
|
|
8884
10019
|
#messages;
|
|
8885
10020
|
#input;
|
|
@@ -8935,6 +10070,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8935
10070
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
8936
10071
|
// it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
|
|
8937
10072
|
#streamDeltas = 0;
|
|
10073
|
+
// The accumulated answer the next render will draw. Deltas overwrite it
|
|
10074
|
+
// (each one carries the whole answer), so a frame always draws the latest.
|
|
10075
|
+
#streamBuffer = "";
|
|
10076
|
+
// The frame that render is queued on, or `null` when nothing is queued —
|
|
10077
|
+
// also the flag saying a delta is still undrawn.
|
|
10078
|
+
#streamFrame = null;
|
|
8938
10079
|
#pending = null;
|
|
8939
10080
|
// The current assistant turn's grouping container. One `.answer`
|
|
8940
10081
|
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
@@ -8949,9 +10090,23 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8949
10090
|
#thoughts = null;
|
|
8950
10091
|
#threadId = "";
|
|
8951
10092
|
// Per-instance suffix for the origin-scoped storage keys (collapsed / theme /
|
|
8952
|
-
//
|
|
8953
|
-
//
|
|
10093
|
+
// size), so two instances on one origin don't clobber each other. Empty ⇒ the
|
|
10094
|
+
// pre-namespacing global keys (back-compat). Resolved on connect; the
|
|
10095
|
+
// conversation adds `user-key` on top of it, see #conversationNs.
|
|
8954
10096
|
#storageNs = "";
|
|
10097
|
+
// The entry this element put in CLAIMED_NAMESPACES, to take back out on
|
|
10098
|
+
// disconnect. `null` when it claimed nothing (no id, no endpoint, or it lost
|
|
10099
|
+
// the claim to an element that mounted first).
|
|
10100
|
+
#claimedNs = null;
|
|
10101
|
+
// The fallback namespace minted when the preferred one was already claimed,
|
|
10102
|
+
// with the preferred value it was minted for — so the element keeps it across
|
|
10103
|
+
// remounts, but re-resolves if the host answers the warning with an `id`.
|
|
10104
|
+
#generatedNs = "";
|
|
10105
|
+
#generatedFor = "";
|
|
10106
|
+
// The `sessionStorage`-backed store, which the element may therefore re-scope
|
|
10107
|
+
// on a principal change. `null` when the host injected a store of its own
|
|
10108
|
+
// kind, whose keying the element does not know and must not guess at.
|
|
10109
|
+
#builtinStore = null;
|
|
8955
10110
|
// Bumped on every #rehydrate; a replay whose generation is stale (a newer
|
|
8956
10111
|
// thread switch started while it awaited a slow store) drops its result.
|
|
8957
10112
|
#rehydrateGeneration = 0;
|
|
@@ -8977,6 +10132,23 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8977
10132
|
this.#launcher = document.createElement("button");
|
|
8978
10133
|
this.#badge = document.createElement("span");
|
|
8979
10134
|
this.#emptyWrap = document.createElement("div");
|
|
10135
|
+
this.registerActivityRenderer({
|
|
10136
|
+
type: COMPACTION_ACTIVITY_TYPE,
|
|
10137
|
+
render: (content) => {
|
|
10138
|
+
const removed = compactionRemoved(content);
|
|
10139
|
+
return removed === null ? null : renderRunNotice(
|
|
10140
|
+
"\u{1F5DC}",
|
|
10141
|
+
this.#strings.historyCompacted.replace("{count}", String(removed)),
|
|
10142
|
+
"compaction"
|
|
10143
|
+
);
|
|
10144
|
+
}
|
|
10145
|
+
});
|
|
10146
|
+
this.registerActivityRenderer({
|
|
10147
|
+
type: SUGGESTIONS_ACTIVITY_TYPE,
|
|
10148
|
+
render: (content) => renderSuggestionChips(content, this.#strings, (prompt) => {
|
|
10149
|
+
void this.sendMessage(prompt);
|
|
10150
|
+
})
|
|
10151
|
+
});
|
|
8980
10152
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
8981
10153
|
this.#drawer = new ThreadDrawer({
|
|
8982
10154
|
onSelect: (threadId) => {
|
|
@@ -9007,7 +10179,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9007
10179
|
if (this.#runIndex === null) {
|
|
9008
10180
|
this.#runIndex = new RunIndex(
|
|
9009
10181
|
url,
|
|
9010
|
-
() => this.#
|
|
10182
|
+
() => this.#headersFor(url),
|
|
9011
10183
|
() => this.#requestCredentials()
|
|
9012
10184
|
);
|
|
9013
10185
|
}
|
|
@@ -9041,6 +10213,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9041
10213
|
endpoint,
|
|
9042
10214
|
headers: this.#requestHeaders(),
|
|
9043
10215
|
getHeaders: () => this.#requestHeaders(),
|
|
10216
|
+
trustedOrigins: this.trustedOrigins,
|
|
9044
10217
|
...this.#credentialsOption(),
|
|
9045
10218
|
threadId: this.#threadId,
|
|
9046
10219
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
@@ -9049,7 +10222,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9049
10222
|
const client = new AgUiClient({
|
|
9050
10223
|
agent,
|
|
9051
10224
|
handlers: this.#handlers(),
|
|
9052
|
-
getTools: () => this
|
|
10225
|
+
getTools: () => this.#advertiseTools(),
|
|
9053
10226
|
getContext: () => this.#buildContext(),
|
|
9054
10227
|
executeTool: (call) => this.#executeTool(call),
|
|
9055
10228
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -9060,11 +10233,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9060
10233
|
/** Load the checkpoint panel with the runs that can actually be continued. */
|
|
9061
10234
|
async #refreshCheckpoints() {
|
|
9062
10235
|
const index = this.#runs();
|
|
10236
|
+
this.#checkpoints.setRelativeTimeFormatter(this.formatRelativeTime);
|
|
9063
10237
|
this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
|
|
9064
10238
|
}
|
|
9065
10239
|
/** Attributes the element reacts to after it has been connected. */
|
|
9066
10240
|
static get observedAttributes() {
|
|
9067
|
-
return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
|
|
10241
|
+
return ["title-text", "placement", "credentials", "user-key", ...CONNECT_TIME_ATTRIBUTES];
|
|
9068
10242
|
}
|
|
9069
10243
|
attributeChangedCallback(name, previous, value) {
|
|
9070
10244
|
if (name === "credentials") {
|
|
@@ -9084,6 +10258,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9084
10258
|
this.#title.textContent = value ?? this.#strings.title;
|
|
9085
10259
|
return;
|
|
9086
10260
|
}
|
|
10261
|
+
if (name === "user-key") {
|
|
10262
|
+
if (this.#connected && (previous ?? "") !== (value ?? "")) {
|
|
10263
|
+
this.#changePrincipal(previous ?? "", value ?? "");
|
|
10264
|
+
}
|
|
10265
|
+
return;
|
|
10266
|
+
}
|
|
9087
10267
|
if (previous === value || !this.#connected) {
|
|
9088
10268
|
return;
|
|
9089
10269
|
}
|
|
@@ -9091,7 +10271,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9091
10271
|
`<ag-ui-chat>: "${name}" was changed after the element connected, and is read only while connecting \u2014 this assignment has no effect. Set it before the element enters the DOM (in the markup, or on the element before appending it); frameworks that patch attributes after mount should bind it at creation. To apply a new value now, remove and re-insert the element.`
|
|
9092
10272
|
);
|
|
9093
10273
|
}
|
|
9094
|
-
/**
|
|
10274
|
+
/**
|
|
10275
|
+
* Declare a frontend tool the agent may call.
|
|
10276
|
+
*
|
|
10277
|
+
* **A handler's thrown message leaves the browser.** When a handler rejects,
|
|
10278
|
+
* its `Error.message` is posted back as that call's tool result — into the
|
|
10279
|
+
* conversation, on to the AG-UI endpoint, persisted server-side, and
|
|
10280
|
+
* forwarded to the model provider on every later round. That is deliberate,
|
|
10281
|
+
* since it is what lets the agent recover from a failure it caused; but it
|
|
10282
|
+
* means an internal hostname, a signed URL or a stack-derived path in a
|
|
10283
|
+
* rethrown error is disclosed to parties the host never chose. Throw the
|
|
10284
|
+
* message you would be content for the model to read, and log the detail.
|
|
10285
|
+
*/
|
|
9095
10286
|
registerTool(tool) {
|
|
9096
10287
|
this.#toolRegistry.register(tool);
|
|
9097
10288
|
}
|
|
@@ -9239,9 +10430,21 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9239
10430
|
});
|
|
9240
10431
|
this.#confirmAbort = null;
|
|
9241
10432
|
this.#updateEmptyState();
|
|
9242
|
-
this.#
|
|
10433
|
+
this.#scroller.follow();
|
|
9243
10434
|
return answer;
|
|
9244
10435
|
}
|
|
10436
|
+
/**
|
|
10437
|
+
* The catalog for the round about to start, remembering what it offered.
|
|
10438
|
+
*
|
|
10439
|
+
* Every path to a frontend tool goes through here first — the client asks
|
|
10440
|
+
* for `RunAgentInput.tools` at the top of each round — so this is the one
|
|
10441
|
+
* place that can know what the agent was actually told about.
|
|
10442
|
+
*/
|
|
10443
|
+
#advertiseTools() {
|
|
10444
|
+
const tools = this.getTools();
|
|
10445
|
+
this.#advertisedTools = new Set(tools.map((tool) => tool.name));
|
|
10446
|
+
return tools;
|
|
10447
|
+
}
|
|
9245
10448
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
9246
10449
|
#resolveTool(name) {
|
|
9247
10450
|
const builtin = this.#builtinTools().find((t) => t.name === name);
|
|
@@ -9260,6 +10463,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9260
10463
|
set endpoint(value) {
|
|
9261
10464
|
this.setAttribute("endpoint", value);
|
|
9262
10465
|
}
|
|
10466
|
+
/**
|
|
10467
|
+
* Who the stored conversation belongs to, from the `user-key` attribute.
|
|
10468
|
+
*
|
|
10469
|
+
* Set it to whatever identifies the signed-in principal — a user id, an
|
|
10470
|
+
* account id, a hash of one. The value joins the storage namespace, so two
|
|
10471
|
+
* principals in the same tab cannot read each other's transcript, and
|
|
10472
|
+
* **changing it purges what the previous one left behind**.
|
|
10473
|
+
*
|
|
10474
|
+
* That purge is the reason this is a live attribute rather than a
|
|
10475
|
+
* connect-time one. `sessionStorage` survives same-tab navigation, so it
|
|
10476
|
+
* survives a logout; and a single-page app signs out through its own router
|
|
10477
|
+
* without remounting anything, so there is no other moment at which the
|
|
10478
|
+
* element could find out. The host naming the new principal — or dropping the
|
|
10479
|
+
* attribute — is the signal.
|
|
10480
|
+
*
|
|
10481
|
+
* Absent means exactly today's behaviour, which is why nothing breaks by
|
|
10482
|
+
* leaving it off: the conversation is scoped to the element and to nobody in
|
|
10483
|
+
* particular, and on a shared workstation it carries into whoever signs in
|
|
10484
|
+
* next in the same tab.
|
|
10485
|
+
*
|
|
10486
|
+
* The first value to arrive is treated as a host naming the user who was
|
|
10487
|
+
* already there, not as a handover: the conversation in progress moves into
|
|
10488
|
+
* the principal's namespace rather than being destroyed, so an element
|
|
10489
|
+
* configured by an async auth handshake keeps what is on screen.
|
|
10490
|
+
*/
|
|
10491
|
+
get userKey() {
|
|
10492
|
+
return this.getAttribute("user-key") ?? "";
|
|
10493
|
+
}
|
|
10494
|
+
set userKey(value) {
|
|
10495
|
+
this.setAttribute("user-key", value);
|
|
10496
|
+
}
|
|
9263
10497
|
/**
|
|
9264
10498
|
* Cookie policy for **every** request this element makes, as `fetch`'s own
|
|
9265
10499
|
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
@@ -9304,6 +10538,24 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9304
10538
|
#requestHeaders() {
|
|
9305
10539
|
return { ...this.headers, ...this.getHeaders?.() };
|
|
9306
10540
|
}
|
|
10541
|
+
/**
|
|
10542
|
+
* The request headers, having first reported the destination if it is foreign.
|
|
10543
|
+
*
|
|
10544
|
+
* Every caller that sends these headers knows its URL, and `#requestHeaders`
|
|
10545
|
+
* does not -- so the check lives here, on the path that has both, rather than
|
|
10546
|
+
* being repeated at each call site with a chance to be forgotten at the next
|
|
10547
|
+
* one added.
|
|
10548
|
+
*/
|
|
10549
|
+
#headersFor(url) {
|
|
10550
|
+
const headers = this.#requestHeaders();
|
|
10551
|
+
warnOnCrossOriginCredentials(
|
|
10552
|
+
url,
|
|
10553
|
+
Object.keys(headers),
|
|
10554
|
+
this.trustedOrigins,
|
|
10555
|
+
this.#warnedOrigins
|
|
10556
|
+
);
|
|
10557
|
+
return headers;
|
|
10558
|
+
}
|
|
9307
10559
|
/** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
|
|
9308
10560
|
#requestCredentials() {
|
|
9309
10561
|
return this.credentials ?? void 0;
|
|
@@ -9322,8 +10574,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9322
10574
|
return credentials === void 0 ? {} : { credentials };
|
|
9323
10575
|
}
|
|
9324
10576
|
/** The `fetch` init for the element's own plain GETs (catalogs). */
|
|
9325
|
-
#fetchInit() {
|
|
9326
|
-
return withCredentials({ headers: this.#
|
|
10577
|
+
#fetchInit(url) {
|
|
10578
|
+
return withCredentials({ headers: this.#headersFor(url) }, this.#requestCredentials());
|
|
9327
10579
|
}
|
|
9328
10580
|
/**
|
|
9329
10581
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
@@ -9344,7 +10596,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9344
10596
|
this.setAttribute("data-tool-display", value);
|
|
9345
10597
|
}
|
|
9346
10598
|
connectedCallback() {
|
|
9347
|
-
this.#storageNs = this
|
|
10599
|
+
this.#storageNs = this.#claimNamespace();
|
|
9348
10600
|
this.#applySize(this.#readSize());
|
|
9349
10601
|
requestAnimationFrame(() => this.#syncResizeAnchor());
|
|
9350
10602
|
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
@@ -9362,8 +10614,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9362
10614
|
}
|
|
9363
10615
|
this.#syncLauncher();
|
|
9364
10616
|
this.#initSkills();
|
|
9365
|
-
if (this
|
|
9366
|
-
|
|
10617
|
+
if (this.conversationStore instanceof SessionStorageStore) {
|
|
10618
|
+
const namespace = this.#conversationNs();
|
|
10619
|
+
this.#builtinStore = namespace === "" ? this.conversationStore : new SessionStorageStore(namespace);
|
|
10620
|
+
this.conversationStore = this.#builtinStore;
|
|
9367
10621
|
}
|
|
9368
10622
|
this.#wireThreadStore();
|
|
9369
10623
|
this.#wireAttachments();
|
|
@@ -9427,9 +10681,20 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9427
10681
|
*/
|
|
9428
10682
|
disconnectedCallback() {
|
|
9429
10683
|
this.#connected = false;
|
|
10684
|
+
if (this.#claimedNs !== null) {
|
|
10685
|
+
CLAIMED_NAMESPACES.delete(this.#claimedNs);
|
|
10686
|
+
this.#claimedNs = null;
|
|
10687
|
+
}
|
|
9430
10688
|
this.#cancelRun();
|
|
10689
|
+
this.#pageQuote?.detach();
|
|
10690
|
+
this.#pageQuote = null;
|
|
9431
10691
|
this.#attachTray?.dispose();
|
|
9432
10692
|
this.#voice?.dispose();
|
|
10693
|
+
this.#scroller.dispose();
|
|
10694
|
+
if (this.#announceTimer !== null) {
|
|
10695
|
+
clearTimeout(this.#announceTimer);
|
|
10696
|
+
this.#announceTimer = null;
|
|
10697
|
+
}
|
|
9433
10698
|
}
|
|
9434
10699
|
/**
|
|
9435
10700
|
* Read an opt-in flag attribute the way HTML reads a boolean attribute.
|
|
@@ -9496,7 +10761,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9496
10761
|
}
|
|
9497
10762
|
return (file, onProgress, signal) => uploadAttachment(file, {
|
|
9498
10763
|
url,
|
|
9499
|
-
headers: this.#
|
|
10764
|
+
headers: this.#headersFor(url),
|
|
9500
10765
|
...this.#credentialsOption(),
|
|
9501
10766
|
onProgress,
|
|
9502
10767
|
signal
|
|
@@ -9529,7 +10794,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9529
10794
|
}
|
|
9530
10795
|
return (audio) => transcribeAudio(audio, {
|
|
9531
10796
|
url,
|
|
9532
|
-
headers: this.#
|
|
10797
|
+
headers: this.#headersFor(url),
|
|
9533
10798
|
...this.#credentialsOption()
|
|
9534
10799
|
});
|
|
9535
10800
|
}
|
|
@@ -9540,6 +10805,111 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9540
10805
|
this.#onInput();
|
|
9541
10806
|
this.#input.focus();
|
|
9542
10807
|
}
|
|
10808
|
+
/**
|
|
10809
|
+
* Put `text` into the composer as a markdown quotation, and focus it.
|
|
10810
|
+
*
|
|
10811
|
+
* Deliberately **not** a send. Quoting is how a question narrows to one part
|
|
10812
|
+
* of an answer, so the quotation is the preamble and the question is what
|
|
10813
|
+
* comes next -- the caret is left after it, on its own line.
|
|
10814
|
+
*
|
|
10815
|
+
* This is also the seam for the half of this feature the component cannot
|
|
10816
|
+
* build: selection in the **host page**. A widget mounted beside a table can
|
|
10817
|
+
* be asked about a row, and nothing in a chat's own transcript can offer
|
|
10818
|
+
* that. A host reads its own selection, however it likes, and calls this.
|
|
10819
|
+
*
|
|
10820
|
+
* No-ops on text that is empty or only whitespace.
|
|
10821
|
+
*/
|
|
10822
|
+
quote(text3) {
|
|
10823
|
+
const quoted = asQuote(text3);
|
|
10824
|
+
if (quoted === "") {
|
|
10825
|
+
return;
|
|
10826
|
+
}
|
|
10827
|
+
const current = this.#input.value.replace(/\s+$/, "");
|
|
10828
|
+
this.#input.value = current === "" ? quoted : `${current}
|
|
10829
|
+
|
|
10830
|
+
${quoted}`;
|
|
10831
|
+
this.#autoGrow();
|
|
10832
|
+
this.#input.focus();
|
|
10833
|
+
const end = this.#input.value.length;
|
|
10834
|
+
this.#input.setSelectionRange(end, end);
|
|
10835
|
+
}
|
|
10836
|
+
/**
|
|
10837
|
+
* Offer to quote what the user selects in the **host page**, not just in the
|
|
10838
|
+
* transcript. Returns a function that stops offering.
|
|
10839
|
+
*
|
|
10840
|
+
* The same select-then-offer gesture, over a table, a diff, a report -- the
|
|
10841
|
+
* surface the user actually works in, which is the half of quoting no hosted
|
|
10842
|
+
* chat can reach. Opt-in, because it listens on the host's document and that
|
|
10843
|
+
* is theirs to grant.
|
|
10844
|
+
*
|
|
10845
|
+
* Deliberately **not** a four-line recipe, which is how this shipped first
|
|
10846
|
+
* and was wrong: a page listener that quotes every settled selection appends
|
|
10847
|
+
* to the composer on every drag made to read, to copy or to fix a typo -- and
|
|
10848
|
+
* it cannot tell a selection in the page's prose from one inside the user's
|
|
10849
|
+
* own half-typed `<input>`, because Chrome reports a field's internal
|
|
10850
|
+
* selection as an ordinary range over the field's *wrapper*. See
|
|
10851
|
+
* {@link attachQuoteOffer} for the guards.
|
|
10852
|
+
*
|
|
10853
|
+
* Detached automatically when the element leaves the document; a host that
|
|
10854
|
+
* re-mounts it calls this again.
|
|
10855
|
+
*/
|
|
10856
|
+
offerQuoteInPage(within = document.body) {
|
|
10857
|
+
this.#pageQuote?.detach();
|
|
10858
|
+
const offer = attachQuoteOffer({
|
|
10859
|
+
within,
|
|
10860
|
+
label: this.#strings.quoteSelection,
|
|
10861
|
+
exclude: this,
|
|
10862
|
+
onQuote: (text3) => this.quote(text3)
|
|
10863
|
+
});
|
|
10864
|
+
this.#pageQuote = offer;
|
|
10865
|
+
return () => {
|
|
10866
|
+
offer.detach();
|
|
10867
|
+
if (this.#pageQuote === offer) {
|
|
10868
|
+
this.#pageQuote = null;
|
|
10869
|
+
}
|
|
10870
|
+
};
|
|
10871
|
+
}
|
|
10872
|
+
/** Whether the transcript offers to quote what the user selects. */
|
|
10873
|
+
#quoteEnabled() {
|
|
10874
|
+
return this.getAttribute("data-quote-selection") !== "false";
|
|
10875
|
+
}
|
|
10876
|
+
/**
|
|
10877
|
+
* Offer to quote the settled selection, or retire the offer.
|
|
10878
|
+
*
|
|
10879
|
+
* `event` is passed for its coordinates and only those: they say which line
|
|
10880
|
+
* of a selection spanning several messages the offer should hang from. A
|
|
10881
|
+
* keyboard selection has none, and the first line is used instead.
|
|
10882
|
+
*/
|
|
10883
|
+
#onSelectionSettled(event) {
|
|
10884
|
+
if (!this.#quoteEnabled()) {
|
|
10885
|
+
return;
|
|
10886
|
+
}
|
|
10887
|
+
const near = event === void 0 ? void 0 : { x: event.clientX, y: event.clientY };
|
|
10888
|
+
const selected = quotableSelection(this.#messages, [this.#root], near);
|
|
10889
|
+
if (selected === null) {
|
|
10890
|
+
this.#hideQuote();
|
|
10891
|
+
return;
|
|
10892
|
+
}
|
|
10893
|
+
this.#quoting = selected.text;
|
|
10894
|
+
this.#placeQuote(selected.rect);
|
|
10895
|
+
}
|
|
10896
|
+
/** Float the offer beside `rect`, kept inside the transcript's own box. */
|
|
10897
|
+
#placeQuote(rect) {
|
|
10898
|
+
this.#quoteButton.hidden = false;
|
|
10899
|
+
const wrap = this.#messagesWrap.getBoundingClientRect();
|
|
10900
|
+
const top = rect.top - wrap.top;
|
|
10901
|
+
const below = top < QUOTE_GAP + this.#quoteButton.offsetHeight;
|
|
10902
|
+
this.#quoteButton.dataset["below"] = String(below);
|
|
10903
|
+
this.#quoteButton.style.top = `${below ? rect.bottom - wrap.top + QUOTE_GAP : top - QUOTE_GAP}px`;
|
|
10904
|
+
const half = this.#quoteButton.offsetWidth / 2;
|
|
10905
|
+
const centre = rect.left + rect.width / 2 - wrap.left;
|
|
10906
|
+
this.#quoteButton.style.left = `${Math.min(Math.max(centre, half), wrap.width - half)}px`;
|
|
10907
|
+
}
|
|
10908
|
+
/** Retire the offer, and forget what it was pointing at. */
|
|
10909
|
+
#hideQuote() {
|
|
10910
|
+
this.#quoteButton.hidden = true;
|
|
10911
|
+
this.#quoting = "";
|
|
10912
|
+
}
|
|
9543
10913
|
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
9544
10914
|
#attachmentMaxBytes() {
|
|
9545
10915
|
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
@@ -9584,15 +10954,21 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9584
10954
|
* delete through that server endpoint (wrapping the current store as the
|
|
9585
10955
|
* client-only fallback), so the history drawer shows durable, cross-device
|
|
9586
10956
|
* threads. Without it, the client store's per-tab threads are used.
|
|
10957
|
+
*
|
|
10958
|
+
* `data-threads-cache="false"` drops the local copy of the message bodies —
|
|
10959
|
+
* for the deployment that pointed history at a server precisely so that
|
|
10960
|
+
* transcripts do not sit in the browser. The client-only concerns (the active
|
|
10961
|
+
* thread id, the navigation checkpoint) keep their local store either way.
|
|
9587
10962
|
*/
|
|
9588
10963
|
#wireThreadStore() {
|
|
9589
10964
|
const url = this.getAttribute("data-threads-url");
|
|
9590
10965
|
if (url !== null) {
|
|
9591
10966
|
this.conversationStore = new RemoteConversationStore(
|
|
9592
10967
|
url,
|
|
9593
|
-
() => this.#
|
|
10968
|
+
() => this.#headersFor(url),
|
|
9594
10969
|
this.conversationStore,
|
|
9595
|
-
() => this.#requestCredentials()
|
|
10970
|
+
() => this.#requestCredentials(),
|
|
10971
|
+
this.getAttribute("data-threads-cache") !== "false"
|
|
9596
10972
|
);
|
|
9597
10973
|
}
|
|
9598
10974
|
}
|
|
@@ -9603,7 +10979,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9603
10979
|
return;
|
|
9604
10980
|
}
|
|
9605
10981
|
try {
|
|
9606
|
-
const response = await fetch(url, this.#fetchInit());
|
|
10982
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9607
10983
|
this.#toolCatalog = parseToolCatalog(await response.json());
|
|
9608
10984
|
} catch {
|
|
9609
10985
|
}
|
|
@@ -9646,7 +11022,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9646
11022
|
return;
|
|
9647
11023
|
}
|
|
9648
11024
|
try {
|
|
9649
|
-
const response = await fetch(url, this.#fetchInit());
|
|
11025
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9650
11026
|
this.#backendSkills = parseSkills(await response.json());
|
|
9651
11027
|
this.#recomputeSkills();
|
|
9652
11028
|
} catch {
|
|
@@ -9729,7 +11105,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9729
11105
|
} else {
|
|
9730
11106
|
this.removeAttribute("collapsed");
|
|
9731
11107
|
}
|
|
9732
|
-
|
|
11108
|
+
writeStoredItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
9733
11109
|
this.#setUnread(0);
|
|
9734
11110
|
this.dispatchEvent(
|
|
9735
11111
|
new CustomEvent(TOGGLE_EVENT, {
|
|
@@ -9761,7 +11137,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9761
11137
|
toggleTheme() {
|
|
9762
11138
|
const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
|
|
9763
11139
|
this.setAttribute("theme", next);
|
|
9764
|
-
|
|
11140
|
+
writeStoredItem(this.#storageKey(THEME_KEY), next);
|
|
9765
11141
|
this.#syncThemeGlyph();
|
|
9766
11142
|
}
|
|
9767
11143
|
/**
|
|
@@ -9863,7 +11239,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9863
11239
|
/** Persist a dragged size per tab, alongside the collapsed/theme preferences. */
|
|
9864
11240
|
#persistSize(size) {
|
|
9865
11241
|
const stored = { ...this.#readSize(), ...size };
|
|
9866
|
-
|
|
11242
|
+
writeStoredItem(this.#storageKey(SIZE_KEY), JSON.stringify(stored));
|
|
9867
11243
|
}
|
|
9868
11244
|
/** The persisted size for this instance, or an empty record. */
|
|
9869
11245
|
#readSize() {
|
|
@@ -9878,6 +11254,106 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9878
11254
|
return {};
|
|
9879
11255
|
}
|
|
9880
11256
|
}
|
|
11257
|
+
/**
|
|
11258
|
+
* Claim this element's storage namespace: its `id`, else its `endpoint`.
|
|
11259
|
+
*
|
|
11260
|
+
* The endpoint fallback exists so a lone widget restores its conversation
|
|
11261
|
+
* across reloads with nothing asked of the page author. It stops working the
|
|
11262
|
+
* moment there are two of them — a docked support panel and an inline page
|
|
11263
|
+
* assistant against one agent mount, neither carrying an `id`, which nothing
|
|
11264
|
+
* requires — because both resolve to the same string and then share a thread
|
|
11265
|
+
* pointer, a drawer index and every message key. Whichever mounts second
|
|
11266
|
+
* adopts the first's active thread and rehydrates its transcript into its own
|
|
11267
|
+
* panel: one conversation's content inside another, on the same page.
|
|
11268
|
+
*
|
|
11269
|
+
* So the namespace is claimed by the first element to mount under it, and a
|
|
11270
|
+
* second is given one of its own plus a warning naming the fix. The first
|
|
11271
|
+
* element keeps the endpoint namespace, which is what leaves the ordinary
|
|
11272
|
+
* single-element case exactly as it was.
|
|
11273
|
+
*
|
|
11274
|
+
* The generated namespace is random rather than derived from mount order.
|
|
11275
|
+
* That costs the second element its history across reloads — the warning says
|
|
11276
|
+
* so, and an `id` fixes it — which is the honest trade against an order-based
|
|
11277
|
+
* name that would silently hand a stored conversation to whichever element
|
|
11278
|
+
* happened to mount second on the next load.
|
|
11279
|
+
*/
|
|
11280
|
+
#claimNamespace() {
|
|
11281
|
+
const preferred = this.id !== "" ? this.id : this.endpoint;
|
|
11282
|
+
if (preferred === "") {
|
|
11283
|
+
return "";
|
|
11284
|
+
}
|
|
11285
|
+
if (this.#generatedFor === preferred) {
|
|
11286
|
+
return this.#generatedNs;
|
|
11287
|
+
}
|
|
11288
|
+
if (!CLAIMED_NAMESPACES.has(preferred)) {
|
|
11289
|
+
CLAIMED_NAMESPACES.add(preferred);
|
|
11290
|
+
this.#claimedNs = preferred;
|
|
11291
|
+
return preferred;
|
|
11292
|
+
}
|
|
11293
|
+
this.#generatedFor = preferred;
|
|
11294
|
+
this.#generatedNs = `${preferred}~${randomUUID5()}`;
|
|
11295
|
+
console.warn(
|
|
11296
|
+
`<ag-ui-chat>: another element on this page already stores its conversation under "${preferred}", so this one has been given a throwaway namespace of its own \u2014 the two would otherwise share a thread pointer, a history drawer and every message. Give each <ag-ui-chat> its own id to keep them apart and let this one restore its conversation across reloads.`
|
|
11297
|
+
);
|
|
11298
|
+
return this.#generatedNs;
|
|
11299
|
+
}
|
|
11300
|
+
/**
|
|
11301
|
+
* The conversation store's namespace: this element's, scoped to the principal
|
|
11302
|
+
* {@link userKey} names.
|
|
11303
|
+
*
|
|
11304
|
+
* Only the conversation is principal-scoped. The panel's own collapsed / size
|
|
11305
|
+
* / theme preferences stay on `#storageNs`, because they are this element's
|
|
11306
|
+
* UI state rather than anyone's data — they carry no word of what was said —
|
|
11307
|
+
* and because they are read once while connecting, so re-scoping them under a
|
|
11308
|
+
* live element would rearrange the panel around a user who had only just
|
|
11309
|
+
* signed in.
|
|
11310
|
+
*/
|
|
11311
|
+
#conversationNs(key = this.userKey) {
|
|
11312
|
+
return key === "" ? this.#storageNs : `${this.#storageNs}#${key}`;
|
|
11313
|
+
}
|
|
11314
|
+
/**
|
|
11315
|
+
* Move the element's client state from one principal to another.
|
|
11316
|
+
*
|
|
11317
|
+
* The whole reason {@link userKey} is live: `sessionStorage` outlives a
|
|
11318
|
+
* logout, because a logout is a navigation (or, in a single-page app, not
|
|
11319
|
+
* even that) rather than a tab close. Nothing remounts, so the host naming
|
|
11320
|
+
* the new principal is the only signal the element will ever get.
|
|
11321
|
+
*/
|
|
11322
|
+
#changePrincipal(previousKey, nextKey) {
|
|
11323
|
+
const previous = this.#conversationNs(previousKey);
|
|
11324
|
+
const next = this.#conversationNs(nextKey);
|
|
11325
|
+
if (previousKey === "") {
|
|
11326
|
+
SessionStorageStore.adopt(previous, next);
|
|
11327
|
+
this.#rescopeStore(next);
|
|
11328
|
+
return;
|
|
11329
|
+
}
|
|
11330
|
+
SessionStorageStore.purge(previous);
|
|
11331
|
+
this.#rescopeStore(next);
|
|
11332
|
+
this.#cancelRun();
|
|
11333
|
+
this.#resetState();
|
|
11334
|
+
this.#setRunning(false);
|
|
11335
|
+
this.#setUnread(0);
|
|
11336
|
+
this.#threadId = this.conversationStore.threadId();
|
|
11337
|
+
void this.#rehydrate();
|
|
11338
|
+
void this.#refreshDrawer();
|
|
11339
|
+
}
|
|
11340
|
+
/**
|
|
11341
|
+
* Rebuild the `sessionStorage` store under `namespace`, re-wrapping it for
|
|
11342
|
+
* `data-threads-url` exactly as connecting did.
|
|
11343
|
+
*
|
|
11344
|
+
* A store of the host's own kind is left alone: a store that holds its data
|
|
11345
|
+
* somewhere the element cannot see has to scope itself. The transcript on
|
|
11346
|
+
* screen is still cleared either way — the host swapped principals, and that
|
|
11347
|
+
* much is the element's to act on.
|
|
11348
|
+
*/
|
|
11349
|
+
#rescopeStore(namespace) {
|
|
11350
|
+
if (this.#builtinStore === null) {
|
|
11351
|
+
return;
|
|
11352
|
+
}
|
|
11353
|
+
this.#builtinStore = new SessionStorageStore(namespace);
|
|
11354
|
+
this.conversationStore = this.#builtinStore;
|
|
11355
|
+
this.#wireThreadStore();
|
|
11356
|
+
}
|
|
9881
11357
|
/** This instance's namespaced form of an origin-scoped storage key. */
|
|
9882
11358
|
#storageKey(base) {
|
|
9883
11359
|
return this.#storageNs === "" ? base : `${base}:${this.#storageNs}`;
|
|
@@ -9963,7 +11439,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9963
11439
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
9964
11440
|
#resetState() {
|
|
9965
11441
|
this.#client = null;
|
|
9966
|
-
this.#
|
|
11442
|
+
this.#clearTranscript();
|
|
11443
|
+
this.#initialMessages = [];
|
|
11444
|
+
}
|
|
11445
|
+
/**
|
|
11446
|
+
* Wipe the rendered transcript and everything that indexes into it.
|
|
11447
|
+
*
|
|
11448
|
+
* Split from {@link #resetState} because a retry re-renders the transcript
|
|
11449
|
+
* while keeping the *client*: dropping the client there would take the
|
|
11450
|
+
* agent's message list with it, which is the thing being truncated.
|
|
11451
|
+
*/
|
|
11452
|
+
#clearTranscript() {
|
|
11453
|
+
this.#endStream();
|
|
9967
11454
|
this.#currentGroup = null;
|
|
9968
11455
|
this.#thoughts = null;
|
|
9969
11456
|
this.#hidePending();
|
|
@@ -9971,11 +11458,43 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9971
11458
|
this.#serverSettled.clear();
|
|
9972
11459
|
this.#cardElements.clear();
|
|
9973
11460
|
this.#activityBlocks.clear();
|
|
9974
|
-
this.#
|
|
11461
|
+
this.#retryOwner = null;
|
|
9975
11462
|
this.#attachTray?.clear();
|
|
9976
11463
|
this.#messages.replaceChildren(this.#emptyWrap);
|
|
9977
11464
|
this.#updateEmptyState();
|
|
9978
11465
|
}
|
|
11466
|
+
/**
|
|
11467
|
+
* Ask the same question again and replace the answer.
|
|
11468
|
+
*
|
|
11469
|
+
* History is truncated to the most recent user message inclusive and the run
|
|
11470
|
+
* repeats, so the agent answers what it was asked rather than being told its
|
|
11471
|
+
* last answer was wrong. Returns `false` when there is nothing to retry or a
|
|
11472
|
+
* run is already in flight.
|
|
11473
|
+
*
|
|
11474
|
+
* Public because a host with its own message UI wants the same button, and
|
|
11475
|
+
* because the failed-run notice reaches it from outside the action row.
|
|
11476
|
+
*
|
|
11477
|
+
* **A retried turn re-runs its tools**, which for a page-driving agent is not
|
|
11478
|
+
* neutral: the previous attempt already clicked what it clicked, and this
|
|
11479
|
+
* does not undo it. Confirmation still applies, so a destructive tool asks
|
|
11480
|
+
* again -- unless the user waived it for this session.
|
|
11481
|
+
*/
|
|
11482
|
+
async retryLastTurn() {
|
|
11483
|
+
if (this.#running) {
|
|
11484
|
+
return false;
|
|
11485
|
+
}
|
|
11486
|
+
const client = this.#ensureClient();
|
|
11487
|
+
const kept = client.truncateToLastUser();
|
|
11488
|
+
if (kept === null) {
|
|
11489
|
+
return false;
|
|
11490
|
+
}
|
|
11491
|
+
this.#clearTranscript();
|
|
11492
|
+
for (const message of kept) {
|
|
11493
|
+
this.#renderHistoricMessage(message);
|
|
11494
|
+
}
|
|
11495
|
+
await client.resume();
|
|
11496
|
+
return true;
|
|
11497
|
+
}
|
|
9979
11498
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
9980
11499
|
async #switchThread(threadId) {
|
|
9981
11500
|
if (threadId === this.#threadId) {
|
|
@@ -10004,6 +11523,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10004
11523
|
}
|
|
10005
11524
|
/** Reload the drawer's thread list, marking the active thread. */
|
|
10006
11525
|
async #refreshDrawer() {
|
|
11526
|
+
this.#drawer.setRelativeTimeFormatter(this.formatRelativeTime);
|
|
10007
11527
|
this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
|
|
10008
11528
|
}
|
|
10009
11529
|
/**
|
|
@@ -10095,7 +11615,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10095
11615
|
}
|
|
10096
11616
|
if (message.role === MESSAGE_ROLE.ASSISTANT) {
|
|
10097
11617
|
if (text3 !== "") {
|
|
10098
|
-
this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3)
|
|
11618
|
+
const restoredBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, text3);
|
|
11619
|
+
restoredBubble.classList.add("message--restored");
|
|
11620
|
+
this.#attachActions(restoredBubble);
|
|
10099
11621
|
}
|
|
10100
11622
|
for (const call of restoredToolCalls(message.toolCalls)) {
|
|
10101
11623
|
const restored = {
|
|
@@ -10116,8 +11638,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10116
11638
|
}
|
|
10117
11639
|
if (message.role === "activity") {
|
|
10118
11640
|
const activity = message;
|
|
10119
|
-
if (activity.activityType ===
|
|
10120
|
-
this.#
|
|
11641
|
+
if (typeof activity.activityType === "string") {
|
|
11642
|
+
this.#drawActivity(message.id, activity.activityType, activity.content);
|
|
10121
11643
|
}
|
|
10122
11644
|
return;
|
|
10123
11645
|
}
|
|
@@ -10182,7 +11704,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10182
11704
|
this.#messages.appendChild(bubble);
|
|
10183
11705
|
}
|
|
10184
11706
|
this.#updateEmptyState();
|
|
10185
|
-
|
|
11707
|
+
if (role === MESSAGE_ROLE.USER) {
|
|
11708
|
+
this.#scroller.jump();
|
|
11709
|
+
} else {
|
|
11710
|
+
this.#scroller.follow();
|
|
11711
|
+
}
|
|
10186
11712
|
return bubble;
|
|
10187
11713
|
}
|
|
10188
11714
|
/**
|
|
@@ -10204,8 +11730,6 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10204
11730
|
return this.#currentGroup;
|
|
10205
11731
|
}
|
|
10206
11732
|
#render() {
|
|
10207
|
-
const style = document.createElement("style");
|
|
10208
|
-
style.textContent = STYLES;
|
|
10209
11733
|
this.#chat.className = "chat";
|
|
10210
11734
|
this.#chat.setAttribute("part", "panel");
|
|
10211
11735
|
const header = document.createElement("div");
|
|
@@ -10229,8 +11753,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10229
11753
|
checkpoints.addEventListener("click", () => this.toggleCheckpoints());
|
|
10230
11754
|
const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
|
|
10231
11755
|
newChat.addEventListener("click", () => this.newChat());
|
|
10232
|
-
const
|
|
10233
|
-
|
|
11756
|
+
const collapse2 = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
11757
|
+
collapse2.addEventListener("click", () => this.toggleCollapsed());
|
|
10234
11758
|
if (this.#runs() !== null) {
|
|
10235
11759
|
controls.append(history, checkpoints, newChat);
|
|
10236
11760
|
} else {
|
|
@@ -10246,13 +11770,46 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10246
11770
|
this.#syncThemeGlyph();
|
|
10247
11771
|
controls.append(this.#themeToggle);
|
|
10248
11772
|
}
|
|
10249
|
-
controls.append(
|
|
11773
|
+
controls.append(collapse2);
|
|
10250
11774
|
header.append(title, headerActions, controls);
|
|
10251
11775
|
this.#messages.className = "messages";
|
|
10252
11776
|
this.#messages.setAttribute("part", "messages");
|
|
10253
11777
|
this.#messages.setAttribute("role", "log");
|
|
10254
|
-
this.#messages.setAttribute("aria-live", "
|
|
11778
|
+
this.#messages.setAttribute("aria-live", "off");
|
|
10255
11779
|
this.#messages.setAttribute("aria-label", this.#strings.conversation);
|
|
11780
|
+
this.#jumpButton.className = "jump-latest";
|
|
11781
|
+
this.#jumpButton.type = "button";
|
|
11782
|
+
this.#jumpButton.setAttribute("part", "jump-latest");
|
|
11783
|
+
this.#jumpButton.textContent = this.#strings.jumpToLatest;
|
|
11784
|
+
this.#jumpButton.addEventListener("click", () => {
|
|
11785
|
+
this.#scroller.jump();
|
|
11786
|
+
});
|
|
11787
|
+
this.#quoteButton.className = "quote-selection";
|
|
11788
|
+
this.#quoteButton.type = "button";
|
|
11789
|
+
this.#quoteButton.setAttribute("part", "quote-selection");
|
|
11790
|
+
this.#quoteButton.textContent = this.#strings.quoteSelection;
|
|
11791
|
+
this.#quoteButton.hidden = true;
|
|
11792
|
+
this.#quoteButton.addEventListener("mousedown", (event) => {
|
|
11793
|
+
event.preventDefault();
|
|
11794
|
+
});
|
|
11795
|
+
this.#quoteButton.addEventListener("click", () => {
|
|
11796
|
+
this.quote(this.#quoting);
|
|
11797
|
+
window.getSelection()?.removeAllRanges();
|
|
11798
|
+
this.#hideQuote();
|
|
11799
|
+
});
|
|
11800
|
+
this.#messages.addEventListener("mouseup", (event) => this.#onSelectionSettled(event));
|
|
11801
|
+
this.#messages.addEventListener("keyup", () => this.#onSelectionSettled());
|
|
11802
|
+
this.#messages.addEventListener("mousedown", () => this.#hideQuote());
|
|
11803
|
+
this.#scroller = createStickToBottom({
|
|
11804
|
+
viewport: this.#messages,
|
|
11805
|
+
onMissedContent: (missed) => {
|
|
11806
|
+
this.#jumpButton.dataset["missed"] = String(missed);
|
|
11807
|
+
}
|
|
11808
|
+
});
|
|
11809
|
+
this.#announcer.className = "sr-only";
|
|
11810
|
+
this.#announcer.setAttribute("role", "status");
|
|
11811
|
+
this.#announcer.setAttribute("aria-live", "polite");
|
|
11812
|
+
this.#announcer.setAttribute("aria-atomic", "true");
|
|
10256
11813
|
this.#emptyWrap.className = "empty";
|
|
10257
11814
|
this.#emptyWrap.setAttribute("part", "empty");
|
|
10258
11815
|
const emptySlot = document.createElement("slot");
|
|
@@ -10315,9 +11872,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10315
11872
|
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
10316
11873
|
composer.append(this.#input, tools);
|
|
10317
11874
|
inputRow.append(composer, this.#fileInput);
|
|
11875
|
+
this.#messagesWrap.className = "messages-wrap";
|
|
11876
|
+
this.#messagesWrap.append(this.#messages, this.#jumpButton, this.#quoteButton);
|
|
10318
11877
|
this.#chat.append(
|
|
10319
11878
|
header,
|
|
10320
|
-
this.#
|
|
11879
|
+
this.#messagesWrap,
|
|
10321
11880
|
this.#skillsMenu.palette,
|
|
10322
11881
|
this.#skillsMenu.chips,
|
|
10323
11882
|
this.#skillHint,
|
|
@@ -10363,7 +11922,66 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10363
11922
|
label: this.#strings.resizePanel
|
|
10364
11923
|
})
|
|
10365
11924
|
);
|
|
10366
|
-
this.#
|
|
11925
|
+
this.#adoptStyles();
|
|
11926
|
+
this.#root.append(this.#announcer, this.#chat, this.#launcher);
|
|
11927
|
+
}
|
|
11928
|
+
/**
|
|
11929
|
+
* Attach the stylesheet without an inline `<style>` element.
|
|
11930
|
+
*
|
|
11931
|
+
* A host with a strict `style-src` and no `'unsafe-inline'` drops an injected
|
|
11932
|
+
* `<style>` silently: the component mounts, functions, and renders completely
|
|
11933
|
+
* unstyled, with nothing in the console to point at. `adoptedStyleSheets`
|
|
11934
|
+
* carries no inline-style origin, so it is unaffected by that policy.
|
|
11935
|
+
*
|
|
11936
|
+
* The sheet is constructed **per instance** rather than shared at module
|
|
11937
|
+
* scope. A shared sheet would additionally avoid re-parsing the stylesheet
|
|
11938
|
+
* once per mounted element, which is what `adoptedStyleSheets` is usually
|
|
11939
|
+
* reached for -- but a module-level singleton is exactly what this package
|
|
11940
|
+
* forbids, and the CSP defect is fixed either way. Per instance is no worse
|
|
11941
|
+
* than the `<style>` element it replaces, which also parsed once per mount.
|
|
11942
|
+
*
|
|
11943
|
+
* No fallback: constructible `CSSStyleSheet` is Chrome 73, Firefox 101 and
|
|
11944
|
+
* Safari 16.4, all below this package's declared Safari 17 runtime target. A
|
|
11945
|
+
* guard here would be code no supported browser can reach, and the only way
|
|
11946
|
+
* to keep it would be to exempt it from the coverage gate.
|
|
11947
|
+
*/
|
|
11948
|
+
/**
|
|
11949
|
+
* Say one short thing to a screen reader, without touching the transcript.
|
|
11950
|
+
*
|
|
11951
|
+
* The transcript cannot do this job. It is rewritten on every animation
|
|
11952
|
+
* frame while an answer streams, so as a live region it re-announced the
|
|
11953
|
+
* whole answer tens of times per turn -- not merely unhelpful but actively
|
|
11954
|
+
* hostile. The published fix for this exact bug (Microsoft's Bot Framework
|
|
11955
|
+
* WebChat #3236) is architectural rather than a matter of tuning attributes:
|
|
11956
|
+
* demote the visible transcript out of live-region duty and put one
|
|
11957
|
+
* synthesised status per event into a separate invisible region. MDN and
|
|
11958
|
+
* Scott O'Hara prescribe the same empty-region-then-inject shape.
|
|
11959
|
+
*
|
|
11960
|
+
* Roughly four calls land per turn -- responding, answered, a card is waiting,
|
|
11961
|
+
* stopped or failed -- so the user is told what happened and reads the answer
|
|
11962
|
+
* itself by navigating the log, at their own pace, rather than having it
|
|
11963
|
+
* shouted at them a token at a time.
|
|
11964
|
+
*
|
|
11965
|
+
* **The clear is load-bearing, twice.** A reader announces a live region when
|
|
11966
|
+
* its content *changes*, so setting the same string twice running -- two turns
|
|
11967
|
+
* in a row both starting -- is not a change and is silently not announced.
|
|
11968
|
+
* Emptying first makes the next set a change again. It also stops a stale
|
|
11969
|
+
* status being read out when a reader later lands on the region.
|
|
11970
|
+
*/
|
|
11971
|
+
#announce(message) {
|
|
11972
|
+
if (this.#announceTimer !== null) {
|
|
11973
|
+
clearTimeout(this.#announceTimer);
|
|
11974
|
+
}
|
|
11975
|
+
this.#announcer.textContent = message;
|
|
11976
|
+
this.#announceTimer = setTimeout(() => {
|
|
11977
|
+
this.#announceTimer = null;
|
|
11978
|
+
this.#announcer.textContent = "";
|
|
11979
|
+
}, ANNOUNCE_CLEAR_MS);
|
|
11980
|
+
}
|
|
11981
|
+
#adoptStyles() {
|
|
11982
|
+
const sheet = new CSSStyleSheet();
|
|
11983
|
+
sheet.replaceSync(STYLES);
|
|
11984
|
+
this.#root.adoptedStyleSheets = [sheet];
|
|
10367
11985
|
}
|
|
10368
11986
|
/**
|
|
10369
11987
|
* Build a header control button: a named slot a host can project markup into,
|
|
@@ -10644,6 +12262,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10644
12262
|
// token must still reach every request — the factory's fetch wrapper
|
|
10645
12263
|
// re-reads this on each call.
|
|
10646
12264
|
getHeaders: () => this.#requestHeaders(),
|
|
12265
|
+
trustedOrigins: this.trustedOrigins,
|
|
10647
12266
|
...this.#credentialsOption(),
|
|
10648
12267
|
threadId: this.#threadId,
|
|
10649
12268
|
initialMessages: this.#initialMessages,
|
|
@@ -10652,7 +12271,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10652
12271
|
this.#client = new AgUiClient({
|
|
10653
12272
|
agent,
|
|
10654
12273
|
handlers: this.#handlers(),
|
|
10655
|
-
getTools: () => this
|
|
12274
|
+
getTools: () => this.#advertiseTools(),
|
|
10656
12275
|
getContext: () => this.#buildContext(),
|
|
10657
12276
|
executeTool: (call) => this.#executeTool(call),
|
|
10658
12277
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -10674,15 +12293,69 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10674
12293
|
})
|
|
10675
12294
|
);
|
|
10676
12295
|
}
|
|
10677
|
-
/**
|
|
10678
|
-
|
|
12296
|
+
/**
|
|
12297
|
+
* Give a finished assistant bubble its action row, and hand it Retry.
|
|
12298
|
+
*
|
|
12299
|
+
* Every finished bubble gets copy and feedback -- both are safe on a message
|
|
12300
|
+
* of any age. Retry moves to the newest, because it is the only one where
|
|
12301
|
+
* re-running answers the same question rather than rewriting history.
|
|
12302
|
+
*/
|
|
12303
|
+
#attachActions(bubble, options = {}) {
|
|
12304
|
+
attachMessageActions(bubble, {
|
|
12305
|
+
strings: this.#strings,
|
|
12306
|
+
// Read at click time, not captured: a bubble rendered from markdown holds
|
|
12307
|
+
// its text in the DOM, and that is what the user sees and means to copy.
|
|
12308
|
+
text: () => bubble.textContent,
|
|
12309
|
+
// A failed run is copyable -- error text is what people paste into a bug
|
|
12310
|
+
// report -- but not rateable: a rating is a statement about an *answer*,
|
|
12311
|
+
// and mixing "the connection dropped" into that signal makes the host's
|
|
12312
|
+
// feedback data say less than it did before.
|
|
12313
|
+
...options.rateable === false ? {} : {
|
|
12314
|
+
onFeedback: (rating) => {
|
|
12315
|
+
this.dispatchEvent(
|
|
12316
|
+
new CustomEvent(FEEDBACK_EVENT, {
|
|
12317
|
+
detail: { content: bubble.textContent, rating },
|
|
12318
|
+
bubbles: true,
|
|
12319
|
+
composed: true
|
|
12320
|
+
})
|
|
12321
|
+
);
|
|
12322
|
+
}
|
|
12323
|
+
}
|
|
12324
|
+
});
|
|
12325
|
+
this.#moveRetryTo(messageActionBar(bubble, this.#strings));
|
|
12326
|
+
}
|
|
12327
|
+
/** Move the Retry button onto `bar`, taking it off whoever held it. */
|
|
12328
|
+
#moveRetryTo(bar) {
|
|
12329
|
+
this.#retryOwner?.querySelector(".message-action--retry")?.remove();
|
|
12330
|
+
const retry = messageActionButton("retry", this.#strings.retryMessage, "\u21BB");
|
|
12331
|
+
retry.addEventListener("click", () => {
|
|
12332
|
+
void this.retryLastTurn();
|
|
12333
|
+
});
|
|
12334
|
+
bar.prepend(retry);
|
|
12335
|
+
this.#retryOwner = bar;
|
|
12336
|
+
}
|
|
12337
|
+
/**
|
|
12338
|
+
* Which rule gates `call`, or `null` when it runs straight through.
|
|
12339
|
+
*
|
|
12340
|
+
* The rule, rather than a bare boolean, because it decides whether the user
|
|
12341
|
+
* may *waive* the prompt for the rest of the session. Only the default
|
|
12342
|
+
* `x-destructive` gate is waivable: `confirmPredicate` is documented as
|
|
12343
|
+
* authoritative, so letting one click retire it would silently defeat a host
|
|
12344
|
+
* policy — and the session allowlist is consulted on the same path it can
|
|
12345
|
+
* be added from, so the button is never offered where honouring it would be
|
|
12346
|
+
* refused.
|
|
12347
|
+
*/
|
|
12348
|
+
async #confirmationRule(call, tool) {
|
|
10679
12349
|
if (this.autoConfirm) {
|
|
10680
|
-
return
|
|
12350
|
+
return null;
|
|
10681
12351
|
}
|
|
10682
12352
|
if (this.confirmPredicate !== null) {
|
|
10683
|
-
return await this.confirmPredicate(call.name, call.args) === true;
|
|
12353
|
+
return await this.confirmPredicate(call.name, call.args) === true ? "predicate" : null;
|
|
12354
|
+
}
|
|
12355
|
+
if (this.#sessionApproved.has(call.name)) {
|
|
12356
|
+
return null;
|
|
10684
12357
|
}
|
|
10685
|
-
return isDestructive(tool.parameters);
|
|
12358
|
+
return isDestructive(tool.parameters) ? "destructive" : null;
|
|
10686
12359
|
}
|
|
10687
12360
|
async #executeTool(call) {
|
|
10688
12361
|
if (skillNameFrom(call) !== null) {
|
|
@@ -10691,7 +12364,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10691
12364
|
const card = this.#cardFor(call);
|
|
10692
12365
|
this.#toolCards.delete(call.id);
|
|
10693
12366
|
this.#cardElements.set(call.id, card.element);
|
|
10694
|
-
const tool = this.#resolveTool(call.name);
|
|
12367
|
+
const tool = this.#advertisedTools.has(call.name) ? this.#resolveTool(call.name) : null;
|
|
10695
12368
|
if (tool === null) {
|
|
10696
12369
|
if (!this.#serverSettled.has(call.id)) {
|
|
10697
12370
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10704,7 +12377,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10704
12377
|
this.#showPending();
|
|
10705
12378
|
return { content: `Error: ${message}`, error: message };
|
|
10706
12379
|
}
|
|
10707
|
-
|
|
12380
|
+
const rule = await this.#confirmationRule(call, tool);
|
|
12381
|
+
if (rule !== null) {
|
|
10708
12382
|
const request = { toolName: call.name, args: call.args };
|
|
10709
12383
|
const confirmText = tool.parameters[X_CONFIRM_KEY];
|
|
10710
12384
|
if (typeof confirmText === "string") {
|
|
@@ -10713,10 +12387,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10713
12387
|
this.#confirmAbort = new AbortController();
|
|
10714
12388
|
const decision = requestConfirmation(this.#ensureGroup(), request, {
|
|
10715
12389
|
signal: this.#confirmAbort.signal,
|
|
10716
|
-
strings: this.#strings
|
|
12390
|
+
strings: this.#strings,
|
|
12391
|
+
// Offered only where it can be honoured -- see `#confirmationRule`.
|
|
12392
|
+
...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
|
|
10717
12393
|
});
|
|
10718
12394
|
this.#updateEmptyState();
|
|
10719
|
-
this.#
|
|
12395
|
+
this.#scroller.follow();
|
|
10720
12396
|
const accepted = await decision;
|
|
10721
12397
|
this.#confirmAbort = null;
|
|
10722
12398
|
card.recordDecision(accepted ? "approved" : "declined");
|
|
@@ -10778,6 +12454,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10778
12454
|
*/
|
|
10779
12455
|
async #resolveInterrupts(interrupts) {
|
|
10780
12456
|
this.#confirmAbort = new AbortController();
|
|
12457
|
+
this.#announce(
|
|
12458
|
+
this.#strings.announceAwaitingDecision.replace("{count}", String(interrupts.length))
|
|
12459
|
+
);
|
|
10781
12460
|
this.#hidePending();
|
|
10782
12461
|
const signal = this.#confirmAbort.signal;
|
|
10783
12462
|
const answered = await Promise.all(
|
|
@@ -10792,10 +12471,20 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10792
12471
|
if (toolName !== null && toolName !== void 0) {
|
|
10793
12472
|
request.toolName = toolName;
|
|
10794
12473
|
}
|
|
12474
|
+
let editedArgs;
|
|
12475
|
+
const editable = this.approveWithEdits && card !== void 0;
|
|
12476
|
+
if (editable) {
|
|
12477
|
+
request.args = card.args;
|
|
12478
|
+
}
|
|
10795
12479
|
card?.mark(TOOL_CALL_STATUS.DEFERRED);
|
|
10796
12480
|
const approved = this.approvalRenderer !== null ? await this.approvalRenderer(request, { signal }) : await requestApproval(card?.approvalSlot ?? this.#ensureGroup(), request, {
|
|
10797
12481
|
signal,
|
|
10798
|
-
strings: this.#strings
|
|
12482
|
+
strings: this.#strings,
|
|
12483
|
+
...editable ? {
|
|
12484
|
+
onEdit: (args) => {
|
|
12485
|
+
editedArgs = args;
|
|
12486
|
+
}
|
|
12487
|
+
} : {}
|
|
10799
12488
|
});
|
|
10800
12489
|
card?.recordDecision(approved ? "approved" : "declined");
|
|
10801
12490
|
if (approved) {
|
|
@@ -10803,21 +12492,28 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10803
12492
|
} else {
|
|
10804
12493
|
card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
|
|
10805
12494
|
}
|
|
10806
|
-
return { id: interrupt.id, approved };
|
|
12495
|
+
return { id: interrupt.id, approved, editedArgs };
|
|
10807
12496
|
})
|
|
10808
12497
|
);
|
|
10809
12498
|
this.#updateEmptyState();
|
|
10810
|
-
this.#
|
|
12499
|
+
this.#scroller.follow();
|
|
10811
12500
|
this.#confirmAbort = null;
|
|
10812
12501
|
const responses = {};
|
|
10813
|
-
for (const { id, approved } of answered) {
|
|
10814
|
-
responses[id] = approved ? {
|
|
12502
|
+
for (const { id, approved, editedArgs } of answered) {
|
|
12503
|
+
responses[id] = approved ? {
|
|
12504
|
+
status: "resolved",
|
|
12505
|
+
payload: editedArgs === void 0 ? { approved: true } : { approved: true, editedArgs }
|
|
12506
|
+
} : { status: "cancelled" };
|
|
10815
12507
|
}
|
|
10816
12508
|
return responses;
|
|
10817
12509
|
}
|
|
10818
12510
|
#handlers() {
|
|
10819
12511
|
return {
|
|
10820
12512
|
onRunStart: () => {
|
|
12513
|
+
if (!this.#running) {
|
|
12514
|
+
this.#announcedOutcome = false;
|
|
12515
|
+
this.#announce(this.#strings.announceResponding);
|
|
12516
|
+
}
|
|
10821
12517
|
this.#setRunning(true);
|
|
10822
12518
|
this.#ensureGroup();
|
|
10823
12519
|
this.#showPending();
|
|
@@ -10834,7 +12530,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10834
12530
|
onTextDelta: (buffer) => {
|
|
10835
12531
|
this.#hidePending();
|
|
10836
12532
|
this.#thoughts?.collapse();
|
|
10837
|
-
this.#
|
|
12533
|
+
this.#queueStream(buffer);
|
|
10838
12534
|
this.#streamDeltas += 1;
|
|
10839
12535
|
},
|
|
10840
12536
|
onTextEnd: (buffer) => {
|
|
@@ -10843,7 +12539,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10843
12539
|
this.#revealWords(bubble);
|
|
10844
12540
|
}
|
|
10845
12541
|
attachCopyButtons(bubble, this.#strings);
|
|
10846
|
-
this.#
|
|
12542
|
+
this.#attachActions(bubble);
|
|
12543
|
+
this.#endStream();
|
|
10847
12544
|
this.#noteUnread();
|
|
10848
12545
|
},
|
|
10849
12546
|
onToolCall: (call) => {
|
|
@@ -10855,25 +12552,24 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10855
12552
|
this.#cardFor(call);
|
|
10856
12553
|
},
|
|
10857
12554
|
onActivity: (activityType, content, messageId) => {
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
}
|
|
10864
|
-
if (activityType !== COMPACTION_ACTIVITY_TYPE) {
|
|
10865
|
-
return;
|
|
10866
|
-
}
|
|
10867
|
-
const removed = compactionRemoved(content);
|
|
10868
|
-
if (removed === null) {
|
|
12555
|
+
this.#drawActivity(messageId, activityType, content);
|
|
12556
|
+
},
|
|
12557
|
+
onCustomEvent: (name, value) => {
|
|
12558
|
+
if (name === INVALIDATE_CUSTOM_NAME) {
|
|
12559
|
+
this.#dispatchInvalidation(value);
|
|
10869
12560
|
return;
|
|
10870
12561
|
}
|
|
10871
|
-
this
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
12562
|
+
this.dispatchEvent(
|
|
12563
|
+
new CustomEvent(CUSTOM_AGENT_EVENT, {
|
|
12564
|
+
detail: { name, value },
|
|
12565
|
+
bubbles: true,
|
|
12566
|
+
composed: true
|
|
12567
|
+
})
|
|
10875
12568
|
);
|
|
10876
12569
|
},
|
|
12570
|
+
onMessagesSnapshot: () => {
|
|
12571
|
+
this.#appendNotice("\u{1F504}", this.#strings.historyReplaced, "history-replaced");
|
|
12572
|
+
},
|
|
10877
12573
|
onToolResult: (toolCallId, content) => {
|
|
10878
12574
|
const card = this.#toolCards.get(toolCallId);
|
|
10879
12575
|
if (card === void 0) {
|
|
@@ -10884,28 +12580,36 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10884
12580
|
this.#showPending();
|
|
10885
12581
|
},
|
|
10886
12582
|
onActivityChanged: (messageId, activityType, content) => {
|
|
10887
|
-
|
|
10888
|
-
this.#drawActivityChart(messageId, content);
|
|
10889
|
-
}
|
|
12583
|
+
this.#drawActivity(messageId, activityType, content);
|
|
10890
12584
|
},
|
|
10891
12585
|
onRunEnd: () => {
|
|
10892
12586
|
this.#hidePending();
|
|
10893
|
-
this.#
|
|
12587
|
+
this.#endStream();
|
|
10894
12588
|
},
|
|
10895
12589
|
onError: (message) => {
|
|
12590
|
+
this.#announcedOutcome = true;
|
|
12591
|
+
this.#announce(this.#strings.announceFailed);
|
|
10896
12592
|
this.#hidePending();
|
|
10897
|
-
this
|
|
10898
|
-
|
|
12593
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`);
|
|
12594
|
+
bubble.classList.add("message--failed");
|
|
12595
|
+
this.#attachActions(bubble, { rateable: false });
|
|
12596
|
+
this.#revealWords(bubble);
|
|
12597
|
+
this.#endStream();
|
|
10899
12598
|
},
|
|
10900
12599
|
onCancelled: () => {
|
|
12600
|
+
this.#announcedOutcome = true;
|
|
12601
|
+
this.#announce(this.#strings.announceStopped);
|
|
10901
12602
|
this.#hidePending();
|
|
10902
12603
|
this.#appendStoppedNote();
|
|
10903
|
-
this.#
|
|
12604
|
+
this.#endStream();
|
|
10904
12605
|
},
|
|
10905
12606
|
onSettled: () => {
|
|
12607
|
+
if (!this.#announcedOutcome) {
|
|
12608
|
+
this.#announce(this.#strings.announceAnswerReady);
|
|
12609
|
+
}
|
|
10906
12610
|
this.#hidePending();
|
|
10907
12611
|
this.#setRunning(false);
|
|
10908
|
-
this.#
|
|
12612
|
+
this.#endStream();
|
|
10909
12613
|
for (const card of this.#toolCards.values()) {
|
|
10910
12614
|
if (!card.settled) {
|
|
10911
12615
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10936,9 +12640,42 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10936
12640
|
side: this.#serverSettled.has(id) ? "server" : "client"
|
|
10937
12641
|
}));
|
|
10938
12642
|
this.#runTools = [];
|
|
12643
|
+
const invalidated = [...this.#runInvalidated];
|
|
12644
|
+
this.#runInvalidated = /* @__PURE__ */ new Set();
|
|
10939
12645
|
this.dispatchEvent(
|
|
10940
12646
|
new CustomEvent(RUN_FINISHED_EVENT, {
|
|
10941
|
-
detail: { tools },
|
|
12647
|
+
detail: { tools, invalidated },
|
|
12648
|
+
bubbles: true,
|
|
12649
|
+
composed: true
|
|
12650
|
+
})
|
|
12651
|
+
);
|
|
12652
|
+
}
|
|
12653
|
+
/**
|
|
12654
|
+
* Route one invalidation to the host, and remember it for the run summary.
|
|
12655
|
+
*
|
|
12656
|
+
* Dispatched immediately rather than only at the end, because that is what
|
|
12657
|
+
* makes a long multi-step run feel live -- the list refreshes as the third of
|
|
12658
|
+
* eight writes lands. The accumulated set rides
|
|
12659
|
+
* {@link RUN_FINISHED_EVENT} as well, so a host that would rather refetch once
|
|
12660
|
+
* upgrades by reading one extra field instead of adding a listener.
|
|
12661
|
+
*
|
|
12662
|
+
* Nothing is rendered, persisted or replayed. An invalidation is an
|
|
12663
|
+
* imperative: it has no place in the transcript and no meaning once acted on,
|
|
12664
|
+
* and replaying one on every thread load would be a refetch storm. That is the
|
|
12665
|
+
* whole reason the server sends it as `CUSTOM` rather than as an activity.
|
|
12666
|
+
*/
|
|
12667
|
+
#dispatchInvalidation(value) {
|
|
12668
|
+
const payload = value ?? {};
|
|
12669
|
+
const keys = Array.isArray(payload.keys) ? payload.keys.filter((key) => typeof key === "string") : [];
|
|
12670
|
+
if (keys.length === 0) {
|
|
12671
|
+
return;
|
|
12672
|
+
}
|
|
12673
|
+
for (const key of keys) {
|
|
12674
|
+
this.#runInvalidated.add(key);
|
|
12675
|
+
}
|
|
12676
|
+
this.dispatchEvent(
|
|
12677
|
+
new CustomEvent(INVALIDATE_EVENT, {
|
|
12678
|
+
detail: { keys, reason: typeof payload.reason === "string" ? payload.reason : null },
|
|
10942
12679
|
bubbles: true,
|
|
10943
12680
|
composed: true
|
|
10944
12681
|
})
|
|
@@ -10953,7 +12690,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10953
12690
|
note.textContent = this.#strings.stopped;
|
|
10954
12691
|
this.#ensureGroup().appendChild(note);
|
|
10955
12692
|
this.#updateEmptyState();
|
|
10956
|
-
this.#
|
|
12693
|
+
this.#scroller.follow();
|
|
10957
12694
|
}
|
|
10958
12695
|
/**
|
|
10959
12696
|
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
@@ -10977,7 +12714,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10977
12714
|
this.#pending = pending;
|
|
10978
12715
|
this.#ensureGroup().appendChild(pending);
|
|
10979
12716
|
this.#updateEmptyState();
|
|
10980
|
-
this.#
|
|
12717
|
+
this.#scroller.follow();
|
|
10981
12718
|
}
|
|
10982
12719
|
/** Remove the pending indicator if shown. */
|
|
10983
12720
|
#hidePending() {
|
|
@@ -10995,19 +12732,76 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10995
12732
|
const group = this.#ensureGroup();
|
|
10996
12733
|
group.insertBefore(this.#thoughts.element, group.firstChild);
|
|
10997
12734
|
this.#updateEmptyState();
|
|
10998
|
-
this.#
|
|
12735
|
+
this.#scroller.follow();
|
|
10999
12736
|
}
|
|
11000
12737
|
return this.#thoughts;
|
|
11001
12738
|
}
|
|
11002
|
-
|
|
12739
|
+
/**
|
|
12740
|
+
* The bubble the current answer streams into, opening it on first sight.
|
|
12741
|
+
*
|
|
12742
|
+
* Opened the moment a token arrives rather than on the frame that draws it,
|
|
12743
|
+
* so the answer's container replaces the pending dots straight away and the
|
|
12744
|
+
* turn never shows a gap while the first render waits for a frame.
|
|
12745
|
+
*/
|
|
12746
|
+
#openStream() {
|
|
11003
12747
|
if (this.#streamingBubble === null) {
|
|
11004
12748
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
11005
12749
|
this.#streamDeltas = 0;
|
|
11006
12750
|
}
|
|
11007
|
-
this.#streamingBubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
11008
|
-
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
11009
12751
|
return this.#streamingBubble;
|
|
11010
12752
|
}
|
|
12753
|
+
/**
|
|
12754
|
+
* Queue a render of the answer so far, at most one per frame.
|
|
12755
|
+
*
|
|
12756
|
+
* Each `TEXT_MESSAGE_CONTENT` event carries the *whole* accumulated answer,
|
|
12757
|
+
* and drawing it means marked + DOMPurify over the entire document and a
|
|
12758
|
+
* wholesale replacement of the bubble's subtree. Once per token that is
|
|
12759
|
+
* quadratic in the answer's length — a long answer is agent-controlled, so
|
|
12760
|
+
* an ordinary run becomes a progressively stalling tab — and every rebuild
|
|
12761
|
+
* takes any selection or focus inside the bubble with it.
|
|
12762
|
+
*
|
|
12763
|
+
* A frame is the right grain: it is the fastest anything on screen can
|
|
12764
|
+
* change anyway, so a burst of tokens costs one parse and the text still
|
|
12765
|
+
* appears to flow rather than in visible chunks.
|
|
12766
|
+
*/
|
|
12767
|
+
#queueStream(buffer) {
|
|
12768
|
+
this.#streamBuffer = buffer;
|
|
12769
|
+
this.#openStream();
|
|
12770
|
+
if (this.#streamFrame !== null) {
|
|
12771
|
+
return;
|
|
12772
|
+
}
|
|
12773
|
+
this.#streamFrame = requestAnimationFrame(() => {
|
|
12774
|
+
this.#streamFrame = null;
|
|
12775
|
+
this.#streamInto(this.#streamBuffer);
|
|
12776
|
+
});
|
|
12777
|
+
}
|
|
12778
|
+
/** Render `buffer` into the streaming bubble now, dropping any queued frame. */
|
|
12779
|
+
#streamInto(buffer) {
|
|
12780
|
+
if (this.#streamFrame !== null) {
|
|
12781
|
+
cancelAnimationFrame(this.#streamFrame);
|
|
12782
|
+
this.#streamFrame = null;
|
|
12783
|
+
}
|
|
12784
|
+
this.#streamBuffer = buffer;
|
|
12785
|
+
const bubble = this.#openStream();
|
|
12786
|
+
bubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
12787
|
+
this.#scroller.follow();
|
|
12788
|
+
return bubble;
|
|
12789
|
+
}
|
|
12790
|
+
/**
|
|
12791
|
+
* Close the current answer's streaming bubble.
|
|
12792
|
+
*
|
|
12793
|
+
* Draws a queued render first. A run that ends without a text end — a
|
|
12794
|
+
* cancel, an error, a round boundary — leaves the last delta sitting in the
|
|
12795
|
+
* queue, and simply dropping the bubble here would strand it: the partial
|
|
12796
|
+
* answer the user stopped mid-sentence would lose its final tokens, or be an
|
|
12797
|
+
* empty bubble above the stopped note.
|
|
12798
|
+
*/
|
|
12799
|
+
#endStream() {
|
|
12800
|
+
if (this.#streamFrame !== null) {
|
|
12801
|
+
this.#streamInto(this.#streamBuffer);
|
|
12802
|
+
}
|
|
12803
|
+
this.#streamingBubble = null;
|
|
12804
|
+
}
|
|
11011
12805
|
/**
|
|
11012
12806
|
* The card for ``call``, creating and appending it on first sight.
|
|
11013
12807
|
*
|
|
@@ -11038,7 +12832,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11038
12832
|
#appendNotice(icon, text3, kind) {
|
|
11039
12833
|
this.#ensureGroup().appendChild(renderRunNotice(icon, text3, kind));
|
|
11040
12834
|
this.#updateEmptyState();
|
|
11041
|
-
this.#
|
|
12835
|
+
this.#scroller.follow();
|
|
11042
12836
|
}
|
|
11043
12837
|
/**
|
|
11044
12838
|
* Turn on chart rendering, by whichever route this consumer wants.
|
|
@@ -11059,9 +12853,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11059
12853
|
* arrives is not something to switch on for everybody.
|
|
11060
12854
|
*/
|
|
11061
12855
|
enableCharts(routes = ["tool", "activity"]) {
|
|
11062
|
-
const first = !this.#
|
|
12856
|
+
const first = !this.#activityRenderers.has(CHART_ACTIVITY_TYPE) && !this.#toolRegistry.has(CHART_TOOL_NAME);
|
|
11063
12857
|
if (routes.includes("activity")) {
|
|
11064
|
-
this
|
|
12858
|
+
this.registerActivityRenderer({
|
|
12859
|
+
type: CHART_ACTIVITY_TYPE,
|
|
12860
|
+
render: (content) => {
|
|
12861
|
+
const spec = chartSpecFrom(content);
|
|
12862
|
+
return spec === null ? null : renderChart(spec);
|
|
12863
|
+
},
|
|
12864
|
+
removedNotice: this.#strings.chartUndrawable
|
|
12865
|
+
});
|
|
11065
12866
|
}
|
|
11066
12867
|
if (routes.includes("tool")) {
|
|
11067
12868
|
this.registerTool(createChartTool());
|
|
@@ -11094,27 +12895,120 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11094
12895
|
this.#cardElements.get(call.id)?.after(node);
|
|
11095
12896
|
this.#afterTranscriptGrew();
|
|
11096
12897
|
}
|
|
11097
|
-
/**
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
12898
|
+
/**
|
|
12899
|
+
* Teach this element to draw one kind of AG-UI activity.
|
|
12900
|
+
*
|
|
12901
|
+
* `activity_type` is one of exactly two fields the protocol leaves an open
|
|
12902
|
+
* string, and it is the **content** one: an activity is materialised into a
|
|
12903
|
+
* message, persisted with the thread, and replayed on every restore. Its
|
|
12904
|
+
* sibling `CUSTOM` carries an imperative and is dispatched to the page
|
|
12905
|
+
* instead ({@link CUSTOM_AGENT_EVENT}).
|
|
12906
|
+
*
|
|
12907
|
+
* That asymmetry decides which carrier a server should use. Content has a
|
|
12908
|
+
* place in the conversation and should come back; an imperative has no place
|
|
12909
|
+
* and no meaning once acted on.
|
|
12910
|
+
*
|
|
12911
|
+
* ```js
|
|
12912
|
+
* chat.registerActivityRenderer({
|
|
12913
|
+
* type: "build_status",
|
|
12914
|
+
* render: (content) => {
|
|
12915
|
+
* const el = document.createElement("div");
|
|
12916
|
+
* el.textContent = `Build ${content.status}`;
|
|
12917
|
+
* return el;
|
|
12918
|
+
* },
|
|
12919
|
+
* });
|
|
12920
|
+
* ```
|
|
12921
|
+
*
|
|
12922
|
+
* Registering a type twice replaces the earlier renderer, so a host can
|
|
12923
|
+
* override a built-in -- `chart` and `compaction` are registrations like any
|
|
12924
|
+
* other, not privileged branches.
|
|
12925
|
+
*
|
|
12926
|
+
* ⚠ `render` runs again on every thread load. See {@link ActivityRenderer}
|
|
12927
|
+
* for what that requires of it.
|
|
12928
|
+
*/
|
|
12929
|
+
registerActivityRenderer(registration) {
|
|
12930
|
+
this.#activityRenderers.set(registration.type, registration);
|
|
12931
|
+
this.#unhandledActivityTypes.delete(registration.type);
|
|
12932
|
+
}
|
|
12933
|
+
/**
|
|
12934
|
+
* Activity types that arrived with nobody registered to draw them.
|
|
12935
|
+
*
|
|
12936
|
+
* Deliberately the only trace an unhandled activity leaves. Ignoring an
|
|
12937
|
+
* unknown name is the protocol's own answer and the whole point of an open
|
|
12938
|
+
* field, so warning would fire on every forward-compatible server -- but
|
|
12939
|
+
* "nothing happened and nothing was said" is impossible to debug, so the set
|
|
12940
|
+
* is readable. Accumulates for the element's lifetime, across threads.
|
|
12941
|
+
*/
|
|
12942
|
+
get unhandledActivityTypes() {
|
|
12943
|
+
return [...this.#unhandledActivityTypes];
|
|
12944
|
+
}
|
|
12945
|
+
/**
|
|
12946
|
+
* Draw, replace or remove one activity, whatever kind it is.
|
|
12947
|
+
*
|
|
12948
|
+
* The single path for all three routes an activity arrives by -- pushed
|
|
12949
|
+
* (`onActivity`), patched (`onActivityChanged`) and replayed from history --
|
|
12950
|
+
* which is why the renderer contract has to be pure: the same content is
|
|
12951
|
+
* drawn again on every thread load.
|
|
12952
|
+
*
|
|
12953
|
+
* An unregistered type draws nothing and says nothing. That is the protocol's
|
|
12954
|
+
* own answer -- a client that does not know a name ignores the event -- and a
|
|
12955
|
+
* warning here would fire on every well-behaved forward-compatible server,
|
|
12956
|
+
* while a placeholder would put the protocol's growth in the user's face.
|
|
12957
|
+
* {@link unhandledActivityTypes} is the way to find out what arrived.
|
|
12958
|
+
*/
|
|
12959
|
+
#drawActivity(messageId, activityType, content) {
|
|
12960
|
+
const registration = this.#activityRenderers.get(activityType);
|
|
12961
|
+
if (registration === void 0) {
|
|
12962
|
+
this.#unhandledActivityTypes.add(activityType);
|
|
12963
|
+
return;
|
|
12964
|
+
}
|
|
12965
|
+
let node;
|
|
12966
|
+
try {
|
|
12967
|
+
node = registration.render(content);
|
|
12968
|
+
} catch (error) {
|
|
12969
|
+
console.warn(`ag-ui-chat: render failed for activity ${activityType}`, error);
|
|
12970
|
+
node = null;
|
|
12971
|
+
}
|
|
12972
|
+
if (node === null) {
|
|
12973
|
+
this.#removeActivity(messageId, activityType, registration.removedNotice, content);
|
|
11104
12974
|
return;
|
|
11105
12975
|
}
|
|
11106
12976
|
const existing = this.#activityBlocks.get(messageId);
|
|
11107
12977
|
if (existing === void 0) {
|
|
11108
|
-
this.#ensureGroup().appendChild(
|
|
12978
|
+
this.#ensureGroup().appendChild(node);
|
|
11109
12979
|
} else {
|
|
11110
|
-
existing.replaceWith(
|
|
12980
|
+
existing.replaceWith(node);
|
|
11111
12981
|
}
|
|
11112
|
-
this.#activityBlocks.set(messageId,
|
|
12982
|
+
this.#activityBlocks.set(messageId, node);
|
|
11113
12983
|
this.#afterTranscriptGrew();
|
|
11114
12984
|
}
|
|
12985
|
+
/**
|
|
12986
|
+
* Take away an activity whose content stopped being drawable.
|
|
12987
|
+
*
|
|
12988
|
+
* Leaving the old one up is the worst available answer: it shows values that
|
|
12989
|
+
* have been retracted, reading as current, and a reload drops it anyway
|
|
12990
|
+
* because the *stored* content is the version that could not be drawn. Live
|
|
12991
|
+
* and reload should agree, and both should say "gone".
|
|
12992
|
+
*
|
|
12993
|
+
* Removing is right; doing it in silence was not. A chart that had been drawn
|
|
12994
|
+
* simply disappeared, with no `console` call anywhere on the path -- which
|
|
12995
|
+
* nobody reports as a bug, they report as "the charts are flaky".
|
|
12996
|
+
*/
|
|
12997
|
+
#removeActivity(messageId, activityType, notice, content) {
|
|
12998
|
+
const had = this.#activityBlocks.has(messageId);
|
|
12999
|
+
this.#activityBlocks.get(messageId)?.remove();
|
|
13000
|
+
this.#activityBlocks.delete(messageId);
|
|
13001
|
+
console.warn(
|
|
13002
|
+
`ag-ui-chat: activity ${messageId} (${activityType}) was not drawable and has been removed. A chart's points must each be a finite JSON number; a numeric column serialised as a string (a Decimal, typically) is rejected rather than coerced.`,
|
|
13003
|
+
content
|
|
13004
|
+
);
|
|
13005
|
+
if (had && notice !== void 0) {
|
|
13006
|
+
this.#appendNotice("\u{1F4C9}", notice, "chart-undrawable");
|
|
13007
|
+
}
|
|
13008
|
+
}
|
|
11115
13009
|
#afterTranscriptGrew() {
|
|
11116
13010
|
this.#updateEmptyState();
|
|
11117
|
-
this.#
|
|
13011
|
+
this.#scroller.follow();
|
|
11118
13012
|
}
|
|
11119
13013
|
#cardFor(call) {
|
|
11120
13014
|
const existing = this.#toolCards.get(call.id);
|
|
@@ -11122,12 +13016,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11122
13016
|
return existing;
|
|
11123
13017
|
}
|
|
11124
13018
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
11125
|
-
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
|
|
13019
|
+
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name]?.summary ?? prettifyToolName(call.name);
|
|
11126
13020
|
const card = new ToolCallCard(call.name, call.args, summary, this.#strings);
|
|
11127
13021
|
this.#toolCards.set(call.id, card);
|
|
11128
13022
|
this.#ensureGroup().appendChild(card.element);
|
|
11129
13023
|
this.#updateEmptyState();
|
|
11130
|
-
this.#
|
|
13024
|
+
this.#scroller.follow();
|
|
11131
13025
|
return card;
|
|
11132
13026
|
}
|
|
11133
13027
|
};
|
|
@@ -11197,7 +13091,7 @@ function setControlValue(el2, value) {
|
|
|
11197
13091
|
}
|
|
11198
13092
|
|
|
11199
13093
|
// src/version.ts
|
|
11200
|
-
var VERSION = "0.
|
|
13094
|
+
var VERSION = "0.29.0";
|
|
11201
13095
|
export {
|
|
11202
13096
|
ATTACHMENT_EVENT,
|
|
11203
13097
|
AgUiChat,
|
|
@@ -11205,12 +13099,19 @@ export {
|
|
|
11205
13099
|
CHART_ACTIVITY_TYPE,
|
|
11206
13100
|
CHART_TOOL_NAME,
|
|
11207
13101
|
COMPACTION_ACTIVITY_TYPE,
|
|
13102
|
+
CUSTOM_AGENT_EVENT,
|
|
11208
13103
|
CheckpointMenu,
|
|
11209
13104
|
ClientToolRegistry,
|
|
11210
13105
|
ConnectionLostError,
|
|
11211
13106
|
DEFAULT_UI_STRINGS,
|
|
11212
13107
|
ELEMENT_TAG,
|
|
13108
|
+
FEEDBACK_EVENT,
|
|
13109
|
+
INVALIDATE_CUSTOM_NAME,
|
|
13110
|
+
INVALIDATE_EVENT,
|
|
11213
13111
|
LOAD_CAPABILITY_TOOL,
|
|
13112
|
+
MAX_QUOTE_CHARS,
|
|
13113
|
+
MAX_SUGGESTIONS,
|
|
13114
|
+
MAX_SUGGESTION_CHARS,
|
|
11214
13115
|
MAX_TOOL_ROUNDS,
|
|
11215
13116
|
MESSAGE_ROLE,
|
|
11216
13117
|
PAGE_ACTIONS,
|
|
@@ -11219,6 +13120,7 @@ export {
|
|
|
11219
13120
|
RunIndex,
|
|
11220
13121
|
STATE_EVENT,
|
|
11221
13122
|
SUBMIT_EVENT,
|
|
13123
|
+
SUGGESTIONS_ACTIVITY_TYPE,
|
|
11222
13124
|
SessionStorageStore,
|
|
11223
13125
|
TOGGLE_EVENT,
|
|
11224
13126
|
TOOL_CALL_STATUS,
|
|
@@ -11230,6 +13132,9 @@ export {
|
|
|
11230
13132
|
X_DESTRUCTIVE_KEY,
|
|
11231
13133
|
X_NAVIGATES_KEY,
|
|
11232
13134
|
X_SUMMARY_KEY,
|
|
13135
|
+
asQuote,
|
|
13136
|
+
attachMessageActions,
|
|
13137
|
+
attachQuoteOffer,
|
|
11233
13138
|
chartSpecFrom,
|
|
11234
13139
|
clickElement,
|
|
11235
13140
|
createHttpAgent,
|
|
@@ -11246,14 +13151,18 @@ export {
|
|
|
11246
13151
|
isDestructive,
|
|
11247
13152
|
isNavigates,
|
|
11248
13153
|
mergeUiStrings,
|
|
13154
|
+
messageActionBar,
|
|
11249
13155
|
messageAttachments,
|
|
11250
13156
|
parseToolCatalog,
|
|
11251
13157
|
prefersReducedMotion,
|
|
11252
13158
|
pressButton,
|
|
11253
13159
|
pressThenClick,
|
|
11254
13160
|
prettifyToolName,
|
|
13161
|
+
quotableSelection,
|
|
13162
|
+
relativeTime,
|
|
11255
13163
|
renderChart,
|
|
11256
13164
|
renderMarkdown,
|
|
13165
|
+
renderSuggestionChips,
|
|
11257
13166
|
requestApproval,
|
|
11258
13167
|
requestConfirmation,
|
|
11259
13168
|
requestQuestion,
|
|
@@ -11263,6 +13172,7 @@ export {
|
|
|
11263
13172
|
setControlValue,
|
|
11264
13173
|
setNativeChecked,
|
|
11265
13174
|
setNativeValue,
|
|
13175
|
+
suggestionPrompts,
|
|
11266
13176
|
toggleCheckbox,
|
|
11267
13177
|
toggleControl,
|
|
11268
13178
|
transcribeAudio,
|