@artooi/ag-ui-web-component 0.3.1 → 0.5.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 +59 -1
- package/README.md +25 -1
- package/dist/ag-ui-web-component.bundle.js +251 -54
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +18 -2
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +37 -8
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +35 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +740 -13
- package/dist/index.js.map +4 -4
- package/dist/ui/confirmation_card.d.ts +10 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +11 -0
- package/dist/ui/relative_time.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/thread_drawer.d.ts +33 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +163 -8
- package/src/core/agui_client.ts +63 -3
- package/src/core/conversation_store.ts +148 -9
- package/src/core/remote_conversation_store.ts +147 -0
- package/src/index.ts +7 -1
- package/src/ui/confirmation_card.ts +22 -0
- package/src/ui/relative_time.ts +28 -0
- package/src/ui/styles.ts +197 -0
- package/src/ui/thread_drawer.ts +200 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -239,7 +239,7 @@ function actionButton(modifier, label) {
|
|
|
239
239
|
button.textContent = label;
|
|
240
240
|
return button;
|
|
241
241
|
}
|
|
242
|
-
function requestConfirmation(host, request) {
|
|
242
|
+
function requestConfirmation(host, request, options = {}) {
|
|
243
243
|
return new Promise((resolve) => {
|
|
244
244
|
const card = document.createElement("div");
|
|
245
245
|
card.className = "confirm";
|
|
@@ -256,7 +256,12 @@ function requestConfirmation(host, request) {
|
|
|
256
256
|
actions.className = "confirm-actions";
|
|
257
257
|
const cancel = actionButton("cancel", "Cancel");
|
|
258
258
|
const confirm = actionButton("confirm", "Confirm");
|
|
259
|
+
let settled = false;
|
|
259
260
|
const close = (accepted) => {
|
|
261
|
+
if (settled) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
settled = true;
|
|
260
265
|
cancel.disabled = true;
|
|
261
266
|
confirm.disabled = true;
|
|
262
267
|
card.setAttribute("data-resolved", accepted ? "confirmed" : "declined");
|
|
@@ -264,9 +269,14 @@ function requestConfirmation(host, request) {
|
|
|
264
269
|
};
|
|
265
270
|
cancel.addEventListener("click", () => close(false));
|
|
266
271
|
confirm.addEventListener("click", () => close(true));
|
|
272
|
+
options.signal?.addEventListener("abort", () => close(false), { once: true });
|
|
267
273
|
actions.append(cancel, confirm);
|
|
268
274
|
card.append(body, args, actions);
|
|
269
275
|
host.appendChild(card);
|
|
276
|
+
if (options.signal?.aborted === true) {
|
|
277
|
+
close(false);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
270
280
|
confirm.focus();
|
|
271
281
|
});
|
|
272
282
|
}
|
|
@@ -3449,6 +3459,19 @@ var STYLES = `
|
|
|
3449
3459
|
cursor: default;
|
|
3450
3460
|
}
|
|
3451
3461
|
|
|
3462
|
+
/* The composer button doubles as the Stop control while a run is in flight. */
|
|
3463
|
+
.send[data-state="running"] {
|
|
3464
|
+
background: var(--ag-ui-muted);
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
/* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
|
|
3468
|
+
.stopped-note {
|
|
3469
|
+
align-self: flex-start;
|
|
3470
|
+
color: var(--ag-ui-muted);
|
|
3471
|
+
font-size: 12px;
|
|
3472
|
+
padding: 2px 4px;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3452
3475
|
/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */
|
|
3453
3476
|
.confirm {
|
|
3454
3477
|
align-self: stretch;
|
|
@@ -3579,8 +3602,370 @@ var STYLES = `
|
|
|
3579
3602
|
font-size: 0.85em;
|
|
3580
3603
|
color: var(--ag-ui-danger);
|
|
3581
3604
|
}
|
|
3605
|
+
|
|
3606
|
+
/* Chat-history drawer \u2014 a slide-over within the chat panel. */
|
|
3607
|
+
.drawer {
|
|
3608
|
+
position: absolute;
|
|
3609
|
+
inset: 0;
|
|
3610
|
+
z-index: 5;
|
|
3611
|
+
display: flex;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
.drawer[hidden] {
|
|
3615
|
+
display: none;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
.drawer-backdrop {
|
|
3619
|
+
position: absolute;
|
|
3620
|
+
inset: 0;
|
|
3621
|
+
background: rgba(20, 20, 50, 0.32);
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
.drawer-panel {
|
|
3625
|
+
position: relative;
|
|
3626
|
+
display: flex;
|
|
3627
|
+
flex-direction: column;
|
|
3628
|
+
width: min(300px, 85%);
|
|
3629
|
+
height: 100%;
|
|
3630
|
+
background: var(--ag-ui-bg);
|
|
3631
|
+
border-right: 1px solid var(--ag-ui-border);
|
|
3632
|
+
box-shadow: var(--ag-ui-shadow);
|
|
3633
|
+
overflow: hidden;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
.drawer-header {
|
|
3637
|
+
display: flex;
|
|
3638
|
+
align-items: center;
|
|
3639
|
+
justify-content: space-between;
|
|
3640
|
+
gap: var(--ag-ui-space);
|
|
3641
|
+
padding: var(--ag-ui-pad);
|
|
3642
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
.drawer-title {
|
|
3646
|
+
font-weight: 600;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
.drawer-new {
|
|
3650
|
+
border: 1px solid var(--ag-ui-border);
|
|
3651
|
+
border-radius: var(--ag-ui-radius);
|
|
3652
|
+
background: var(--ag-ui-bg);
|
|
3653
|
+
color: var(--ag-ui-accent);
|
|
3654
|
+
padding: 4px 10px;
|
|
3655
|
+
font: inherit;
|
|
3656
|
+
font-size: 0.85em;
|
|
3657
|
+
cursor: pointer;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
.drawer-list {
|
|
3661
|
+
flex: 1;
|
|
3662
|
+
min-height: 0;
|
|
3663
|
+
overflow-y: auto;
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
.drawer-empty {
|
|
3667
|
+
padding: var(--ag-ui-pad);
|
|
3668
|
+
font-size: 0.9em;
|
|
3669
|
+
color: var(--ag-ui-muted);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
.drawer-row {
|
|
3673
|
+
display: flex;
|
|
3674
|
+
align-items: stretch;
|
|
3675
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
.drawer-row--active {
|
|
3679
|
+
background: var(--ag-ui-assistant-bg);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
.drawer-row-select {
|
|
3683
|
+
flex: 1;
|
|
3684
|
+
min-width: 0;
|
|
3685
|
+
display: flex;
|
|
3686
|
+
flex-direction: column;
|
|
3687
|
+
gap: 2px;
|
|
3688
|
+
padding: 8px 12px;
|
|
3689
|
+
border: none;
|
|
3690
|
+
background: none;
|
|
3691
|
+
color: inherit;
|
|
3692
|
+
font: inherit;
|
|
3693
|
+
text-align: left;
|
|
3694
|
+
cursor: pointer;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
.drawer-row-title {
|
|
3698
|
+
font-weight: 600;
|
|
3699
|
+
overflow: hidden;
|
|
3700
|
+
white-space: nowrap;
|
|
3701
|
+
text-overflow: ellipsis;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
.drawer-row-time {
|
|
3705
|
+
font-size: 0.72em;
|
|
3706
|
+
color: var(--ag-ui-muted);
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
.drawer-row-preview {
|
|
3710
|
+
font-size: 0.8em;
|
|
3711
|
+
color: var(--ag-ui-muted);
|
|
3712
|
+
overflow: hidden;
|
|
3713
|
+
white-space: nowrap;
|
|
3714
|
+
text-overflow: ellipsis;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
.drawer-row-actions {
|
|
3718
|
+
display: flex;
|
|
3719
|
+
align-items: center;
|
|
3720
|
+
gap: 2px;
|
|
3721
|
+
padding: 0 6px;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
.drawer-row-rename,
|
|
3725
|
+
.drawer-row-delete {
|
|
3726
|
+
border: none;
|
|
3727
|
+
background: none;
|
|
3728
|
+
color: var(--ag-ui-muted);
|
|
3729
|
+
font-size: 0.9em;
|
|
3730
|
+
padding: 4px;
|
|
3731
|
+
cursor: pointer;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
.drawer-rename-input {
|
|
3735
|
+
flex: 1;
|
|
3736
|
+
min-width: 0;
|
|
3737
|
+
margin: 6px 10px;
|
|
3738
|
+
padding: 4px 8px;
|
|
3739
|
+
border: 1px solid var(--ag-ui-accent);
|
|
3740
|
+
border-radius: 6px;
|
|
3741
|
+
background: var(--ag-ui-input-bg);
|
|
3742
|
+
color: var(--ag-ui-fg);
|
|
3743
|
+
font: inherit;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
.drawer-confirm {
|
|
3747
|
+
display: flex;
|
|
3748
|
+
align-items: center;
|
|
3749
|
+
gap: 8px;
|
|
3750
|
+
padding: 8px 12px;
|
|
3751
|
+
font-size: 0.85em;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
.drawer-confirm-label {
|
|
3755
|
+
color: var(--ag-ui-danger);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
.drawer-confirm-yes {
|
|
3759
|
+
border: none;
|
|
3760
|
+
border-radius: 6px;
|
|
3761
|
+
background: var(--ag-ui-danger);
|
|
3762
|
+
color: #ffffff;
|
|
3763
|
+
padding: 3px 10px;
|
|
3764
|
+
font: inherit;
|
|
3765
|
+
cursor: pointer;
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
.drawer-confirm-no {
|
|
3769
|
+
border: 1px solid var(--ag-ui-border);
|
|
3770
|
+
border-radius: 6px;
|
|
3771
|
+
background: none;
|
|
3772
|
+
color: inherit;
|
|
3773
|
+
padding: 3px 10px;
|
|
3774
|
+
font: inherit;
|
|
3775
|
+
cursor: pointer;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
/* Embedded placement: an inline, flush side panel rather than a dimmed,
|
|
3779
|
+
floating slide-over. */
|
|
3780
|
+
:host([placement="embedded"]) .drawer-backdrop {
|
|
3781
|
+
background: none;
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
:host([placement="embedded"]) .drawer-panel {
|
|
3785
|
+
width: 100%;
|
|
3786
|
+
border-right: none;
|
|
3787
|
+
box-shadow: none;
|
|
3788
|
+
}
|
|
3582
3789
|
`;
|
|
3583
3790
|
|
|
3791
|
+
// src/ui/relative_time.ts
|
|
3792
|
+
function relativeTime(timestamp, now = Date.now()) {
|
|
3793
|
+
const seconds = Math.round((now - timestamp) / 1e3);
|
|
3794
|
+
if (seconds < 60) {
|
|
3795
|
+
return "just now";
|
|
3796
|
+
}
|
|
3797
|
+
const minutes = Math.round(seconds / 60);
|
|
3798
|
+
if (minutes < 60) {
|
|
3799
|
+
return `${minutes}m ago`;
|
|
3800
|
+
}
|
|
3801
|
+
const hours = Math.round(minutes / 60);
|
|
3802
|
+
if (hours < 24) {
|
|
3803
|
+
return `${hours}h ago`;
|
|
3804
|
+
}
|
|
3805
|
+
const days = Math.round(hours / 24);
|
|
3806
|
+
if (days < 7) {
|
|
3807
|
+
return `${days}d ago`;
|
|
3808
|
+
}
|
|
3809
|
+
return `${Math.round(days / 7)}w ago`;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// src/ui/thread_drawer.ts
|
|
3813
|
+
var ThreadDrawer = class {
|
|
3814
|
+
/** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
|
|
3815
|
+
element;
|
|
3816
|
+
#callbacks;
|
|
3817
|
+
#list;
|
|
3818
|
+
#threads = [];
|
|
3819
|
+
#activeId = "";
|
|
3820
|
+
constructor(callbacks) {
|
|
3821
|
+
this.#callbacks = callbacks;
|
|
3822
|
+
this.element = document.createElement("div");
|
|
3823
|
+
this.element.className = "drawer";
|
|
3824
|
+
this.element.hidden = true;
|
|
3825
|
+
const backdrop = document.createElement("div");
|
|
3826
|
+
backdrop.className = "drawer-backdrop";
|
|
3827
|
+
backdrop.addEventListener("click", () => this.close());
|
|
3828
|
+
const panel = document.createElement("div");
|
|
3829
|
+
panel.className = "drawer-panel";
|
|
3830
|
+
panel.setAttribute("role", "dialog");
|
|
3831
|
+
panel.setAttribute("aria-label", "Chat history");
|
|
3832
|
+
const header = document.createElement("div");
|
|
3833
|
+
header.className = "drawer-header";
|
|
3834
|
+
const heading = document.createElement("span");
|
|
3835
|
+
heading.className = "drawer-title";
|
|
3836
|
+
heading.textContent = "Chats";
|
|
3837
|
+
const newButton = document.createElement("button");
|
|
3838
|
+
newButton.type = "button";
|
|
3839
|
+
newButton.className = "drawer-new";
|
|
3840
|
+
newButton.textContent = "New chat";
|
|
3841
|
+
newButton.addEventListener("click", () => {
|
|
3842
|
+
this.close();
|
|
3843
|
+
this.#callbacks.onNew();
|
|
3844
|
+
});
|
|
3845
|
+
header.append(heading, newButton);
|
|
3846
|
+
this.#list = document.createElement("div");
|
|
3847
|
+
this.#list.className = "drawer-list";
|
|
3848
|
+
panel.append(header, this.#list);
|
|
3849
|
+
this.element.append(backdrop, panel);
|
|
3850
|
+
}
|
|
3851
|
+
isOpen() {
|
|
3852
|
+
return !this.element.hidden;
|
|
3853
|
+
}
|
|
3854
|
+
open() {
|
|
3855
|
+
this.element.hidden = false;
|
|
3856
|
+
}
|
|
3857
|
+
close() {
|
|
3858
|
+
this.element.hidden = true;
|
|
3859
|
+
}
|
|
3860
|
+
toggle() {
|
|
3861
|
+
this.element.hidden = !this.element.hidden;
|
|
3862
|
+
}
|
|
3863
|
+
/** Render the rows (or the empty state), highlighting the active thread. */
|
|
3864
|
+
setThreads(threads, activeId) {
|
|
3865
|
+
this.#threads = threads;
|
|
3866
|
+
this.#activeId = activeId;
|
|
3867
|
+
this.#renderList();
|
|
3868
|
+
}
|
|
3869
|
+
#renderList() {
|
|
3870
|
+
this.#list.replaceChildren();
|
|
3871
|
+
if (this.#threads.length === 0) {
|
|
3872
|
+
const empty = document.createElement("div");
|
|
3873
|
+
empty.className = "drawer-empty";
|
|
3874
|
+
empty.textContent = "No conversations yet.";
|
|
3875
|
+
this.#list.appendChild(empty);
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
for (const meta of this.#threads) {
|
|
3879
|
+
this.#list.appendChild(this.#renderRow(meta));
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
#renderRow(meta) {
|
|
3883
|
+
const row = document.createElement("div");
|
|
3884
|
+
row.className = "drawer-row";
|
|
3885
|
+
if (meta.threadId === this.#activeId) {
|
|
3886
|
+
row.classList.add("drawer-row--active");
|
|
3887
|
+
}
|
|
3888
|
+
const select = document.createElement("button");
|
|
3889
|
+
select.type = "button";
|
|
3890
|
+
select.className = "drawer-row-select";
|
|
3891
|
+
const title = document.createElement("span");
|
|
3892
|
+
title.className = "drawer-row-title";
|
|
3893
|
+
title.textContent = meta.title;
|
|
3894
|
+
const time = document.createElement("span");
|
|
3895
|
+
time.className = "drawer-row-time";
|
|
3896
|
+
time.textContent = relativeTime(meta.updatedAt);
|
|
3897
|
+
const preview = document.createElement("span");
|
|
3898
|
+
preview.className = "drawer-row-preview";
|
|
3899
|
+
preview.textContent = meta.preview;
|
|
3900
|
+
select.append(title, time, preview);
|
|
3901
|
+
select.addEventListener("click", () => {
|
|
3902
|
+
this.close();
|
|
3903
|
+
this.#callbacks.onSelect(meta.threadId);
|
|
3904
|
+
});
|
|
3905
|
+
const rename = document.createElement("button");
|
|
3906
|
+
rename.type = "button";
|
|
3907
|
+
rename.className = "drawer-row-rename";
|
|
3908
|
+
rename.title = "Rename";
|
|
3909
|
+
rename.setAttribute("aria-label", "Rename conversation");
|
|
3910
|
+
rename.textContent = "\u270E";
|
|
3911
|
+
rename.addEventListener("click", () => this.#startRename(row, meta));
|
|
3912
|
+
const remove = document.createElement("button");
|
|
3913
|
+
remove.type = "button";
|
|
3914
|
+
remove.className = "drawer-row-delete";
|
|
3915
|
+
remove.title = "Delete";
|
|
3916
|
+
remove.setAttribute("aria-label", "Delete conversation");
|
|
3917
|
+
remove.textContent = "\u{1F5D1}";
|
|
3918
|
+
remove.addEventListener("click", () => this.#confirmDelete(row, meta));
|
|
3919
|
+
const actions = document.createElement("div");
|
|
3920
|
+
actions.className = "drawer-row-actions";
|
|
3921
|
+
actions.append(rename, remove);
|
|
3922
|
+
row.append(select, actions);
|
|
3923
|
+
return row;
|
|
3924
|
+
}
|
|
3925
|
+
/** Swap a row for an inline rename input; Enter commits, Escape cancels. */
|
|
3926
|
+
#startRename(row, meta) {
|
|
3927
|
+
const input = document.createElement("input");
|
|
3928
|
+
input.type = "text";
|
|
3929
|
+
input.className = "drawer-rename-input";
|
|
3930
|
+
input.value = meta.title;
|
|
3931
|
+
input.addEventListener("keydown", (event) => {
|
|
3932
|
+
if (event.key === "Enter") {
|
|
3933
|
+
const value = input.value.trim();
|
|
3934
|
+
if (value === "") {
|
|
3935
|
+
this.#renderList();
|
|
3936
|
+
} else {
|
|
3937
|
+
this.#callbacks.onRename(meta.threadId, value);
|
|
3938
|
+
}
|
|
3939
|
+
} else if (event.key === "Escape") {
|
|
3940
|
+
this.#renderList();
|
|
3941
|
+
}
|
|
3942
|
+
});
|
|
3943
|
+
row.replaceChildren(input);
|
|
3944
|
+
input.focus();
|
|
3945
|
+
input.select();
|
|
3946
|
+
}
|
|
3947
|
+
/** Swap a row for an inline "Delete? [Delete] [Cancel]" confirm. */
|
|
3948
|
+
#confirmDelete(row, meta) {
|
|
3949
|
+
const confirm = document.createElement("div");
|
|
3950
|
+
confirm.className = "drawer-confirm";
|
|
3951
|
+
const label = document.createElement("span");
|
|
3952
|
+
label.className = "drawer-confirm-label";
|
|
3953
|
+
label.textContent = "Delete?";
|
|
3954
|
+
const yes = document.createElement("button");
|
|
3955
|
+
yes.type = "button";
|
|
3956
|
+
yes.className = "drawer-confirm-yes";
|
|
3957
|
+
yes.textContent = "Delete";
|
|
3958
|
+
yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
|
|
3959
|
+
const no = document.createElement("button");
|
|
3960
|
+
no.type = "button";
|
|
3961
|
+
no.className = "drawer-confirm-no";
|
|
3962
|
+
no.textContent = "Cancel";
|
|
3963
|
+
no.addEventListener("click", () => this.#renderList());
|
|
3964
|
+
confirm.append(label, yes, no);
|
|
3965
|
+
row.replaceChildren(confirm);
|
|
3966
|
+
}
|
|
3967
|
+
};
|
|
3968
|
+
|
|
3584
3969
|
// src/ui/tool_call_card.ts
|
|
3585
3970
|
var STATUS_LABEL = {
|
|
3586
3971
|
[TOOL_CALL_STATUS.PENDING]: "running\u2026",
|
|
@@ -3669,6 +4054,9 @@ var AgUiClient = class {
|
|
|
3669
4054
|
#getContext;
|
|
3670
4055
|
#executeTool;
|
|
3671
4056
|
#onPersist;
|
|
4057
|
+
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
4058
|
+
// a cancel between frontend-tool rounds doesn't start another round.
|
|
4059
|
+
#cancelled = false;
|
|
3672
4060
|
constructor(config) {
|
|
3673
4061
|
this.#agent = config.agent;
|
|
3674
4062
|
this.#handlers = config.handlers;
|
|
@@ -3711,23 +4099,52 @@ var AgUiClient = class {
|
|
|
3711
4099
|
this.#agent.addMessage({ id: randomUUID(), role: "tool", content, toolCallId });
|
|
3712
4100
|
this.#onPersist(this.#agent.messages);
|
|
3713
4101
|
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Cancel the in-flight run (AG-UI has no server cancel route — aborting the
|
|
4104
|
+
* streaming request is the protocol's cancel; the server observes the
|
|
4105
|
+
* disconnect). Safe to call with no run in flight. Partial text already
|
|
4106
|
+
* streamed stays in history; {@link AgUiClientHandlers.onCancelled} fires
|
|
4107
|
+
* instead of `onError`, and `onSettled` still follows.
|
|
4108
|
+
*/
|
|
4109
|
+
cancel() {
|
|
4110
|
+
this.#cancelled = true;
|
|
4111
|
+
this.#agent.abortRun();
|
|
4112
|
+
}
|
|
3714
4113
|
async #run() {
|
|
4114
|
+
this.#cancelled = false;
|
|
3715
4115
|
try {
|
|
3716
4116
|
await this.#runLoop();
|
|
4117
|
+
if (this.#cancelled) {
|
|
4118
|
+
this.#onCancelled();
|
|
4119
|
+
}
|
|
3717
4120
|
} catch (error) {
|
|
3718
|
-
this.#
|
|
4121
|
+
if (this.#cancelled || isAbortError(error)) {
|
|
4122
|
+
this.#onCancelled();
|
|
4123
|
+
} else {
|
|
4124
|
+
this.#handlers.onError(error instanceof Error ? error.message : String(error));
|
|
4125
|
+
}
|
|
3719
4126
|
} finally {
|
|
3720
4127
|
this.#handlers.onSettled();
|
|
3721
4128
|
}
|
|
3722
4129
|
}
|
|
4130
|
+
#onCancelled() {
|
|
4131
|
+
this.#onPersist(this.#agent.messages);
|
|
4132
|
+
this.#handlers.onCancelled();
|
|
4133
|
+
}
|
|
3723
4134
|
async #runLoop() {
|
|
3724
4135
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
4136
|
+
if (this.#cancelled) {
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
3725
4139
|
const pending = [];
|
|
3726
4140
|
await this.#agent.runAgent(
|
|
3727
4141
|
{ tools: this.#getTools(), context: this.#getContext() },
|
|
3728
4142
|
this.#buildSubscriber(pending)
|
|
3729
4143
|
);
|
|
3730
4144
|
this.#onPersist(this.#agent.messages);
|
|
4145
|
+
if (this.#cancelled) {
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
3731
4148
|
if (this.#executeTool === null || pending.length === 0) {
|
|
3732
4149
|
return;
|
|
3733
4150
|
}
|
|
@@ -3787,12 +4204,19 @@ var AgUiClient = class {
|
|
|
3787
4204
|
};
|
|
3788
4205
|
}
|
|
3789
4206
|
};
|
|
4207
|
+
function isAbortError(error) {
|
|
4208
|
+
return error instanceof Error && error.name === "AbortError";
|
|
4209
|
+
}
|
|
3790
4210
|
|
|
3791
4211
|
// src/core/conversation_store.ts
|
|
3792
4212
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
3793
4213
|
var THREAD_KEY = "ag-ui-chat:thread";
|
|
4214
|
+
var THREADS_KEY = "ag-ui-chat:threads";
|
|
3794
4215
|
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
3795
4216
|
var CHECKPOINT_PREFIX = "ag-ui-chat:checkpoint:";
|
|
4217
|
+
var TITLE_LIMIT = 60;
|
|
4218
|
+
var PREVIEW_LIMIT = 100;
|
|
4219
|
+
var DEFAULT_TITLE = "New conversation";
|
|
3796
4220
|
var SessionStorageStore = class {
|
|
3797
4221
|
threadId() {
|
|
3798
4222
|
const existing = sessionStorage.getItem(THREAD_KEY);
|
|
@@ -3808,6 +4232,7 @@ var SessionStorageStore = class {
|
|
|
3808
4232
|
}
|
|
3809
4233
|
saveMessages(threadId, messages) {
|
|
3810
4234
|
sessionStorage.setItem(MESSAGES_PREFIX + threadId, JSON.stringify(messages));
|
|
4235
|
+
this.#touchThread(threadId, messages);
|
|
3811
4236
|
}
|
|
3812
4237
|
loadCheckpoint(threadId) {
|
|
3813
4238
|
return this.#readJson(CHECKPOINT_PREFIX + threadId);
|
|
@@ -3823,7 +4248,60 @@ var SessionStorageStore = class {
|
|
|
3823
4248
|
clear(threadId) {
|
|
3824
4249
|
sessionStorage.removeItem(MESSAGES_PREFIX + threadId);
|
|
3825
4250
|
sessionStorage.removeItem(CHECKPOINT_PREFIX + threadId);
|
|
3826
|
-
|
|
4251
|
+
this.#writeThreads(this.#readThreads().filter((thread) => thread.threadId !== threadId));
|
|
4252
|
+
if (sessionStorage.getItem(THREAD_KEY) === threadId) {
|
|
4253
|
+
sessionStorage.removeItem(THREAD_KEY);
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
listThreads() {
|
|
4257
|
+
const metas = this.#readThreads().sort((a, b2) => b2.updatedAt - a.updatedAt).map(({ threadId, title, updatedAt, preview }) => ({ threadId, title, updatedAt, preview }));
|
|
4258
|
+
return Promise.resolve(metas);
|
|
4259
|
+
}
|
|
4260
|
+
setActiveThread(threadId) {
|
|
4261
|
+
sessionStorage.setItem(THREAD_KEY, threadId);
|
|
4262
|
+
}
|
|
4263
|
+
renameThread(threadId, title) {
|
|
4264
|
+
const threads = this.#readThreads();
|
|
4265
|
+
const entry = threads.find((thread) => thread.threadId === threadId);
|
|
4266
|
+
if (entry === void 0) {
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
entry.title = title;
|
|
4270
|
+
entry.titleCustom = true;
|
|
4271
|
+
this.#writeThreads(threads);
|
|
4272
|
+
}
|
|
4273
|
+
/** Add or refresh a thread's drawer metadata from its latest messages. */
|
|
4274
|
+
#touchThread(threadId, messages) {
|
|
4275
|
+
const threads = this.#readThreads();
|
|
4276
|
+
const entry = threads.find((thread) => thread.threadId === threadId);
|
|
4277
|
+
const preview = derivePreview(messages);
|
|
4278
|
+
const updatedAt = Date.now();
|
|
4279
|
+
if (entry === void 0) {
|
|
4280
|
+
threads.push({
|
|
4281
|
+
threadId,
|
|
4282
|
+
title: deriveTitle(messages),
|
|
4283
|
+
titleCustom: false,
|
|
4284
|
+
preview,
|
|
4285
|
+
updatedAt
|
|
4286
|
+
});
|
|
4287
|
+
} else {
|
|
4288
|
+
entry.preview = preview;
|
|
4289
|
+
entry.updatedAt = updatedAt;
|
|
4290
|
+
if (!entry.titleCustom) {
|
|
4291
|
+
entry.title = deriveTitle(messages);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
this.#writeThreads(threads);
|
|
4295
|
+
}
|
|
4296
|
+
#readThreads() {
|
|
4297
|
+
return this.#readJson(THREADS_KEY) ?? [];
|
|
4298
|
+
}
|
|
4299
|
+
#writeThreads(threads) {
|
|
4300
|
+
if (threads.length === 0) {
|
|
4301
|
+
sessionStorage.removeItem(THREADS_KEY);
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
sessionStorage.setItem(THREADS_KEY, JSON.stringify(threads));
|
|
3827
4305
|
}
|
|
3828
4306
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
3829
4307
|
#readJson(key) {
|
|
@@ -3838,6 +4316,32 @@ var SessionStorageStore = class {
|
|
|
3838
4316
|
}
|
|
3839
4317
|
}
|
|
3840
4318
|
};
|
|
4319
|
+
function deriveTitle(messages) {
|
|
4320
|
+
for (const message of messages) {
|
|
4321
|
+
if (message.role === "user") {
|
|
4322
|
+
const text2 = cleanText(message.content);
|
|
4323
|
+
if (text2 !== "") {
|
|
4324
|
+
return truncate(text2, TITLE_LIMIT);
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
return DEFAULT_TITLE;
|
|
4329
|
+
}
|
|
4330
|
+
function derivePreview(messages) {
|
|
4331
|
+
for (const message of [...messages].reverse()) {
|
|
4332
|
+
const text2 = cleanText(message.content);
|
|
4333
|
+
if (text2 !== "") {
|
|
4334
|
+
return truncate(text2, PREVIEW_LIMIT);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
return "";
|
|
4338
|
+
}
|
|
4339
|
+
function cleanText(content) {
|
|
4340
|
+
return typeof content === "string" ? content.replace(/\s+/g, " ").trim() : "";
|
|
4341
|
+
}
|
|
4342
|
+
function truncate(text2, limit) {
|
|
4343
|
+
return text2.length <= limit ? text2 : `${text2.slice(0, limit - 1).trimEnd()}\u2026`;
|
|
4344
|
+
}
|
|
3841
4345
|
|
|
3842
4346
|
// src/core/create_http_agent.ts
|
|
3843
4347
|
import { HttpAgent } from "@ag-ui/client";
|
|
@@ -3869,6 +4373,96 @@ function createHttpAgent(options) {
|
|
|
3869
4373
|
});
|
|
3870
4374
|
}
|
|
3871
4375
|
|
|
4376
|
+
// src/core/remote_conversation_store.ts
|
|
4377
|
+
var RemoteConversationStore = class {
|
|
4378
|
+
#url;
|
|
4379
|
+
#headers;
|
|
4380
|
+
#local;
|
|
4381
|
+
#dropped = /* @__PURE__ */ new Set();
|
|
4382
|
+
#renamed = /* @__PURE__ */ new Map();
|
|
4383
|
+
constructor(url, headers = () => ({}), local = new SessionStorageStore()) {
|
|
4384
|
+
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
4385
|
+
this.#headers = headers;
|
|
4386
|
+
this.#local = local;
|
|
4387
|
+
}
|
|
4388
|
+
threadId() {
|
|
4389
|
+
return this.#local.threadId();
|
|
4390
|
+
}
|
|
4391
|
+
setActiveThread(threadId) {
|
|
4392
|
+
this.#local.setActiveThread(threadId);
|
|
4393
|
+
}
|
|
4394
|
+
saveMessages(threadId, messages) {
|
|
4395
|
+
this.#local.saveMessages(threadId, messages);
|
|
4396
|
+
}
|
|
4397
|
+
loadCheckpoint(threadId) {
|
|
4398
|
+
return this.#local.loadCheckpoint(threadId);
|
|
4399
|
+
}
|
|
4400
|
+
saveCheckpoint(threadId, checkpoint) {
|
|
4401
|
+
this.#local.saveCheckpoint(threadId, checkpoint);
|
|
4402
|
+
}
|
|
4403
|
+
renameThread(threadId, title) {
|
|
4404
|
+
this.#local.renameThread(threadId, title);
|
|
4405
|
+
this.#renamed.set(threadId, title);
|
|
4406
|
+
void this.#mutate(threadId, "PATCH", { title });
|
|
4407
|
+
}
|
|
4408
|
+
clear(threadId) {
|
|
4409
|
+
this.#local.clear(threadId);
|
|
4410
|
+
this.#dropped.add(threadId);
|
|
4411
|
+
void this.#mutate(threadId, "DELETE");
|
|
4412
|
+
}
|
|
4413
|
+
async listThreads() {
|
|
4414
|
+
const rows = await this.#fetchThreads();
|
|
4415
|
+
if (rows === null) {
|
|
4416
|
+
return this.#local.listThreads();
|
|
4417
|
+
}
|
|
4418
|
+
return rows.filter((row) => !this.#dropped.has(row.thread_id)).map((row) => this.#toMeta(row));
|
|
4419
|
+
}
|
|
4420
|
+
async loadMessages(threadId) {
|
|
4421
|
+
const response = await this.#get(this.#url + encodeURIComponent(threadId) + "/");
|
|
4422
|
+
if (response === null || !response.ok) {
|
|
4423
|
+
return this.#local.loadMessages(threadId);
|
|
4424
|
+
}
|
|
4425
|
+
const body = await response.json();
|
|
4426
|
+
return body.messages ?? null;
|
|
4427
|
+
}
|
|
4428
|
+
async #fetchThreads() {
|
|
4429
|
+
const response = await this.#get(this.#url);
|
|
4430
|
+
if (response === null || !response.ok) {
|
|
4431
|
+
return null;
|
|
4432
|
+
}
|
|
4433
|
+
const body = await response.json();
|
|
4434
|
+
return body.threads ?? [];
|
|
4435
|
+
}
|
|
4436
|
+
#toMeta(row) {
|
|
4437
|
+
return {
|
|
4438
|
+
threadId: row.thread_id,
|
|
4439
|
+
title: this.#renamed.get(row.thread_id) ?? row.title,
|
|
4440
|
+
updatedAt: row.updated_at === null ? 0 : Date.parse(row.updated_at),
|
|
4441
|
+
preview: row.preview
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
/** GET that resolves to the `Response`, or `null` on a network error. */
|
|
4445
|
+
async #get(url) {
|
|
4446
|
+
try {
|
|
4447
|
+
return await fetch(url, { headers: this.#headers() });
|
|
4448
|
+
} catch {
|
|
4449
|
+
return null;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
/** Fire a best-effort write to the thread endpoint; failures are tolerated. */
|
|
4453
|
+
async #mutate(threadId, method, body) {
|
|
4454
|
+
const headers = this.#headers();
|
|
4455
|
+
try {
|
|
4456
|
+
await fetch(this.#url + encodeURIComponent(threadId) + "/", {
|
|
4457
|
+
method,
|
|
4458
|
+
headers: body === void 0 ? headers : { ...headers, "content-type": "application/json" },
|
|
4459
|
+
body: body === void 0 ? null : JSON.stringify(body)
|
|
4460
|
+
});
|
|
4461
|
+
} catch {
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
};
|
|
4465
|
+
|
|
3872
4466
|
// src/core/ag_ui_chat.ts
|
|
3873
4467
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
3874
4468
|
var AgUiChat = class extends HTMLElement {
|
|
@@ -3979,8 +4573,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
3979
4573
|
#send;
|
|
3980
4574
|
#title;
|
|
3981
4575
|
#skillsMenu;
|
|
4576
|
+
#drawer;
|
|
3982
4577
|
#skillHint;
|
|
3983
4578
|
#client = null;
|
|
4579
|
+
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
4580
|
+
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
4581
|
+
// rounds, but the user must still be able to stop there.
|
|
4582
|
+
#running = false;
|
|
4583
|
+
// Aborting this dismisses (declines) an open confirmation card when the run
|
|
4584
|
+
// is cancelled while the card awaits a decision. One controller per card.
|
|
4585
|
+
#confirmAbort = null;
|
|
3984
4586
|
#streamingBubble = null;
|
|
3985
4587
|
// Text deltas applied to the current streaming bubble. >1 ⇒ the message
|
|
3986
4588
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
@@ -4003,6 +4605,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4003
4605
|
this.#title = document.createElement("span");
|
|
4004
4606
|
this.#skillHint = document.createElement("div");
|
|
4005
4607
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
4608
|
+
this.#drawer = new ThreadDrawer({
|
|
4609
|
+
onSelect: (threadId) => {
|
|
4610
|
+
void this.#switchThread(threadId);
|
|
4611
|
+
},
|
|
4612
|
+
onNew: () => {
|
|
4613
|
+
this.newChat();
|
|
4614
|
+
void this.#refreshDrawer();
|
|
4615
|
+
},
|
|
4616
|
+
onRename: (threadId, title) => {
|
|
4617
|
+
this.conversationStore.renameThread(threadId, title);
|
|
4618
|
+
void this.#refreshDrawer();
|
|
4619
|
+
},
|
|
4620
|
+
onDelete: (threadId) => {
|
|
4621
|
+
this.#deleteThread(threadId);
|
|
4622
|
+
}
|
|
4623
|
+
});
|
|
4006
4624
|
}
|
|
4007
4625
|
/** Attributes whose late changes must reflect in already-rendered chrome. */
|
|
4008
4626
|
static get observedAttributes() {
|
|
@@ -4098,9 +4716,26 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4098
4716
|
}
|
|
4099
4717
|
this.#initSkills();
|
|
4100
4718
|
void this.#fetchToolCatalog();
|
|
4719
|
+
this.#wireThreadStore();
|
|
4101
4720
|
this.#threadId = this.conversationStore.threadId();
|
|
4102
4721
|
void this.#rehydrate();
|
|
4103
4722
|
}
|
|
4723
|
+
/**
|
|
4724
|
+
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
4725
|
+
* delete through that server endpoint (wrapping the current store as the
|
|
4726
|
+
* client-only fallback), so the history drawer shows durable, cross-device
|
|
4727
|
+
* threads. Without it, the client store's per-tab threads are used.
|
|
4728
|
+
*/
|
|
4729
|
+
#wireThreadStore() {
|
|
4730
|
+
const url = this.getAttribute("data-threads-url");
|
|
4731
|
+
if (url !== null) {
|
|
4732
|
+
this.conversationStore = new RemoteConversationStore(
|
|
4733
|
+
url,
|
|
4734
|
+
() => this.headers,
|
|
4735
|
+
this.conversationStore
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4104
4739
|
/** Fetch the server tool-label catalog from `data-tools-url`, if set. */
|
|
4105
4740
|
async #fetchToolCatalog() {
|
|
4106
4741
|
const url = this.getAttribute("data-tools-url");
|
|
@@ -4216,7 +4851,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4216
4851
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
4217
4852
|
*/
|
|
4218
4853
|
newChat() {
|
|
4854
|
+
this.#cancelRun();
|
|
4219
4855
|
this.conversationStore.clear(this.#threadId);
|
|
4856
|
+
this.#resetState();
|
|
4857
|
+
this.#threadId = this.conversationStore.threadId();
|
|
4858
|
+
this.#setRunning(false);
|
|
4859
|
+
}
|
|
4860
|
+
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
4861
|
+
#resetState() {
|
|
4220
4862
|
this.#client = null;
|
|
4221
4863
|
this.#streamingBubble = null;
|
|
4222
4864
|
this.#hidePending();
|
|
@@ -4224,8 +4866,36 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4224
4866
|
this.#serverSettled.clear();
|
|
4225
4867
|
this.#initialMessages = [];
|
|
4226
4868
|
this.#messages.replaceChildren();
|
|
4227
|
-
|
|
4228
|
-
|
|
4869
|
+
}
|
|
4870
|
+
/** Switch the active conversation to an existing thread and replay it. */
|
|
4871
|
+
async #switchThread(threadId) {
|
|
4872
|
+
if (threadId === this.#threadId) {
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
this.#cancelRun();
|
|
4876
|
+
this.#resetState();
|
|
4877
|
+
this.conversationStore.setActiveThread(threadId);
|
|
4878
|
+
this.#threadId = threadId;
|
|
4879
|
+
this.#setRunning(false);
|
|
4880
|
+
await this.#rehydrate();
|
|
4881
|
+
}
|
|
4882
|
+
/** Delete a thread; if it was the active one, fall back to a fresh chat. */
|
|
4883
|
+
#deleteThread(threadId) {
|
|
4884
|
+
const wasActive = threadId === this.#threadId;
|
|
4885
|
+
if (wasActive) {
|
|
4886
|
+
this.#cancelRun();
|
|
4887
|
+
}
|
|
4888
|
+
this.conversationStore.clear(threadId);
|
|
4889
|
+
if (wasActive) {
|
|
4890
|
+
this.#resetState();
|
|
4891
|
+
this.#threadId = this.conversationStore.threadId();
|
|
4892
|
+
this.#setRunning(false);
|
|
4893
|
+
}
|
|
4894
|
+
void this.#refreshDrawer();
|
|
4895
|
+
}
|
|
4896
|
+
/** Reload the drawer's thread list, marking the active thread. */
|
|
4897
|
+
async #refreshDrawer() {
|
|
4898
|
+
this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
|
|
4229
4899
|
}
|
|
4230
4900
|
/**
|
|
4231
4901
|
* Restore the conversation from the store on mount, then — if a navigating
|
|
@@ -4339,6 +5009,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4339
5009
|
title.textContent = this.getAttribute("title-text") ?? "Assistant";
|
|
4340
5010
|
const controls = document.createElement("div");
|
|
4341
5011
|
controls.className = "header-controls";
|
|
5012
|
+
const history = document.createElement("button");
|
|
5013
|
+
history.type = "button";
|
|
5014
|
+
history.className = "header-btn header-btn--history";
|
|
5015
|
+
history.title = "Chat history";
|
|
5016
|
+
history.setAttribute("aria-label", "Chat history");
|
|
5017
|
+
history.textContent = "\u2630";
|
|
5018
|
+
history.addEventListener("click", () => {
|
|
5019
|
+
void this.#refreshDrawer();
|
|
5020
|
+
this.#drawer.open();
|
|
5021
|
+
});
|
|
4342
5022
|
const newChat = document.createElement("button");
|
|
4343
5023
|
newChat.type = "button";
|
|
4344
5024
|
newChat.className = "header-btn header-btn--new";
|
|
@@ -4353,7 +5033,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4353
5033
|
collapse.setAttribute("aria-label", "Collapse");
|
|
4354
5034
|
collapse.textContent = "\u2014";
|
|
4355
5035
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
4356
|
-
controls.append(newChat, collapse);
|
|
5036
|
+
controls.append(history, newChat, collapse);
|
|
4357
5037
|
header.append(title, controls);
|
|
4358
5038
|
this.#messages.className = "messages";
|
|
4359
5039
|
this.#messages.setAttribute("role", "log");
|
|
@@ -4370,7 +5050,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4370
5050
|
this.#send.className = "send";
|
|
4371
5051
|
this.#send.type = "button";
|
|
4372
5052
|
this.#send.textContent = "Send";
|
|
5053
|
+
this.#send.setAttribute("aria-label", "Send");
|
|
5054
|
+
this.#send.dataset["state"] = "idle";
|
|
4373
5055
|
this.#send.addEventListener("click", () => {
|
|
5056
|
+
if (this.#running) {
|
|
5057
|
+
this.#cancelRun();
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
4374
5060
|
void this.#submit();
|
|
4375
5061
|
});
|
|
4376
5062
|
this.#skillHint.className = "skill-hint";
|
|
@@ -4382,7 +5068,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4382
5068
|
this.#skillsMenu.palette,
|
|
4383
5069
|
this.#skillsMenu.chips,
|
|
4384
5070
|
this.#skillHint,
|
|
4385
|
-
inputRow
|
|
5071
|
+
inputRow,
|
|
5072
|
+
this.#drawer.element
|
|
4386
5073
|
);
|
|
4387
5074
|
this.#root.append(style, this.#chat);
|
|
4388
5075
|
}
|
|
@@ -4396,11 +5083,34 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4396
5083
|
event.preventDefault();
|
|
4397
5084
|
return;
|
|
4398
5085
|
}
|
|
5086
|
+
if (event.key === "Escape" && this.#running) {
|
|
5087
|
+
event.preventDefault();
|
|
5088
|
+
this.#cancelRun();
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
4399
5091
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
4400
5092
|
event.preventDefault();
|
|
4401
5093
|
void this.#submit();
|
|
4402
5094
|
}
|
|
4403
5095
|
}
|
|
5096
|
+
/**
|
|
5097
|
+
* Stop the in-flight run: decline any confirmation card awaiting a decision
|
|
5098
|
+
* (the loop is suspended on it), then cancel the client run — the abort
|
|
5099
|
+
* closes the streaming request, which is AG-UI's cancel (the server
|
|
5100
|
+
* observes the disconnect).
|
|
5101
|
+
*/
|
|
5102
|
+
#cancelRun() {
|
|
5103
|
+
this.#confirmAbort?.abort();
|
|
5104
|
+
this.#client?.cancel();
|
|
5105
|
+
}
|
|
5106
|
+
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
5107
|
+
#setRunning(running) {
|
|
5108
|
+
this.#running = running;
|
|
5109
|
+
const label = running ? "Stop" : "Send";
|
|
5110
|
+
this.#send.textContent = label;
|
|
5111
|
+
this.#send.setAttribute("aria-label", label);
|
|
5112
|
+
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
5113
|
+
}
|
|
4404
5114
|
async #submit() {
|
|
4405
5115
|
const content = this.#input.value.trim();
|
|
4406
5116
|
if (content === "") {
|
|
@@ -4472,9 +5182,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4472
5182
|
if (typeof confirmText === "string") {
|
|
4473
5183
|
request.message = confirmText;
|
|
4474
5184
|
}
|
|
4475
|
-
|
|
5185
|
+
this.#confirmAbort = new AbortController();
|
|
5186
|
+
const decision = requestConfirmation(this.#messages, request, {
|
|
5187
|
+
signal: this.#confirmAbort.signal
|
|
5188
|
+
});
|
|
4476
5189
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
4477
5190
|
const accepted = await decision;
|
|
5191
|
+
this.#confirmAbort = null;
|
|
4478
5192
|
if (!accepted) {
|
|
4479
5193
|
const message = "User declined the action.";
|
|
4480
5194
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
@@ -4509,7 +5223,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4509
5223
|
#handlers() {
|
|
4510
5224
|
return {
|
|
4511
5225
|
onRunStart: () => {
|
|
4512
|
-
this.#
|
|
5226
|
+
this.#setRunning(true);
|
|
4513
5227
|
this.#showPending();
|
|
4514
5228
|
},
|
|
4515
5229
|
onTextDelta: (buffer) => {
|
|
@@ -4538,22 +5252,34 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4538
5252
|
},
|
|
4539
5253
|
onRunEnd: () => {
|
|
4540
5254
|
this.#hidePending();
|
|
4541
|
-
this.#send.disabled = false;
|
|
4542
5255
|
this.#streamingBubble = null;
|
|
4543
5256
|
},
|
|
4544
5257
|
onError: (message) => {
|
|
4545
5258
|
this.#hidePending();
|
|
4546
5259
|
this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`));
|
|
4547
|
-
this.#
|
|
5260
|
+
this.#streamingBubble = null;
|
|
5261
|
+
},
|
|
5262
|
+
onCancelled: () => {
|
|
5263
|
+
this.#hidePending();
|
|
5264
|
+
this.#appendStoppedNote();
|
|
4548
5265
|
this.#streamingBubble = null;
|
|
4549
5266
|
},
|
|
4550
5267
|
onSettled: () => {
|
|
4551
5268
|
this.#hidePending();
|
|
4552
|
-
this.#
|
|
5269
|
+
this.#setRunning(false);
|
|
4553
5270
|
this.#streamingBubble = null;
|
|
4554
5271
|
}
|
|
4555
5272
|
};
|
|
4556
5273
|
}
|
|
5274
|
+
/** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
|
|
5275
|
+
#appendStoppedNote() {
|
|
5276
|
+
const note = document.createElement("div");
|
|
5277
|
+
note.className = "stopped-note";
|
|
5278
|
+
note.setAttribute("role", "status");
|
|
5279
|
+
note.textContent = "\u23F9 Stopped";
|
|
5280
|
+
this.#messages.appendChild(note);
|
|
5281
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5282
|
+
}
|
|
4557
5283
|
/**
|
|
4558
5284
|
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
4559
5285
|
* silent stretch before the first token and the gap after a tool result
|
|
@@ -4775,7 +5501,7 @@ function setControlValue(el, value) {
|
|
|
4775
5501
|
}
|
|
4776
5502
|
|
|
4777
5503
|
// src/version.ts
|
|
4778
|
-
var VERSION = "0.
|
|
5504
|
+
var VERSION = "0.5.0";
|
|
4779
5505
|
export {
|
|
4780
5506
|
AgUiChat,
|
|
4781
5507
|
AgUiClient,
|
|
@@ -4783,6 +5509,7 @@ export {
|
|
|
4783
5509
|
ELEMENT_TAG,
|
|
4784
5510
|
MAX_TOOL_ROUNDS,
|
|
4785
5511
|
MESSAGE_ROLE,
|
|
5512
|
+
RemoteConversationStore,
|
|
4786
5513
|
SUBMIT_EVENT,
|
|
4787
5514
|
SessionStorageStore,
|
|
4788
5515
|
TOGGLE_EVENT,
|