@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/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
- sessionStorage.removeItem(THREAD_KEY);
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
- this.#threadId = this.conversationStore.threadId();
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.4.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,