@artooi/ag-ui-web-component 0.26.1 → 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 +291 -1
- package/README.md +191 -8
- 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 +61 -3
- 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 +58 -3
- 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 +29 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +42 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +602 -104
- 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 +444 -45
- package/src/core/agui_client.ts +43 -2
- package/src/core/conversation_store.ts +146 -49
- package/src/core/create_http_agent.ts +24 -2
- package/src/core/remote_conversation_store.ts +45 -3
- package/src/core/utils.ts +83 -1
- 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
|
}
|
|
@@ -8029,6 +8096,7 @@ var AgUiClient = class {
|
|
|
8029
8096
|
#buildSubscriber(pending, runState) {
|
|
8030
8097
|
const h = this.#handlers;
|
|
8031
8098
|
const closed = this.#closedMessageIds;
|
|
8099
|
+
const cancelled = () => this.#cancelled;
|
|
8032
8100
|
const pendingDeltas = /* @__PURE__ */ new Set();
|
|
8033
8101
|
return {
|
|
8034
8102
|
onRunInitialized() {
|
|
@@ -8103,6 +8171,16 @@ var AgUiClient = class {
|
|
|
8103
8171
|
onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
|
|
8104
8172
|
h.onReasoningDelta(reasoningMessageBuffer);
|
|
8105
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
|
+
},
|
|
8106
8184
|
onReasoningEndEvent() {
|
|
8107
8185
|
h.onReasoningEnd();
|
|
8108
8186
|
},
|
|
@@ -8115,6 +8193,9 @@ var AgUiClient = class {
|
|
|
8115
8193
|
onRunErrorEvent({ event }) {
|
|
8116
8194
|
runState.terminal = true;
|
|
8117
8195
|
runState.errored = true;
|
|
8196
|
+
if (cancelled()) {
|
|
8197
|
+
return;
|
|
8198
|
+
}
|
|
8118
8199
|
h.onError(event.message);
|
|
8119
8200
|
},
|
|
8120
8201
|
onRunFinalized() {
|
|
@@ -8125,7 +8206,10 @@ var AgUiClient = class {
|
|
|
8125
8206
|
}
|
|
8126
8207
|
};
|
|
8127
8208
|
function isAbortError(error) {
|
|
8128
|
-
|
|
8209
|
+
if (!(error instanceof Error)) {
|
|
8210
|
+
return false;
|
|
8211
|
+
}
|
|
8212
|
+
return error.name === "AbortError" || error instanceof TypeError && /abort/i.test(error.message);
|
|
8129
8213
|
}
|
|
8130
8214
|
|
|
8131
8215
|
// src/core/attachment.ts
|
|
@@ -8152,23 +8236,77 @@ var MINTED_SUFFIX = "minted:";
|
|
|
8152
8236
|
var TITLE_LIMIT = 60;
|
|
8153
8237
|
var PREVIEW_LIMIT = 100;
|
|
8154
8238
|
var DEFAULT_TITLE = "New conversation";
|
|
8155
|
-
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 {
|
|
8156
8257
|
#root;
|
|
8157
8258
|
constructor(namespace = "") {
|
|
8158
|
-
this.#root = namespace
|
|
8259
|
+
this.#root = rootFor(namespace);
|
|
8159
8260
|
if (namespace !== "") {
|
|
8160
|
-
|
|
8261
|
+
_SessionStorageStore.adopt("", namespace);
|
|
8161
8262
|
}
|
|
8162
8263
|
}
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
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);
|
|
8168
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);
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
threadId() {
|
|
8304
|
+
return sessionStorage.getItem(this.#key(THREAD_SUFFIX)) ?? this.newThread();
|
|
8305
|
+
}
|
|
8306
|
+
newThread() {
|
|
8169
8307
|
const id = randomUUID3();
|
|
8170
|
-
|
|
8171
|
-
|
|
8308
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), id);
|
|
8309
|
+
writeStoredItem(this.#key(MINTED_SUFFIX + id), "1");
|
|
8172
8310
|
return id;
|
|
8173
8311
|
}
|
|
8174
8312
|
isUnsent(threadId) {
|
|
@@ -8178,7 +8316,7 @@ var SessionStorageStore = class {
|
|
|
8178
8316
|
return Promise.resolve(this.#readJson(this.#key(MESSAGES_SUFFIX + threadId)));
|
|
8179
8317
|
}
|
|
8180
8318
|
saveMessages(threadId, messages) {
|
|
8181
|
-
|
|
8319
|
+
writeStoredItem(this.#key(MESSAGES_SUFFIX + threadId), JSON.stringify(messages));
|
|
8182
8320
|
sessionStorage.removeItem(this.#key(MINTED_SUFFIX + threadId));
|
|
8183
8321
|
this.#touchThread(threadId, messages);
|
|
8184
8322
|
}
|
|
@@ -8191,7 +8329,7 @@ var SessionStorageStore = class {
|
|
|
8191
8329
|
sessionStorage.removeItem(key);
|
|
8192
8330
|
return;
|
|
8193
8331
|
}
|
|
8194
|
-
|
|
8332
|
+
writeStoredItem(key, JSON.stringify(checkpoint));
|
|
8195
8333
|
}
|
|
8196
8334
|
clear(threadId) {
|
|
8197
8335
|
sessionStorage.removeItem(this.#key(MESSAGES_SUFFIX + threadId));
|
|
@@ -8207,7 +8345,7 @@ var SessionStorageStore = class {
|
|
|
8207
8345
|
return Promise.resolve(metas);
|
|
8208
8346
|
}
|
|
8209
8347
|
setActiveThread(threadId) {
|
|
8210
|
-
|
|
8348
|
+
writeStoredItem(this.#key(THREAD_SUFFIX), threadId);
|
|
8211
8349
|
}
|
|
8212
8350
|
renameThread(threadId, title) {
|
|
8213
8351
|
const threads = this.#readThreads();
|
|
@@ -8251,40 +8389,12 @@ var SessionStorageStore = class {
|
|
|
8251
8389
|
sessionStorage.removeItem(key);
|
|
8252
8390
|
return;
|
|
8253
8391
|
}
|
|
8254
|
-
|
|
8392
|
+
writeStoredItem(key, JSON.stringify(threads));
|
|
8255
8393
|
}
|
|
8256
8394
|
/** This store's fully-qualified key for a suffix (namespaced when set). */
|
|
8257
8395
|
#key(suffix) {
|
|
8258
8396
|
return `${this.#root}:${suffix}`;
|
|
8259
8397
|
}
|
|
8260
|
-
/**
|
|
8261
|
-
* One-time move of un-namespaced `ag-ui-chat:*` keys into this instance's
|
|
8262
|
-
* namespace, so an existing conversation isn't orphaned. Only this store's own
|
|
8263
|
-
* keys move — the element's `collapsed` / `theme` keys are left alone. The
|
|
8264
|
-
* first namespaced instance to mount adopts the data; a second namespace
|
|
8265
|
-
* finds it gone and starts fresh.
|
|
8266
|
-
*/
|
|
8267
|
-
#migrateLegacyKeys() {
|
|
8268
|
-
const legacyRoot = `${KEY_ROOT}:`;
|
|
8269
|
-
const moves = [];
|
|
8270
|
-
for (let i = 0; i < sessionStorage.length; i += 1) {
|
|
8271
|
-
const key = sessionStorage.key(i);
|
|
8272
|
-
if (key === null || !key.startsWith(legacyRoot)) {
|
|
8273
|
-
continue;
|
|
8274
|
-
}
|
|
8275
|
-
const suffix = key.slice(legacyRoot.length);
|
|
8276
|
-
if (isOwnedSuffix(suffix)) {
|
|
8277
|
-
moves.push([key, this.#key(suffix)]);
|
|
8278
|
-
}
|
|
8279
|
-
}
|
|
8280
|
-
for (const [from, to] of moves) {
|
|
8281
|
-
const value = sessionStorage.getItem(from);
|
|
8282
|
-
if (value !== null && sessionStorage.getItem(to) === null) {
|
|
8283
|
-
sessionStorage.setItem(to, value);
|
|
8284
|
-
}
|
|
8285
|
-
sessionStorage.removeItem(from);
|
|
8286
|
-
}
|
|
8287
|
-
}
|
|
8288
8398
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
8289
8399
|
#readJson(key) {
|
|
8290
8400
|
const raw = sessionStorage.getItem(key);
|
|
@@ -8298,8 +8408,22 @@ var SessionStorageStore = class {
|
|
|
8298
8408
|
}
|
|
8299
8409
|
}
|
|
8300
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
|
+
}
|
|
8301
8425
|
function isOwnedSuffix(suffix) {
|
|
8302
|
-
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);
|
|
8303
8427
|
}
|
|
8304
8428
|
function deriveTitle(messages) {
|
|
8305
8429
|
for (const message of messages) {
|
|
@@ -8332,15 +8456,39 @@ function truncate(text3, limit) {
|
|
|
8332
8456
|
import { HttpAgent } from "@ag-ui/client";
|
|
8333
8457
|
|
|
8334
8458
|
// src/core/utils.ts
|
|
8459
|
+
import { randomUUID as randomUUID4 } from "@ag-ui/client";
|
|
8335
8460
|
function withCredentials(init, credentials) {
|
|
8336
8461
|
return credentials === void 0 ? init : { ...init, credentials };
|
|
8337
8462
|
}
|
|
8463
|
+
function mintThread(store) {
|
|
8464
|
+
if (store.newThread !== void 0) {
|
|
8465
|
+
return store.newThread();
|
|
8466
|
+
}
|
|
8467
|
+
const id = randomUUID4();
|
|
8468
|
+
store.setActiveThread(id);
|
|
8469
|
+
return id;
|
|
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
|
+
}
|
|
8338
8484
|
|
|
8339
8485
|
// src/core/create_http_agent.ts
|
|
8340
8486
|
function createHttpAgent(options) {
|
|
8487
|
+
const staticHeaders = options.headers ?? {};
|
|
8488
|
+
const warned = /* @__PURE__ */ new Set();
|
|
8341
8489
|
return new HttpAgent({
|
|
8342
8490
|
url: options.endpoint,
|
|
8343
|
-
headers:
|
|
8491
|
+
headers: staticHeaders,
|
|
8344
8492
|
initialState: { ...options.initialState ?? {} },
|
|
8345
8493
|
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
8346
8494
|
// rebinding the global `fetch` to the agent instance — "Illegal invocation"
|
|
@@ -8349,6 +8497,10 @@ function createHttpAgent(options) {
|
|
|
8349
8497
|
// own config having no seam for either.
|
|
8350
8498
|
fetch: (url, init) => {
|
|
8351
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);
|
|
8352
8504
|
if (fresh === void 0) {
|
|
8353
8505
|
return fetch(url, withCredentials(init, options.credentials));
|
|
8354
8506
|
}
|
|
@@ -8371,13 +8523,34 @@ var RemoteConversationStore = class {
|
|
|
8371
8523
|
#headers;
|
|
8372
8524
|
#local;
|
|
8373
8525
|
#credentials;
|
|
8526
|
+
#cacheMessages;
|
|
8374
8527
|
#dropped = /* @__PURE__ */ new Set();
|
|
8375
8528
|
#renamed = /* @__PURE__ */ new Map();
|
|
8376
|
-
|
|
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) {
|
|
8377
8549
|
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
8378
8550
|
this.#headers = headers;
|
|
8379
8551
|
this.#local = local;
|
|
8380
8552
|
this.#credentials = credentials;
|
|
8553
|
+
this.#cacheMessages = cacheMessages;
|
|
8381
8554
|
}
|
|
8382
8555
|
threadId() {
|
|
8383
8556
|
return this.#local.threadId();
|
|
@@ -8385,12 +8558,20 @@ var RemoteConversationStore = class {
|
|
|
8385
8558
|
setActiveThread(threadId) {
|
|
8386
8559
|
this.#local.setActiveThread(threadId);
|
|
8387
8560
|
}
|
|
8561
|
+
/**
|
|
8562
|
+
* Delegated to the local store, which owns the active id — and deliberately
|
|
8563
|
+
* silent on the wire: the server learns of a thread when its first message is
|
|
8564
|
+
* persisted, so an abandoned new chat costs no round-trip and leaves no row.
|
|
8565
|
+
*/
|
|
8566
|
+
newThread() {
|
|
8567
|
+
return mintThread(this.#local);
|
|
8568
|
+
}
|
|
8388
8569
|
/** Delegated, so wrapping a store does not lose what it knows about its own ids. */
|
|
8389
8570
|
isUnsent(threadId) {
|
|
8390
8571
|
return this.#local.isUnsent?.(threadId) === true;
|
|
8391
8572
|
}
|
|
8392
8573
|
saveMessages(threadId, messages) {
|
|
8393
|
-
this.#local.saveMessages(threadId, messages);
|
|
8574
|
+
this.#local.saveMessages(threadId, this.#cacheMessages ? messages : []);
|
|
8394
8575
|
}
|
|
8395
8576
|
loadCheckpoint(threadId) {
|
|
8396
8577
|
return this.#local.loadCheckpoint(threadId);
|
|
@@ -8655,6 +8836,7 @@ var CONNECT_TIME_ATTRIBUTES = [
|
|
|
8655
8836
|
"data-attachment-max-bytes",
|
|
8656
8837
|
"data-transcribe-url",
|
|
8657
8838
|
"data-threads-url",
|
|
8839
|
+
"data-threads-cache",
|
|
8658
8840
|
"data-tools-url",
|
|
8659
8841
|
"data-skills-url",
|
|
8660
8842
|
"data-skills",
|
|
@@ -8671,6 +8853,7 @@ function isCredentialsMode(value) {
|
|
|
8671
8853
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
8672
8854
|
var SIZE_KEY = "ag-ui-chat:size";
|
|
8673
8855
|
var THEME_KEY = "ag-ui-chat:theme";
|
|
8856
|
+
var CLAIMED_NAMESPACES = /* @__PURE__ */ new Set();
|
|
8674
8857
|
var AgUiChat = class extends HTMLElement {
|
|
8675
8858
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
8676
8859
|
agentFactory = createHttpAgent;
|
|
@@ -8694,6 +8877,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8694
8877
|
* `Authorization` are configured independently and neither drops the other.
|
|
8695
8878
|
*/
|
|
8696
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 = [];
|
|
8697
8896
|
/**
|
|
8698
8897
|
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
8699
8898
|
* model-controlled image URL is fetched with no user interaction, which
|
|
@@ -8829,13 +9028,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8829
9028
|
*/
|
|
8830
9029
|
resolvePageTarget = (target) => document.querySelector(target);
|
|
8831
9030
|
/**
|
|
8832
|
-
*
|
|
8833
|
-
*
|
|
8834
|
-
*
|
|
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.
|
|
8835
9036
|
*/
|
|
8836
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();
|
|
8837
9044
|
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
8838
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();
|
|
8839
9062
|
#toolRegistry = new ClientToolRegistry();
|
|
8840
9063
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
8841
9064
|
#toolCards = /* @__PURE__ */ new Map();
|
|
@@ -8913,6 +9136,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8913
9136
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
8914
9137
|
// it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
|
|
8915
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;
|
|
8916
9145
|
#pending = null;
|
|
8917
9146
|
// The current assistant turn's grouping container. One `.answer`
|
|
8918
9147
|
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
@@ -8927,9 +9156,23 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8927
9156
|
#thoughts = null;
|
|
8928
9157
|
#threadId = "";
|
|
8929
9158
|
// Per-instance suffix for the origin-scoped storage keys (collapsed / theme /
|
|
8930
|
-
//
|
|
8931
|
-
//
|
|
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.
|
|
8932
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;
|
|
8933
9176
|
// Bumped on every #rehydrate; a replay whose generation is stale (a newer
|
|
8934
9177
|
// thread switch started while it awaited a slow store) drops its result.
|
|
8935
9178
|
#rehydrateGeneration = 0;
|
|
@@ -8985,7 +9228,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8985
9228
|
if (this.#runIndex === null) {
|
|
8986
9229
|
this.#runIndex = new RunIndex(
|
|
8987
9230
|
url,
|
|
8988
|
-
() => this.#
|
|
9231
|
+
() => this.#headersFor(url),
|
|
8989
9232
|
() => this.#requestCredentials()
|
|
8990
9233
|
);
|
|
8991
9234
|
}
|
|
@@ -9019,6 +9262,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9019
9262
|
endpoint,
|
|
9020
9263
|
headers: this.#requestHeaders(),
|
|
9021
9264
|
getHeaders: () => this.#requestHeaders(),
|
|
9265
|
+
trustedOrigins: this.trustedOrigins,
|
|
9022
9266
|
...this.#credentialsOption(),
|
|
9023
9267
|
threadId: this.#threadId,
|
|
9024
9268
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
@@ -9027,7 +9271,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9027
9271
|
const client = new AgUiClient({
|
|
9028
9272
|
agent,
|
|
9029
9273
|
handlers: this.#handlers(),
|
|
9030
|
-
getTools: () => this
|
|
9274
|
+
getTools: () => this.#advertiseTools(),
|
|
9031
9275
|
getContext: () => this.#buildContext(),
|
|
9032
9276
|
executeTool: (call) => this.#executeTool(call),
|
|
9033
9277
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -9042,7 +9286,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9042
9286
|
}
|
|
9043
9287
|
/** Attributes the element reacts to after it has been connected. */
|
|
9044
9288
|
static get observedAttributes() {
|
|
9045
|
-
return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
|
|
9289
|
+
return ["title-text", "placement", "credentials", "user-key", ...CONNECT_TIME_ATTRIBUTES];
|
|
9046
9290
|
}
|
|
9047
9291
|
attributeChangedCallback(name, previous, value) {
|
|
9048
9292
|
if (name === "credentials") {
|
|
@@ -9062,6 +9306,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9062
9306
|
this.#title.textContent = value ?? this.#strings.title;
|
|
9063
9307
|
return;
|
|
9064
9308
|
}
|
|
9309
|
+
if (name === "user-key") {
|
|
9310
|
+
if (this.#connected && (previous ?? "") !== (value ?? "")) {
|
|
9311
|
+
this.#changePrincipal(previous ?? "", value ?? "");
|
|
9312
|
+
}
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9065
9315
|
if (previous === value || !this.#connected) {
|
|
9066
9316
|
return;
|
|
9067
9317
|
}
|
|
@@ -9069,7 +9319,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9069
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.`
|
|
9070
9320
|
);
|
|
9071
9321
|
}
|
|
9072
|
-
/**
|
|
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
|
+
*/
|
|
9073
9334
|
registerTool(tool) {
|
|
9074
9335
|
this.#toolRegistry.register(tool);
|
|
9075
9336
|
}
|
|
@@ -9220,6 +9481,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9220
9481
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
9221
9482
|
return answer;
|
|
9222
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
|
+
}
|
|
9223
9496
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
9224
9497
|
#resolveTool(name) {
|
|
9225
9498
|
const builtin = this.#builtinTools().find((t) => t.name === name);
|
|
@@ -9238,6 +9511,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9238
9511
|
set endpoint(value) {
|
|
9239
9512
|
this.setAttribute("endpoint", value);
|
|
9240
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
|
+
}
|
|
9241
9545
|
/**
|
|
9242
9546
|
* Cookie policy for **every** request this element makes, as `fetch`'s own
|
|
9243
9547
|
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
@@ -9282,6 +9586,24 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9282
9586
|
#requestHeaders() {
|
|
9283
9587
|
return { ...this.headers, ...this.getHeaders?.() };
|
|
9284
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
|
+
}
|
|
9285
9607
|
/** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
|
|
9286
9608
|
#requestCredentials() {
|
|
9287
9609
|
return this.credentials ?? void 0;
|
|
@@ -9300,8 +9622,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9300
9622
|
return credentials === void 0 ? {} : { credentials };
|
|
9301
9623
|
}
|
|
9302
9624
|
/** The `fetch` init for the element's own plain GETs (catalogs). */
|
|
9303
|
-
#fetchInit() {
|
|
9304
|
-
return withCredentials({ headers: this.#
|
|
9625
|
+
#fetchInit(url) {
|
|
9626
|
+
return withCredentials({ headers: this.#headersFor(url) }, this.#requestCredentials());
|
|
9305
9627
|
}
|
|
9306
9628
|
/**
|
|
9307
9629
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
@@ -9322,7 +9644,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9322
9644
|
this.setAttribute("data-tool-display", value);
|
|
9323
9645
|
}
|
|
9324
9646
|
connectedCallback() {
|
|
9325
|
-
this.#storageNs = this
|
|
9647
|
+
this.#storageNs = this.#claimNamespace();
|
|
9326
9648
|
this.#applySize(this.#readSize());
|
|
9327
9649
|
requestAnimationFrame(() => this.#syncResizeAnchor());
|
|
9328
9650
|
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
@@ -9340,8 +9662,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9340
9662
|
}
|
|
9341
9663
|
this.#syncLauncher();
|
|
9342
9664
|
this.#initSkills();
|
|
9343
|
-
if (this
|
|
9344
|
-
|
|
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;
|
|
9345
9669
|
}
|
|
9346
9670
|
this.#wireThreadStore();
|
|
9347
9671
|
this.#wireAttachments();
|
|
@@ -9405,6 +9729,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9405
9729
|
*/
|
|
9406
9730
|
disconnectedCallback() {
|
|
9407
9731
|
this.#connected = false;
|
|
9732
|
+
if (this.#claimedNs !== null) {
|
|
9733
|
+
CLAIMED_NAMESPACES.delete(this.#claimedNs);
|
|
9734
|
+
this.#claimedNs = null;
|
|
9735
|
+
}
|
|
9408
9736
|
this.#cancelRun();
|
|
9409
9737
|
this.#attachTray?.dispose();
|
|
9410
9738
|
this.#voice?.dispose();
|
|
@@ -9474,7 +9802,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9474
9802
|
}
|
|
9475
9803
|
return (file, onProgress, signal) => uploadAttachment(file, {
|
|
9476
9804
|
url,
|
|
9477
|
-
headers: this.#
|
|
9805
|
+
headers: this.#headersFor(url),
|
|
9478
9806
|
...this.#credentialsOption(),
|
|
9479
9807
|
onProgress,
|
|
9480
9808
|
signal
|
|
@@ -9507,7 +9835,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9507
9835
|
}
|
|
9508
9836
|
return (audio) => transcribeAudio(audio, {
|
|
9509
9837
|
url,
|
|
9510
|
-
headers: this.#
|
|
9838
|
+
headers: this.#headersFor(url),
|
|
9511
9839
|
...this.#credentialsOption()
|
|
9512
9840
|
});
|
|
9513
9841
|
}
|
|
@@ -9562,15 +9890,21 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9562
9890
|
* delete through that server endpoint (wrapping the current store as the
|
|
9563
9891
|
* client-only fallback), so the history drawer shows durable, cross-device
|
|
9564
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.
|
|
9565
9898
|
*/
|
|
9566
9899
|
#wireThreadStore() {
|
|
9567
9900
|
const url = this.getAttribute("data-threads-url");
|
|
9568
9901
|
if (url !== null) {
|
|
9569
9902
|
this.conversationStore = new RemoteConversationStore(
|
|
9570
9903
|
url,
|
|
9571
|
-
() => this.#
|
|
9904
|
+
() => this.#headersFor(url),
|
|
9572
9905
|
this.conversationStore,
|
|
9573
|
-
() => this.#requestCredentials()
|
|
9906
|
+
() => this.#requestCredentials(),
|
|
9907
|
+
this.getAttribute("data-threads-cache") !== "false"
|
|
9574
9908
|
);
|
|
9575
9909
|
}
|
|
9576
9910
|
}
|
|
@@ -9581,7 +9915,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9581
9915
|
return;
|
|
9582
9916
|
}
|
|
9583
9917
|
try {
|
|
9584
|
-
const response = await fetch(url, this.#fetchInit());
|
|
9918
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9585
9919
|
this.#toolCatalog = parseToolCatalog(await response.json());
|
|
9586
9920
|
} catch {
|
|
9587
9921
|
}
|
|
@@ -9624,7 +9958,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9624
9958
|
return;
|
|
9625
9959
|
}
|
|
9626
9960
|
try {
|
|
9627
|
-
const response = await fetch(url, this.#fetchInit());
|
|
9961
|
+
const response = await fetch(url, this.#fetchInit(url));
|
|
9628
9962
|
this.#backendSkills = parseSkills(await response.json());
|
|
9629
9963
|
this.#recomputeSkills();
|
|
9630
9964
|
} catch {
|
|
@@ -9707,7 +10041,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9707
10041
|
} else {
|
|
9708
10042
|
this.removeAttribute("collapsed");
|
|
9709
10043
|
}
|
|
9710
|
-
|
|
10044
|
+
writeStoredItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
9711
10045
|
this.#setUnread(0);
|
|
9712
10046
|
this.dispatchEvent(
|
|
9713
10047
|
new CustomEvent(TOGGLE_EVENT, {
|
|
@@ -9739,7 +10073,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9739
10073
|
toggleTheme() {
|
|
9740
10074
|
const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
|
|
9741
10075
|
this.setAttribute("theme", next);
|
|
9742
|
-
|
|
10076
|
+
writeStoredItem(this.#storageKey(THEME_KEY), next);
|
|
9743
10077
|
this.#syncThemeGlyph();
|
|
9744
10078
|
}
|
|
9745
10079
|
/**
|
|
@@ -9841,7 +10175,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9841
10175
|
/** Persist a dragged size per tab, alongside the collapsed/theme preferences. */
|
|
9842
10176
|
#persistSize(size) {
|
|
9843
10177
|
const stored = { ...this.#readSize(), ...size };
|
|
9844
|
-
|
|
10178
|
+
writeStoredItem(this.#storageKey(SIZE_KEY), JSON.stringify(stored));
|
|
9845
10179
|
}
|
|
9846
10180
|
/** The persisted size for this instance, or an empty record. */
|
|
9847
10181
|
#readSize() {
|
|
@@ -9856,6 +10190,106 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9856
10190
|
return {};
|
|
9857
10191
|
}
|
|
9858
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
|
+
}
|
|
9859
10293
|
/** This instance's namespaced form of an origin-scoped storage key. */
|
|
9860
10294
|
#storageKey(base) {
|
|
9861
10295
|
return this.#storageNs === "" ? base : `${base}:${this.#storageNs}`;
|
|
@@ -9921,21 +10355,27 @@ var AgUiChat = class extends HTMLElement {
|
|
|
9921
10355
|
this.openCheckpoints();
|
|
9922
10356
|
}
|
|
9923
10357
|
/**
|
|
9924
|
-
* Start a fresh conversation:
|
|
9925
|
-
*
|
|
10358
|
+
* Start a fresh conversation: drop the in-memory run state, clear the
|
|
10359
|
+
* transcript, and mint a new thread id.
|
|
10360
|
+
*
|
|
10361
|
+
* The conversation being left is kept, and stays in the history drawer to
|
|
10362
|
+
* return to. Deleting one is the drawer row's own action; a button that
|
|
10363
|
+
* starts something new must not be the button that destroys what was there.
|
|
9926
10364
|
*/
|
|
9927
10365
|
newChat() {
|
|
9928
10366
|
this.#cancelRun();
|
|
9929
|
-
this.conversationStore.
|
|
10367
|
+
if (this.conversationStore.isUnsent?.(this.#threadId) === true) {
|
|
10368
|
+
this.conversationStore.clear(this.#threadId);
|
|
10369
|
+
}
|
|
9930
10370
|
this.#resetState();
|
|
9931
|
-
this.#threadId = this.conversationStore
|
|
10371
|
+
this.#threadId = mintThread(this.conversationStore);
|
|
9932
10372
|
this.#setRunning(false);
|
|
9933
10373
|
this.#setUnread(0);
|
|
9934
10374
|
}
|
|
9935
10375
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
9936
10376
|
#resetState() {
|
|
9937
10377
|
this.#client = null;
|
|
9938
|
-
this.#
|
|
10378
|
+
this.#endStream();
|
|
9939
10379
|
this.#currentGroup = null;
|
|
9940
10380
|
this.#thoughts = null;
|
|
9941
10381
|
this.#hidePending();
|
|
@@ -10616,6 +11056,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10616
11056
|
// token must still reach every request — the factory's fetch wrapper
|
|
10617
11057
|
// re-reads this on each call.
|
|
10618
11058
|
getHeaders: () => this.#requestHeaders(),
|
|
11059
|
+
trustedOrigins: this.trustedOrigins,
|
|
10619
11060
|
...this.#credentialsOption(),
|
|
10620
11061
|
threadId: this.#threadId,
|
|
10621
11062
|
initialMessages: this.#initialMessages,
|
|
@@ -10624,7 +11065,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10624
11065
|
this.#client = new AgUiClient({
|
|
10625
11066
|
agent,
|
|
10626
11067
|
handlers: this.#handlers(),
|
|
10627
|
-
getTools: () => this
|
|
11068
|
+
getTools: () => this.#advertiseTools(),
|
|
10628
11069
|
getContext: () => this.#buildContext(),
|
|
10629
11070
|
executeTool: (call) => this.#executeTool(call),
|
|
10630
11071
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
@@ -10663,7 +11104,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10663
11104
|
const card = this.#cardFor(call);
|
|
10664
11105
|
this.#toolCards.delete(call.id);
|
|
10665
11106
|
this.#cardElements.set(call.id, card.element);
|
|
10666
|
-
const tool = this.#resolveTool(call.name);
|
|
11107
|
+
const tool = this.#advertisedTools.has(call.name) ? this.#resolveTool(call.name) : null;
|
|
10667
11108
|
if (tool === null) {
|
|
10668
11109
|
if (!this.#serverSettled.has(call.id)) {
|
|
10669
11110
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10806,7 +11247,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10806
11247
|
onTextDelta: (buffer) => {
|
|
10807
11248
|
this.#hidePending();
|
|
10808
11249
|
this.#thoughts?.collapse();
|
|
10809
|
-
this.#
|
|
11250
|
+
this.#queueStream(buffer);
|
|
10810
11251
|
this.#streamDeltas += 1;
|
|
10811
11252
|
},
|
|
10812
11253
|
onTextEnd: (buffer) => {
|
|
@@ -10815,7 +11256,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10815
11256
|
this.#revealWords(bubble);
|
|
10816
11257
|
}
|
|
10817
11258
|
attachCopyButtons(bubble, this.#strings);
|
|
10818
|
-
this.#
|
|
11259
|
+
this.#endStream();
|
|
10819
11260
|
this.#noteUnread();
|
|
10820
11261
|
},
|
|
10821
11262
|
onToolCall: (call) => {
|
|
@@ -10862,22 +11303,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10862
11303
|
},
|
|
10863
11304
|
onRunEnd: () => {
|
|
10864
11305
|
this.#hidePending();
|
|
10865
|
-
this.#
|
|
11306
|
+
this.#endStream();
|
|
10866
11307
|
},
|
|
10867
11308
|
onError: (message) => {
|
|
10868
11309
|
this.#hidePending();
|
|
10869
11310
|
this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`));
|
|
10870
|
-
this.#
|
|
11311
|
+
this.#endStream();
|
|
10871
11312
|
},
|
|
10872
11313
|
onCancelled: () => {
|
|
10873
11314
|
this.#hidePending();
|
|
10874
11315
|
this.#appendStoppedNote();
|
|
10875
|
-
this.#
|
|
11316
|
+
this.#endStream();
|
|
10876
11317
|
},
|
|
10877
11318
|
onSettled: () => {
|
|
10878
11319
|
this.#hidePending();
|
|
10879
11320
|
this.#setRunning(false);
|
|
10880
|
-
this.#
|
|
11321
|
+
this.#endStream();
|
|
10881
11322
|
for (const card of this.#toolCards.values()) {
|
|
10882
11323
|
if (!card.settled) {
|
|
10883
11324
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
@@ -10971,15 +11412,72 @@ var AgUiChat = class extends HTMLElement {
|
|
|
10971
11412
|
}
|
|
10972
11413
|
return this.#thoughts;
|
|
10973
11414
|
}
|
|
10974
|
-
|
|
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() {
|
|
10975
11423
|
if (this.#streamingBubble === null) {
|
|
10976
11424
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
10977
11425
|
this.#streamDeltas = 0;
|
|
10978
11426
|
}
|
|
10979
|
-
this.#streamingBubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
10980
|
-
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
10981
11427
|
return this.#streamingBubble;
|
|
10982
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
|
+
}
|
|
10983
11481
|
/**
|
|
10984
11482
|
* The card for ``call``, creating and appending it on first sight.
|
|
10985
11483
|
*
|
|
@@ -11094,7 +11592,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
11094
11592
|
return existing;
|
|
11095
11593
|
}
|
|
11096
11594
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
11097
|
-
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);
|
|
11098
11596
|
const card = new ToolCallCard(call.name, call.args, summary, this.#strings);
|
|
11099
11597
|
this.#toolCards.set(call.id, card);
|
|
11100
11598
|
this.#ensureGroup().appendChild(card.element);
|
|
@@ -11169,7 +11667,7 @@ function setControlValue(el2, value) {
|
|
|
11169
11667
|
}
|
|
11170
11668
|
|
|
11171
11669
|
// src/version.ts
|
|
11172
|
-
var VERSION = "0.
|
|
11670
|
+
var VERSION = "0.28.0";
|
|
11173
11671
|
export {
|
|
11174
11672
|
ATTACHMENT_EVENT,
|
|
11175
11673
|
AgUiChat,
|