@artooi/ag-ui-web-component 0.20.0 → 0.21.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 +135 -1
- package/README.md +286 -20
- package/dist/ag-ui-web-component.bundle.js +407 -382
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +82 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +9 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +8 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +8 -1
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +7 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +9 -0
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/core/utils.d.ts +12 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/dom/animations.d.ts +51 -11
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/dom_driver.d.ts +12 -7
- package/dist/dom/dom_driver.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +766 -430
- package/dist/index.js.map +3 -3
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +292 -31
- package/src/core/create_http_agent.ts +14 -3
- package/src/core/remote_conversation_store.ts +25 -6
- package/src/core/run_index.ts +27 -5
- package/src/core/transcribe_audio.ts +20 -5
- package/src/core/upload_attachment.ts +13 -0
- package/src/core/utils.ts +18 -0
- package/src/dom/animations.ts +175 -31
- package/src/dom/dom_driver.ts +18 -12
- package/src/index.ts +2 -0
- package/src/ui/styles.ts +377 -352
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -75,6 +75,7 @@ import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
|
75
75
|
import { RunIndex } from "./run_index.js";
|
|
76
76
|
import { type TranscribeHandler, transcribeAudio } from "./transcribe_audio.js";
|
|
77
77
|
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
78
|
+
import { withCredentials } from "./utils.js";
|
|
78
79
|
|
|
79
80
|
/** The role a rendered chat message takes. */
|
|
80
81
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -133,6 +134,18 @@ const CONNECT_TIME_ATTRIBUTES = [
|
|
|
133
134
|
"data-icon-url",
|
|
134
135
|
] as const;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The cookie policies `fetch` accepts. Anything else is a configuration
|
|
139
|
+
* mistake, and one that would otherwise surface as an unexplained 401 from a
|
|
140
|
+
* request the browser silently sent anonymously.
|
|
141
|
+
*/
|
|
142
|
+
const CREDENTIALS_MODES: readonly string[] = ["omit", "same-origin", "include"];
|
|
143
|
+
|
|
144
|
+
/** Whether `value` is one of the three modes `fetch` understands. */
|
|
145
|
+
function isCredentialsMode(value: string): value is RequestCredentials {
|
|
146
|
+
return CREDENTIALS_MODES.includes(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
/** Per-tab persistence key for the collapsed state (survives MPA reloads). */
|
|
137
150
|
const COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
138
151
|
|
|
@@ -158,9 +171,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
158
171
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
159
172
|
agentFactory: AgentFactory = createHttpAgent;
|
|
160
173
|
|
|
161
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Static extra HTTP headers, sent with **every** request this element makes —
|
|
176
|
+
* the agent run, the thread index and its messages, the tool and skill
|
|
177
|
+
* catalogs, the run index, uploads and transcription.
|
|
178
|
+
*
|
|
179
|
+
* Right for values fixed for the element's lifetime. A credential that
|
|
180
|
+
* rotates (a short-lived JWT, a re-issued CSRF token) belongs in
|
|
181
|
+
* {@link getHeaders} instead: this is read at request time, but only a
|
|
182
|
+
* re-assignment updates it, so a token captured here is pinned until the host
|
|
183
|
+
* remembers to assign again.
|
|
184
|
+
*/
|
|
162
185
|
headers: Record<string, string> = {};
|
|
163
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Live header source, consulted immediately before every request — the way to
|
|
189
|
+
* supply rotating credentials.
|
|
190
|
+
*
|
|
191
|
+
* Set it to a function and each request calls it afresh: a token refreshed by
|
|
192
|
+
* the host between two requests reaches the second one, with nothing to
|
|
193
|
+
* re-assign and nothing to keep in sync.
|
|
194
|
+
*
|
|
195
|
+
* Composes with {@link headers} rather than replacing it: the two are merged
|
|
196
|
+
* per key with `getHeaders()` winning, so a static `X-Client` and a rotating
|
|
197
|
+
* `Authorization` can be configured independently and neither silently drops
|
|
198
|
+
* the other.
|
|
199
|
+
*/
|
|
200
|
+
getHeaders: (() => Record<string, string>) | null = null;
|
|
201
|
+
|
|
164
202
|
/**
|
|
165
203
|
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
166
204
|
* model-controlled image URL is fetched with no user interaction, which
|
|
@@ -474,7 +512,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
474
512
|
return null;
|
|
475
513
|
}
|
|
476
514
|
if (this.#runIndex === null) {
|
|
477
|
-
this.#runIndex = new RunIndex(
|
|
515
|
+
this.#runIndex = new RunIndex(
|
|
516
|
+
url,
|
|
517
|
+
() => this.#requestHeaders(),
|
|
518
|
+
() => this.#requestCredentials(),
|
|
519
|
+
);
|
|
478
520
|
}
|
|
479
521
|
return this.#runIndex;
|
|
480
522
|
}
|
|
@@ -507,8 +549,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
507
549
|
const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
|
|
508
550
|
const agent = this.agentFactory({
|
|
509
551
|
endpoint,
|
|
510
|
-
headers: this
|
|
511
|
-
getHeaders: () => this
|
|
552
|
+
headers: this.#requestHeaders(),
|
|
553
|
+
getHeaders: () => this.#requestHeaders(),
|
|
554
|
+
...this.#credentialsOption(),
|
|
512
555
|
threadId: this.#threadId,
|
|
513
556
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
514
557
|
initialMessages: [],
|
|
@@ -533,13 +576,31 @@ export class AgUiChat extends HTMLElement {
|
|
|
533
576
|
|
|
534
577
|
/** Attributes the element reacts to after it has been connected. */
|
|
535
578
|
static get observedAttributes(): string[] {
|
|
536
|
-
return ["title-text", "placement", ...CONNECT_TIME_ATTRIBUTES];
|
|
579
|
+
return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
|
|
537
580
|
}
|
|
538
581
|
|
|
539
582
|
attributeChangedCallback(name: string, previous: string | null, value: string | null): void {
|
|
583
|
+
if (name === "credentials") {
|
|
584
|
+
// Reported the moment the attribute is written — before connect, and
|
|
585
|
+
// whether it came from markup or the property setter. An unrecognised
|
|
586
|
+
// mode is otherwise inert, and the request it was meant to authorise
|
|
587
|
+
// goes out anonymously with nothing to show for it.
|
|
588
|
+
if (value !== null && !isCredentialsMode(value)) {
|
|
589
|
+
console.error(
|
|
590
|
+
`<ag-ui-chat>: credentials="${value}" is not a fetch credentials mode ` +
|
|
591
|
+
`(${CREDENTIALS_MODES.join(" / ")}) — it is being ignored, so requests use ` +
|
|
592
|
+
"the browser default and cross-origin cookies will not be sent.",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
540
597
|
if (name === "placement") {
|
|
541
|
-
//
|
|
542
|
-
//
|
|
598
|
+
// A placement owns the axes it fixes, so hand those back before anything
|
|
599
|
+
// else: a size dragged under the previous placement would otherwise sit
|
|
600
|
+
// inline and outrank the new one.
|
|
601
|
+
this.#releaseOwnedAxes();
|
|
602
|
+
// Placement also moves the panel, so the edges its layout holds still
|
|
603
|
+
// change with it. Deferred a frame so the new rules have applied.
|
|
543
604
|
requestAnimationFrame(() => this.#syncResizeAnchor());
|
|
544
605
|
return;
|
|
545
606
|
}
|
|
@@ -773,6 +834,82 @@ export class AgUiChat extends HTMLElement {
|
|
|
773
834
|
this.setAttribute("endpoint", value);
|
|
774
835
|
}
|
|
775
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Cookie policy for **every** request this element makes, as `fetch`'s own
|
|
839
|
+
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
840
|
+
* the `credentials` attribute, so markup embeds can set it without script.
|
|
841
|
+
*
|
|
842
|
+
* `null` (the default) leaves the browser's default of `same-origin` in
|
|
843
|
+
* place. That default sends **no cookies at all** when the endpoints live on
|
|
844
|
+
* a different origin from the page — app.example.com calling
|
|
845
|
+
* api.example.com is cross-origin — and the request goes out anonymously
|
|
846
|
+
* rather than failing, so the symptom is a 401 from a server that looks
|
|
847
|
+
* correctly configured. A cookie-authenticated cross-origin deployment wants
|
|
848
|
+
* `"include"`, plus `Access-Control-Allow-Credentials: true` and a concrete
|
|
849
|
+
* (non-wildcard) `Access-Control-Allow-Origin` on the server.
|
|
850
|
+
*
|
|
851
|
+
* Read per request, so a late assignment applies to everything after it.
|
|
852
|
+
* `"omit"` cannot be honoured by the built-in **upload** transport, which is
|
|
853
|
+
* an `XMLHttpRequest` and only has a two-state cookie switch; every other
|
|
854
|
+
* endpoint honours all three modes.
|
|
855
|
+
*/
|
|
856
|
+
get credentials(): RequestCredentials | null {
|
|
857
|
+
const attr = this.getAttribute("credentials");
|
|
858
|
+
return attr !== null && isCredentialsMode(attr) ? attr : null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
set credentials(value: RequestCredentials | null) {
|
|
862
|
+
if (value === null) {
|
|
863
|
+
this.removeAttribute("credentials");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
// Thrown, not warned: an unrecognised mode is inert at request time, and
|
|
867
|
+
// the whole failure this option exists to fix is a request that goes out
|
|
868
|
+
// wrong without saying so. Fail where the mistake was made instead.
|
|
869
|
+
if (!isCredentialsMode(value)) {
|
|
870
|
+
throw new TypeError(
|
|
871
|
+
`<ag-ui-chat>: credentials must be one of ${CREDENTIALS_MODES.map((mode) => `"${mode}"`).join(", ")} ` +
|
|
872
|
+
`(got ${JSON.stringify(value)}).`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
this.setAttribute("credentials", value);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The headers for the request about to go out: the static {@link headers}
|
|
880
|
+
* with {@link getHeaders}'s live values overlaid, per key.
|
|
881
|
+
*
|
|
882
|
+
* Every request site goes through here, so "how this element authenticates"
|
|
883
|
+
* is one answer rather than one per endpoint.
|
|
884
|
+
*/
|
|
885
|
+
#requestHeaders(): Record<string, string> {
|
|
886
|
+
return { ...this.headers, ...this.getHeaders?.() };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
|
|
890
|
+
#requestCredentials(): RequestCredentials | undefined {
|
|
891
|
+
return this.credentials ?? undefined;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* The `credentials` entry for an {@link AgentFactory} call, or nothing at all.
|
|
896
|
+
*
|
|
897
|
+
* Spread rather than assigned: `exactOptionalPropertyTypes` rejects an
|
|
898
|
+
* explicit `credentials: undefined`, and a factory should see the field
|
|
899
|
+
* absent — not present-and-empty — when no policy is configured. The agent
|
|
900
|
+
* reads it when it is built (first send, thread switch, continuation), by
|
|
901
|
+
* which time any host configuration has landed.
|
|
902
|
+
*/
|
|
903
|
+
#credentialsOption(): { credentials?: RequestCredentials } {
|
|
904
|
+
const credentials = this.#requestCredentials();
|
|
905
|
+
return credentials === undefined ? {} : { credentials };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** The `fetch` init for the element's own plain GETs (catalogs). */
|
|
909
|
+
#fetchInit(): RequestInit | undefined {
|
|
910
|
+
return withCredentials({ headers: this.#requestHeaders() }, this.#requestCredentials());
|
|
911
|
+
}
|
|
912
|
+
|
|
776
913
|
/**
|
|
777
914
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
778
915
|
* attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
|
|
@@ -829,7 +966,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
829
966
|
}
|
|
830
967
|
this.#syncRail();
|
|
831
968
|
this.#initSkills();
|
|
832
|
-
void this.#fetchToolCatalog();
|
|
833
969
|
// Namespace the built-in default store too (a host-injected store is used
|
|
834
970
|
// verbatim). Must precede #wireThreadStore, which wraps the current store.
|
|
835
971
|
if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
|
|
@@ -839,12 +975,72 @@ export class AgUiChat extends HTMLElement {
|
|
|
839
975
|
this.#wireAttachments();
|
|
840
976
|
this.#wireVoice();
|
|
841
977
|
this.#threadId = this.conversationStore.threadId();
|
|
978
|
+
// The catalog requests go out a microtask later, so a host configuring
|
|
979
|
+
// through a framework ref still has a chance to be heard — see #startup.
|
|
980
|
+
queueMicrotask(() => this.#startup());
|
|
842
981
|
void this.#rehydrate();
|
|
843
982
|
// Last: everything above reads (and some of it sets) attributes, and none
|
|
844
983
|
// of that should trip the connect-time warning.
|
|
845
984
|
this.#connected = true;
|
|
846
985
|
}
|
|
847
986
|
|
|
987
|
+
/**
|
|
988
|
+
* The catalog requests the element issues on startup: the tool labels
|
|
989
|
+
* (`data-tools-url`) and the backend skills (`data-skills-url`).
|
|
990
|
+
*
|
|
991
|
+
* Deliberately one microtask behind `connectedCallback`. A host that
|
|
992
|
+
* configures the element through a framework ref necessarily does so *after*
|
|
993
|
+
* inserting the node — React attaches refs and runs layout effects in the
|
|
994
|
+
* same commit as the insertion, but strictly afterwards — so a request issued
|
|
995
|
+
* from `connectedCallback` itself goes out before `headers`,
|
|
996
|
+
* {@link getHeaders} or {@link credentials} exist, and comes back 401 in a
|
|
997
|
+
* way that reads as a server fault rather than a mis-timed assignment. A
|
|
998
|
+
* microtask lands after that commit and still before paint.
|
|
999
|
+
*
|
|
1000
|
+
* Two things it is **not**. It is not a fix for configuration that arrives
|
|
1001
|
+
* later than the commit (a passive `useEffect`, an awaited token fetch):
|
|
1002
|
+
* configure before insertion (`createElement` → configure → `append`) or call
|
|
1003
|
+
* {@link reload} once configured, because a longer timer would hide that race
|
|
1004
|
+
* rather than close it. And it deliberately excludes the *history* replay,
|
|
1005
|
+
* which stays in `connectedCallback`: the replay renders into the transcript,
|
|
1006
|
+
* so deferring it lets a `sendMessage()` issued in the same task land first
|
|
1007
|
+
* and the replay then duplicate it. The thread history is therefore the one
|
|
1008
|
+
* request that can still go out before a ref is attached — {@link reload}
|
|
1009
|
+
* covers it.
|
|
1010
|
+
*/
|
|
1011
|
+
#startup(): void {
|
|
1012
|
+
// An element can be inserted and removed inside one task (a discarded
|
|
1013
|
+
// render, a double-mount); nothing should go out for a node that has
|
|
1014
|
+
// already left the document.
|
|
1015
|
+
if (!this.#connected) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
void this.#fetchToolCatalog();
|
|
1019
|
+
void this.#fetchSkills();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Re-run everything the element loads on startup — the tool-label catalog,
|
|
1024
|
+
* the backend skill catalog and the thread's history — with the transport
|
|
1025
|
+
* configuration as it stands now.
|
|
1026
|
+
*
|
|
1027
|
+
* This is the answer for a host that can only configure the element after the
|
|
1028
|
+
* fact (a token fetched in a passive effect, an async auth handshake): the
|
|
1029
|
+
* startup requests already went out with whatever was set then, and this says
|
|
1030
|
+
* "try again, properly authenticated" without removing and re-inserting the
|
|
1031
|
+
* node.
|
|
1032
|
+
*
|
|
1033
|
+
* A reload, not a merge — the in-flight run is cancelled and the transcript
|
|
1034
|
+
* is rebuilt from the persisted history, so anything streamed since is
|
|
1035
|
+
* dropped. Call it once, when configuration lands; not between turns.
|
|
1036
|
+
*/
|
|
1037
|
+
async reload(): Promise<void> {
|
|
1038
|
+
this.#cancelRun();
|
|
1039
|
+
this.#resetState();
|
|
1040
|
+
this.#setRunning(false);
|
|
1041
|
+
await Promise.all([this.#fetchToolCatalog(), this.#fetchSkills(), this.#rehydrate()]);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
848
1044
|
/**
|
|
849
1045
|
* Tear down live resources when the element leaves the DOM (a removed node, a
|
|
850
1046
|
* client-side route swap): cancel the in-flight run so its SSE stream closes,
|
|
@@ -936,7 +1132,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
936
1132
|
// Forward the tray's abort signal so removing a chip (or tearing the
|
|
937
1133
|
// element down) cancels the XHR.
|
|
938
1134
|
return (file, onProgress, signal) =>
|
|
939
|
-
uploadAttachment(file, {
|
|
1135
|
+
uploadAttachment(file, {
|
|
1136
|
+
url,
|
|
1137
|
+
headers: this.#requestHeaders(),
|
|
1138
|
+
...this.#credentialsOption(),
|
|
1139
|
+
onProgress,
|
|
1140
|
+
signal,
|
|
1141
|
+
});
|
|
940
1142
|
}
|
|
941
1143
|
|
|
942
1144
|
/**
|
|
@@ -965,7 +1167,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
965
1167
|
if (url === null) {
|
|
966
1168
|
return null;
|
|
967
1169
|
}
|
|
968
|
-
return (audio) =>
|
|
1170
|
+
return (audio) =>
|
|
1171
|
+
transcribeAudio(audio, {
|
|
1172
|
+
url,
|
|
1173
|
+
headers: this.#requestHeaders(),
|
|
1174
|
+
...this.#credentialsOption(),
|
|
1175
|
+
});
|
|
969
1176
|
}
|
|
970
1177
|
|
|
971
1178
|
/** Drop a voice transcript into the composer (appended to any typed text). */
|
|
@@ -1046,8 +1253,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1046
1253
|
if (url !== null) {
|
|
1047
1254
|
this.conversationStore = new RemoteConversationStore(
|
|
1048
1255
|
url,
|
|
1049
|
-
() => this
|
|
1256
|
+
() => this.#requestHeaders(),
|
|
1050
1257
|
this.conversationStore,
|
|
1258
|
+
() => this.#requestCredentials(),
|
|
1051
1259
|
);
|
|
1052
1260
|
}
|
|
1053
1261
|
}
|
|
@@ -1059,7 +1267,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1059
1267
|
return;
|
|
1060
1268
|
}
|
|
1061
1269
|
try {
|
|
1062
|
-
const response = await fetch(url,
|
|
1270
|
+
const response = await fetch(url, this.#fetchInit());
|
|
1063
1271
|
this.#toolCatalog = parseToolCatalog(await response.json());
|
|
1064
1272
|
} catch {
|
|
1065
1273
|
// Network/parse failure: cards fall back to toolSummaries / raw names.
|
|
@@ -1075,13 +1283,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1075
1283
|
this.#recomputeSkills();
|
|
1076
1284
|
}
|
|
1077
1285
|
|
|
1078
|
-
/**
|
|
1286
|
+
/**
|
|
1287
|
+
* Wire the skill surfaces: opt-in flags and the embedded catalog. The backend
|
|
1288
|
+
* catalog is fetched from `#startup`, a microtask later, so it carries the
|
|
1289
|
+
* host's transport configuration.
|
|
1290
|
+
*/
|
|
1079
1291
|
#initSkills(): void {
|
|
1080
1292
|
this.#skillsMenu.enableChips(this.#flag("data-prompt-chips"));
|
|
1081
1293
|
this.#skillsMenu.enableSlash(this.#flag("data-slash-commands"));
|
|
1082
1294
|
this.#embedSkills = this.#readEmbeddedSkills();
|
|
1083
1295
|
this.#recomputeSkills();
|
|
1084
|
-
void this.#fetchSkills();
|
|
1085
1296
|
}
|
|
1086
1297
|
|
|
1087
1298
|
/** Parse the inline `data-skills` JSON catalog (empty when absent/malformed). */
|
|
@@ -1104,7 +1315,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1104
1315
|
return;
|
|
1105
1316
|
}
|
|
1106
1317
|
try {
|
|
1107
|
-
const response = await fetch(url,
|
|
1318
|
+
const response = await fetch(url, this.#fetchInit());
|
|
1108
1319
|
this.#backendSkills = parseSkills(await response.json());
|
|
1109
1320
|
this.#recomputeSkills();
|
|
1110
1321
|
} catch {
|
|
@@ -1306,22 +1517,47 @@ export class AgUiChat extends HTMLElement {
|
|
|
1306
1517
|
}
|
|
1307
1518
|
|
|
1308
1519
|
/**
|
|
1309
|
-
* Write a dragged size onto the host
|
|
1520
|
+
* Write a dragged size onto the host, on the axes this placement leaves free.
|
|
1521
|
+
*
|
|
1522
|
+
* ⚠ Writing the custom property rather than inline `width` / `height` does
|
|
1523
|
+
* **not** by itself leave placement in charge — an inline custom property
|
|
1524
|
+
* still outranks a `:host([placement=…])` rule setting the same property, so
|
|
1525
|
+
* a height dragged while floating capped a docked sidebar that had asked for
|
|
1526
|
+
* `100vh`. The cascade cannot arbitrate this; the axis check has to.
|
|
1310
1527
|
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
* panel dragged while floating would keep that width after switching to
|
|
1314
|
-
* fullscreen.
|
|
1528
|
+
* So the rule is explicit: a placement owns the axes it fixes, and a
|
|
1529
|
+
* persisted size is only ever applied to the ones it does not.
|
|
1315
1530
|
*/
|
|
1316
1531
|
#applySize(size: ResizeSize): void {
|
|
1532
|
+
const axis = this.#resizeAxis();
|
|
1533
|
+
if (axis === "none") {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1317
1536
|
if (size.width !== undefined) {
|
|
1318
1537
|
this.style.setProperty("--ag-ui-width", `${size.width}px`);
|
|
1319
1538
|
}
|
|
1320
|
-
if (size.height !== undefined) {
|
|
1539
|
+
if (size.height !== undefined && axis === "both") {
|
|
1321
1540
|
this.style.setProperty("--ag-ui-height", `${size.height}px`);
|
|
1322
1541
|
}
|
|
1323
1542
|
}
|
|
1324
1543
|
|
|
1544
|
+
/**
|
|
1545
|
+
* Drop any dragged size the new placement has taken ownership of.
|
|
1546
|
+
*
|
|
1547
|
+
* Without this a size survives the switch as an inline property and silently
|
|
1548
|
+
* overrides the placement it moved to — the panel keeps a floating height
|
|
1549
|
+
* while docked, and reads as a component that cannot do full height.
|
|
1550
|
+
*/
|
|
1551
|
+
#releaseOwnedAxes(): void {
|
|
1552
|
+
const axis = this.#resizeAxis();
|
|
1553
|
+
if (axis !== "both") {
|
|
1554
|
+
this.style.removeProperty("--ag-ui-height");
|
|
1555
|
+
}
|
|
1556
|
+
if (axis === "none") {
|
|
1557
|
+
this.style.removeProperty("--ag-ui-width");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1325
1561
|
/** Persist a dragged size per tab, alongside the collapsed/theme preferences. */
|
|
1326
1562
|
#persistSize(size: ResizeSize): void {
|
|
1327
1563
|
const stored = { ...this.#readSize(), ...size };
|
|
@@ -1368,6 +1604,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
1368
1604
|
this.#themeToggle.textContent = dark ? "☀️" : "🌙";
|
|
1369
1605
|
}
|
|
1370
1606
|
|
|
1607
|
+
/**
|
|
1608
|
+
* Open the thread-history drawer: the imperative route to the control that
|
|
1609
|
+
* renders as `::part(history-button)`.
|
|
1610
|
+
*
|
|
1611
|
+
* A host that hides `::part(header)` to render its own title bar hides the
|
|
1612
|
+
* history, new-chat and collapse buttons with it — and thread switching then
|
|
1613
|
+
* has no route at all, because those controls live inside the header. Each of
|
|
1614
|
+
* them has a method, so a host chrome can rebuild the set: this one,
|
|
1615
|
+
* {@link openCheckpoints}, {@link newChat}, {@link toggleCollapsed} and
|
|
1616
|
+
* {@link toggleTheme}.
|
|
1617
|
+
*/
|
|
1618
|
+
openThreads(): void {
|
|
1619
|
+
void this.#refreshDrawer();
|
|
1620
|
+
this.#drawer.open();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Open the checkpoints panel (the `::part(checkpoints-button)` route).
|
|
1625
|
+
*
|
|
1626
|
+
* It lists the runs the `data-runs-url` server reports as continuable;
|
|
1627
|
+
* without that attribute the built-in button is never rendered and this opens
|
|
1628
|
+
* an empty panel.
|
|
1629
|
+
*/
|
|
1630
|
+
openCheckpoints(): void {
|
|
1631
|
+
void this.#refreshCheckpoints();
|
|
1632
|
+
this.#checkpoints.open();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1371
1635
|
/**
|
|
1372
1636
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
1373
1637
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -1684,17 +1948,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1684
1948
|
controls.className = "header-controls";
|
|
1685
1949
|
controls.setAttribute("part", "header-controls");
|
|
1686
1950
|
|
|
1951
|
+
// Both controls delegate to the public methods, so a host chrome driving
|
|
1952
|
+
// them imperatively takes exactly the path the built-in button takes.
|
|
1687
1953
|
const history = this.#headerButton("history", this.#strings.chatHistory, "☰");
|
|
1688
|
-
history.addEventListener("click", () =>
|
|
1689
|
-
void this.#refreshDrawer();
|
|
1690
|
-
this.#drawer.open();
|
|
1691
|
-
});
|
|
1954
|
+
history.addEventListener("click", () => this.openThreads());
|
|
1692
1955
|
|
|
1693
1956
|
const checkpoints = this.#headerButton("checkpoints", this.#strings.checkpoints, "⭯");
|
|
1694
|
-
checkpoints.addEventListener("click", () =>
|
|
1695
|
-
void this.#refreshCheckpoints();
|
|
1696
|
-
this.#checkpoints.open();
|
|
1697
|
-
});
|
|
1957
|
+
checkpoints.addEventListener("click", () => this.openCheckpoints());
|
|
1698
1958
|
|
|
1699
1959
|
const newChat = this.#headerButton("new", this.#strings.newChat, "✚");
|
|
1700
1960
|
newChat.addEventListener("click", () => this.newChat());
|
|
@@ -2069,11 +2329,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2069
2329
|
if (this.#client === null) {
|
|
2070
2330
|
const agent = this.agentFactory({
|
|
2071
2331
|
endpoint: this.endpoint,
|
|
2072
|
-
headers: this
|
|
2332
|
+
headers: this.#requestHeaders(),
|
|
2073
2333
|
// Live getter: the client is built once and cached, but a rotated
|
|
2074
2334
|
// token must still reach every request — the factory's fetch wrapper
|
|
2075
2335
|
// re-reads this on each call.
|
|
2076
|
-
getHeaders: () => this
|
|
2336
|
+
getHeaders: () => this.#requestHeaders(),
|
|
2337
|
+
...this.#credentialsOption(),
|
|
2077
2338
|
threadId: this.#threadId,
|
|
2078
2339
|
initialMessages: this.#initialMessages,
|
|
2079
2340
|
initialState: this.#sharedState,
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { type AbstractAgent, HttpAgent } from "@ag-ui/client";
|
|
2
2
|
import type { Message } from "@ag-ui/core";
|
|
3
|
+
import { withCredentials } from "./utils.js";
|
|
3
4
|
|
|
4
5
|
/** Config for {@link createHttpAgent}. */
|
|
5
6
|
export interface HttpAgentOptions {
|
|
6
7
|
endpoint: string;
|
|
7
8
|
headers?: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Cookie policy for the run request, as `fetch`'s own `credentials` mode.
|
|
11
|
+
* Unset leaves the browser default (`same-origin`) alone — which sends **no**
|
|
12
|
+
* cookies when the agent endpoint is on a different origin (or a different
|
|
13
|
+
* subdomain) from the page. A cookie-authenticated cross-origin deployment
|
|
14
|
+
* needs `"include"` here, and a server that answers it with
|
|
15
|
+
* `Access-Control-Allow-Credentials: true` and a concrete origin.
|
|
16
|
+
*/
|
|
17
|
+
credentials?: RequestCredentials;
|
|
8
18
|
/**
|
|
9
19
|
* Live header source, re-read on **every** request. `HttpAgent` bakes the
|
|
10
20
|
* static `headers` into its constructor and the element caches the agent
|
|
@@ -43,17 +53,18 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
|
|
|
43
53
|
// "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
|
|
44
54
|
// a free function with the correct receiver. The wrapper also overlays
|
|
45
55
|
// `getHeaders()` per request, so header rotation (CSRF, short-lived JWT)
|
|
46
|
-
// reaches the stream even though the agent instance is cached
|
|
56
|
+
// reaches the stream even though the agent instance is cached, and applies
|
|
57
|
+
// the configured cookie policy — the agent's own config has no seam for it.
|
|
47
58
|
fetch: (url, init) => {
|
|
48
59
|
const fresh = options.getHeaders?.();
|
|
49
60
|
if (fresh === undefined) {
|
|
50
|
-
return fetch(url, init);
|
|
61
|
+
return fetch(url, withCredentials(init, options.credentials));
|
|
51
62
|
}
|
|
52
63
|
const headers = new Headers(init?.headers);
|
|
53
64
|
for (const [name, value] of Object.entries(fresh)) {
|
|
54
65
|
headers.set(name, value);
|
|
55
66
|
}
|
|
56
|
-
return fetch(url, { ...init, headers });
|
|
67
|
+
return fetch(url, withCredentials({ ...init, headers }, options.credentials));
|
|
57
68
|
},
|
|
58
69
|
// Spread conditionally: under `exactOptionalPropertyTypes` an explicit
|
|
59
70
|
// `undefined` is not assignable to these optional config fields.
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
SessionStorageStore,
|
|
6
6
|
type ThreadMeta,
|
|
7
7
|
} from "./conversation_store.js";
|
|
8
|
+
import { withCredentials } from "./utils.js";
|
|
8
9
|
|
|
9
10
|
/** One row of the server thread index (django-ag-ui's `ThreadsView` wire shape). */
|
|
10
11
|
interface ServerThreadRow {
|
|
@@ -17,6 +18,14 @@ interface ServerThreadRow {
|
|
|
17
18
|
/** Live header source, read per request so rotated tokens / CSRF reach the server. */
|
|
18
19
|
type HeadersProvider = () => Record<string, string>;
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Live cookie policy, read per request. A provider rather than a value because
|
|
23
|
+
* the store is built once (on connect) and kept, while a host may configure the
|
|
24
|
+
* element after inserting it — a captured value would pin whatever was set
|
|
25
|
+
* during that first frame.
|
|
26
|
+
*/
|
|
27
|
+
type CredentialsProvider = () => RequestCredentials | undefined;
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* A {@link ClientConversationStore} backed by a server thread-index endpoint —
|
|
22
31
|
* django-ag-ui's owner-scoped `ThreadsView`, the URL passed to `<ag-ui-chat>`
|
|
@@ -37,6 +46,7 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
37
46
|
readonly #url: string;
|
|
38
47
|
readonly #headers: HeadersProvider;
|
|
39
48
|
readonly #local: ClientConversationStore;
|
|
49
|
+
readonly #credentials: CredentialsProvider;
|
|
40
50
|
readonly #dropped = new Set<string>();
|
|
41
51
|
readonly #renamed = new Map<string, string>();
|
|
42
52
|
|
|
@@ -44,10 +54,12 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
44
54
|
url: string,
|
|
45
55
|
headers: HeadersProvider = () => ({}),
|
|
46
56
|
local: ClientConversationStore = new SessionStorageStore(),
|
|
57
|
+
credentials: CredentialsProvider = () => undefined,
|
|
47
58
|
) {
|
|
48
59
|
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
49
60
|
this.#headers = headers;
|
|
50
61
|
this.#local = local;
|
|
62
|
+
this.#credentials = credentials;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
threadId(): string {
|
|
@@ -142,7 +154,7 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
142
154
|
/** GET that resolves to the `Response`, or `null` on a network error. */
|
|
143
155
|
async #get(url: string): Promise<Response | null> {
|
|
144
156
|
try {
|
|
145
|
-
return await fetch(url, { headers: this.#headers() });
|
|
157
|
+
return await fetch(url, withCredentials({ headers: this.#headers() }, this.#credentials()));
|
|
146
158
|
} catch {
|
|
147
159
|
return null;
|
|
148
160
|
}
|
|
@@ -156,11 +168,18 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
156
168
|
): Promise<void> {
|
|
157
169
|
const headers = this.#headers();
|
|
158
170
|
try {
|
|
159
|
-
await fetch(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
await fetch(
|
|
172
|
+
`${this.#url}${encodeURIComponent(threadId)}/`,
|
|
173
|
+
withCredentials(
|
|
174
|
+
{
|
|
175
|
+
method,
|
|
176
|
+
headers:
|
|
177
|
+
body === undefined ? headers : { ...headers, "content-type": "application/json" },
|
|
178
|
+
body: body === undefined ? null : JSON.stringify(body),
|
|
179
|
+
},
|
|
180
|
+
this.#credentials(),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
164
183
|
} catch {
|
|
165
184
|
// Best-effort; the optimistic overlay keeps the drawer consistent.
|
|
166
185
|
}
|
package/src/core/run_index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withCredentials } from "./utils.js";
|
|
2
|
+
|
|
1
3
|
/** One row of the server run index (django-ag-ui's `RunsView` wire shape). */
|
|
2
4
|
export interface RunRow {
|
|
3
5
|
readonly run_id: string;
|
|
@@ -11,6 +13,14 @@ export interface RunRow {
|
|
|
11
13
|
/** Live header source, read per request so rotated tokens / CSRF reach the server. */
|
|
12
14
|
type HeadersProvider = () => Record<string, string>;
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Live cookie policy, read per request. A provider rather than a value because
|
|
18
|
+
* the index is built once (on connect) and kept, while a host may configure the
|
|
19
|
+
* element after inserting it — a captured value would pin whatever was set
|
|
20
|
+
* during that first frame.
|
|
21
|
+
*/
|
|
22
|
+
type CredentialsProvider = () => RequestCredentials | undefined;
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Reads the server's run index and derives the resume / fork URLs beside it.
|
|
16
26
|
*
|
|
@@ -34,10 +44,16 @@ type HeadersProvider = () => Record<string, string>;
|
|
|
34
44
|
export class RunIndex {
|
|
35
45
|
readonly #url: string;
|
|
36
46
|
readonly #headers: HeadersProvider;
|
|
47
|
+
readonly #credentials: CredentialsProvider;
|
|
37
48
|
|
|
38
|
-
constructor(
|
|
49
|
+
constructor(
|
|
50
|
+
url: string,
|
|
51
|
+
headers: HeadersProvider = () => ({}),
|
|
52
|
+
credentials: CredentialsProvider = () => undefined,
|
|
53
|
+
) {
|
|
39
54
|
this.#url = url.endsWith("/") ? url : `${url}/`;
|
|
40
55
|
this.#headers = headers;
|
|
56
|
+
this.#credentials = credentials;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
/**
|
|
@@ -48,10 +64,16 @@ export class RunIndex {
|
|
|
48
64
|
*/
|
|
49
65
|
async list(): Promise<readonly RunRow[]> {
|
|
50
66
|
try {
|
|
51
|
-
const response = await fetch(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
const response = await fetch(
|
|
68
|
+
this.#url,
|
|
69
|
+
withCredentials(
|
|
70
|
+
{
|
|
71
|
+
method: "GET",
|
|
72
|
+
headers: { Accept: "application/json", ...this.#headers() },
|
|
73
|
+
},
|
|
74
|
+
this.#credentials(),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
55
77
|
if (!response.ok) {
|
|
56
78
|
return [];
|
|
57
79
|
}
|