@artooi/ag-ui-web-component 0.4.0 → 0.6.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 +50 -1
- package/README.md +69 -2
- package/dist/ag-ui-web-component.bundle.js +355 -59
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +17 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +7 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +35 -0
- package/dist/core/attachment.d.ts.map +1 -0
- 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/core/upload_attachment.d.ts +32 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1198 -24
- package/dist/index.js.map +4 -4
- package/dist/ui/attachment_chips.d.ts +13 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +42 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -0
- 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/constants.ts +18 -0
- package/src/core/ag_ui_chat.ts +267 -14
- package/src/core/agui_client.ts +15 -2
- package/src/core/attachment.ts +39 -0
- package/src/core/conversation_store.ts +148 -9
- package/src/core/remote_conversation_store.ts +147 -0
- package/src/core/upload_attachment.ts +113 -0
- package/src/index.ts +8 -0
- package/src/ui/attachment_chips.ts +68 -0
- package/src/ui/attachment_tray.ts +237 -0
- package/src/ui/relative_time.ts +28 -0
- package/src/ui/styles.ts +294 -0
- package/src/ui/thread_drawer.ts +200 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,12 @@ var TOOL_CALL_STATUS = {
|
|
|
17
17
|
ERROR: "error",
|
|
18
18
|
DECLINED: "declined"
|
|
19
19
|
};
|
|
20
|
+
var ATTACHMENT_STATUS = {
|
|
21
|
+
UPLOADING: "uploading",
|
|
22
|
+
READY: "ready",
|
|
23
|
+
ERROR: "error"
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
20
26
|
var TOOL_DISPLAY = {
|
|
21
27
|
MINIMAL: "minimal",
|
|
22
28
|
COMPACT: "compact",
|
|
@@ -231,6 +237,227 @@ function createStateHookTools(hook) {
|
|
|
231
237
|
return tools;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
// src/ui/attachment_chips.ts
|
|
241
|
+
function renderAttachmentChips(refs) {
|
|
242
|
+
const list = document.createElement("div");
|
|
243
|
+
list.className = "attachment-chips";
|
|
244
|
+
for (const ref of refs) {
|
|
245
|
+
list.appendChild(renderChip(ref));
|
|
246
|
+
}
|
|
247
|
+
return list;
|
|
248
|
+
}
|
|
249
|
+
function renderChip(ref) {
|
|
250
|
+
const chip = document.createElement("div");
|
|
251
|
+
chip.className = "attachment-chip attachment-chip--ready";
|
|
252
|
+
const icon = document.createElement("span");
|
|
253
|
+
icon.className = "attachment-chip-icon";
|
|
254
|
+
icon.textContent = iconFor(ref.mime);
|
|
255
|
+
icon.setAttribute("aria-hidden", "true");
|
|
256
|
+
const name = document.createElement("span");
|
|
257
|
+
name.className = "attachment-chip-name";
|
|
258
|
+
name.textContent = ref.name;
|
|
259
|
+
name.title = ref.name;
|
|
260
|
+
const size = document.createElement("span");
|
|
261
|
+
size.className = "attachment-chip-size";
|
|
262
|
+
size.textContent = formatBytes(ref.size);
|
|
263
|
+
chip.append(icon, name, size);
|
|
264
|
+
return chip;
|
|
265
|
+
}
|
|
266
|
+
function iconFor(mime) {
|
|
267
|
+
if (mime.startsWith("image/")) {
|
|
268
|
+
return "\u{1F5BC}";
|
|
269
|
+
}
|
|
270
|
+
if (mime === "application/pdf") {
|
|
271
|
+
return "\u{1F4D5}";
|
|
272
|
+
}
|
|
273
|
+
if (mime.startsWith("text/")) {
|
|
274
|
+
return "\u{1F4C4}";
|
|
275
|
+
}
|
|
276
|
+
return "\u{1F4CE}";
|
|
277
|
+
}
|
|
278
|
+
function formatBytes(bytes) {
|
|
279
|
+
if (bytes < 1024) {
|
|
280
|
+
return `${bytes} B`;
|
|
281
|
+
}
|
|
282
|
+
const units = ["KB", "MB", "GB"];
|
|
283
|
+
let value = bytes / 1024;
|
|
284
|
+
let unit = 0;
|
|
285
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
286
|
+
value /= 1024;
|
|
287
|
+
unit += 1;
|
|
288
|
+
}
|
|
289
|
+
const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
|
|
290
|
+
return `${rounded} ${units[unit]}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/ui/attachment_tray.ts
|
|
294
|
+
import { randomUUID } from "@ag-ui/client";
|
|
295
|
+
var AttachmentTray = class {
|
|
296
|
+
/** The tray root; append above the input row. Hidden while empty. */
|
|
297
|
+
element;
|
|
298
|
+
#config;
|
|
299
|
+
#items = [];
|
|
300
|
+
constructor(config) {
|
|
301
|
+
this.#config = config;
|
|
302
|
+
this.element = document.createElement("div");
|
|
303
|
+
this.element.className = "attachment-tray";
|
|
304
|
+
this.element.hidden = true;
|
|
305
|
+
}
|
|
306
|
+
/** Queue a file: reject oversize/disallowed into an error chip, else upload. */
|
|
307
|
+
add(file) {
|
|
308
|
+
const item = {
|
|
309
|
+
localId: randomUUID(),
|
|
310
|
+
file,
|
|
311
|
+
status: ATTACHMENT_STATUS.UPLOADING,
|
|
312
|
+
progress: 0,
|
|
313
|
+
ref: null,
|
|
314
|
+
error: ""
|
|
315
|
+
};
|
|
316
|
+
this.#items.push(item);
|
|
317
|
+
const rejection = this.#reject(file);
|
|
318
|
+
if (rejection !== null) {
|
|
319
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
320
|
+
item.error = rejection;
|
|
321
|
+
this.#render();
|
|
322
|
+
this.#config.onChange?.();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.#render();
|
|
326
|
+
this.#config.onChange?.();
|
|
327
|
+
this.#upload(item);
|
|
328
|
+
}
|
|
329
|
+
/** The durable refs of every chip that finished uploading. */
|
|
330
|
+
readyRefs() {
|
|
331
|
+
const refs = [];
|
|
332
|
+
for (const item of this.#items) {
|
|
333
|
+
if (item.ref !== null) {
|
|
334
|
+
refs.push(item.ref);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return refs;
|
|
338
|
+
}
|
|
339
|
+
/** Whether any chip is still uploading (a send would drop nothing if false). */
|
|
340
|
+
hasPending() {
|
|
341
|
+
return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
342
|
+
}
|
|
343
|
+
/** Whether the tray holds no chips. */
|
|
344
|
+
isEmpty() {
|
|
345
|
+
return this.#items.length === 0;
|
|
346
|
+
}
|
|
347
|
+
/** Drop the settled (ready / error) chips, leaving any still uploading. */
|
|
348
|
+
clearReady() {
|
|
349
|
+
this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
350
|
+
this.#render();
|
|
351
|
+
}
|
|
352
|
+
/** Drop every chip (a reset / new-chat). */
|
|
353
|
+
clear() {
|
|
354
|
+
this.#items = [];
|
|
355
|
+
this.#render();
|
|
356
|
+
}
|
|
357
|
+
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
358
|
+
#reject(file) {
|
|
359
|
+
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
360
|
+
return `Too large (max ${formatBytes(this.#config.maxBytes)})`;
|
|
361
|
+
}
|
|
362
|
+
if (!accepts(this.#config.accept, file)) {
|
|
363
|
+
return "File type not allowed";
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
#upload(item) {
|
|
368
|
+
item.status = ATTACHMENT_STATUS.UPLOADING;
|
|
369
|
+
item.progress = 0;
|
|
370
|
+
item.error = "";
|
|
371
|
+
this.#render();
|
|
372
|
+
this.#config.upload(item.file, (fraction) => {
|
|
373
|
+
item.progress = fraction;
|
|
374
|
+
this.#render();
|
|
375
|
+
}).then((ref) => {
|
|
376
|
+
item.status = ATTACHMENT_STATUS.READY;
|
|
377
|
+
item.ref = ref;
|
|
378
|
+
}).catch((error) => {
|
|
379
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
380
|
+
item.error = error instanceof Error ? error.message : "upload failed";
|
|
381
|
+
}).finally(() => {
|
|
382
|
+
this.#render();
|
|
383
|
+
this.#config.onChange?.();
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
#remove(item) {
|
|
387
|
+
this.#items = this.#items.filter((other) => other !== item);
|
|
388
|
+
this.#render();
|
|
389
|
+
this.#config.onChange?.();
|
|
390
|
+
}
|
|
391
|
+
#render() {
|
|
392
|
+
this.element.replaceChildren();
|
|
393
|
+
this.element.hidden = this.#items.length === 0;
|
|
394
|
+
for (const item of this.#items) {
|
|
395
|
+
this.element.appendChild(this.#renderChip(item));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
#renderChip(item) {
|
|
399
|
+
const chip = document.createElement("div");
|
|
400
|
+
chip.className = `attachment-chip attachment-chip--${item.status}`;
|
|
401
|
+
const icon = document.createElement("span");
|
|
402
|
+
icon.className = "attachment-chip-icon";
|
|
403
|
+
icon.textContent = iconFor(item.file.type);
|
|
404
|
+
icon.setAttribute("aria-hidden", "true");
|
|
405
|
+
const name = document.createElement("span");
|
|
406
|
+
name.className = "attachment-chip-name";
|
|
407
|
+
name.textContent = item.file.name;
|
|
408
|
+
name.title = item.file.name;
|
|
409
|
+
const meta = document.createElement("span");
|
|
410
|
+
meta.className = "attachment-chip-size";
|
|
411
|
+
meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
|
|
412
|
+
chip.append(icon, name, meta);
|
|
413
|
+
if (item.status === ATTACHMENT_STATUS.UPLOADING) {
|
|
414
|
+
const bar = document.createElement("div");
|
|
415
|
+
bar.className = "attachment-chip-bar";
|
|
416
|
+
const fill = document.createElement("div");
|
|
417
|
+
fill.className = "attachment-chip-bar-fill";
|
|
418
|
+
fill.style.width = `${Math.round(item.progress * 100)}%`;
|
|
419
|
+
bar.appendChild(fill);
|
|
420
|
+
chip.appendChild(bar);
|
|
421
|
+
}
|
|
422
|
+
if (item.status === ATTACHMENT_STATUS.ERROR) {
|
|
423
|
+
const retry = document.createElement("button");
|
|
424
|
+
retry.type = "button";
|
|
425
|
+
retry.className = "attachment-chip-retry";
|
|
426
|
+
retry.title = "Retry";
|
|
427
|
+
retry.setAttribute("aria-label", "Retry upload");
|
|
428
|
+
retry.textContent = "\u21BB";
|
|
429
|
+
retry.addEventListener("click", () => this.#upload(item));
|
|
430
|
+
chip.appendChild(retry);
|
|
431
|
+
}
|
|
432
|
+
const remove = document.createElement("button");
|
|
433
|
+
remove.type = "button";
|
|
434
|
+
remove.className = "attachment-chip-remove";
|
|
435
|
+
remove.title = "Remove";
|
|
436
|
+
remove.setAttribute("aria-label", "Remove attachment");
|
|
437
|
+
remove.textContent = "\u2715";
|
|
438
|
+
remove.addEventListener("click", () => this.#remove(item));
|
|
439
|
+
chip.appendChild(remove);
|
|
440
|
+
return chip;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
function accepts(accept, file) {
|
|
444
|
+
const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
|
|
445
|
+
if (tokens.length === 0) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
const mime = file.type.toLowerCase();
|
|
449
|
+
const name = file.name.toLowerCase();
|
|
450
|
+
return tokens.some((token) => {
|
|
451
|
+
if (token.startsWith(".")) {
|
|
452
|
+
return name.endsWith(token);
|
|
453
|
+
}
|
|
454
|
+
if (token.endsWith("/*")) {
|
|
455
|
+
return mime.startsWith(token.slice(0, -1));
|
|
456
|
+
}
|
|
457
|
+
return mime === token;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
234
461
|
// src/ui/confirmation_card.ts
|
|
235
462
|
function actionButton(modifier, label) {
|
|
236
463
|
const button = document.createElement("button");
|
|
@@ -3464,6 +3691,116 @@ var STYLES = `
|
|
|
3464
3691
|
background: var(--ag-ui-muted);
|
|
3465
3692
|
}
|
|
3466
3693
|
|
|
3694
|
+
/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3695
|
+
/* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
|
|
3696
|
+
.attach-btn {
|
|
3697
|
+
border: 1px solid var(--ag-ui-border);
|
|
3698
|
+
border-radius: 8px;
|
|
3699
|
+
padding: 0 10px;
|
|
3700
|
+
background: var(--ag-ui-input-bg);
|
|
3701
|
+
color: inherit;
|
|
3702
|
+
font: inherit;
|
|
3703
|
+
cursor: pointer;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
.attach-btn:hover {
|
|
3707
|
+
border-color: var(--ag-ui-accent);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
.attach-input {
|
|
3711
|
+
display: none;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
3715
|
+
.attachment-slot {
|
|
3716
|
+
display: contents;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
.attachment-tray {
|
|
3720
|
+
display: flex;
|
|
3721
|
+
flex-wrap: wrap;
|
|
3722
|
+
gap: 6px;
|
|
3723
|
+
padding: 8px 12px 0;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
.attachment-chips {
|
|
3727
|
+
display: flex;
|
|
3728
|
+
flex-wrap: wrap;
|
|
3729
|
+
gap: 6px;
|
|
3730
|
+
margin-top: 6px;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
.attachment-chip {
|
|
3734
|
+
display: inline-flex;
|
|
3735
|
+
align-items: center;
|
|
3736
|
+
gap: 6px;
|
|
3737
|
+
max-width: 100%;
|
|
3738
|
+
padding: 4px 8px;
|
|
3739
|
+
border: 1px solid var(--ag-ui-border);
|
|
3740
|
+
border-radius: 999px;
|
|
3741
|
+
background: var(--ag-ui-assistant-bg);
|
|
3742
|
+
font-size: 0.85em;
|
|
3743
|
+
position: relative;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
.attachment-chip--error {
|
|
3747
|
+
border-color: var(--ag-ui-danger);
|
|
3748
|
+
color: var(--ag-ui-danger);
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
.attachment-chip-name {
|
|
3752
|
+
overflow: hidden;
|
|
3753
|
+
text-overflow: ellipsis;
|
|
3754
|
+
white-space: nowrap;
|
|
3755
|
+
max-width: 14ch;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
.attachment-chip-size {
|
|
3759
|
+
color: var(--ag-ui-muted);
|
|
3760
|
+
white-space: nowrap;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
.attachment-chip--error .attachment-chip-size {
|
|
3764
|
+
color: var(--ag-ui-danger);
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
/* The progress bar fills as the file uploads. */
|
|
3768
|
+
.attachment-chip-bar {
|
|
3769
|
+
flex-basis: 100%;
|
|
3770
|
+
height: 3px;
|
|
3771
|
+
border-radius: 2px;
|
|
3772
|
+
background: var(--ag-ui-border);
|
|
3773
|
+
overflow: hidden;
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
.attachment-chip-bar-fill {
|
|
3777
|
+
height: 100%;
|
|
3778
|
+
background: var(--ag-ui-accent);
|
|
3779
|
+
transition: width 0.15s ease;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
.attachment-chip-remove,
|
|
3783
|
+
.attachment-chip-retry {
|
|
3784
|
+
border: none;
|
|
3785
|
+
background: none;
|
|
3786
|
+
color: inherit;
|
|
3787
|
+
cursor: pointer;
|
|
3788
|
+
padding: 0;
|
|
3789
|
+
line-height: 1;
|
|
3790
|
+
opacity: 0.7;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
.attachment-chip-remove:hover,
|
|
3794
|
+
.attachment-chip-retry:hover {
|
|
3795
|
+
opacity: 1;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
/* A subtle outline while a file is dragged over the shell. */
|
|
3799
|
+
.chat--dragover {
|
|
3800
|
+
outline: 2px dashed var(--ag-ui-accent);
|
|
3801
|
+
outline-offset: -4px;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3467
3804
|
/* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
|
|
3468
3805
|
.stopped-note {
|
|
3469
3806
|
align-self: flex-start;
|
|
@@ -3602,8 +3939,370 @@ var STYLES = `
|
|
|
3602
3939
|
font-size: 0.85em;
|
|
3603
3940
|
color: var(--ag-ui-danger);
|
|
3604
3941
|
}
|
|
3942
|
+
|
|
3943
|
+
/* Chat-history drawer \u2014 a slide-over within the chat panel. */
|
|
3944
|
+
.drawer {
|
|
3945
|
+
position: absolute;
|
|
3946
|
+
inset: 0;
|
|
3947
|
+
z-index: 5;
|
|
3948
|
+
display: flex;
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
.drawer[hidden] {
|
|
3952
|
+
display: none;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
.drawer-backdrop {
|
|
3956
|
+
position: absolute;
|
|
3957
|
+
inset: 0;
|
|
3958
|
+
background: rgba(20, 20, 50, 0.32);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
.drawer-panel {
|
|
3962
|
+
position: relative;
|
|
3963
|
+
display: flex;
|
|
3964
|
+
flex-direction: column;
|
|
3965
|
+
width: min(300px, 85%);
|
|
3966
|
+
height: 100%;
|
|
3967
|
+
background: var(--ag-ui-bg);
|
|
3968
|
+
border-right: 1px solid var(--ag-ui-border);
|
|
3969
|
+
box-shadow: var(--ag-ui-shadow);
|
|
3970
|
+
overflow: hidden;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
.drawer-header {
|
|
3974
|
+
display: flex;
|
|
3975
|
+
align-items: center;
|
|
3976
|
+
justify-content: space-between;
|
|
3977
|
+
gap: var(--ag-ui-space);
|
|
3978
|
+
padding: var(--ag-ui-pad);
|
|
3979
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
.drawer-title {
|
|
3983
|
+
font-weight: 600;
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
.drawer-new {
|
|
3987
|
+
border: 1px solid var(--ag-ui-border);
|
|
3988
|
+
border-radius: var(--ag-ui-radius);
|
|
3989
|
+
background: var(--ag-ui-bg);
|
|
3990
|
+
color: var(--ag-ui-accent);
|
|
3991
|
+
padding: 4px 10px;
|
|
3992
|
+
font: inherit;
|
|
3993
|
+
font-size: 0.85em;
|
|
3994
|
+
cursor: pointer;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
.drawer-list {
|
|
3998
|
+
flex: 1;
|
|
3999
|
+
min-height: 0;
|
|
4000
|
+
overflow-y: auto;
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
.drawer-empty {
|
|
4004
|
+
padding: var(--ag-ui-pad);
|
|
4005
|
+
font-size: 0.9em;
|
|
4006
|
+
color: var(--ag-ui-muted);
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
.drawer-row {
|
|
4010
|
+
display: flex;
|
|
4011
|
+
align-items: stretch;
|
|
4012
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
.drawer-row--active {
|
|
4016
|
+
background: var(--ag-ui-assistant-bg);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
.drawer-row-select {
|
|
4020
|
+
flex: 1;
|
|
4021
|
+
min-width: 0;
|
|
4022
|
+
display: flex;
|
|
4023
|
+
flex-direction: column;
|
|
4024
|
+
gap: 2px;
|
|
4025
|
+
padding: 8px 12px;
|
|
4026
|
+
border: none;
|
|
4027
|
+
background: none;
|
|
4028
|
+
color: inherit;
|
|
4029
|
+
font: inherit;
|
|
4030
|
+
text-align: left;
|
|
4031
|
+
cursor: pointer;
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
.drawer-row-title {
|
|
4035
|
+
font-weight: 600;
|
|
4036
|
+
overflow: hidden;
|
|
4037
|
+
white-space: nowrap;
|
|
4038
|
+
text-overflow: ellipsis;
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
.drawer-row-time {
|
|
4042
|
+
font-size: 0.72em;
|
|
4043
|
+
color: var(--ag-ui-muted);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
.drawer-row-preview {
|
|
4047
|
+
font-size: 0.8em;
|
|
4048
|
+
color: var(--ag-ui-muted);
|
|
4049
|
+
overflow: hidden;
|
|
4050
|
+
white-space: nowrap;
|
|
4051
|
+
text-overflow: ellipsis;
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
.drawer-row-actions {
|
|
4055
|
+
display: flex;
|
|
4056
|
+
align-items: center;
|
|
4057
|
+
gap: 2px;
|
|
4058
|
+
padding: 0 6px;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
.drawer-row-rename,
|
|
4062
|
+
.drawer-row-delete {
|
|
4063
|
+
border: none;
|
|
4064
|
+
background: none;
|
|
4065
|
+
color: var(--ag-ui-muted);
|
|
4066
|
+
font-size: 0.9em;
|
|
4067
|
+
padding: 4px;
|
|
4068
|
+
cursor: pointer;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
.drawer-rename-input {
|
|
4072
|
+
flex: 1;
|
|
4073
|
+
min-width: 0;
|
|
4074
|
+
margin: 6px 10px;
|
|
4075
|
+
padding: 4px 8px;
|
|
4076
|
+
border: 1px solid var(--ag-ui-accent);
|
|
4077
|
+
border-radius: 6px;
|
|
4078
|
+
background: var(--ag-ui-input-bg);
|
|
4079
|
+
color: var(--ag-ui-fg);
|
|
4080
|
+
font: inherit;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
.drawer-confirm {
|
|
4084
|
+
display: flex;
|
|
4085
|
+
align-items: center;
|
|
4086
|
+
gap: 8px;
|
|
4087
|
+
padding: 8px 12px;
|
|
4088
|
+
font-size: 0.85em;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
.drawer-confirm-label {
|
|
4092
|
+
color: var(--ag-ui-danger);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
.drawer-confirm-yes {
|
|
4096
|
+
border: none;
|
|
4097
|
+
border-radius: 6px;
|
|
4098
|
+
background: var(--ag-ui-danger);
|
|
4099
|
+
color: #ffffff;
|
|
4100
|
+
padding: 3px 10px;
|
|
4101
|
+
font: inherit;
|
|
4102
|
+
cursor: pointer;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
.drawer-confirm-no {
|
|
4106
|
+
border: 1px solid var(--ag-ui-border);
|
|
4107
|
+
border-radius: 6px;
|
|
4108
|
+
background: none;
|
|
4109
|
+
color: inherit;
|
|
4110
|
+
padding: 3px 10px;
|
|
4111
|
+
font: inherit;
|
|
4112
|
+
cursor: pointer;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
/* Embedded placement: an inline, flush side panel rather than a dimmed,
|
|
4116
|
+
floating slide-over. */
|
|
4117
|
+
:host([placement="embedded"]) .drawer-backdrop {
|
|
4118
|
+
background: none;
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
:host([placement="embedded"]) .drawer-panel {
|
|
4122
|
+
width: 100%;
|
|
4123
|
+
border-right: none;
|
|
4124
|
+
box-shadow: none;
|
|
4125
|
+
}
|
|
3605
4126
|
`;
|
|
3606
4127
|
|
|
4128
|
+
// src/ui/relative_time.ts
|
|
4129
|
+
function relativeTime(timestamp, now = Date.now()) {
|
|
4130
|
+
const seconds = Math.round((now - timestamp) / 1e3);
|
|
4131
|
+
if (seconds < 60) {
|
|
4132
|
+
return "just now";
|
|
4133
|
+
}
|
|
4134
|
+
const minutes = Math.round(seconds / 60);
|
|
4135
|
+
if (minutes < 60) {
|
|
4136
|
+
return `${minutes}m ago`;
|
|
4137
|
+
}
|
|
4138
|
+
const hours = Math.round(minutes / 60);
|
|
4139
|
+
if (hours < 24) {
|
|
4140
|
+
return `${hours}h ago`;
|
|
4141
|
+
}
|
|
4142
|
+
const days = Math.round(hours / 24);
|
|
4143
|
+
if (days < 7) {
|
|
4144
|
+
return `${days}d ago`;
|
|
4145
|
+
}
|
|
4146
|
+
return `${Math.round(days / 7)}w ago`;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// src/ui/thread_drawer.ts
|
|
4150
|
+
var ThreadDrawer = class {
|
|
4151
|
+
/** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
|
|
4152
|
+
element;
|
|
4153
|
+
#callbacks;
|
|
4154
|
+
#list;
|
|
4155
|
+
#threads = [];
|
|
4156
|
+
#activeId = "";
|
|
4157
|
+
constructor(callbacks) {
|
|
4158
|
+
this.#callbacks = callbacks;
|
|
4159
|
+
this.element = document.createElement("div");
|
|
4160
|
+
this.element.className = "drawer";
|
|
4161
|
+
this.element.hidden = true;
|
|
4162
|
+
const backdrop = document.createElement("div");
|
|
4163
|
+
backdrop.className = "drawer-backdrop";
|
|
4164
|
+
backdrop.addEventListener("click", () => this.close());
|
|
4165
|
+
const panel = document.createElement("div");
|
|
4166
|
+
panel.className = "drawer-panel";
|
|
4167
|
+
panel.setAttribute("role", "dialog");
|
|
4168
|
+
panel.setAttribute("aria-label", "Chat history");
|
|
4169
|
+
const header = document.createElement("div");
|
|
4170
|
+
header.className = "drawer-header";
|
|
4171
|
+
const heading = document.createElement("span");
|
|
4172
|
+
heading.className = "drawer-title";
|
|
4173
|
+
heading.textContent = "Chats";
|
|
4174
|
+
const newButton = document.createElement("button");
|
|
4175
|
+
newButton.type = "button";
|
|
4176
|
+
newButton.className = "drawer-new";
|
|
4177
|
+
newButton.textContent = "New chat";
|
|
4178
|
+
newButton.addEventListener("click", () => {
|
|
4179
|
+
this.close();
|
|
4180
|
+
this.#callbacks.onNew();
|
|
4181
|
+
});
|
|
4182
|
+
header.append(heading, newButton);
|
|
4183
|
+
this.#list = document.createElement("div");
|
|
4184
|
+
this.#list.className = "drawer-list";
|
|
4185
|
+
panel.append(header, this.#list);
|
|
4186
|
+
this.element.append(backdrop, panel);
|
|
4187
|
+
}
|
|
4188
|
+
isOpen() {
|
|
4189
|
+
return !this.element.hidden;
|
|
4190
|
+
}
|
|
4191
|
+
open() {
|
|
4192
|
+
this.element.hidden = false;
|
|
4193
|
+
}
|
|
4194
|
+
close() {
|
|
4195
|
+
this.element.hidden = true;
|
|
4196
|
+
}
|
|
4197
|
+
toggle() {
|
|
4198
|
+
this.element.hidden = !this.element.hidden;
|
|
4199
|
+
}
|
|
4200
|
+
/** Render the rows (or the empty state), highlighting the active thread. */
|
|
4201
|
+
setThreads(threads, activeId) {
|
|
4202
|
+
this.#threads = threads;
|
|
4203
|
+
this.#activeId = activeId;
|
|
4204
|
+
this.#renderList();
|
|
4205
|
+
}
|
|
4206
|
+
#renderList() {
|
|
4207
|
+
this.#list.replaceChildren();
|
|
4208
|
+
if (this.#threads.length === 0) {
|
|
4209
|
+
const empty = document.createElement("div");
|
|
4210
|
+
empty.className = "drawer-empty";
|
|
4211
|
+
empty.textContent = "No conversations yet.";
|
|
4212
|
+
this.#list.appendChild(empty);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
for (const meta of this.#threads) {
|
|
4216
|
+
this.#list.appendChild(this.#renderRow(meta));
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
#renderRow(meta) {
|
|
4220
|
+
const row = document.createElement("div");
|
|
4221
|
+
row.className = "drawer-row";
|
|
4222
|
+
if (meta.threadId === this.#activeId) {
|
|
4223
|
+
row.classList.add("drawer-row--active");
|
|
4224
|
+
}
|
|
4225
|
+
const select = document.createElement("button");
|
|
4226
|
+
select.type = "button";
|
|
4227
|
+
select.className = "drawer-row-select";
|
|
4228
|
+
const title = document.createElement("span");
|
|
4229
|
+
title.className = "drawer-row-title";
|
|
4230
|
+
title.textContent = meta.title;
|
|
4231
|
+
const time = document.createElement("span");
|
|
4232
|
+
time.className = "drawer-row-time";
|
|
4233
|
+
time.textContent = relativeTime(meta.updatedAt);
|
|
4234
|
+
const preview = document.createElement("span");
|
|
4235
|
+
preview.className = "drawer-row-preview";
|
|
4236
|
+
preview.textContent = meta.preview;
|
|
4237
|
+
select.append(title, time, preview);
|
|
4238
|
+
select.addEventListener("click", () => {
|
|
4239
|
+
this.close();
|
|
4240
|
+
this.#callbacks.onSelect(meta.threadId);
|
|
4241
|
+
});
|
|
4242
|
+
const rename = document.createElement("button");
|
|
4243
|
+
rename.type = "button";
|
|
4244
|
+
rename.className = "drawer-row-rename";
|
|
4245
|
+
rename.title = "Rename";
|
|
4246
|
+
rename.setAttribute("aria-label", "Rename conversation");
|
|
4247
|
+
rename.textContent = "\u270E";
|
|
4248
|
+
rename.addEventListener("click", () => this.#startRename(row, meta));
|
|
4249
|
+
const remove = document.createElement("button");
|
|
4250
|
+
remove.type = "button";
|
|
4251
|
+
remove.className = "drawer-row-delete";
|
|
4252
|
+
remove.title = "Delete";
|
|
4253
|
+
remove.setAttribute("aria-label", "Delete conversation");
|
|
4254
|
+
remove.textContent = "\u{1F5D1}";
|
|
4255
|
+
remove.addEventListener("click", () => this.#confirmDelete(row, meta));
|
|
4256
|
+
const actions = document.createElement("div");
|
|
4257
|
+
actions.className = "drawer-row-actions";
|
|
4258
|
+
actions.append(rename, remove);
|
|
4259
|
+
row.append(select, actions);
|
|
4260
|
+
return row;
|
|
4261
|
+
}
|
|
4262
|
+
/** Swap a row for an inline rename input; Enter commits, Escape cancels. */
|
|
4263
|
+
#startRename(row, meta) {
|
|
4264
|
+
const input = document.createElement("input");
|
|
4265
|
+
input.type = "text";
|
|
4266
|
+
input.className = "drawer-rename-input";
|
|
4267
|
+
input.value = meta.title;
|
|
4268
|
+
input.addEventListener("keydown", (event) => {
|
|
4269
|
+
if (event.key === "Enter") {
|
|
4270
|
+
const value = input.value.trim();
|
|
4271
|
+
if (value === "") {
|
|
4272
|
+
this.#renderList();
|
|
4273
|
+
} else {
|
|
4274
|
+
this.#callbacks.onRename(meta.threadId, value);
|
|
4275
|
+
}
|
|
4276
|
+
} else if (event.key === "Escape") {
|
|
4277
|
+
this.#renderList();
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
row.replaceChildren(input);
|
|
4281
|
+
input.focus();
|
|
4282
|
+
input.select();
|
|
4283
|
+
}
|
|
4284
|
+
/** Swap a row for an inline "Delete? [Delete] [Cancel]" confirm. */
|
|
4285
|
+
#confirmDelete(row, meta) {
|
|
4286
|
+
const confirm = document.createElement("div");
|
|
4287
|
+
confirm.className = "drawer-confirm";
|
|
4288
|
+
const label = document.createElement("span");
|
|
4289
|
+
label.className = "drawer-confirm-label";
|
|
4290
|
+
label.textContent = "Delete?";
|
|
4291
|
+
const yes = document.createElement("button");
|
|
4292
|
+
yes.type = "button";
|
|
4293
|
+
yes.className = "drawer-confirm-yes";
|
|
4294
|
+
yes.textContent = "Delete";
|
|
4295
|
+
yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
|
|
4296
|
+
const no = document.createElement("button");
|
|
4297
|
+
no.type = "button";
|
|
4298
|
+
no.className = "drawer-confirm-no";
|
|
4299
|
+
no.textContent = "Cancel";
|
|
4300
|
+
no.addEventListener("click", () => this.#renderList());
|
|
4301
|
+
confirm.append(label, yes, no);
|
|
4302
|
+
row.replaceChildren(confirm);
|
|
4303
|
+
}
|
|
4304
|
+
};
|
|
4305
|
+
|
|
3607
4306
|
// src/ui/tool_call_card.ts
|
|
3608
4307
|
var STATUS_LABEL = {
|
|
3609
4308
|
[TOOL_CALL_STATUS.PENDING]: "running\u2026",
|
|
@@ -3684,7 +4383,7 @@ ${text2}`;
|
|
|
3684
4383
|
};
|
|
3685
4384
|
|
|
3686
4385
|
// src/core/agui_client.ts
|
|
3687
|
-
import { randomUUID } from "@ag-ui/client";
|
|
4386
|
+
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
3688
4387
|
var AgUiClient = class {
|
|
3689
4388
|
#agent;
|
|
3690
4389
|
#handlers;
|
|
@@ -3718,9 +4417,18 @@ var AgUiClient = class {
|
|
|
3718
4417
|
* When the agent calls frontend tools, this executes them and re-runs the
|
|
3719
4418
|
* agent with the results, looping until the agent stops calling frontend
|
|
3720
4419
|
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
4420
|
+
*
|
|
4421
|
+
* `attachments` ride on the user message as a non-standard field so the
|
|
4422
|
+
* default client store round-trips them for history replay; the agent learns
|
|
4423
|
+
* the ids from the run context (the server's strict validation ignores the
|
|
4424
|
+
* unknown message field), then reads bytes via the `read_attachment` tool.
|
|
3721
4425
|
*/
|
|
3722
|
-
async send(content) {
|
|
3723
|
-
|
|
4426
|
+
async send(content, attachments = []) {
|
|
4427
|
+
const message = { id: randomUUID2(), role: "user", content };
|
|
4428
|
+
if (attachments.length > 0) {
|
|
4429
|
+
message.attachments = attachments;
|
|
4430
|
+
}
|
|
4431
|
+
this.#agent.addMessage(message);
|
|
3724
4432
|
this.#onPersist(this.#agent.messages);
|
|
3725
4433
|
await this.#run();
|
|
3726
4434
|
}
|
|
@@ -3734,7 +4442,7 @@ var AgUiClient = class {
|
|
|
3734
4442
|
}
|
|
3735
4443
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
3736
4444
|
addToolResult(toolCallId, content) {
|
|
3737
|
-
this.#agent.addMessage({ id:
|
|
4445
|
+
this.#agent.addMessage({ id: randomUUID2(), role: "tool", content, toolCallId });
|
|
3738
4446
|
this.#onPersist(this.#agent.messages);
|
|
3739
4447
|
}
|
|
3740
4448
|
/**
|
|
@@ -3796,7 +4504,7 @@ var AgUiClient = class {
|
|
|
3796
4504
|
return;
|
|
3797
4505
|
}
|
|
3798
4506
|
this.#agent.addMessage({
|
|
3799
|
-
id:
|
|
4507
|
+
id: randomUUID2(),
|
|
3800
4508
|
role: "tool",
|
|
3801
4509
|
content: result.content,
|
|
3802
4510
|
toolCallId: call.id
|
|
@@ -3846,18 +4554,28 @@ function isAbortError(error) {
|
|
|
3846
4554
|
return error instanceof Error && error.name === "AbortError";
|
|
3847
4555
|
}
|
|
3848
4556
|
|
|
4557
|
+
// src/core/attachment.ts
|
|
4558
|
+
function messageAttachments(message) {
|
|
4559
|
+
const refs = message.attachments;
|
|
4560
|
+
return Array.isArray(refs) ? refs : [];
|
|
4561
|
+
}
|
|
4562
|
+
|
|
3849
4563
|
// src/core/conversation_store.ts
|
|
3850
|
-
import { randomUUID as
|
|
4564
|
+
import { randomUUID as randomUUID3 } from "@ag-ui/client";
|
|
3851
4565
|
var THREAD_KEY = "ag-ui-chat:thread";
|
|
4566
|
+
var THREADS_KEY = "ag-ui-chat:threads";
|
|
3852
4567
|
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
3853
4568
|
var CHECKPOINT_PREFIX = "ag-ui-chat:checkpoint:";
|
|
4569
|
+
var TITLE_LIMIT = 60;
|
|
4570
|
+
var PREVIEW_LIMIT = 100;
|
|
4571
|
+
var DEFAULT_TITLE = "New conversation";
|
|
3854
4572
|
var SessionStorageStore = class {
|
|
3855
4573
|
threadId() {
|
|
3856
4574
|
const existing = sessionStorage.getItem(THREAD_KEY);
|
|
3857
4575
|
if (existing !== null) {
|
|
3858
4576
|
return existing;
|
|
3859
4577
|
}
|
|
3860
|
-
const id =
|
|
4578
|
+
const id = randomUUID3();
|
|
3861
4579
|
sessionStorage.setItem(THREAD_KEY, id);
|
|
3862
4580
|
return id;
|
|
3863
4581
|
}
|
|
@@ -3866,6 +4584,7 @@ var SessionStorageStore = class {
|
|
|
3866
4584
|
}
|
|
3867
4585
|
saveMessages(threadId, messages) {
|
|
3868
4586
|
sessionStorage.setItem(MESSAGES_PREFIX + threadId, JSON.stringify(messages));
|
|
4587
|
+
this.#touchThread(threadId, messages);
|
|
3869
4588
|
}
|
|
3870
4589
|
loadCheckpoint(threadId) {
|
|
3871
4590
|
return this.#readJson(CHECKPOINT_PREFIX + threadId);
|
|
@@ -3881,7 +4600,60 @@ var SessionStorageStore = class {
|
|
|
3881
4600
|
clear(threadId) {
|
|
3882
4601
|
sessionStorage.removeItem(MESSAGES_PREFIX + threadId);
|
|
3883
4602
|
sessionStorage.removeItem(CHECKPOINT_PREFIX + threadId);
|
|
3884
|
-
|
|
4603
|
+
this.#writeThreads(this.#readThreads().filter((thread) => thread.threadId !== threadId));
|
|
4604
|
+
if (sessionStorage.getItem(THREAD_KEY) === threadId) {
|
|
4605
|
+
sessionStorage.removeItem(THREAD_KEY);
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
listThreads() {
|
|
4609
|
+
const metas = this.#readThreads().sort((a, b2) => b2.updatedAt - a.updatedAt).map(({ threadId, title, updatedAt, preview }) => ({ threadId, title, updatedAt, preview }));
|
|
4610
|
+
return Promise.resolve(metas);
|
|
4611
|
+
}
|
|
4612
|
+
setActiveThread(threadId) {
|
|
4613
|
+
sessionStorage.setItem(THREAD_KEY, threadId);
|
|
4614
|
+
}
|
|
4615
|
+
renameThread(threadId, title) {
|
|
4616
|
+
const threads = this.#readThreads();
|
|
4617
|
+
const entry = threads.find((thread) => thread.threadId === threadId);
|
|
4618
|
+
if (entry === void 0) {
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
entry.title = title;
|
|
4622
|
+
entry.titleCustom = true;
|
|
4623
|
+
this.#writeThreads(threads);
|
|
4624
|
+
}
|
|
4625
|
+
/** Add or refresh a thread's drawer metadata from its latest messages. */
|
|
4626
|
+
#touchThread(threadId, messages) {
|
|
4627
|
+
const threads = this.#readThreads();
|
|
4628
|
+
const entry = threads.find((thread) => thread.threadId === threadId);
|
|
4629
|
+
const preview = derivePreview(messages);
|
|
4630
|
+
const updatedAt = Date.now();
|
|
4631
|
+
if (entry === void 0) {
|
|
4632
|
+
threads.push({
|
|
4633
|
+
threadId,
|
|
4634
|
+
title: deriveTitle(messages),
|
|
4635
|
+
titleCustom: false,
|
|
4636
|
+
preview,
|
|
4637
|
+
updatedAt
|
|
4638
|
+
});
|
|
4639
|
+
} else {
|
|
4640
|
+
entry.preview = preview;
|
|
4641
|
+
entry.updatedAt = updatedAt;
|
|
4642
|
+
if (!entry.titleCustom) {
|
|
4643
|
+
entry.title = deriveTitle(messages);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
this.#writeThreads(threads);
|
|
4647
|
+
}
|
|
4648
|
+
#readThreads() {
|
|
4649
|
+
return this.#readJson(THREADS_KEY) ?? [];
|
|
4650
|
+
}
|
|
4651
|
+
#writeThreads(threads) {
|
|
4652
|
+
if (threads.length === 0) {
|
|
4653
|
+
sessionStorage.removeItem(THREADS_KEY);
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
sessionStorage.setItem(THREADS_KEY, JSON.stringify(threads));
|
|
3885
4657
|
}
|
|
3886
4658
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
3887
4659
|
#readJson(key) {
|
|
@@ -3896,6 +4668,32 @@ var SessionStorageStore = class {
|
|
|
3896
4668
|
}
|
|
3897
4669
|
}
|
|
3898
4670
|
};
|
|
4671
|
+
function deriveTitle(messages) {
|
|
4672
|
+
for (const message of messages) {
|
|
4673
|
+
if (message.role === "user") {
|
|
4674
|
+
const text2 = cleanText(message.content);
|
|
4675
|
+
if (text2 !== "") {
|
|
4676
|
+
return truncate(text2, TITLE_LIMIT);
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return DEFAULT_TITLE;
|
|
4681
|
+
}
|
|
4682
|
+
function derivePreview(messages) {
|
|
4683
|
+
for (const message of [...messages].reverse()) {
|
|
4684
|
+
const text2 = cleanText(message.content);
|
|
4685
|
+
if (text2 !== "") {
|
|
4686
|
+
return truncate(text2, PREVIEW_LIMIT);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
return "";
|
|
4690
|
+
}
|
|
4691
|
+
function cleanText(content) {
|
|
4692
|
+
return typeof content === "string" ? content.replace(/\s+/g, " ").trim() : "";
|
|
4693
|
+
}
|
|
4694
|
+
function truncate(text2, limit) {
|
|
4695
|
+
return text2.length <= limit ? text2 : `${text2.slice(0, limit - 1).trimEnd()}\u2026`;
|
|
4696
|
+
}
|
|
3899
4697
|
|
|
3900
4698
|
// src/core/create_http_agent.ts
|
|
3901
4699
|
import { HttpAgent } from "@ag-ui/client";
|
|
@@ -3927,6 +4725,160 @@ function createHttpAgent(options) {
|
|
|
3927
4725
|
});
|
|
3928
4726
|
}
|
|
3929
4727
|
|
|
4728
|
+
// src/core/remote_conversation_store.ts
|
|
4729
|
+
var RemoteConversationStore = class {
|
|
4730
|
+
#url;
|
|
4731
|
+
#headers;
|
|
4732
|
+
#local;
|
|
4733
|
+
#dropped = /* @__PURE__ */ new Set();
|
|
4734
|
+
#renamed = /* @__PURE__ */ new Map();
|
|
4735
|
+
constructor(url, headers = () => ({}), local = new SessionStorageStore()) {
|
|
4736
|
+
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
4737
|
+
this.#headers = headers;
|
|
4738
|
+
this.#local = local;
|
|
4739
|
+
}
|
|
4740
|
+
threadId() {
|
|
4741
|
+
return this.#local.threadId();
|
|
4742
|
+
}
|
|
4743
|
+
setActiveThread(threadId) {
|
|
4744
|
+
this.#local.setActiveThread(threadId);
|
|
4745
|
+
}
|
|
4746
|
+
saveMessages(threadId, messages) {
|
|
4747
|
+
this.#local.saveMessages(threadId, messages);
|
|
4748
|
+
}
|
|
4749
|
+
loadCheckpoint(threadId) {
|
|
4750
|
+
return this.#local.loadCheckpoint(threadId);
|
|
4751
|
+
}
|
|
4752
|
+
saveCheckpoint(threadId, checkpoint) {
|
|
4753
|
+
this.#local.saveCheckpoint(threadId, checkpoint);
|
|
4754
|
+
}
|
|
4755
|
+
renameThread(threadId, title) {
|
|
4756
|
+
this.#local.renameThread(threadId, title);
|
|
4757
|
+
this.#renamed.set(threadId, title);
|
|
4758
|
+
void this.#mutate(threadId, "PATCH", { title });
|
|
4759
|
+
}
|
|
4760
|
+
clear(threadId) {
|
|
4761
|
+
this.#local.clear(threadId);
|
|
4762
|
+
this.#dropped.add(threadId);
|
|
4763
|
+
void this.#mutate(threadId, "DELETE");
|
|
4764
|
+
}
|
|
4765
|
+
async listThreads() {
|
|
4766
|
+
const rows = await this.#fetchThreads();
|
|
4767
|
+
if (rows === null) {
|
|
4768
|
+
return this.#local.listThreads();
|
|
4769
|
+
}
|
|
4770
|
+
return rows.filter((row) => !this.#dropped.has(row.thread_id)).map((row) => this.#toMeta(row));
|
|
4771
|
+
}
|
|
4772
|
+
async loadMessages(threadId) {
|
|
4773
|
+
const response = await this.#get(this.#url + encodeURIComponent(threadId) + "/");
|
|
4774
|
+
if (response === null || !response.ok) {
|
|
4775
|
+
return this.#local.loadMessages(threadId);
|
|
4776
|
+
}
|
|
4777
|
+
const body = await response.json();
|
|
4778
|
+
return body.messages ?? null;
|
|
4779
|
+
}
|
|
4780
|
+
async #fetchThreads() {
|
|
4781
|
+
const response = await this.#get(this.#url);
|
|
4782
|
+
if (response === null || !response.ok) {
|
|
4783
|
+
return null;
|
|
4784
|
+
}
|
|
4785
|
+
const body = await response.json();
|
|
4786
|
+
return body.threads ?? [];
|
|
4787
|
+
}
|
|
4788
|
+
#toMeta(row) {
|
|
4789
|
+
return {
|
|
4790
|
+
threadId: row.thread_id,
|
|
4791
|
+
title: this.#renamed.get(row.thread_id) ?? row.title,
|
|
4792
|
+
updatedAt: row.updated_at === null ? 0 : Date.parse(row.updated_at),
|
|
4793
|
+
preview: row.preview
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4796
|
+
/** GET that resolves to the `Response`, or `null` on a network error. */
|
|
4797
|
+
async #get(url) {
|
|
4798
|
+
try {
|
|
4799
|
+
return await fetch(url, { headers: this.#headers() });
|
|
4800
|
+
} catch {
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
/** Fire a best-effort write to the thread endpoint; failures are tolerated. */
|
|
4805
|
+
async #mutate(threadId, method, body) {
|
|
4806
|
+
const headers = this.#headers();
|
|
4807
|
+
try {
|
|
4808
|
+
await fetch(this.#url + encodeURIComponent(threadId) + "/", {
|
|
4809
|
+
method,
|
|
4810
|
+
headers: body === void 0 ? headers : { ...headers, "content-type": "application/json" },
|
|
4811
|
+
body: body === void 0 ? null : JSON.stringify(body)
|
|
4812
|
+
});
|
|
4813
|
+
} catch {
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
};
|
|
4817
|
+
|
|
4818
|
+
// src/core/upload_attachment.ts
|
|
4819
|
+
function uploadAttachment(file, options) {
|
|
4820
|
+
return new Promise((resolve, reject) => {
|
|
4821
|
+
const form = new FormData();
|
|
4822
|
+
form.append("file", file);
|
|
4823
|
+
const xhr = new XMLHttpRequest();
|
|
4824
|
+
xhr.open("POST", options.url);
|
|
4825
|
+
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
4826
|
+
xhr.setRequestHeader(key, value);
|
|
4827
|
+
}
|
|
4828
|
+
const onProgress = options.onProgress;
|
|
4829
|
+
if (onProgress !== void 0) {
|
|
4830
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
4831
|
+
if (event.lengthComputable) {
|
|
4832
|
+
onProgress(event.total === 0 ? 0 : event.loaded / event.total);
|
|
4833
|
+
}
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4836
|
+
xhr.addEventListener("load", () => {
|
|
4837
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
4838
|
+
try {
|
|
4839
|
+
resolve(parseRef(JSON.parse(xhr.responseText)));
|
|
4840
|
+
} catch {
|
|
4841
|
+
reject(new Error("upload returned an unreadable response"));
|
|
4842
|
+
}
|
|
4843
|
+
} else {
|
|
4844
|
+
reject(new Error(errorMessage(xhr)));
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
4848
|
+
xhr.addEventListener("abort", () => reject(new Error("upload cancelled")));
|
|
4849
|
+
const signal = options.signal;
|
|
4850
|
+
if (signal !== void 0) {
|
|
4851
|
+
signal.addEventListener("abort", () => xhr.abort());
|
|
4852
|
+
}
|
|
4853
|
+
xhr.send(form);
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
function parseRef(body) {
|
|
4857
|
+
if (typeof body !== "object" || body === null) {
|
|
4858
|
+
throw new Error("not an object");
|
|
4859
|
+
}
|
|
4860
|
+
const o = body;
|
|
4861
|
+
const id = o["id"];
|
|
4862
|
+
const name = o["name"];
|
|
4863
|
+
const mime = o["mime"];
|
|
4864
|
+
const size = o["size"];
|
|
4865
|
+
const url = o["url"];
|
|
4866
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof mime !== "string" || typeof size !== "number") {
|
|
4867
|
+
throw new Error("missing fields");
|
|
4868
|
+
}
|
|
4869
|
+
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
4870
|
+
}
|
|
4871
|
+
function errorMessage(xhr) {
|
|
4872
|
+
try {
|
|
4873
|
+
const body = JSON.parse(xhr.responseText);
|
|
4874
|
+
if (typeof body.error === "string") {
|
|
4875
|
+
return body.error;
|
|
4876
|
+
}
|
|
4877
|
+
} catch {
|
|
4878
|
+
}
|
|
4879
|
+
return `upload failed (${xhr.status})`;
|
|
4880
|
+
}
|
|
4881
|
+
|
|
3930
4882
|
// src/core/ag_ui_chat.ts
|
|
3931
4883
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
3932
4884
|
var AgUiChat = class extends HTMLElement {
|
|
@@ -3967,9 +4919,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
3967
4919
|
];
|
|
3968
4920
|
/**
|
|
3969
4921
|
* Per-run context provider. Defaults to the compact page map (when a
|
|
3970
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
4922
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
4923
|
+
* plus a one-line manifest of the files attached to the message being sent,
|
|
4924
|
+
* so the agent knows which `read_attachment` ids are available.
|
|
3971
4925
|
*/
|
|
3972
|
-
getContext = () =>
|
|
4926
|
+
getContext = () => [
|
|
4927
|
+
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
4928
|
+
...this.#attachmentContext()
|
|
4929
|
+
];
|
|
3973
4930
|
/**
|
|
3974
4931
|
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
3975
4932
|
* A compact summary also rides in each run's context.
|
|
@@ -3991,6 +4948,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
3991
4948
|
* server-backed store for cross-tab/device durability.
|
|
3992
4949
|
*/
|
|
3993
4950
|
conversationStore = new SessionStorageStore();
|
|
4951
|
+
/**
|
|
4952
|
+
* How attached files are uploaded. `null` (default) uses the built-in
|
|
4953
|
+
* multipart `POST` to `data-attachments-url`. Set a custom
|
|
4954
|
+
* {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
|
|
4955
|
+
* swap the transport (e.g. a `tus-js-client` resumable adapter or
|
|
4956
|
+
* direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
|
|
4957
|
+
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
4958
|
+
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
4959
|
+
*/
|
|
4960
|
+
uploadHandler = null;
|
|
3994
4961
|
/**
|
|
3995
4962
|
* Builds the tool result a navigating tool resumes with after the page
|
|
3996
4963
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -4037,7 +5004,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4037
5004
|
#send;
|
|
4038
5005
|
#title;
|
|
4039
5006
|
#skillsMenu;
|
|
5007
|
+
#drawer;
|
|
4040
5008
|
#skillHint;
|
|
5009
|
+
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
5010
|
+
#attachButton;
|
|
5011
|
+
#fileInput;
|
|
5012
|
+
#attachSlot;
|
|
5013
|
+
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5014
|
+
#attachTray = null;
|
|
5015
|
+
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5016
|
+
#runAttachments = [];
|
|
4041
5017
|
#client = null;
|
|
4042
5018
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
4043
5019
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
@@ -4067,7 +5043,26 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4067
5043
|
this.#send = document.createElement("button");
|
|
4068
5044
|
this.#title = document.createElement("span");
|
|
4069
5045
|
this.#skillHint = document.createElement("div");
|
|
5046
|
+
this.#attachButton = document.createElement("button");
|
|
5047
|
+
this.#fileInput = document.createElement("input");
|
|
5048
|
+
this.#attachSlot = document.createElement("div");
|
|
4070
5049
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
5050
|
+
this.#drawer = new ThreadDrawer({
|
|
5051
|
+
onSelect: (threadId) => {
|
|
5052
|
+
void this.#switchThread(threadId);
|
|
5053
|
+
},
|
|
5054
|
+
onNew: () => {
|
|
5055
|
+
this.newChat();
|
|
5056
|
+
void this.#refreshDrawer();
|
|
5057
|
+
},
|
|
5058
|
+
onRename: (threadId, title) => {
|
|
5059
|
+
this.conversationStore.renameThread(threadId, title);
|
|
5060
|
+
void this.#refreshDrawer();
|
|
5061
|
+
},
|
|
5062
|
+
onDelete: (threadId) => {
|
|
5063
|
+
this.#deleteThread(threadId);
|
|
5064
|
+
}
|
|
5065
|
+
});
|
|
4071
5066
|
}
|
|
4072
5067
|
/** Attributes whose late changes must reflect in already-rendered chrome. */
|
|
4073
5068
|
static get observedAttributes() {
|
|
@@ -4163,9 +5158,113 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4163
5158
|
}
|
|
4164
5159
|
this.#initSkills();
|
|
4165
5160
|
void this.#fetchToolCatalog();
|
|
5161
|
+
this.#wireThreadStore();
|
|
5162
|
+
this.#wireAttachments();
|
|
4166
5163
|
this.#threadId = this.conversationStore.threadId();
|
|
4167
5164
|
void this.#rehydrate();
|
|
4168
5165
|
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
5168
|
+
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
5169
|
+
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
5170
|
+
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
5171
|
+
* stays hidden and the chat degrades to text-only.
|
|
5172
|
+
*/
|
|
5173
|
+
#wireAttachments() {
|
|
5174
|
+
const url = this.getAttribute("data-attachments-url");
|
|
5175
|
+
const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
|
|
5176
|
+
if (upload === null) {
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
5180
|
+
this.#attachTray = new AttachmentTray({
|
|
5181
|
+
upload,
|
|
5182
|
+
maxBytes: this.#attachmentMaxBytes(),
|
|
5183
|
+
accept
|
|
5184
|
+
});
|
|
5185
|
+
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
5186
|
+
this.#fileInput.accept = accept;
|
|
5187
|
+
this.#attachButton.hidden = false;
|
|
5188
|
+
this.#enableDragAndDrop();
|
|
5189
|
+
}
|
|
5190
|
+
/** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
|
|
5191
|
+
#defaultUploadHandler(url) {
|
|
5192
|
+
if (url === null) {
|
|
5193
|
+
return null;
|
|
5194
|
+
}
|
|
5195
|
+
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
5196
|
+
}
|
|
5197
|
+
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5198
|
+
#attachmentMaxBytes() {
|
|
5199
|
+
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
5200
|
+
if (attr === null) {
|
|
5201
|
+
return DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5202
|
+
}
|
|
5203
|
+
const parsed = Number.parseInt(attr, 10);
|
|
5204
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5205
|
+
}
|
|
5206
|
+
/** Queue every file from the picker into the tray, then reset the input. */
|
|
5207
|
+
#onFilesPicked() {
|
|
5208
|
+
const files = this.#fileInput.files;
|
|
5209
|
+
if (files !== null) {
|
|
5210
|
+
for (const file of Array.from(files)) {
|
|
5211
|
+
this.#attachTray?.add(file);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
this.#fileInput.value = "";
|
|
5215
|
+
}
|
|
5216
|
+
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
5217
|
+
#enableDragAndDrop() {
|
|
5218
|
+
this.#chat.addEventListener("dragover", (event) => {
|
|
5219
|
+
event.preventDefault();
|
|
5220
|
+
this.#chat.classList.add("chat--dragover");
|
|
5221
|
+
});
|
|
5222
|
+
this.#chat.addEventListener("dragleave", () => {
|
|
5223
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5224
|
+
});
|
|
5225
|
+
this.#chat.addEventListener("drop", (event) => {
|
|
5226
|
+
event.preventDefault();
|
|
5227
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5228
|
+
const files = event.dataTransfer?.files;
|
|
5229
|
+
if (files !== void 0) {
|
|
5230
|
+
for (const file of Array.from(files)) {
|
|
5231
|
+
this.#attachTray?.add(file);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
/** The one-line manifest of the message's attachments, for the run context. */
|
|
5237
|
+
#attachmentContext() {
|
|
5238
|
+
if (this.#runAttachments.length === 0) {
|
|
5239
|
+
return [];
|
|
5240
|
+
}
|
|
5241
|
+
const lines = this.#runAttachments.map(
|
|
5242
|
+
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`
|
|
5243
|
+
);
|
|
5244
|
+
return [
|
|
5245
|
+
{
|
|
5246
|
+
description: "Files the user attached to this message",
|
|
5247
|
+
value: `${lines.join("\n")}
|
|
5248
|
+
Use the read_attachment tool with an id to read a file's contents.`
|
|
5249
|
+
}
|
|
5250
|
+
];
|
|
5251
|
+
}
|
|
5252
|
+
/**
|
|
5253
|
+
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
5254
|
+
* delete through that server endpoint (wrapping the current store as the
|
|
5255
|
+
* client-only fallback), so the history drawer shows durable, cross-device
|
|
5256
|
+
* threads. Without it, the client store's per-tab threads are used.
|
|
5257
|
+
*/
|
|
5258
|
+
#wireThreadStore() {
|
|
5259
|
+
const url = this.getAttribute("data-threads-url");
|
|
5260
|
+
if (url !== null) {
|
|
5261
|
+
this.conversationStore = new RemoteConversationStore(
|
|
5262
|
+
url,
|
|
5263
|
+
() => this.headers,
|
|
5264
|
+
this.conversationStore
|
|
5265
|
+
);
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
4169
5268
|
/** Fetch the server tool-label catalog from `data-tools-url`, if set. */
|
|
4170
5269
|
async #fetchToolCatalog() {
|
|
4171
5270
|
const url = this.getAttribute("data-tools-url");
|
|
@@ -4283,15 +5382,51 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4283
5382
|
newChat() {
|
|
4284
5383
|
this.#cancelRun();
|
|
4285
5384
|
this.conversationStore.clear(this.#threadId);
|
|
5385
|
+
this.#resetState();
|
|
5386
|
+
this.#threadId = this.conversationStore.threadId();
|
|
5387
|
+
this.#setRunning(false);
|
|
5388
|
+
}
|
|
5389
|
+
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
5390
|
+
#resetState() {
|
|
4286
5391
|
this.#client = null;
|
|
4287
5392
|
this.#streamingBubble = null;
|
|
4288
5393
|
this.#hidePending();
|
|
4289
5394
|
this.#toolCards.clear();
|
|
4290
5395
|
this.#serverSettled.clear();
|
|
4291
5396
|
this.#initialMessages = [];
|
|
5397
|
+
this.#runAttachments = [];
|
|
5398
|
+
this.#attachTray?.clear();
|
|
4292
5399
|
this.#messages.replaceChildren();
|
|
4293
|
-
|
|
5400
|
+
}
|
|
5401
|
+
/** Switch the active conversation to an existing thread and replay it. */
|
|
5402
|
+
async #switchThread(threadId) {
|
|
5403
|
+
if (threadId === this.#threadId) {
|
|
5404
|
+
return;
|
|
5405
|
+
}
|
|
5406
|
+
this.#cancelRun();
|
|
5407
|
+
this.#resetState();
|
|
5408
|
+
this.conversationStore.setActiveThread(threadId);
|
|
5409
|
+
this.#threadId = threadId;
|
|
4294
5410
|
this.#setRunning(false);
|
|
5411
|
+
await this.#rehydrate();
|
|
5412
|
+
}
|
|
5413
|
+
/** Delete a thread; if it was the active one, fall back to a fresh chat. */
|
|
5414
|
+
#deleteThread(threadId) {
|
|
5415
|
+
const wasActive = threadId === this.#threadId;
|
|
5416
|
+
if (wasActive) {
|
|
5417
|
+
this.#cancelRun();
|
|
5418
|
+
}
|
|
5419
|
+
this.conversationStore.clear(threadId);
|
|
5420
|
+
if (wasActive) {
|
|
5421
|
+
this.#resetState();
|
|
5422
|
+
this.#threadId = this.conversationStore.threadId();
|
|
5423
|
+
this.#setRunning(false);
|
|
5424
|
+
}
|
|
5425
|
+
void this.#refreshDrawer();
|
|
5426
|
+
}
|
|
5427
|
+
/** Reload the drawer's thread list, marking the active thread. */
|
|
5428
|
+
async #refreshDrawer() {
|
|
5429
|
+
this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
|
|
4295
5430
|
}
|
|
4296
5431
|
/**
|
|
4297
5432
|
* Restore the conversation from the store on mount, then — if a navigating
|
|
@@ -4320,8 +5455,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4320
5455
|
#renderHistoricMessage(message) {
|
|
4321
5456
|
const text2 = typeof message.content === "string" ? message.content : "";
|
|
4322
5457
|
if (message.role === MESSAGE_ROLE.USER) {
|
|
4323
|
-
|
|
4324
|
-
|
|
5458
|
+
const attachments = messageAttachments(message);
|
|
5459
|
+
if (text2 !== "" || attachments.length > 0) {
|
|
5460
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, text2);
|
|
5461
|
+
if (attachments.length > 0) {
|
|
5462
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
5463
|
+
}
|
|
4325
5464
|
}
|
|
4326
5465
|
return;
|
|
4327
5466
|
}
|
|
@@ -4405,6 +5544,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4405
5544
|
title.textContent = this.getAttribute("title-text") ?? "Assistant";
|
|
4406
5545
|
const controls = document.createElement("div");
|
|
4407
5546
|
controls.className = "header-controls";
|
|
5547
|
+
const history = document.createElement("button");
|
|
5548
|
+
history.type = "button";
|
|
5549
|
+
history.className = "header-btn header-btn--history";
|
|
5550
|
+
history.title = "Chat history";
|
|
5551
|
+
history.setAttribute("aria-label", "Chat history");
|
|
5552
|
+
history.textContent = "\u2630";
|
|
5553
|
+
history.addEventListener("click", () => {
|
|
5554
|
+
void this.#refreshDrawer();
|
|
5555
|
+
this.#drawer.open();
|
|
5556
|
+
});
|
|
4408
5557
|
const newChat = document.createElement("button");
|
|
4409
5558
|
newChat.type = "button";
|
|
4410
5559
|
newChat.className = "header-btn header-btn--new";
|
|
@@ -4419,7 +5568,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4419
5568
|
collapse.setAttribute("aria-label", "Collapse");
|
|
4420
5569
|
collapse.textContent = "\u2014";
|
|
4421
5570
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
4422
|
-
controls.append(newChat, collapse);
|
|
5571
|
+
controls.append(history, newChat, collapse);
|
|
4423
5572
|
header.append(title, controls);
|
|
4424
5573
|
this.#messages.className = "messages";
|
|
4425
5574
|
this.#messages.setAttribute("role", "log");
|
|
@@ -4447,14 +5596,29 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4447
5596
|
});
|
|
4448
5597
|
this.#skillHint.className = "skill-hint";
|
|
4449
5598
|
this.#skillHint.hidden = true;
|
|
4450
|
-
|
|
5599
|
+
this.#attachButton.className = "attach-btn";
|
|
5600
|
+
this.#attachButton.type = "button";
|
|
5601
|
+
this.#attachButton.textContent = "\u{1F4CE}";
|
|
5602
|
+
this.#attachButton.title = "Attach files";
|
|
5603
|
+
this.#attachButton.setAttribute("aria-label", "Attach files");
|
|
5604
|
+
this.#attachButton.hidden = true;
|
|
5605
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
5606
|
+
this.#fileInput.className = "attach-input";
|
|
5607
|
+
this.#fileInput.type = "file";
|
|
5608
|
+
this.#fileInput.multiple = true;
|
|
5609
|
+
this.#fileInput.hidden = true;
|
|
5610
|
+
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
5611
|
+
this.#attachSlot.className = "attachment-slot";
|
|
5612
|
+
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
4451
5613
|
this.#chat.append(
|
|
4452
5614
|
header,
|
|
4453
5615
|
this.#messages,
|
|
4454
5616
|
this.#skillsMenu.palette,
|
|
4455
5617
|
this.#skillsMenu.chips,
|
|
4456
5618
|
this.#skillHint,
|
|
4457
|
-
|
|
5619
|
+
this.#attachSlot,
|
|
5620
|
+
inputRow,
|
|
5621
|
+
this.#drawer.element
|
|
4458
5622
|
);
|
|
4459
5623
|
this.#root.append(style, this.#chat);
|
|
4460
5624
|
}
|
|
@@ -4498,25 +5662,31 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4498
5662
|
}
|
|
4499
5663
|
async #submit() {
|
|
4500
5664
|
const content = this.#input.value.trim();
|
|
4501
|
-
|
|
5665
|
+
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
5666
|
+
if (content === "" && attachments.length === 0) {
|
|
4502
5667
|
return;
|
|
4503
5668
|
}
|
|
4504
|
-
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
5669
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
5670
|
+
if (attachments.length > 0) {
|
|
5671
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
5672
|
+
}
|
|
4505
5673
|
this.#input.value = "";
|
|
5674
|
+
this.#attachTray?.clearReady();
|
|
5675
|
+
this.#runAttachments = attachments;
|
|
4506
5676
|
this.dispatchEvent(
|
|
4507
5677
|
new CustomEvent(SUBMIT_EVENT, {
|
|
4508
|
-
detail: { content },
|
|
5678
|
+
detail: { content, attachments },
|
|
4509
5679
|
bubbles: true,
|
|
4510
5680
|
composed: true
|
|
4511
5681
|
})
|
|
4512
5682
|
);
|
|
4513
|
-
await this.#client_send(content);
|
|
5683
|
+
await this.#client_send(content, attachments);
|
|
4514
5684
|
}
|
|
4515
|
-
async #client_send(content) {
|
|
5685
|
+
async #client_send(content, attachments) {
|
|
4516
5686
|
if (this.endpoint === "") {
|
|
4517
5687
|
return;
|
|
4518
5688
|
}
|
|
4519
|
-
await this.#ensureClient().send(content);
|
|
5689
|
+
await this.#ensureClient().send(content, attachments);
|
|
4520
5690
|
}
|
|
4521
5691
|
#ensureClient() {
|
|
4522
5692
|
if (this.#client === null) {
|
|
@@ -4653,6 +5823,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4653
5823
|
this.#hidePending();
|
|
4654
5824
|
this.#setRunning(false);
|
|
4655
5825
|
this.#streamingBubble = null;
|
|
5826
|
+
this.#runAttachments = [];
|
|
4656
5827
|
}
|
|
4657
5828
|
};
|
|
4658
5829
|
}
|
|
@@ -4886,7 +6057,7 @@ function setControlValue(el, value) {
|
|
|
4886
6057
|
}
|
|
4887
6058
|
|
|
4888
6059
|
// src/version.ts
|
|
4889
|
-
var VERSION = "0.
|
|
6060
|
+
var VERSION = "0.6.0";
|
|
4890
6061
|
export {
|
|
4891
6062
|
AgUiChat,
|
|
4892
6063
|
AgUiClient,
|
|
@@ -4894,6 +6065,7 @@ export {
|
|
|
4894
6065
|
ELEMENT_TAG,
|
|
4895
6066
|
MAX_TOOL_ROUNDS,
|
|
4896
6067
|
MESSAGE_ROLE,
|
|
6068
|
+
RemoteConversationStore,
|
|
4897
6069
|
SUBMIT_EVENT,
|
|
4898
6070
|
SessionStorageStore,
|
|
4899
6071
|
TOGGLE_EVENT,
|
|
@@ -4916,6 +6088,7 @@ export {
|
|
|
4916
6088
|
highlightThenClick,
|
|
4917
6089
|
isDestructive,
|
|
4918
6090
|
isNavigates,
|
|
6091
|
+
messageAttachments,
|
|
4919
6092
|
parseToolCatalog,
|
|
4920
6093
|
prefersReducedMotion,
|
|
4921
6094
|
pressButton,
|
|
@@ -4931,7 +6104,8 @@ export {
|
|
|
4931
6104
|
setNativeValue,
|
|
4932
6105
|
toggleCheckbox,
|
|
4933
6106
|
toggleControl,
|
|
4934
|
-
typeInto
|
|
6107
|
+
typeInto,
|
|
6108
|
+
uploadAttachment
|
|
4935
6109
|
};
|
|
4936
6110
|
/*! Bundled license information:
|
|
4937
6111
|
|