@artooi/ag-ui-web-component 0.27.0 → 0.28.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 +247 -1
- package/README.md +186 -6
- package/dist/ag-ui-web-component.bundle.js +50 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +55 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +8 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +43 -1
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +23 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +28 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +564 -94
- package/dist/index.js.map +4 -4
- package/dist/tools/is_destructive.d.ts +8 -2
- package/dist/tools/is_destructive.d.ts.map +1 -1
- package/dist/tools/parse_tool_catalog.d.ts +11 -4
- package/dist/tools/parse_tool_catalog.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +23 -5
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +5 -1
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +13 -7
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +431 -41
- package/src/core/agui_client.ts +18 -1
- package/src/core/conversation_store.ts +128 -42
- package/src/core/create_http_agent.ts +24 -2
- package/src/core/remote_conversation_store.ts +35 -2
- package/src/core/utils.ts +58 -0
- package/src/tools/is_destructive.ts +8 -2
- package/src/tools/parse_tool_catalog.ts +18 -6
- package/src/ui/render_markdown.ts +111 -21
- package/src/ui/resize_handle.ts +32 -2
- package/src/ui/ui_strings.ts +19 -8
- package/src/ui/voice_input.ts +43 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -48,6 +48,9 @@ var ICON_FILE_PDF = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><
|
|
|
48
48
|
var ICON_FILE_TEXT = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M8.5 13.5h7M8.5 17h4.5"/></svg>`;
|
|
49
49
|
var CHART_ACTIVITY_TYPE = "chart";
|
|
50
50
|
|
|
51
|
+
// src/core/ag_ui_chat.ts
|
|
52
|
+
import { randomUUID as randomUUID5 } from "@ag-ui/client";
|
|
53
|
+
|
|
51
54
|
// src/skills/fill_template.ts
|
|
52
55
|
var PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
53
56
|
function fillTemplate(template, values) {
|
|
@@ -441,8 +444,9 @@ function parseToolCatalog(data) {
|
|
|
441
444
|
const record = entry;
|
|
442
445
|
const name = record["name"];
|
|
443
446
|
const summary = record["summary"];
|
|
447
|
+
const description = record["description"];
|
|
444
448
|
if (typeof name === "string" && typeof summary === "string") {
|
|
445
|
-
out[name] = summary;
|
|
449
|
+
out[name] = typeof description === "string" ? { name, summary, description } : { name, summary };
|
|
446
450
|
}
|
|
447
451
|
}
|
|
448
452
|
return out;
|
|
@@ -555,6 +559,7 @@ var DEFAULT_UI_STRINGS = {
|
|
|
555
559
|
stopRecording: "Stop recording",
|
|
556
560
|
transcribing: "Transcribing\u2026",
|
|
557
561
|
transcriptionFailed: "Transcription failed",
|
|
562
|
+
recordingLimit: "Stopped at the {n}-minute limit \u2014 transcribing what was recorded.",
|
|
558
563
|
toolRunning: "running\u2026",
|
|
559
564
|
toolDeferred: "waiting for you",
|
|
560
565
|
toolDone: "\u2713 done",
|
|
@@ -4596,23 +4601,51 @@ var ALLOWED_TAGS = [
|
|
|
4596
4601
|
"th",
|
|
4597
4602
|
"td"
|
|
4598
4603
|
];
|
|
4599
|
-
var ALLOWED_ATTR = ["href", "title", "class"];
|
|
4600
|
-
var
|
|
4601
|
-
|
|
4604
|
+
var ALLOWED_ATTR = ["href", "title", "class", "target", "rel"];
|
|
4605
|
+
var SANITIZE_CONFIG = {
|
|
4606
|
+
ALLOWED_TAGS,
|
|
4607
|
+
ALLOWED_ATTR,
|
|
4608
|
+
ALLOW_DATA_ATTR: false,
|
|
4609
|
+
ALLOW_ARIA_ATTR: false
|
|
4610
|
+
};
|
|
4611
|
+
var SANITIZE_CONFIG_WITH_IMAGES = {
|
|
4612
|
+
...SANITIZE_CONFIG,
|
|
4613
|
+
ALLOWED_TAGS: [...ALLOWED_TAGS, "img"],
|
|
4614
|
+
ALLOWED_ATTR: [...ALLOWED_ATTR, "src", "alt", "width", "height"]
|
|
4615
|
+
};
|
|
4616
|
+
var LANGUAGE_CLASS = /^language-[A-Za-z0-9_+#.-]+$/;
|
|
4617
|
+
var LANGUAGE_HOSTS = /* @__PURE__ */ new Set(["CODE", "PRE"]);
|
|
4618
|
+
function harden(node) {
|
|
4619
|
+
if (node.nodeName === "A" && node.hasAttribute("href")) {
|
|
4620
|
+
node.setAttribute("target", "_blank");
|
|
4621
|
+
node.setAttribute("rel", "noopener noreferrer");
|
|
4622
|
+
} else {
|
|
4623
|
+
node.removeAttribute("target");
|
|
4624
|
+
node.removeAttribute("rel");
|
|
4625
|
+
}
|
|
4626
|
+
const classes = node.getAttribute("class");
|
|
4627
|
+
if (classes === null) {
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
const kept = LANGUAGE_HOSTS.has(node.nodeName) ? classes.split(/\s+/).filter((token) => LANGUAGE_CLASS.test(token)) : [];
|
|
4631
|
+
if (kept.length === 0) {
|
|
4632
|
+
node.removeAttribute("class");
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
node.setAttribute("class", kept.join(" "));
|
|
4636
|
+
}
|
|
4637
|
+
var purifier = null;
|
|
4638
|
+
function sanitizer() {
|
|
4639
|
+
if (purifier === null) {
|
|
4640
|
+
purifier = purify();
|
|
4641
|
+
purifier.addHook("afterSanitizeAttributes", harden);
|
|
4642
|
+
}
|
|
4643
|
+
return purifier;
|
|
4644
|
+
}
|
|
4602
4645
|
function renderMarkdown(text3, options) {
|
|
4603
4646
|
const allowImages = options?.allowImages === true;
|
|
4604
4647
|
const rendered = parser.parse(text3, { async: false });
|
|
4605
|
-
|
|
4606
|
-
ALLOWED_TAGS: allowImages ? ALLOWED_TAGS_WITH_IMAGES : ALLOWED_TAGS,
|
|
4607
|
-
ALLOWED_ATTR: allowImages ? ALLOWED_ATTR_WITH_IMAGES : ALLOWED_ATTR
|
|
4608
|
-
});
|
|
4609
|
-
const template = document.createElement("template");
|
|
4610
|
-
template.innerHTML = clean;
|
|
4611
|
-
for (const anchor of template.content.querySelectorAll("a[href]")) {
|
|
4612
|
-
anchor.setAttribute("target", "_blank");
|
|
4613
|
-
anchor.setAttribute("rel", "noopener noreferrer");
|
|
4614
|
-
}
|
|
4615
|
-
return template.innerHTML.trim();
|
|
4648
|
+
return sanitizer().sanitize(rendered, allowImages ? SANITIZE_CONFIG_WITH_IMAGES : SANITIZE_CONFIG).trim();
|
|
4616
4649
|
}
|
|
4617
4650
|
|
|
4618
4651
|
// src/ui/resize_handle.ts
|
|
@@ -4655,6 +4688,15 @@ function createResizeHandle(options) {
|
|
|
4655
4688
|
window.addEventListener("pointerup", onUp);
|
|
4656
4689
|
event.preventDefault();
|
|
4657
4690
|
});
|
|
4691
|
+
let pending = null;
|
|
4692
|
+
const settle = () => {
|
|
4693
|
+
if (pending === null) {
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
const size = pending;
|
|
4697
|
+
pending = null;
|
|
4698
|
+
options.commit(size);
|
|
4699
|
+
};
|
|
4658
4700
|
handle.addEventListener("keydown", (event) => {
|
|
4659
4701
|
const axis = options.axis();
|
|
4660
4702
|
if (axis === "none") {
|
|
@@ -4680,8 +4722,10 @@ function createResizeHandle(options) {
|
|
|
4680
4722
|
}
|
|
4681
4723
|
event.preventDefault();
|
|
4682
4724
|
options.apply(next);
|
|
4683
|
-
|
|
4725
|
+
pending = next;
|
|
4684
4726
|
});
|
|
4727
|
+
handle.addEventListener("keyup", settle);
|
|
4728
|
+
handle.addEventListener("blur", settle);
|
|
4685
4729
|
return handle;
|
|
4686
4730
|
}
|
|
4687
4731
|
|
|
@@ -7704,6 +7748,7 @@ var ToolCallCard = class {
|
|
|
7704
7748
|
};
|
|
7705
7749
|
|
|
7706
7750
|
// src/ui/voice_input.ts
|
|
7751
|
+
var MAX_RECORDING_MS = 12e4;
|
|
7707
7752
|
var VoiceInput = class {
|
|
7708
7753
|
/** The mic button; mount this in the composer. */
|
|
7709
7754
|
element;
|
|
@@ -7714,6 +7759,8 @@ var VoiceInput = class {
|
|
|
7714
7759
|
#recorder = null;
|
|
7715
7760
|
#stream = null;
|
|
7716
7761
|
#chunks = [];
|
|
7762
|
+
#capTimer = null;
|
|
7763
|
+
#hitCap = false;
|
|
7717
7764
|
#disposed = false;
|
|
7718
7765
|
constructor(options) {
|
|
7719
7766
|
this.#transcribe = options.transcribe;
|
|
@@ -7753,6 +7800,7 @@ var VoiceInput = class {
|
|
|
7753
7800
|
}
|
|
7754
7801
|
this.#stream = stream;
|
|
7755
7802
|
this.#chunks = [];
|
|
7803
|
+
this.#hitCap = false;
|
|
7756
7804
|
const recorder = new MediaRecorder(stream);
|
|
7757
7805
|
recorder.addEventListener("dataavailable", (event) => {
|
|
7758
7806
|
this.#chunks.push(event.data);
|
|
@@ -7762,11 +7810,23 @@ var VoiceInput = class {
|
|
|
7762
7810
|
});
|
|
7763
7811
|
this.#recorder = recorder;
|
|
7764
7812
|
recorder.start();
|
|
7813
|
+
this.#capTimer = setTimeout(() => {
|
|
7814
|
+
this.#hitCap = true;
|
|
7815
|
+
this.#stop();
|
|
7816
|
+
}, MAX_RECORDING_MS);
|
|
7765
7817
|
this.#setState("recording");
|
|
7766
7818
|
}
|
|
7767
7819
|
#stop() {
|
|
7820
|
+
this.#clearCap();
|
|
7768
7821
|
this.#recorder?.stop();
|
|
7769
7822
|
}
|
|
7823
|
+
/** Drop the cap timer; recording is over, by whichever route. */
|
|
7824
|
+
#clearCap() {
|
|
7825
|
+
if (this.#capTimer !== null) {
|
|
7826
|
+
clearTimeout(this.#capTimer);
|
|
7827
|
+
this.#capTimer = null;
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7770
7830
|
/**
|
|
7771
7831
|
* Tear the control down, for a host element removed mid-recording. Stops any
|
|
7772
7832
|
* live `MediaRecorder`, releases the mic tracks so the browser's recording
|
|
@@ -7775,6 +7835,7 @@ var VoiceInput = class {
|
|
|
7775
7835
|
*/
|
|
7776
7836
|
dispose() {
|
|
7777
7837
|
this.#disposed = true;
|
|
7838
|
+
this.#clearCap();
|
|
7778
7839
|
if (this.#recorder !== null && this.#recorder.state !== "inactive") {
|
|
7779
7840
|
this.#recorder.stop();
|
|
7780
7841
|
}
|
|
@@ -7791,6 +7852,12 @@ var VoiceInput = class {
|
|
|
7791
7852
|
try {
|
|
7792
7853
|
const text3 = await this.#transcribe(audio);
|
|
7793
7854
|
this.#setState("idle");
|
|
7855
|
+
if (this.#hitCap) {
|
|
7856
|
+
this.element.title = this.#strings.recordingLimit.replace(
|
|
7857
|
+
"{n}",
|
|
7858
|
+
String(MAX_RECORDING_MS / 6e4)
|
|
7859
|
+
);
|
|
7860
|
+
}
|
|
7794
7861
|
if (text3 !== "") {
|
|
7795
7862
|
this.#onText(text3);
|
|
7796
7863
|
}
|
|
@@ -8104,6 +8171,16 @@ var AgUiClient = class {
|
|
|
8104
8171
|
onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
|
|
8105
8172
|
h.onReasoningDelta(reasoningMessageBuffer);
|
|
8106
8173
|
},
|
|
8174
|
+
// The delta callback reports the buffer as it stood *before* the announced
|
|
8175
|
+
// delta was appended, so on its own it always trails the stream by one and
|
|
8176
|
+
// renders nothing at all for a block that arrives as a single delta. The
|
|
8177
|
+
// answer text is spared that because its own end event carries the whole
|
|
8178
|
+
// message; this is the reasoning counterpart, and it has to be
|
|
8179
|
+
// REASONING_MESSAGE_END rather than REASONING_END, because only the former
|
|
8180
|
+
// carries a buffer.
|
|
8181
|
+
onReasoningMessageEndEvent({ reasoningMessageBuffer }) {
|
|
8182
|
+
h.onReasoningDelta(reasoningMessageBuffer);
|
|
8183
|
+
},
|
|
8107
8184
|
onReasoningEndEvent() {
|
|
8108
8185
|
h.onReasoningEnd();
|
|
8109
8186
|
},
|
|
@@ -8159,12 +8236,68 @@ var MINTED_SUFFIX = "minted:";
|
|
|
8159
8236
|
var TITLE_LIMIT = 60;
|
|
8160
8237
|
var PREVIEW_LIMIT = 100;
|
|
8161
8238
|
var DEFAULT_TITLE = "New conversation";
|
|
8162
|
-
var
|
|
8239
|
+
var writeFailureReported = false;
|
|
8240
|
+
function writeStoredItem(key, value) {
|
|
8241
|
+
try {
|
|
8242
|
+
sessionStorage.setItem(key, value);
|
|
8243
|
+
} catch {
|
|
8244
|
+
if (writeFailureReported) {
|
|
8245
|
+
return;
|
|
8246
|
+
}
|
|
8247
|
+
writeFailureReported = true;
|
|
8248
|
+
console.warn(
|
|
8249
|
+
"<ag-ui-chat>: the browser refused a sessionStorage write \u2014 the quota is full, or storage is disabled for this context. The conversation continues, but it will not survive a page reload. Deleting a long conversation from the history drawer frees the quota."
|
|
8250
|
+
);
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
function rootFor(namespace) {
|
|
8254
|
+
return namespace === "" ? KEY_ROOT : `${KEY_ROOT}@${namespace}`;
|
|
8255
|
+
}
|
|
8256
|
+
var SessionStorageStore = class _SessionStorageStore {
|
|
8163
8257
|
#root;
|
|
8164
8258
|
constructor(namespace = "") {
|
|
8165
|
-
this.#root = namespace
|
|
8259
|
+
this.#root = rootFor(namespace);
|
|
8166
8260
|
if (namespace !== "") {
|
|
8167
|
-
|
|
8261
|
+
_SessionStorageStore.adopt("", namespace);
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
/**
|
|
8265
|
+
* Move every key a store owns out of `from`'s namespace and into `to`'s.
|
|
8266
|
+
*
|
|
8267
|
+
* Two callers, one move. The constructor adopts the pre-namespacing global
|
|
8268
|
+
* keys (`from` = `""`); `<ag-ui-chat>` adopts an element-scoped conversation
|
|
8269
|
+
* into a principal-scoped one the first time a `user-key` arrives, which is a
|
|
8270
|
+
* host naming the user who was already there rather than a handover.
|
|
8271
|
+
*
|
|
8272
|
+
* Only this store's own suffixes move — the element's `collapsed` / `size` /
|
|
8273
|
+
* `theme` keys share the global root and are deliberately left where they
|
|
8274
|
+
* are. A value already present at the destination wins: the destination is
|
|
8275
|
+
* the durable record and the source is the stray this move exists to clear.
|
|
8276
|
+
*/
|
|
8277
|
+
static adopt(from, to) {
|
|
8278
|
+
const fromRoot = `${rootFor(from)}:`;
|
|
8279
|
+
const toRoot = `${rootFor(to)}:`;
|
|
8280
|
+
for (const [key, suffix] of ownedKeys(fromRoot)) {
|
|
8281
|
+
const value = sessionStorage.getItem(key);
|
|
8282
|
+
const destination = toRoot + suffix;
|
|
8283
|
+
if (value !== null && sessionStorage.getItem(destination) === null) {
|
|
8284
|
+
writeStoredItem(destination, value);
|
|
8285
|
+
}
|
|
8286
|
+
sessionStorage.removeItem(key);
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
/**
|
|
8290
|
+
* Forget everything a store holds for `namespace`.
|
|
8291
|
+
*
|
|
8292
|
+
* The logout primitive: `<ag-ui-chat>` calls it when its `user-key` changes,
|
|
8293
|
+
* and a host driving its own store can call it from its own sign-out path.
|
|
8294
|
+
* Deliberately narrow — it removes only keys under this exact namespace whose
|
|
8295
|
+
* suffix parses as one this store writes, so it can never reach another
|
|
8296
|
+
* element's conversation or the host's own `sessionStorage` entries.
|
|
8297
|
+
*/
|
|
8298
|
+
static purge(namespace) {
|
|
8299
|
+
for (const [key] of ownedKeys(`${rootFor(namespace)}:`)) {
|
|
8300
|
+
sessionStorage.removeItem(key);
|
|
8168
8301
|
}
|
|
8169
8302
|
}
|
|
8170
8303
|
threadId() {
|
|
@@ -8172,8 +8305,8 @@ var SessionStorageStore = class {
|
|
|
8172
8305
|
}
|
|
8173
8306
|
newThread() {
|
|
8174
8307
|
const id = randomUUID3();
|
|
8175
|
-
|
|
8176
|
-
|
|
8308
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), id);
|
|
8309
|
+
writeStoredItem(this.#key(MINTED_SUFFIX + id), "1");
|
|
8177
8310
|
return id;
|
|
8178
8311
|
}
|
|
8179
8312
|
isUnsent(threadId) {
|
|
@@ -8183,7 +8316,7 @@ var SessionStorageStore = class {
|
|
|
8183
8316
|
return Promise.resolve(this.#readJson(this.#key(MESSAGES_SUFFIX + threadId)));
|
|
8184
8317
|
}
|
|
8185
8318
|
saveMessages(threadId, messages) {
|
|
8186
|
-
|
|
8319
|
+
writeStoredItem(this.#key(MESSAGES_SUFFIX + threadId), JSON.stringify(messages));
|
|
8187
8320
|
sessionStorage.removeItem(this.#key(MINTED_SUFFIX + threadId));
|
|
8188
8321
|
this.#touchThread(threadId, messages);
|
|
8189
8322
|
}
|
|
@@ -8196,7 +8329,7 @@ var SessionStorageStore = class {
|
|
|
8196
8329
|
sessionStorage.removeItem(key);
|
|
8197
8330
|
return;
|
|
8198
8331
|
}
|
|
8199
|
-
|
|
8332
|
+
writeStoredItem(key, JSON.stringify(checkpoint));
|
|
8200
8333
|
}
|
|
8201
8334
|
clear(threadId) {
|
|
8202
8335
|
sessionStorage.removeItem(this.#key(MESSAGES_SUFFIX + threadId));
|
|
@@ -8212,7 +8345,7 @@ var SessionStorageStore = class {
|
|
|
8212
8345
|
return Promise.resolve(metas);
|
|
8213
8346
|
}
|
|
8214
8347
|
setActiveThread(threadId) {
|
|
8215
|
-
|
|
8348
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), threadId);
|
|
8216
8349
|
}
|
|
8217
8350
|
renameThread(threadId, title) {
|
|
8218
8351
|
const threads = this.#readThreads();
|
|
@@ -8256,40 +8389,12 @@ var SessionStorageStore = class {
|
|
|
8256
8389
|
sessionStorage.removeItem(key);
|
|
8257
8390
|
return;
|
|
8258
8391
|
}
|
|
8259
|
-
|
|
8392
|
+
writeStoredItem(key, JSON.stringify(threads));
|
|
8260
8393
|
}
|
|
8261
8394
|
/** This store's fully-qualified key for a suffix (namespaced when set). */
|
|
8262
8395
|
#key(suffix) {
|
|
8263
8396
|
return `${this.#root}:${suffix}`;
|
|
8264
8397
|
}
|
|
8265
|
-
/**
|
|
8266
|
-
* One-time move of un-namespaced `ag-ui-chat:*` keys into this instance's
|
|
8267
|
-
* namespace, so an existing conversation isn't orphaned. Only this store's own
|
|
8268
|
-
* keys move — the element's `collapsed` / `theme` keys are left alone. The
|
|
8269
|
-
* first namespaced instance to mount adopts the data; a second namespace
|
|
8270
|
-
* finds it gone and starts fresh.
|
|
8271
|
-
*/
|
|
8272
|
-
#migrateLegacyKeys() {
|
|
8273
|
-
const legacyRoot = `${KEY_ROOT}:`;
|
|
8274
|
-
const moves = [];
|
|
8275
|
-
for (let i = 0; i < sessionStorage.length; i += 1) {
|
|
8276
|
-
const key = sessionStorage.key(i);
|
|
8277
|
-
if (key === null || !key.startsWith(legacyRoot)) {
|
|
8278
|
-
continue;
|
|
8279
|
-
}
|
|
8280
|
-
const suffix = key.slice(legacyRoot.length);
|
|
8281
|
-
if (isOwnedSuffix(suffix)) {
|
|
8282
|
-
moves.push([key, this.#key(suffix)]);
|
|
8283
|
-
}
|
|
8284
|
-
}
|
|
8285
|
-
for (const [from, to] of moves) {
|
|
8286
|
-
const value = sessionStorage.getItem(from);
|
|
8287
|
-
if (value !== null && sessionStorage.getItem(to) === null) {
|
|
8288
|
-
sessionStorage.setItem(to, value);
|
|
8289
|
-
}
|
|
8290
|
-
sessionStorage.removeItem(from);
|
|
8291
|
-
}
|
|
8292
|
-
}
|
|
8293
8398
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
8294
8399
|
#readJson(key) {
|
|
8295
8400
|
const raw = sessionStorage.getItem(key);
|
|
@@ -8303,8 +8408,22 @@ var SessionStorageStore = class {
|
|
|
8303
8408
|
}
|
|
8304
8409
|
}
|
|
8305
8410
|
};
|
|
8411
|
+
function ownedKeys(root) {
|
|
8412
|
+
const found = [];
|
|
8413
|
+
for (let index = 0; index < sessionStorage.length; index += 1) {
|
|
8414
|
+
const key = sessionStorage.key(index);
|
|
8415
|
+
if (key === null || !key.startsWith(root)) {
|
|
8416
|
+
continue;
|
|
8417
|
+
}
|
|
8418
|
+
const suffix = key.slice(root.length);
|
|
8419
|
+
if (isOwnedSuffix(suffix)) {
|
|
8420
|
+
found.push([key, suffix]);
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8423
|
+
return found;
|
|
8424
|
+
}
|
|
8306
8425
|
function isOwnedSuffix(suffix) {
|
|
8307
|
-
return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX);
|
|
8426
|
+
return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX) || suffix.startsWith(MINTED_SUFFIX);
|
|
8308
8427
|
}
|
|
8309
8428
|
function deriveTitle(messages) {
|
|
8310
8429
|
for (const message of messages) {
|
|
@@ -8349,12 +8468,27 @@ function mintThread(store) {
|
|
|
8349
8468
|
store.setActiveThread(id);
|
|
8350
8469
|
return id;
|
|
8351
8470
|
}
|
|
8471
|
+
function warnOnCrossOriginCredentials(url, credentialNames, trustedOrigins, warned) {
|
|
8472
|
+
if (credentialNames.length === 0) {
|
|
8473
|
+
return;
|
|
8474
|
+
}
|
|
8475
|
+
const destination = new URL(String(url), location.href).origin;
|
|
8476
|
+
if (destination === location.origin || trustedOrigins.includes(destination) || warned.has(destination)) {
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
warned.add(destination);
|
|
8480
|
+
console.warn(
|
|
8481
|
+
`<ag-ui-chat>: sending host credentials (${credentialNames.join(", ")}) to ${destination}, which is not this page's origin (${location.origin}). Those headers are the page's own authentication, and whichever server answers the browser's preflight receives them \u2014 so a URL attribute built from a query parameter or from tenant-authored configuration is a channel for the token to leave on. If this destination is deliberate, name it in \`trustedOrigins\` to confirm it and silence this notice. Reported once per origin.`
|
|
8482
|
+
);
|
|
8483
|
+
}
|
|
8352
8484
|
|
|
8353
8485
|
// src/core/create_http_agent.ts
|
|
8354
8486
|
function createHttpAgent(options) {
|
|
8487
|
+
const staticHeaders = options.headers ?? {};
|
|
8488
|
+
const warned = /* @__PURE__ */ new Set();
|
|
8355
8489
|
return new HttpAgent({
|
|
8356
8490
|
url: options.endpoint,
|
|
8357
|
-
headers:
|
|
8491
|
+
headers: staticHeaders,
|
|
8358
8492
|
initialState: { ...options.initialState ?? {} },
|
|
8359
8493
|
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
8360
8494
|
// rebinding the global `fetch` to the agent instance — "Illegal invocation"
|
|
@@ -8363,6 +8497,10 @@ function createHttpAgent(options) {
|
|
|
8363
8497
|
// own config having no seam for either.
|
|
8364
8498
|
fetch: (url, init) => {
|
|
8365
8499
|
const fresh = options.getHeaders?.();
|
|
8500
|
+
const credentialNames = [
|
|
8501
|
+
.../* @__PURE__ */ new Set([...Object.keys(staticHeaders), ...Object.keys(fresh ?? {})])
|
|
8502
|
+
].sort();
|
|
8503
|
+
warnOnCrossOriginCredentials(url, credentialNames, options.trustedOrigins ?? [], warned);
|
|
8366
8504
|
if (fresh === void 0) {
|
|
8367
8505
|
return fetch(url, withCredentials(init, options.credentials));
|
|
8368
8506
|
}
|
|
@@ -8385,13 +8523,34 @@ var RemoteConversationStore = class {
|
|
|
8385
8523
|
#headers;
|
|
8386
8524
|
#local;
|
|
8387
8525
|
#credentials;
|
|
8526
|
+
#cacheMessages;
|
|
8388
8527
|
#dropped = /* @__PURE__ */ new Set();
|
|
8389
8528
|
#renamed = /* @__PURE__ */ new Map();
|
|
8390
|
-
|
|
8529
|
+
/**
|
|
8530
|
+
* @param cacheMessages Whether to mirror message bodies into the local store.
|
|
8531
|
+
*
|
|
8532
|
+
* `true` (the default, and the behaviour this class has always had) keeps a
|
|
8533
|
+
* local copy of every turn, so the transcript still replays when the thread
|
|
8534
|
+
* endpoint is unreachable. `false` is for the deployment that chose a
|
|
8535
|
+
* server-backed store precisely so transcripts do not sit in the browser:
|
|
8536
|
+
* regulated content, a shared workstation, an operator who has to be able to
|
|
8537
|
+
* say where the conversation lives. It is not the same as passing a local
|
|
8538
|
+
* store that does nothing — the local store also owns the active thread id,
|
|
8539
|
+
* the navigation checkpoint and the "nothing sent here yet" marker, all of
|
|
8540
|
+
* which must keep working — so the opt-out is scoped to the bodies alone.
|
|
8541
|
+
*
|
|
8542
|
+
* The cost is deliberate and worth stating: with no local copy there is
|
|
8543
|
+
* nothing to fall back to, so a failed request shows an empty transcript
|
|
8544
|
+
* rather than a stale one, and the drawer's offline list loses its previews
|
|
8545
|
+
* (a preview is an excerpt of a message, which is the very thing being kept
|
|
8546
|
+
* off the client).
|
|
8547
|
+
*/
|
|
8548
|
+
constructor(url, headers = () => ({}), local = new SessionStorageStore(), credentials = () => void 0, cacheMessages = true) {
|
|
8391
8549
|
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
8392
8550
|
this.#headers = headers;
|
|
8393
8551
|
this.#local = local;
|
|
8394
8552
|
this.#credentials = credentials;
|
|
8553
|
+
this.#cacheMessages = cacheMessages;
|
|
8395
8554
|
}
|
|
8396
8555
|
threadId() {
|
|
8397
8556
|
return this.#local.threadId();
|
|
@@ -8412,7 +8571,7 @@ var RemoteConversationStore = class {
|
|
|
8412
8571
|
return this.#local.isUnsent?.(threadId) === true;
|
|
8413
8572
|
}
|
|
8414
8573
|
saveMessages(threadId, messages) {
|
|
8415
|
-
this.#local.saveMessages(threadId, messages);
|
|
8574
|
+
this.#local.saveMessages(threadId, this.#cacheMessages ? messages : []);
|
|
8416
8575
|
}
|
|
8417
8576
|
loadCheckpoint(threadId) {
|
|
8418
8577
|
return this.#local.loadCheckpoint(threadId);
|
|
@@ -8677,6 +8836,7 @@ var CONNECT_TIME_ATTRIBUTES = [
|
|
|
8677
8836
|
"data-attachment-max-bytes",
|
|
8678
8837
|
"data-transcribe-url",
|
|
8679
8838
|
"data-threads-url",
|
|
8839
|
+
"data-threads-cache",
|
|
8680
8840
|
"data-tools-url",
|
|
8681
8841
|
"data-skills-url",
|
|
8682
8842
|
"data-skills",
|
|
@@ -8693,6 +8853,7 @@ function isCredentialsMode(value) {
|
|
|
8693
8853
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
8694
8854
|
var SIZE_KEY = "ag-ui-chat:size";
|
|
8695
8855
|
var THEME_KEY = "ag-ui-chat:theme";
|
|
8856
|
+
var CLAIMED_NAMESPACES = /* @__PURE__ */ new Set();
|
|
8696
8857
|
var AgUiChat = class extends HTMLElement {
|
|
8697
8858
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
8698
8859
|
agentFactory = createHttpAgent;
|
|
@@ -8716,6 +8877,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8716
8877
|
* `Authorization` are configured independently and neither drops the other.
|
|
8717
8878
|
*/
|
|
8718
8879
|
getHeaders = null;
|
|
8880
|
+
/**
|
|
8881
|
+
* Origins, besides the page's own, that this element may send {@link headers}
|
|
8882
|
+
* and {@link getHeaders} credentials to without saying so on the console.
|
|
8883
|
+
*
|
|
8884
|
+
* Seven attributes name a URL, and every one of them carries these headers.
|
|
8885
|
+
* They are plain HTML, so a page that builds one from a query parameter or
|
|
8886
|
+
* from tenant-authored configuration has handed an attacker the destination,
|
|
8887
|
+
* and the token leaves on the element's first request. Naming the origins you
|
|
8888
|
+
* expect turns that from silent into either confirmed or reported.
|
|
8889
|
+
*
|
|
8890
|
+
* A notice rather than a refusal: a cross-origin agent is a documented
|
|
8891
|
+
* deployment, so refusing would break working installations to defend against
|
|
8892
|
+
* a page that is already interpolating untrusted data into its own markup.
|
|
8893
|
+
* Leaving this empty costs nothing but one console line per foreign origin.
|
|
8894
|
+
*/
|
|
8895
|
+
trustedOrigins = [];
|
|
8719
8896
|
/**
|
|
8720
8897
|
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
8721
8898
|
* model-controlled image URL is fetched with no user interaction, which
|
|
@@ -8851,13 +9028,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8851
9028
|
*/
|
|
8852
9029
|
resolvePageTarget = (target) => document.querySelector(target);
|
|
8853
9030
|
/**
|
|
8854
|
-
*
|
|
8855
|
-
*
|
|
8856
|
-
*
|
|
9031
|
+
* The server tool catalog fetched from `data-tools-url`, keyed by tool
|
|
9032
|
+
* name. Cards label themselves from each entry's `summary`, the base
|
|
9033
|
+
* layer behind {@link toolSummaries}: an explicit entry in `toolSummaries`
|
|
9034
|
+
* wins, this fills the rest. Held as whole entries rather than labels so a
|
|
9035
|
+
* field the server sent is not lost on the way in. Populated once on connect.
|
|
8857
9036
|
*/
|
|
8858
9037
|
#toolCatalog = {};
|
|
9038
|
+
/**
|
|
9039
|
+
* Foreign origins already reported, so the notice is once per origin per
|
|
9040
|
+
* element rather than once per request. Per-element rather than module-level,
|
|
9041
|
+
* because two elements on one page are two separate configurations.
|
|
9042
|
+
*/
|
|
9043
|
+
#warnedOrigins = /* @__PURE__ */ new Set();
|
|
8859
9044
|
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
8860
9045
|
#strings = DEFAULT_UI_STRINGS;
|
|
9046
|
+
/**
|
|
9047
|
+
* The tool names the current round handed the agent, captured as the catalog
|
|
9048
|
+
* went out.
|
|
9049
|
+
*
|
|
9050
|
+
* The registry is mount-wide but {@link getTools} is per-run, so a host is
|
|
9051
|
+
* free to scope what a given page offers — and a call naming a tool this run
|
|
9052
|
+
* withheld must not reach the handler that is merely still registered.
|
|
9053
|
+
* Snapshotted rather than re-asked at dispatch: a provider is a function, and
|
|
9054
|
+
* calling it again asks a question the run already answered, which is exactly
|
|
9055
|
+
* the window a scoped catalog exists to close.
|
|
9056
|
+
*
|
|
9057
|
+
* Empty until the first round advertises, which cannot precede a call: the
|
|
9058
|
+
* client builds `RunAgentInput.tools` at the top of every round, before the
|
|
9059
|
+
* calls that round produces are executed.
|
|
9060
|
+
*/
|
|
9061
|
+
#advertisedTools = /* @__PURE__ */ new Set();
|
|
8861
9062
|
#toolRegistry = new ClientToolRegistry();
|
|
8862
9063
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
8863
9064
|
#toolCards = /* @__PURE__ */ new Map();
|
|
@@ -8935,6 +9136,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8935
9136
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
8936
9137
|
// it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
|
|
8937
9138
|
#streamDeltas = 0;
|
|
9139
|
+
// The accumulated answer the next render will draw. Deltas overwrite it
|
|
9140
|
+
// (each one carries the whole answer), so a frame always draws the latest.
|
|
9141
|
+
#streamBuffer = "";
|
|
9142
|
+
// The frame that render is queued on, or `null` when nothing is queued —
|
|
9143
|
+
// also the flag saying a delta is still undrawn.
|
|
9144
|
+
#streamFrame = null;
|
|
8938
9145
|
#pending = null;
|
|
8939
9146
|
// The current assistant turn's grouping container. One `.answer`
|
|
8940
9147
|
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
@@ -8949,9 +9156,23 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8949
9156
|
#thoughts = null;
|
|
8950
9157
|
#threadId = "";
|
|
8951
9158
|
// Per-instance suffix for the origin-scoped storage keys (collapsed / theme /
|
|
8952
|
-
//
|
|
8953
|
-
//
|
|
9159
|
+
// size), so two instances on one origin don't clobber each other. Empty ⇒ the
|
|
9160
|
+
// pre-namespacing global keys (back-compat). Resolved on connect; the
|
|
9161
|
+
// conversation adds `user-key` on top of it, see #conversationNs.
|
|
8954
9162
|
#storageNs = "";
|
|
9163
|
+
// The entry this element put in CLAIMED_NAMESPACES, to take back out on
|
|
9164
|
+
// disconnect. `null` when it claimed nothing (no id, no endpoint, or it lost
|
|
9165
|
+
// the claim to an element that mounted first).
|
|
9166
|
+
#claimedNs = null;
|
|
9167
|
+
// The fallback namespace minted when the preferred one was already claimed,
|
|
9168
|
+
// with the preferred value it was minted for — so the element keeps it across
|
|
9169
|
+
// remounts, but re-resolves if the host answers the warning with an `id`.
|
|
9170
|
+
#generatedNs = "";
|
|
9171
|
+
#generatedFor = "";
|
|
9172
|
+
// The `sessionStorage`-backed store, which the element may therefore re-scope
|
|
9173
|
+
// on a principal change. `null` when the host injected a store of its own
|
|
9174
|
+
// kind, whose keying the element does not know and must not guess at.
|
|
9175
|
+
#builtinStore = null;
|
|
8955
9176
|
// Bumped on every #rehydrate; a replay whose generation is stale (a newer
|
|
8956
9177
|
// thread switch started while it awaited a slow store) drops its result.
|
|
8957
9178
|
#rehydrateGeneration = 0;
|
|
@@ -9007,7 +9228,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9007
9228
|
if (this.#runIndex === null) {
|
|
9008
9229
|
this.#runIndex = new RunIndex(
|
|
9009
9230
|
url,
|
|
9010
|
-
() => this.#
|
|
9231
|
+
() => this.#headersFor(url),
|
|
9011
9232
|
() => this.#requestCredentials()
|
|
9012
9233
|
);
|
|
9013
9234
|
}
|
|
@@ -9041,6 +9262,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9041
9262
|
endpoint,
|
|
9042
9263
|
headers: this.#requestHeaders(),
|
|
9043
9264
|
getHeaders: () => this.#requestHeaders(),
|
|
9265
|
+
trustedOrigins: this.trustedOrigins,
|
|
9044
9266
|
...this.#credentialsOption(),
|
|
9045
9267
|
threadId: this.#threadId,
|
|
9046
9268
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
@@ -9049,7 +9271,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9049
9271
|
const client = new AgUiClient({
|
|
9050
9272
|
agent,
|
|
9051
9273
|
handlers: this.#handlers(),
|
|
9052
|
-
getTools: () => this
|
|
9274
|
+
getTools: () => this.#advertiseTools(),
|
|
9053
9275
|
getContext: () => this.#buildContext(),
|
|
9054
9276
|
executeTool: (call) => this.#executeTool(call),
|
|
9055
9277
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -9064,7 +9286,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9064
9286
|
}
|
|
9065
9287
|
/** Attributes the element reacts to after it has been connected. */
|
|
9066
9288
|
static get observedAttributes() {
|
|
9067
|
-
return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
|
|
9289
|
+
return ["title-text", "placement", "credentials", "user-key", ...CONNECT_TIME_ATTRIBUTES];
|
|
9068
9290
|
}
|
|
9069
9291
|
attributeChangedCallback(name, previous, value) {
|
|
9070
9292
|
if (name === "credentials") {
|
|
@@ -9084,6 +9306,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9084
9306
|
this.#title.textContent = value ?? this.#strings.title;
|
|
9085
9307
|
return;
|
|
9086
9308
|
}
|
|
9309
|
+
if (name === "user-key") {
|
|
9310
|
+
if (this.#connected && (previous ?? "") !== (value ?? "")) {
|
|
9311
|
+
this.#changePrincipal(previous ?? "", value ?? "");
|
|
9312
|
+
}
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9087
9315
|
if (previous === value || !this.#connected) {
|
|
9088
9316
|
return;
|
|
9089
9317
|
}
|
|
@@ -9091,7 +9319,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9091
9319
|
`<ag-ui-chat>: "${name}" was changed after the element connected, and is read only while connecting \u2014 this assignment has no effect. Set it before the element enters the DOM (in the markup, or on the element before appending it); frameworks that patch attributes after mount should bind it at creation. To apply a new value now, remove and re-insert the element.`
|
|
9092
9320
|
);
|
|
9093
9321
|
}
|
|
9094
|
-
/**
|
|
9322
|
+
/**
|
|
9323
|
+
* Declare a frontend tool the agent may call.
|
|
9324
|
+
*
|
|
9325
|
+
* **A handler's thrown message leaves the browser.** When a handler rejects,
|
|
9326
|
+
* its `Error.message` is posted back as that call's tool result — into the
|
|
9327
|
+
* conversation, on to the AG-UI endpoint, persisted server-side, and
|
|
9328
|
+
* forwarded to the model provider on every later round. That is deliberate,
|
|
9329
|
+
* since it is what lets the agent recover from a failure it caused; but it
|
|
9330
|
+
* means an internal hostname, a signed URL or a stack-derived path in a
|
|
9331
|
+
* rethrown error is disclosed to parties the host never chose. Throw the
|
|
9332
|
+
* message you would be content for the model to read, and log the detail.
|
|
9333
|
+
*/
|
|
9095
9334
|
registerTool(tool) {
|
|
9096
9335
|
this.#toolRegistry.register(tool);
|
|
9097
9336
|
}
|
|
@@ -9242,6 +9481,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9242
9481
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
9243
9482
|
return answer;
|
|
9244
9483
|
}
|
|
9484
|
+
/**
|
|
9485
|
+
* The catalog for the round about to start, remembering what it offered.
|
|
9486
|
+
*
|
|
9487
|
+
* Every path to a frontend tool goes through here first — the client asks
|
|
9488
|
+
* for `RunAgentInput.tools` at the top of each round — so this is the one
|
|
9489
|
+
* place that can know what the agent was actually told about.
|
|
9490
|
+
*/
|
|
9491
|
+
#advertiseTools() {
|
|
9492
|
+
const tools = this.getTools();
|
|
9493
|
+
this.#advertisedTools = new Set(tools.map((tool) => tool.name));
|
|
9494
|
+
return tools;
|
|
9495
|
+
}
|
|
9245
9496
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
9246
9497
|
#resolveTool(name) {
|
|
9247
9498
|
const builtin = this.#builtinTools().find((t) => t.name === name);
|
|
@@ -9260,6 +9511,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9260
9511
|
set endpoint(value) {
|
|
9261
9512
|
this.setAttribute("endpoint", value);
|
|
9262
9513
|
}
|
|
9514
|
+
/**
|
|
9515
|
+
* Who the stored conversation belongs to, from the `user-key` attribute.
|
|
9516
|
+
*
|
|
9517
|
+
* Set it to whatever identifies the signed-in principal — a user id, an
|
|
9518
|
+
* account id, a hash of one. The value joins the storage namespace, so two
|
|
9519
|
+
* principals in the same tab cannot read each other's transcript, and
|
|
9520
|
+
* **changing it purges what the previous one left behind**.
|
|
9521
|
+
*
|
|
9522
|
+
* That purge is the reason this is a live attribute rather than a
|
|
9523
|
+
* connect-time one. `sessionStorage` survives same-tab navigation, so it
|
|
9524
|
+
* survives a logout; and a single-page app signs out through its own router
|
|
9525
|
+
* without remounting anything, so there is no other moment at which the
|
|
9526
|
+
* element could find out. The host naming the new principal — or dropping the
|
|
9527
|
+
* attribute — is the signal.
|
|
9528
|
+
*
|
|
9529
|
+
* Absent means exactly today's behaviour, which is why nothing breaks by
|
|
9530
|
+
* leaving it off: the conversation is scoped to the element and to nobody in
|
|
9531
|
+
* particular, and on a shared workstation it carries into whoever signs in
|
|
9532
|
+
* next in the same tab.
|
|
9533
|
+
*
|
|
9534
|
+
* The first value to arrive is treated as a host naming the user who was
|
|
9535
|
+
* already there, not as a handover: the conversation in progress moves into
|
|
9536
|
+
* the principal's namespace rather than being destroyed, so an element
|
|
9537
|
+
* configured by an async auth handshake keeps what is on screen.
|
|
9538
|
+
*/
|
|
9539
|
+
get userKey() {
|
|
9540
|
+
return this.getAttribute("user-key") ?? "";
|
|
9541
|
+
}
|
|
9542
|
+
set userKey(value) {
|
|
9543
|
+
this.setAttribute("user-key", value);
|
|
9544
|
+
}
|
|
9263
9545
|
/**
|
|
9264
9546
|
* Cookie policy for **every** request this element makes, as `fetch`'s own
|
|
9265
9547
|
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
@@ -9304,6 +9586,24 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9304
9586
|
#requestHeaders() {
|
|
9305
9587
|
return { ...this.headers, ...this.getHeaders?.() };
|
|
9306
9588
|
}
|
|
9589
|
+
/**
|
|
9590
|
+
* The request headers, having first reported the destination if it is foreign.
|
|
9591
|
+
*
|
|
9592
|
+
* Every caller that sends these headers knows its URL, and `#requestHeaders`
|
|
9593
|
+
* does not -- so the check lives here, on the path that has both, rather than
|
|
9594
|
+
* being repeated at each call site with a chance to be forgotten at the next
|
|
9595
|
+
* one added.
|
|
9596
|
+
*/
|
|
9597
|
+
#headersFor(url) {
|
|
9598
|
+
const headers = this.#requestHeaders();
|
|
9599
|
+
warnOnCrossOriginCredentials(
|
|
9600
|
+
url,
|
|
9601
|
+
Object.keys(headers),
|
|
9602
|
+
this.trustedOrigins,
|
|
9603
|
+
this.#warnedOrigins
|
|
9604
|
+
);
|
|
9605
|
+
return headers;
|
|
9606
|
+
}
|
|
9307
9607
|
/** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
|
|
9308
9608
|
#requestCredentials() {
|
|
9309
9609
|
return this.credentials ?? void 0;
|
|
@@ -9322,8 +9622,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9322
9622
|
return credentials === void 0 ? {} : { credentials };
|
|
9323
9623
|
}
|
|
9324
9624
|
/** The `fetch` init for the element's own plain GETs (catalogs). */
|
|
9325
|
-
#fetchInit() {
|
|
9326
|
-
return withCredentials({ headers: this.#
|
|
9625
|
+
#fetchInit(url) {
|
|
9626
|
+
return withCredentials({ headers: this.#headersFor(url) }, this.#requestCredentials());
|
|
9327
9627
|
}
|
|
9328
9628
|
/**
|
|
9329
9629
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
@@ -9344,7 +9644,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9344
9644
|
this.setAttribute("data-tool-display", value);
|
|
9345
9645
|
}
|
|
9346
9646
|
connectedCallback() {
|
|
9347
|
-
this.#storageNs = this
|
|
9647
|
+
this.#storageNs = this.#claimNamespace();
|
|
9348
9648
|
this.#applySize(this.#readSize());
|
|
9349
9649
|
requestAnimationFrame(() => this.#syncResizeAnchor());
|
|
9350
9650
|
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
@@ -9362,8 +9662,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9362
9662
|
}
|
|
9363
9663
|
this.#syncLauncher();
|
|
9364
9664
|
this.#initSkills();
|
|
9365
|
-
if (this
|
|
9366
|
-
|
|
9665
|
+
if (this.conversationStore instanceof SessionStorageStore) {
|
|
9666
|
+
const namespace = this.#conversationNs();
|
|
9667
|
+
this.#builtinStore = namespace === "" ? this.conversationStore : new SessionStorageStore(namespace);
|
|
9668
|
+
this.conversationStore = this.#builtinStore;
|
|
9367
9669
|
}
|
|
9368
9670
|
this.#wireThreadStore();
|
|
9369
9671
|
this.#wireAttachments();
|
|
@@ -9427,6 +9729,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9427
9729
|
*/
|
|
9428
9730
|
disconnectedCallback() {
|
|
9429
9731
|
this.#connected = false;
|
|
9732
|
+
if (this.#claimedNs !== null) {
|
|
9733
|
+
CLAIMED_NAMESPACES.delete(this.#claimedNs);
|
|
9734
|
+
this.#claimedNs = null;
|
|
9735
|
+
}
|
|
9430
9736
|
this.#cancelRun();
|
|
9431
9737
|
this.#attachTray?.dispose();
|
|
9432
9738
|
this.#voice?.dispose();
|
|
@@ -9496,7 +9802,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9496
9802
|
}
|
|
9497
9803
|
return (file, onProgress, signal) => uploadAttachment(file, {
|
|
9498
9804
|
url,
|
|
9499
|
-
headers: this.#
|
|
9805
|
+
headers: this.#headersFor(url),
|
|
9500
9806
|
...this.#credentialsOption(),
|
|
9501
9807
|
onProgress,
|
|
9502
9808
|
signal
|
|
@@ -9529,7 +9835,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9529
9835
|
}
|
|
9530
9836
|
return (audio) => transcribeAudio(audio, {
|
|
9531
9837
|
url,
|
|
9532
|
-
headers: this.#
|
|
9838
|
+
headers: this.#headersFor(url),
|
|
9533
9839
|
...this.#credentialsOption()
|
|
9534
9840
|
});
|
|
9535
9841
|
}
|
|
@@ -9584,15 +9890,21 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9584
9890
|
* delete through that server endpoint (wrapping the current store as the
|
|
9585
9891
|
* client-only fallback), so the history drawer shows durable, cross-device
|
|
9586
9892
|
* threads. Without it, the client store's per-tab threads are used.
|
|
9893
|
+
*
|
|
9894
|
+
* `data-threads-cache="false"` drops the local copy of the message bodies —
|
|
9895
|
+
* for the deployment that pointed history at a server precisely so that
|
|
9896
|
+
* transcripts do not sit in the browser. The client-only concerns (the active
|
|
9897
|
+
* thread id, the navigation checkpoint) keep their local store either way.
|
|
9587
9898
|
*/
|
|
9588
9899
|
#wireThreadStore() {
|
|
9589
9900
|
const url = this.getAttribute("data-threads-url");
|
|
9590
9901
|
if (url !== null) {
|
|
9591
9902
|
this.conversationStore = new RemoteConversationStore(
|
|
9592
9903
|
url,
|
|
9593
|
-
() => this.#
|
|
9904
|
+
() => this.#headersFor(url),
|
|
9594
9905
|
this.conversationStore,
|
|
9595
|
-
() => this.#requestCredentials()
|
|
9906
|
+
() => this.#requestCredentials(),
|
|
9907
|
+
this.getAttribute("data-threads-cache") !== "false"
|
|
9596
9908
|
);
|
|
9597
9909
|
}
|
|
9598
9910
|
}
|
|
@@ -9603,7 +9915,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9603
9915
|
return;
|
|
9604
9916
|
}
|
|
9605
9917
|
try {
|
|
9606
|
-
const response = await fetch(url, this.#fetchInit());
|
|
9918
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9607
9919
|
this.#toolCatalog = parseToolCatalog(await response.json());
|
|
9608
9920
|
} catch {
|
|
9609
9921
|
}
|
|
@@ -9646,7 +9958,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9646
9958
|
return;
|
|
9647
9959
|
}
|
|
9648
9960
|
try {
|
|
9649
|
-
const response = await fetch(url, this.#fetchInit());
|
|
9961
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9650
9962
|
this.#backendSkills = parseSkills(await response.json());
|
|
9651
9963
|
this.#recomputeSkills();
|
|
9652
9964
|
} catch {
|
|
@@ -9729,7 +10041,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9729
10041
|
} else {
|
|
9730
10042
|
this.removeAttribute("collapsed");
|
|
9731
10043
|
}
|
|
9732
|
-
|
|
10044
|
+
writeStoredItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
9733
10045
|
this.#setUnread(0);
|
|
9734
10046
|
this.dispatchEvent(
|
|
9735
10047
|
new CustomEvent(TOGGLE_EVENT, {
|
|
@@ -9761,7 +10073,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9761
10073
|
toggleTheme() {
|
|
9762
10074
|
const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
|
|
9763
10075
|
this.setAttribute("theme", next);
|
|
9764
|
-
|
|
10076
|
+
writeStoredItem(this.#storageKey(THEME_KEY), next);
|
|
9765
10077
|
this.#syncThemeGlyph();
|
|
9766
10078
|
}
|
|
9767
10079
|
/**
|
|
@@ -9863,7 +10175,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9863
10175
|
/** Persist a dragged size per tab, alongside the collapsed/theme preferences. */
|
|
9864
10176
|
#persistSize(size) {
|
|
9865
10177
|
const stored = { ...this.#readSize(), ...size };
|
|
9866
|
-
|
|
10178
|
+
writeStoredItem(this.#storageKey(SIZE_KEY), JSON.stringify(stored));
|
|
9867
10179
|
}
|
|
9868
10180
|
/** The persisted size for this instance, or an empty record. */
|
|
9869
10181
|
#readSize() {
|
|
@@ -9878,6 +10190,106 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9878
10190
|
return {};
|
|
9879
10191
|
}
|
|
9880
10192
|
}
|
|
10193
|
+
/**
|
|
10194
|
+
* Claim this element's storage namespace: its `id`, else its `endpoint`.
|
|
10195
|
+
*
|
|
10196
|
+
* The endpoint fallback exists so a lone widget restores its conversation
|
|
10197
|
+
* across reloads with nothing asked of the page author. It stops working the
|
|
10198
|
+
* moment there are two of them — a docked support panel and an inline page
|
|
10199
|
+
* assistant against one agent mount, neither carrying an `id`, which nothing
|
|
10200
|
+
* requires — because both resolve to the same string and then share a thread
|
|
10201
|
+
* pointer, a drawer index and every message key. Whichever mounts second
|
|
10202
|
+
* adopts the first's active thread and rehydrates its transcript into its own
|
|
10203
|
+
* panel: one conversation's content inside another, on the same page.
|
|
10204
|
+
*
|
|
10205
|
+
* So the namespace is claimed by the first element to mount under it, and a
|
|
10206
|
+
* second is given one of its own plus a warning naming the fix. The first
|
|
10207
|
+
* element keeps the endpoint namespace, which is what leaves the ordinary
|
|
10208
|
+
* single-element case exactly as it was.
|
|
10209
|
+
*
|
|
10210
|
+
* The generated namespace is random rather than derived from mount order.
|
|
10211
|
+
* That costs the second element its history across reloads — the warning says
|
|
10212
|
+
* so, and an `id` fixes it — which is the honest trade against an order-based
|
|
10213
|
+
* name that would silently hand a stored conversation to whichever element
|
|
10214
|
+
* happened to mount second on the next load.
|
|
10215
|
+
*/
|
|
10216
|
+
#claimNamespace() {
|
|
10217
|
+
const preferred = this.id !== "" ? this.id : this.endpoint;
|
|
10218
|
+
if (preferred === "") {
|
|
10219
|
+
return "";
|
|
10220
|
+
}
|
|
10221
|
+
if (this.#generatedFor === preferred) {
|
|
10222
|
+
return this.#generatedNs;
|
|
10223
|
+
}
|
|
10224
|
+
if (!CLAIMED_NAMESPACES.has(preferred)) {
|
|
10225
|
+
CLAIMED_NAMESPACES.add(preferred);
|
|
10226
|
+
this.#claimedNs = preferred;
|
|
10227
|
+
return preferred;
|
|
10228
|
+
}
|
|
10229
|
+
this.#generatedFor = preferred;
|
|
10230
|
+
this.#generatedNs = `${preferred}~${randomUUID5()}`;
|
|
10231
|
+
console.warn(
|
|
10232
|
+
`<ag-ui-chat>: another element on this page already stores its conversation under "${preferred}", so this one has been given a throwaway namespace of its own \u2014 the two would otherwise share a thread pointer, a history drawer and every message. Give each <ag-ui-chat> its own id to keep them apart and let this one restore its conversation across reloads.`
|
|
10233
|
+
);
|
|
10234
|
+
return this.#generatedNs;
|
|
10235
|
+
}
|
|
10236
|
+
/**
|
|
10237
|
+
* The conversation store's namespace: this element's, scoped to the principal
|
|
10238
|
+
* {@link userKey} names.
|
|
10239
|
+
*
|
|
10240
|
+
* Only the conversation is principal-scoped. The panel's own collapsed / size
|
|
10241
|
+
* / theme preferences stay on `#storageNs`, because they are this element's
|
|
10242
|
+
* UI state rather than anyone's data — they carry no word of what was said —
|
|
10243
|
+
* and because they are read once while connecting, so re-scoping them under a
|
|
10244
|
+
* live element would rearrange the panel around a user who had only just
|
|
10245
|
+
* signed in.
|
|
10246
|
+
*/
|
|
10247
|
+
#conversationNs(key = this.userKey) {
|
|
10248
|
+
return key === "" ? this.#storageNs : `${this.#storageNs}#${key}`;
|
|
10249
|
+
}
|
|
10250
|
+
/**
|
|
10251
|
+
* Move the element's client state from one principal to another.
|
|
10252
|
+
*
|
|
10253
|
+
* The whole reason {@link userKey} is live: `sessionStorage` outlives a
|
|
10254
|
+
* logout, because a logout is a navigation (or, in a single-page app, not
|
|
10255
|
+
* even that) rather than a tab close. Nothing remounts, so the host naming
|
|
10256
|
+
* the new principal is the only signal the element will ever get.
|
|
10257
|
+
*/
|
|
10258
|
+
#changePrincipal(previousKey, nextKey) {
|
|
10259
|
+
const previous = this.#conversationNs(previousKey);
|
|
10260
|
+
const next = this.#conversationNs(nextKey);
|
|
10261
|
+
if (previousKey === "") {
|
|
10262
|
+
SessionStorageStore.adopt(previous, next);
|
|
10263
|
+
this.#rescopeStore(next);
|
|
10264
|
+
return;
|
|
10265
|
+
}
|
|
10266
|
+
SessionStorageStore.purge(previous);
|
|
10267
|
+
this.#rescopeStore(next);
|
|
10268
|
+
this.#cancelRun();
|
|
10269
|
+
this.#resetState();
|
|
10270
|
+
this.#setRunning(false);
|
|
10271
|
+
this.#setUnread(0);
|
|
10272
|
+
this.#threadId = this.conversationStore.threadId();
|
|
10273
|
+
void this.#rehydrate();
|
|
10274
|
+
void this.#refreshDrawer();
|
|
10275
|
+
}
|
|
10276
|
+
/**
|
|
10277
|
+
* Rebuild the `sessionStorage` store under `namespace`, re-wrapping it for
|
|
10278
|
+
* `data-threads-url` exactly as connecting did.
|
|
10279
|
+
*
|
|
10280
|
+
* A store of the host's own kind is left alone: a store that holds its data
|
|
10281
|
+
* somewhere the element cannot see has to scope itself. The transcript on
|
|
10282
|
+
* screen is still cleared either way — the host swapped principals, and that
|
|
10283
|
+
* much is the element's to act on.
|
|
10284
|
+
*/
|
|
10285
|
+
#rescopeStore(namespace) {
|
|
10286
|
+
if (this.#builtinStore === null) {
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
this.#builtinStore = new SessionStorageStore(namespace);
|
|
10290
|
+
this.conversationStore = this.#builtinStore;
|
|
10291
|
+
this.#wireThreadStore();
|
|
10292
|
+
}
|
|
9881
10293
|
/** This instance's namespaced form of an origin-scoped storage key. */
|
|
9882
10294
|
#storageKey(base) {
|
|
9883
10295
|
return this.#storageNs === "" ? base : `${base}:${this.#storageNs}`;
|
|
@@ -9963,7 +10375,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9963
10375
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
9964
10376
|
#resetState() {
|
|
9965
10377
|
this.#client = null;
|
|
9966
|
-
this.#
|
|
10378
|
+
this.#endStream();
|
|
9967
10379
|
this.#currentGroup = null;
|
|
9968
10380
|
this.#thoughts = null;
|
|
9969
10381
|
this.#hidePending();
|
|
@@ -10644,6 +11056,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10644
11056
|
// token must still reach every request — the factory's fetch wrapper
|
|
10645
11057
|
// re-reads this on each call.
|
|
10646
11058
|
getHeaders: () => this.#requestHeaders(),
|
|
11059
|
+
trustedOrigins: this.trustedOrigins,
|
|
10647
11060
|
...this.#credentialsOption(),
|
|
10648
11061
|
threadId: this.#threadId,
|
|
10649
11062
|
initialMessages: this.#initialMessages,
|
|
@@ -10652,7 +11065,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10652
11065
|
this.#client = new AgUiClient({
|
|
10653
11066
|
agent,
|
|
10654
11067
|
handlers: this.#handlers(),
|
|
10655
|
-
getTools: () => this
|
|
11068
|
+
getTools: () => this.#advertiseTools(),
|
|
10656
11069
|
getContext: () => this.#buildContext(),
|
|
10657
11070
|
executeTool: (call) => this.#executeTool(call),
|
|
10658
11071
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -10691,7 +11104,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10691
11104
|
const card = this.#cardFor(call);
|
|
10692
11105
|
this.#toolCards.delete(call.id);
|
|
10693
11106
|
this.#cardElements.set(call.id, card.element);
|
|
10694
|
-
const tool = this.#resolveTool(call.name);
|
|
11107
|
+
const tool = this.#advertisedTools.has(call.name) ? this.#resolveTool(call.name) : null;
|
|
10695
11108
|
if (tool === null) {
|
|
10696
11109
|
if (!this.#serverSettled.has(call.id)) {
|
|
10697
11110
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10834,7 +11247,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10834
11247
|
onTextDelta: (buffer) => {
|
|
10835
11248
|
this.#hidePending();
|
|
10836
11249
|
this.#thoughts?.collapse();
|
|
10837
|
-
this.#
|
|
11250
|
+
this.#queueStream(buffer);
|
|
10838
11251
|
this.#streamDeltas += 1;
|
|
10839
11252
|
},
|
|
10840
11253
|
onTextEnd: (buffer) => {
|
|
@@ -10843,7 +11256,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10843
11256
|
this.#revealWords(bubble);
|
|
10844
11257
|
}
|
|
10845
11258
|
attachCopyButtons(bubble, this.#strings);
|
|
10846
|
-
this.#
|
|
11259
|
+
this.#endStream();
|
|
10847
11260
|
this.#noteUnread();
|
|
10848
11261
|
},
|
|
10849
11262
|
onToolCall: (call) => {
|
|
@@ -10890,22 +11303,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10890
11303
|
},
|
|
10891
11304
|
onRunEnd: () => {
|
|
10892
11305
|
this.#hidePending();
|
|
10893
|
-
this.#
|
|
11306
|
+
this.#endStream();
|
|
10894
11307
|
},
|
|
10895
11308
|
onError: (message) => {
|
|
10896
11309
|
this.#hidePending();
|
|
10897
11310
|
this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`));
|
|
10898
|
-
this.#
|
|
11311
|
+
this.#endStream();
|
|
10899
11312
|
},
|
|
10900
11313
|
onCancelled: () => {
|
|
10901
11314
|
this.#hidePending();
|
|
10902
11315
|
this.#appendStoppedNote();
|
|
10903
|
-
this.#
|
|
11316
|
+
this.#endStream();
|
|
10904
11317
|
},
|
|
10905
11318
|
onSettled: () => {
|
|
10906
11319
|
this.#hidePending();
|
|
10907
11320
|
this.#setRunning(false);
|
|
10908
|
-
this.#
|
|
11321
|
+
this.#endStream();
|
|
10909
11322
|
for (const card of this.#toolCards.values()) {
|
|
10910
11323
|
if (!card.settled) {
|
|
10911
11324
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10999,15 +11412,72 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10999
11412
|
}
|
|
11000
11413
|
return this.#thoughts;
|
|
11001
11414
|
}
|
|
11002
|
-
|
|
11415
|
+
/**
|
|
11416
|
+
* The bubble the current answer streams into, opening it on first sight.
|
|
11417
|
+
*
|
|
11418
|
+
* Opened the moment a token arrives rather than on the frame that draws it,
|
|
11419
|
+
* so the answer's container replaces the pending dots straight away and the
|
|
11420
|
+
* turn never shows a gap while the first render waits for a frame.
|
|
11421
|
+
*/
|
|
11422
|
+
#openStream() {
|
|
11003
11423
|
if (this.#streamingBubble === null) {
|
|
11004
11424
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
11005
11425
|
this.#streamDeltas = 0;
|
|
11006
11426
|
}
|
|
11007
|
-
this.#streamingBubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
11008
|
-
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
11009
11427
|
return this.#streamingBubble;
|
|
11010
11428
|
}
|
|
11429
|
+
/**
|
|
11430
|
+
* Queue a render of the answer so far, at most one per frame.
|
|
11431
|
+
*
|
|
11432
|
+
* Each `TEXT_MESSAGE_CONTENT` event carries the *whole* accumulated answer,
|
|
11433
|
+
* and drawing it means marked + DOMPurify over the entire document and a
|
|
11434
|
+
* wholesale replacement of the bubble's subtree. Once per token that is
|
|
11435
|
+
* quadratic in the answer's length — a long answer is agent-controlled, so
|
|
11436
|
+
* an ordinary run becomes a progressively stalling tab — and every rebuild
|
|
11437
|
+
* takes any selection or focus inside the bubble with it.
|
|
11438
|
+
*
|
|
11439
|
+
* A frame is the right grain: it is the fastest anything on screen can
|
|
11440
|
+
* change anyway, so a burst of tokens costs one parse and the text still
|
|
11441
|
+
* appears to flow rather than in visible chunks.
|
|
11442
|
+
*/
|
|
11443
|
+
#queueStream(buffer) {
|
|
11444
|
+
this.#streamBuffer = buffer;
|
|
11445
|
+
this.#openStream();
|
|
11446
|
+
if (this.#streamFrame !== null) {
|
|
11447
|
+
return;
|
|
11448
|
+
}
|
|
11449
|
+
this.#streamFrame = requestAnimationFrame(() => {
|
|
11450
|
+
this.#streamFrame = null;
|
|
11451
|
+
this.#streamInto(this.#streamBuffer);
|
|
11452
|
+
});
|
|
11453
|
+
}
|
|
11454
|
+
/** Render `buffer` into the streaming bubble now, dropping any queued frame. */
|
|
11455
|
+
#streamInto(buffer) {
|
|
11456
|
+
if (this.#streamFrame !== null) {
|
|
11457
|
+
cancelAnimationFrame(this.#streamFrame);
|
|
11458
|
+
this.#streamFrame = null;
|
|
11459
|
+
}
|
|
11460
|
+
this.#streamBuffer = buffer;
|
|
11461
|
+
const bubble = this.#openStream();
|
|
11462
|
+
bubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
11463
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
11464
|
+
return bubble;
|
|
11465
|
+
}
|
|
11466
|
+
/**
|
|
11467
|
+
* Close the current answer's streaming bubble.
|
|
11468
|
+
*
|
|
11469
|
+
* Draws a queued render first. A run that ends without a text end — a
|
|
11470
|
+
* cancel, an error, a round boundary — leaves the last delta sitting in the
|
|
11471
|
+
* queue, and simply dropping the bubble here would strand it: the partial
|
|
11472
|
+
* answer the user stopped mid-sentence would lose its final tokens, or be an
|
|
11473
|
+
* empty bubble above the stopped note.
|
|
11474
|
+
*/
|
|
11475
|
+
#endStream() {
|
|
11476
|
+
if (this.#streamFrame !== null) {
|
|
11477
|
+
this.#streamInto(this.#streamBuffer);
|
|
11478
|
+
}
|
|
11479
|
+
this.#streamingBubble = null;
|
|
11480
|
+
}
|
|
11011
11481
|
/**
|
|
11012
11482
|
* The card for ``call``, creating and appending it on first sight.
|
|
11013
11483
|
*
|
|
@@ -11122,7 +11592,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11122
11592
|
return existing;
|
|
11123
11593
|
}
|
|
11124
11594
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
11125
|
-
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
|
|
11595
|
+
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name]?.summary ?? prettifyToolName(call.name);
|
|
11126
11596
|
const card = new ToolCallCard(call.name, call.args, summary, this.#strings);
|
|
11127
11597
|
this.#toolCards.set(call.id, card);
|
|
11128
11598
|
this.#ensureGroup().appendChild(card.element);
|
|
@@ -11197,7 +11667,7 @@ function setControlValue(el2, value) {
|
|
|
11197
11667
|
}
|
|
11198
11668
|
|
|
11199
11669
|
// src/version.ts
|
|
11200
|
-
var VERSION = "0.
|
|
11670
|
+
var VERSION = "0.28.0";
|
|
11201
11671
|
export {
|
|
11202
11672
|
ATTACHMENT_EVENT,
|
|
11203
11673
|
AgUiChat,
|