@artooi/ag-ui-web-component 0.4.0 → 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 +21 -1
- package/dist/ag-ui-web-component.bundle.js +238 -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/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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +621 -5
- package/dist/index.js.map +4 -4
- 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 +89 -2
- package/src/core/conversation_store.ts +148 -9
- package/src/core/remote_conversation_store.ts +147 -0
- package/src/index.ts +2 -0
- package/src/ui/relative_time.ts +28 -0
- package/src/ui/styles.ts +184 -0
- package/src/ui/thread_drawer.ts +200 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -3602,8 +3602,370 @@ var STYLES = `
|
|
|
3602
3602
|
font-size: 0.85em;
|
|
3603
3603
|
color: var(--ag-ui-danger);
|
|
3604
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
|
+
}
|
|
3605
3789
|
`;
|
|
3606
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
|
+
|
|
3607
3969
|
// src/ui/tool_call_card.ts
|
|
3608
3970
|
var STATUS_LABEL = {
|
|
3609
3971
|
[TOOL_CALL_STATUS.PENDING]: "running\u2026",
|
|
@@ -3849,8 +4211,12 @@ function isAbortError(error) {
|
|
|
3849
4211
|
// src/core/conversation_store.ts
|
|
3850
4212
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
3851
4213
|
var THREAD_KEY = "ag-ui-chat:thread";
|
|
4214
|
+
var THREADS_KEY = "ag-ui-chat:threads";
|
|
3852
4215
|
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
3853
4216
|
var CHECKPOINT_PREFIX = "ag-ui-chat:checkpoint:";
|
|
4217
|
+
var TITLE_LIMIT = 60;
|
|
4218
|
+
var PREVIEW_LIMIT = 100;
|
|
4219
|
+
var DEFAULT_TITLE = "New conversation";
|
|
3854
4220
|
var SessionStorageStore = class {
|
|
3855
4221
|
threadId() {
|
|
3856
4222
|
const existing = sessionStorage.getItem(THREAD_KEY);
|
|
@@ -3866,6 +4232,7 @@ var SessionStorageStore = class {
|
|
|
3866
4232
|
}
|
|
3867
4233
|
saveMessages(threadId, messages) {
|
|
3868
4234
|
sessionStorage.setItem(MESSAGES_PREFIX + threadId, JSON.stringify(messages));
|
|
4235
|
+
this.#touchThread(threadId, messages);
|
|
3869
4236
|
}
|
|
3870
4237
|
loadCheckpoint(threadId) {
|
|
3871
4238
|
return this.#readJson(CHECKPOINT_PREFIX + threadId);
|
|
@@ -3881,7 +4248,60 @@ var SessionStorageStore = class {
|
|
|
3881
4248
|
clear(threadId) {
|
|
3882
4249
|
sessionStorage.removeItem(MESSAGES_PREFIX + threadId);
|
|
3883
4250
|
sessionStorage.removeItem(CHECKPOINT_PREFIX + threadId);
|
|
3884
|
-
|
|
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));
|
|
3885
4305
|
}
|
|
3886
4306
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
3887
4307
|
#readJson(key) {
|
|
@@ -3896,6 +4316,32 @@ var SessionStorageStore = class {
|
|
|
3896
4316
|
}
|
|
3897
4317
|
}
|
|
3898
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
|
+
}
|
|
3899
4345
|
|
|
3900
4346
|
// src/core/create_http_agent.ts
|
|
3901
4347
|
import { HttpAgent } from "@ag-ui/client";
|
|
@@ -3927,6 +4373,96 @@ function createHttpAgent(options) {
|
|
|
3927
4373
|
});
|
|
3928
4374
|
}
|
|
3929
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
|
+
|
|
3930
4466
|
// src/core/ag_ui_chat.ts
|
|
3931
4467
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
3932
4468
|
var AgUiChat = class extends HTMLElement {
|
|
@@ -4037,6 +4573,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4037
4573
|
#send;
|
|
4038
4574
|
#title;
|
|
4039
4575
|
#skillsMenu;
|
|
4576
|
+
#drawer;
|
|
4040
4577
|
#skillHint;
|
|
4041
4578
|
#client = null;
|
|
4042
4579
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
@@ -4068,6 +4605,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4068
4605
|
this.#title = document.createElement("span");
|
|
4069
4606
|
this.#skillHint = document.createElement("div");
|
|
4070
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
|
+
});
|
|
4071
4624
|
}
|
|
4072
4625
|
/** Attributes whose late changes must reflect in already-rendered chrome. */
|
|
4073
4626
|
static get observedAttributes() {
|
|
@@ -4163,9 +4716,26 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4163
4716
|
}
|
|
4164
4717
|
this.#initSkills();
|
|
4165
4718
|
void this.#fetchToolCatalog();
|
|
4719
|
+
this.#wireThreadStore();
|
|
4166
4720
|
this.#threadId = this.conversationStore.threadId();
|
|
4167
4721
|
void this.#rehydrate();
|
|
4168
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
|
+
}
|
|
4169
4739
|
/** Fetch the server tool-label catalog from `data-tools-url`, if set. */
|
|
4170
4740
|
async #fetchToolCatalog() {
|
|
4171
4741
|
const url = this.getAttribute("data-tools-url");
|
|
@@ -4283,6 +4853,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4283
4853
|
newChat() {
|
|
4284
4854
|
this.#cancelRun();
|
|
4285
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() {
|
|
4286
4862
|
this.#client = null;
|
|
4287
4863
|
this.#streamingBubble = null;
|
|
4288
4864
|
this.#hidePending();
|
|
@@ -4290,8 +4866,36 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4290
4866
|
this.#serverSettled.clear();
|
|
4291
4867
|
this.#initialMessages = [];
|
|
4292
4868
|
this.#messages.replaceChildren();
|
|
4293
|
-
|
|
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;
|
|
4294
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);
|
|
4295
4899
|
}
|
|
4296
4900
|
/**
|
|
4297
4901
|
* Restore the conversation from the store on mount, then — if a navigating
|
|
@@ -4405,6 +5009,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4405
5009
|
title.textContent = this.getAttribute("title-text") ?? "Assistant";
|
|
4406
5010
|
const controls = document.createElement("div");
|
|
4407
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
|
+
});
|
|
4408
5022
|
const newChat = document.createElement("button");
|
|
4409
5023
|
newChat.type = "button";
|
|
4410
5024
|
newChat.className = "header-btn header-btn--new";
|
|
@@ -4419,7 +5033,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4419
5033
|
collapse.setAttribute("aria-label", "Collapse");
|
|
4420
5034
|
collapse.textContent = "\u2014";
|
|
4421
5035
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
4422
|
-
controls.append(newChat, collapse);
|
|
5036
|
+
controls.append(history, newChat, collapse);
|
|
4423
5037
|
header.append(title, controls);
|
|
4424
5038
|
this.#messages.className = "messages";
|
|
4425
5039
|
this.#messages.setAttribute("role", "log");
|
|
@@ -4454,7 +5068,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4454
5068
|
this.#skillsMenu.palette,
|
|
4455
5069
|
this.#skillsMenu.chips,
|
|
4456
5070
|
this.#skillHint,
|
|
4457
|
-
inputRow
|
|
5071
|
+
inputRow,
|
|
5072
|
+
this.#drawer.element
|
|
4458
5073
|
);
|
|
4459
5074
|
this.#root.append(style, this.#chat);
|
|
4460
5075
|
}
|
|
@@ -4886,7 +5501,7 @@ function setControlValue(el, value) {
|
|
|
4886
5501
|
}
|
|
4887
5502
|
|
|
4888
5503
|
// src/version.ts
|
|
4889
|
-
var VERSION = "0.
|
|
5504
|
+
var VERSION = "0.5.0";
|
|
4890
5505
|
export {
|
|
4891
5506
|
AgUiChat,
|
|
4892
5507
|
AgUiClient,
|
|
@@ -4894,6 +5509,7 @@ export {
|
|
|
4894
5509
|
ELEMENT_TAG,
|
|
4895
5510
|
MAX_TOOL_ROUNDS,
|
|
4896
5511
|
MESSAGE_ROLE,
|
|
5512
|
+
RemoteConversationStore,
|
|
4897
5513
|
SUBMIT_EVENT,
|
|
4898
5514
|
SessionStorageStore,
|
|
4899
5515
|
TOGGLE_EVENT,
|