@artooi/ag-ui-web-component 0.20.1 → 0.22.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 +192 -1
- package/README.md +404 -28
- package/dist/ag-ui-web-component.bundle.js +781 -451
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +29 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +93 -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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1275 -520
- 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/dist/ui/ui_strings.d.ts +8 -1
- 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/constants.ts +35 -0
- package/src/core/ag_ui_chat.ts +452 -55
- 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 +4 -0
- package/src/ui/styles.ts +751 -421
- package/src/ui/ui_strings.ts +9 -1
- package/src/ui/voice_input.ts +7 -1
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
ATTACHMENT_EVENT,
|
|
4
4
|
COMPACTION_ACTIVITY_TYPE,
|
|
5
5
|
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
6
|
+
ICON_ATTACH,
|
|
7
|
+
ICON_LAUNCHER,
|
|
8
|
+
ICON_SEND,
|
|
9
|
+
ICON_STOP,
|
|
6
10
|
LOAD_CAPABILITY_TOOL,
|
|
7
11
|
MESSAGE_ROLE,
|
|
8
12
|
READ_PAGE_TOOL,
|
|
@@ -11,6 +15,7 @@ import {
|
|
|
11
15
|
TOGGLE_EVENT,
|
|
12
16
|
TOOL_CALL_STATUS,
|
|
13
17
|
TOOL_DISPLAY,
|
|
18
|
+
UNREAD_EVENT,
|
|
14
19
|
X_CONFIRM_KEY,
|
|
15
20
|
X_SUMMARY_KEY,
|
|
16
21
|
} from "../constants.js";
|
|
@@ -75,6 +80,7 @@ import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
|
75
80
|
import { RunIndex } from "./run_index.js";
|
|
76
81
|
import { type TranscribeHandler, transcribeAudio } from "./transcribe_audio.js";
|
|
77
82
|
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
83
|
+
import { withCredentials } from "./utils.js";
|
|
78
84
|
|
|
79
85
|
/** The role a rendered chat message takes. */
|
|
80
86
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -104,6 +110,11 @@ export interface ToggleDetail {
|
|
|
104
110
|
readonly collapsed: boolean;
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
/** `detail` shape of the {@link UNREAD_EVENT} CustomEvent. */
|
|
114
|
+
export interface UnreadDetail {
|
|
115
|
+
readonly unread: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
/**
|
|
108
119
|
* Attributes read once while connecting, to decide what chrome exists at all.
|
|
109
120
|
*
|
|
@@ -133,6 +144,18 @@ const CONNECT_TIME_ATTRIBUTES = [
|
|
|
133
144
|
"data-icon-url",
|
|
134
145
|
] as const;
|
|
135
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The cookie policies `fetch` accepts. Anything else is a configuration
|
|
149
|
+
* mistake, and one that would otherwise surface as an unexplained 401 from a
|
|
150
|
+
* request the browser silently sent anonymously.
|
|
151
|
+
*/
|
|
152
|
+
const CREDENTIALS_MODES: readonly string[] = ["omit", "same-origin", "include"];
|
|
153
|
+
|
|
154
|
+
/** Whether `value` is one of the three modes `fetch` understands. */
|
|
155
|
+
function isCredentialsMode(value: string): value is RequestCredentials {
|
|
156
|
+
return CREDENTIALS_MODES.includes(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
/** Per-tab persistence key for the collapsed state (survives MPA reloads). */
|
|
137
160
|
const COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
138
161
|
|
|
@@ -158,9 +181,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
158
181
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
159
182
|
agentFactory: AgentFactory = createHttpAgent;
|
|
160
183
|
|
|
161
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Static extra HTTP headers, sent with **every** request this element makes —
|
|
186
|
+
* the agent run, the thread index and its messages, the tool and skill
|
|
187
|
+
* catalogs, the run index, uploads and transcription.
|
|
188
|
+
*
|
|
189
|
+
* Right for values fixed for the element's lifetime. A credential that
|
|
190
|
+
* rotates (a short-lived JWT, a re-issued CSRF token) belongs in
|
|
191
|
+
* {@link getHeaders} instead: this is read at request time, but only a
|
|
192
|
+
* re-assignment updates it, so a token captured here is pinned until the host
|
|
193
|
+
* remembers to assign again.
|
|
194
|
+
*/
|
|
162
195
|
headers: Record<string, string> = {};
|
|
163
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Live header source, consulted immediately before every request — the way to
|
|
199
|
+
* supply rotating credentials.
|
|
200
|
+
*
|
|
201
|
+
* Set it to a function and each request calls it afresh: a token refreshed by
|
|
202
|
+
* the host between two requests reaches the second one, with nothing to
|
|
203
|
+
* re-assign and nothing to keep in sync.
|
|
204
|
+
*
|
|
205
|
+
* Composes with {@link headers} rather than replacing it: the two are merged
|
|
206
|
+
* per key with `getHeaders()` winning, so a static `X-Client` and a rotating
|
|
207
|
+
* `Authorization` can be configured independently and neither silently drops
|
|
208
|
+
* the other.
|
|
209
|
+
*/
|
|
210
|
+
getHeaders: (() => Record<string, string>) | null = null;
|
|
211
|
+
|
|
164
212
|
/**
|
|
165
213
|
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
166
214
|
* model-controlled image URL is fetched with no user interaction, which
|
|
@@ -366,8 +414,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
366
414
|
readonly #attachSlot: HTMLDivElement;
|
|
367
415
|
/** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
|
|
368
416
|
readonly #themeToggle: HTMLButtonElement;
|
|
369
|
-
/**
|
|
370
|
-
readonly #
|
|
417
|
+
/** What the collapsed widget shrinks to: the floating launcher, or the sidebar rail. */
|
|
418
|
+
readonly #launcher: HTMLButtonElement;
|
|
419
|
+
/** The launcher's unread badge; hidden at zero, and when the host opts out. */
|
|
420
|
+
readonly #badge: HTMLSpanElement;
|
|
421
|
+
// Answers that finished while the widget was collapsed. Expanding clears it.
|
|
422
|
+
#unread = 0;
|
|
371
423
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
372
424
|
readonly #emptyWrap: HTMLDivElement;
|
|
373
425
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
@@ -443,7 +495,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
443
495
|
this.#attachSlot = document.createElement("div");
|
|
444
496
|
this.#voiceSlot = document.createElement("span");
|
|
445
497
|
this.#themeToggle = document.createElement("button");
|
|
446
|
-
this.#
|
|
498
|
+
this.#launcher = document.createElement("button");
|
|
499
|
+
this.#badge = document.createElement("span");
|
|
447
500
|
this.#emptyWrap = document.createElement("div");
|
|
448
501
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
449
502
|
this.#drawer = new ThreadDrawer({
|
|
@@ -474,7 +527,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
474
527
|
return null;
|
|
475
528
|
}
|
|
476
529
|
if (this.#runIndex === null) {
|
|
477
|
-
this.#runIndex = new RunIndex(
|
|
530
|
+
this.#runIndex = new RunIndex(
|
|
531
|
+
url,
|
|
532
|
+
() => this.#requestHeaders(),
|
|
533
|
+
() => this.#requestCredentials(),
|
|
534
|
+
);
|
|
478
535
|
}
|
|
479
536
|
return this.#runIndex;
|
|
480
537
|
}
|
|
@@ -504,11 +561,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
504
561
|
return;
|
|
505
562
|
}
|
|
506
563
|
this.#input.value = "";
|
|
564
|
+
this.#autoGrow();
|
|
507
565
|
const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
|
|
508
566
|
const agent = this.agentFactory({
|
|
509
567
|
endpoint,
|
|
510
|
-
headers: this
|
|
511
|
-
getHeaders: () => this
|
|
568
|
+
headers: this.#requestHeaders(),
|
|
569
|
+
getHeaders: () => this.#requestHeaders(),
|
|
570
|
+
...this.#credentialsOption(),
|
|
512
571
|
threadId: this.#threadId,
|
|
513
572
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
514
573
|
initialMessages: [],
|
|
@@ -533,10 +592,24 @@ export class AgUiChat extends HTMLElement {
|
|
|
533
592
|
|
|
534
593
|
/** Attributes the element reacts to after it has been connected. */
|
|
535
594
|
static get observedAttributes(): string[] {
|
|
536
|
-
return ["title-text", "placement", ...CONNECT_TIME_ATTRIBUTES];
|
|
595
|
+
return ["title-text", "placement", "credentials", ...CONNECT_TIME_ATTRIBUTES];
|
|
537
596
|
}
|
|
538
597
|
|
|
539
598
|
attributeChangedCallback(name: string, previous: string | null, value: string | null): void {
|
|
599
|
+
if (name === "credentials") {
|
|
600
|
+
// Reported the moment the attribute is written — before connect, and
|
|
601
|
+
// whether it came from markup or the property setter. An unrecognised
|
|
602
|
+
// mode is otherwise inert, and the request it was meant to authorise
|
|
603
|
+
// goes out anonymously with nothing to show for it.
|
|
604
|
+
if (value !== null && !isCredentialsMode(value)) {
|
|
605
|
+
console.error(
|
|
606
|
+
`<ag-ui-chat>: credentials="${value}" is not a fetch credentials mode ` +
|
|
607
|
+
`(${CREDENTIALS_MODES.join(" / ")}) — it is being ignored, so requests use ` +
|
|
608
|
+
"the browser default and cross-origin cookies will not be sent.",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
540
613
|
if (name === "placement") {
|
|
541
614
|
// A placement owns the axes it fixes, so hand those back before anything
|
|
542
615
|
// else: a size dragged under the previous placement would otherwise sit
|
|
@@ -777,6 +850,82 @@ export class AgUiChat extends HTMLElement {
|
|
|
777
850
|
this.setAttribute("endpoint", value);
|
|
778
851
|
}
|
|
779
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Cookie policy for **every** request this element makes, as `fetch`'s own
|
|
855
|
+
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
856
|
+
* the `credentials` attribute, so markup embeds can set it without script.
|
|
857
|
+
*
|
|
858
|
+
* `null` (the default) leaves the browser's default of `same-origin` in
|
|
859
|
+
* place. That default sends **no cookies at all** when the endpoints live on
|
|
860
|
+
* a different origin from the page — app.example.com calling
|
|
861
|
+
* api.example.com is cross-origin — and the request goes out anonymously
|
|
862
|
+
* rather than failing, so the symptom is a 401 from a server that looks
|
|
863
|
+
* correctly configured. A cookie-authenticated cross-origin deployment wants
|
|
864
|
+
* `"include"`, plus `Access-Control-Allow-Credentials: true` and a concrete
|
|
865
|
+
* (non-wildcard) `Access-Control-Allow-Origin` on the server.
|
|
866
|
+
*
|
|
867
|
+
* Read per request, so a late assignment applies to everything after it.
|
|
868
|
+
* `"omit"` cannot be honoured by the built-in **upload** transport, which is
|
|
869
|
+
* an `XMLHttpRequest` and only has a two-state cookie switch; every other
|
|
870
|
+
* endpoint honours all three modes.
|
|
871
|
+
*/
|
|
872
|
+
get credentials(): RequestCredentials | null {
|
|
873
|
+
const attr = this.getAttribute("credentials");
|
|
874
|
+
return attr !== null && isCredentialsMode(attr) ? attr : null;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
set credentials(value: RequestCredentials | null) {
|
|
878
|
+
if (value === null) {
|
|
879
|
+
this.removeAttribute("credentials");
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
// Thrown, not warned: an unrecognised mode is inert at request time, and
|
|
883
|
+
// the whole failure this option exists to fix is a request that goes out
|
|
884
|
+
// wrong without saying so. Fail where the mistake was made instead.
|
|
885
|
+
if (!isCredentialsMode(value)) {
|
|
886
|
+
throw new TypeError(
|
|
887
|
+
`<ag-ui-chat>: credentials must be one of ${CREDENTIALS_MODES.map((mode) => `"${mode}"`).join(", ")} ` +
|
|
888
|
+
`(got ${JSON.stringify(value)}).`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
this.setAttribute("credentials", value);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* The headers for the request about to go out: the static {@link headers}
|
|
896
|
+
* with {@link getHeaders}'s live values overlaid, per key.
|
|
897
|
+
*
|
|
898
|
+
* Every request site goes through here, so "how this element authenticates"
|
|
899
|
+
* is one answer rather than one per endpoint.
|
|
900
|
+
*/
|
|
901
|
+
#requestHeaders(): Record<string, string> {
|
|
902
|
+
return { ...this.headers, ...this.getHeaders?.() };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** The configured cookie policy as `fetch` spells it; `undefined` when unset. */
|
|
906
|
+
#requestCredentials(): RequestCredentials | undefined {
|
|
907
|
+
return this.credentials ?? undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The `credentials` entry for an {@link AgentFactory} call, or nothing at all.
|
|
912
|
+
*
|
|
913
|
+
* Spread rather than assigned: `exactOptionalPropertyTypes` rejects an
|
|
914
|
+
* explicit `credentials: undefined`, and a factory should see the field
|
|
915
|
+
* absent — not present-and-empty — when no policy is configured. The agent
|
|
916
|
+
* reads it when it is built (first send, thread switch, continuation), by
|
|
917
|
+
* which time any host configuration has landed.
|
|
918
|
+
*/
|
|
919
|
+
#credentialsOption(): { credentials?: RequestCredentials } {
|
|
920
|
+
const credentials = this.#requestCredentials();
|
|
921
|
+
return credentials === undefined ? {} : { credentials };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** The `fetch` init for the element's own plain GETs (catalogs). */
|
|
925
|
+
#fetchInit(): RequestInit | undefined {
|
|
926
|
+
return withCredentials({ headers: this.#requestHeaders() }, this.#requestCredentials());
|
|
927
|
+
}
|
|
928
|
+
|
|
780
929
|
/**
|
|
781
930
|
* How much detail tool-call cards show, from the `data-tool-display`
|
|
782
931
|
* attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
|
|
@@ -831,9 +980,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
831
980
|
if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
|
|
832
981
|
this.setAttribute("collapsed", "");
|
|
833
982
|
}
|
|
834
|
-
this.#
|
|
983
|
+
this.#syncLauncher();
|
|
835
984
|
this.#initSkills();
|
|
836
|
-
void this.#fetchToolCatalog();
|
|
837
985
|
// Namespace the built-in default store too (a host-injected store is used
|
|
838
986
|
// verbatim). Must precede #wireThreadStore, which wraps the current store.
|
|
839
987
|
if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
|
|
@@ -843,12 +991,72 @@ export class AgUiChat extends HTMLElement {
|
|
|
843
991
|
this.#wireAttachments();
|
|
844
992
|
this.#wireVoice();
|
|
845
993
|
this.#threadId = this.conversationStore.threadId();
|
|
994
|
+
// The catalog requests go out a microtask later, so a host configuring
|
|
995
|
+
// through a framework ref still has a chance to be heard — see #startup.
|
|
996
|
+
queueMicrotask(() => this.#startup());
|
|
846
997
|
void this.#rehydrate();
|
|
847
998
|
// Last: everything above reads (and some of it sets) attributes, and none
|
|
848
999
|
// of that should trip the connect-time warning.
|
|
849
1000
|
this.#connected = true;
|
|
850
1001
|
}
|
|
851
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* The catalog requests the element issues on startup: the tool labels
|
|
1005
|
+
* (`data-tools-url`) and the backend skills (`data-skills-url`).
|
|
1006
|
+
*
|
|
1007
|
+
* Deliberately one microtask behind `connectedCallback`. A host that
|
|
1008
|
+
* configures the element through a framework ref necessarily does so *after*
|
|
1009
|
+
* inserting the node — React attaches refs and runs layout effects in the
|
|
1010
|
+
* same commit as the insertion, but strictly afterwards — so a request issued
|
|
1011
|
+
* from `connectedCallback` itself goes out before `headers`,
|
|
1012
|
+
* {@link getHeaders} or {@link credentials} exist, and comes back 401 in a
|
|
1013
|
+
* way that reads as a server fault rather than a mis-timed assignment. A
|
|
1014
|
+
* microtask lands after that commit and still before paint.
|
|
1015
|
+
*
|
|
1016
|
+
* Two things it is **not**. It is not a fix for configuration that arrives
|
|
1017
|
+
* later than the commit (a passive `useEffect`, an awaited token fetch):
|
|
1018
|
+
* configure before insertion (`createElement` → configure → `append`) or call
|
|
1019
|
+
* {@link reload} once configured, because a longer timer would hide that race
|
|
1020
|
+
* rather than close it. And it deliberately excludes the *history* replay,
|
|
1021
|
+
* which stays in `connectedCallback`: the replay renders into the transcript,
|
|
1022
|
+
* so deferring it lets a `sendMessage()` issued in the same task land first
|
|
1023
|
+
* and the replay then duplicate it. The thread history is therefore the one
|
|
1024
|
+
* request that can still go out before a ref is attached — {@link reload}
|
|
1025
|
+
* covers it.
|
|
1026
|
+
*/
|
|
1027
|
+
#startup(): void {
|
|
1028
|
+
// An element can be inserted and removed inside one task (a discarded
|
|
1029
|
+
// render, a double-mount); nothing should go out for a node that has
|
|
1030
|
+
// already left the document.
|
|
1031
|
+
if (!this.#connected) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
void this.#fetchToolCatalog();
|
|
1035
|
+
void this.#fetchSkills();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Re-run everything the element loads on startup — the tool-label catalog,
|
|
1040
|
+
* the backend skill catalog and the thread's history — with the transport
|
|
1041
|
+
* configuration as it stands now.
|
|
1042
|
+
*
|
|
1043
|
+
* This is the answer for a host that can only configure the element after the
|
|
1044
|
+
* fact (a token fetched in a passive effect, an async auth handshake): the
|
|
1045
|
+
* startup requests already went out with whatever was set then, and this says
|
|
1046
|
+
* "try again, properly authenticated" without removing and re-inserting the
|
|
1047
|
+
* node.
|
|
1048
|
+
*
|
|
1049
|
+
* A reload, not a merge — the in-flight run is cancelled and the transcript
|
|
1050
|
+
* is rebuilt from the persisted history, so anything streamed since is
|
|
1051
|
+
* dropped. Call it once, when configuration lands; not between turns.
|
|
1052
|
+
*/
|
|
1053
|
+
async reload(): Promise<void> {
|
|
1054
|
+
this.#cancelRun();
|
|
1055
|
+
this.#resetState();
|
|
1056
|
+
this.#setRunning(false);
|
|
1057
|
+
await Promise.all([this.#fetchToolCatalog(), this.#fetchSkills(), this.#rehydrate()]);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
852
1060
|
/**
|
|
853
1061
|
* Tear down live resources when the element leaves the DOM (a removed node, a
|
|
854
1062
|
* client-side route swap): cancel the in-flight run so its SSE stream closes,
|
|
@@ -940,7 +1148,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
940
1148
|
// Forward the tray's abort signal so removing a chip (or tearing the
|
|
941
1149
|
// element down) cancels the XHR.
|
|
942
1150
|
return (file, onProgress, signal) =>
|
|
943
|
-
uploadAttachment(file, {
|
|
1151
|
+
uploadAttachment(file, {
|
|
1152
|
+
url,
|
|
1153
|
+
headers: this.#requestHeaders(),
|
|
1154
|
+
...this.#credentialsOption(),
|
|
1155
|
+
onProgress,
|
|
1156
|
+
signal,
|
|
1157
|
+
});
|
|
944
1158
|
}
|
|
945
1159
|
|
|
946
1160
|
/**
|
|
@@ -969,7 +1183,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
969
1183
|
if (url === null) {
|
|
970
1184
|
return null;
|
|
971
1185
|
}
|
|
972
|
-
return (audio) =>
|
|
1186
|
+
return (audio) =>
|
|
1187
|
+
transcribeAudio(audio, {
|
|
1188
|
+
url,
|
|
1189
|
+
headers: this.#requestHeaders(),
|
|
1190
|
+
...this.#credentialsOption(),
|
|
1191
|
+
});
|
|
973
1192
|
}
|
|
974
1193
|
|
|
975
1194
|
/** Drop a voice transcript into the composer (appended to any typed text). */
|
|
@@ -1050,8 +1269,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1050
1269
|
if (url !== null) {
|
|
1051
1270
|
this.conversationStore = new RemoteConversationStore(
|
|
1052
1271
|
url,
|
|
1053
|
-
() => this
|
|
1272
|
+
() => this.#requestHeaders(),
|
|
1054
1273
|
this.conversationStore,
|
|
1274
|
+
() => this.#requestCredentials(),
|
|
1055
1275
|
);
|
|
1056
1276
|
}
|
|
1057
1277
|
}
|
|
@@ -1063,7 +1283,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1063
1283
|
return;
|
|
1064
1284
|
}
|
|
1065
1285
|
try {
|
|
1066
|
-
const response = await fetch(url,
|
|
1286
|
+
const response = await fetch(url, this.#fetchInit());
|
|
1067
1287
|
this.#toolCatalog = parseToolCatalog(await response.json());
|
|
1068
1288
|
} catch {
|
|
1069
1289
|
// Network/parse failure: cards fall back to toolSummaries / raw names.
|
|
@@ -1079,13 +1299,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1079
1299
|
this.#recomputeSkills();
|
|
1080
1300
|
}
|
|
1081
1301
|
|
|
1082
|
-
/**
|
|
1302
|
+
/**
|
|
1303
|
+
* Wire the skill surfaces: opt-in flags and the embedded catalog. The backend
|
|
1304
|
+
* catalog is fetched from `#startup`, a microtask later, so it carries the
|
|
1305
|
+
* host's transport configuration.
|
|
1306
|
+
*/
|
|
1083
1307
|
#initSkills(): void {
|
|
1084
1308
|
this.#skillsMenu.enableChips(this.#flag("data-prompt-chips"));
|
|
1085
1309
|
this.#skillsMenu.enableSlash(this.#flag("data-slash-commands"));
|
|
1086
1310
|
this.#embedSkills = this.#readEmbeddedSkills();
|
|
1087
1311
|
this.#recomputeSkills();
|
|
1088
|
-
void this.#fetchSkills();
|
|
1089
1312
|
}
|
|
1090
1313
|
|
|
1091
1314
|
/** Parse the inline `data-skills` JSON catalog (empty when absent/malformed). */
|
|
@@ -1108,7 +1331,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1108
1331
|
return;
|
|
1109
1332
|
}
|
|
1110
1333
|
try {
|
|
1111
|
-
const response = await fetch(url,
|
|
1334
|
+
const response = await fetch(url, this.#fetchInit());
|
|
1112
1335
|
this.#backendSkills = parseSkills(await response.json());
|
|
1113
1336
|
this.#recomputeSkills();
|
|
1114
1337
|
} catch {
|
|
@@ -1163,12 +1386,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1163
1386
|
.replace("{fields}", missing.join(", "));
|
|
1164
1387
|
this.#skillHint.hidden = false;
|
|
1165
1388
|
this.#input.value = text;
|
|
1389
|
+
this.#autoGrow();
|
|
1166
1390
|
this.#input.focus();
|
|
1167
1391
|
this.#selectFirstPlaceholder(text);
|
|
1168
1392
|
return;
|
|
1169
1393
|
}
|
|
1170
1394
|
this.#skillHint.hidden = true;
|
|
1171
1395
|
this.#input.value = text;
|
|
1396
|
+
this.#autoGrow();
|
|
1172
1397
|
if (skill.sendImmediately === false) {
|
|
1173
1398
|
this.#input.focus();
|
|
1174
1399
|
return;
|
|
@@ -1213,7 +1438,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1213
1438
|
this.removeAttribute("collapsed");
|
|
1214
1439
|
}
|
|
1215
1440
|
sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
1216
|
-
|
|
1441
|
+
// Expanding is what marks the waiting answers read; collapsing starts a
|
|
1442
|
+
// fresh count. Either way the badge is cleared and the host told.
|
|
1443
|
+
this.#setUnread(0);
|
|
1217
1444
|
this.dispatchEvent(
|
|
1218
1445
|
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
1219
1446
|
detail: { collapsed },
|
|
@@ -1223,6 +1450,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1223
1450
|
);
|
|
1224
1451
|
}
|
|
1225
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* Answers that finished while the widget was collapsed, and that the user has
|
|
1455
|
+
* therefore not seen. Expanding (or {@link newChat}) clears it. The launcher's
|
|
1456
|
+
* badge renders this; {@link UNREAD_EVENT} announces every change, so a host
|
|
1457
|
+
* chrome can render its own instead.
|
|
1458
|
+
*/
|
|
1459
|
+
get unread(): number {
|
|
1460
|
+
return this.#unread;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1226
1463
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
1227
1464
|
toggleCollapsed(): void {
|
|
1228
1465
|
this.setCollapsed(!this.collapsed);
|
|
@@ -1397,6 +1634,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
1397
1634
|
this.#themeToggle.textContent = dark ? "☀️" : "🌙";
|
|
1398
1635
|
}
|
|
1399
1636
|
|
|
1637
|
+
/**
|
|
1638
|
+
* Open the thread-history drawer: the imperative route to the control that
|
|
1639
|
+
* renders as `::part(history-button)`.
|
|
1640
|
+
*
|
|
1641
|
+
* A host that hides `::part(header)` to render its own title bar hides the
|
|
1642
|
+
* history, new-chat and collapse buttons with it — and thread switching then
|
|
1643
|
+
* has no route at all, because those controls live inside the header. Each of
|
|
1644
|
+
* them has a method, so a host chrome can rebuild the set: this one,
|
|
1645
|
+
* {@link openCheckpoints}, {@link newChat}, {@link toggleCollapsed} and
|
|
1646
|
+
* {@link toggleTheme}.
|
|
1647
|
+
*/
|
|
1648
|
+
openThreads(): void {
|
|
1649
|
+
void this.#refreshDrawer();
|
|
1650
|
+
this.#drawer.open();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Open the checkpoints panel (the `::part(checkpoints-button)` route).
|
|
1655
|
+
*
|
|
1656
|
+
* It lists the runs the `data-runs-url` server reports as continuable;
|
|
1657
|
+
* without that attribute the built-in button is never rendered and this opens
|
|
1658
|
+
* an empty panel.
|
|
1659
|
+
*/
|
|
1660
|
+
openCheckpoints(): void {
|
|
1661
|
+
void this.#refreshCheckpoints();
|
|
1662
|
+
this.#checkpoints.open();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1400
1665
|
/**
|
|
1401
1666
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
1402
1667
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -1409,6 +1674,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1409
1674
|
this.#resetState();
|
|
1410
1675
|
this.#threadId = this.conversationStore.threadId();
|
|
1411
1676
|
this.#setRunning(false);
|
|
1677
|
+
this.#setUnread(0);
|
|
1412
1678
|
}
|
|
1413
1679
|
|
|
1414
1680
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
@@ -1713,17 +1979,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1713
1979
|
controls.className = "header-controls";
|
|
1714
1980
|
controls.setAttribute("part", "header-controls");
|
|
1715
1981
|
|
|
1982
|
+
// Both controls delegate to the public methods, so a host chrome driving
|
|
1983
|
+
// them imperatively takes exactly the path the built-in button takes.
|
|
1716
1984
|
const history = this.#headerButton("history", this.#strings.chatHistory, "☰");
|
|
1717
|
-
history.addEventListener("click", () =>
|
|
1718
|
-
void this.#refreshDrawer();
|
|
1719
|
-
this.#drawer.open();
|
|
1720
|
-
});
|
|
1985
|
+
history.addEventListener("click", () => this.openThreads());
|
|
1721
1986
|
|
|
1722
1987
|
const checkpoints = this.#headerButton("checkpoints", this.#strings.checkpoints, "⭯");
|
|
1723
|
-
checkpoints.addEventListener("click", () =>
|
|
1724
|
-
void this.#refreshCheckpoints();
|
|
1725
|
-
this.#checkpoints.open();
|
|
1726
|
-
});
|
|
1988
|
+
checkpoints.addEventListener("click", () => this.openCheckpoints());
|
|
1727
1989
|
|
|
1728
1990
|
const newChat = this.#headerButton("new", this.#strings.newChat, "✚");
|
|
1729
1991
|
newChat.addEventListener("click", () => this.newChat());
|
|
@@ -1776,18 +2038,35 @@ export class AgUiChat extends HTMLElement {
|
|
|
1776
2038
|
inputRow.className = "input-row";
|
|
1777
2039
|
inputRow.setAttribute("part", "composer");
|
|
1778
2040
|
|
|
2041
|
+
// One bordered surface holds the field and the tool row under it, so the
|
|
2042
|
+
// icon buttons stop competing with the field for weight.
|
|
2043
|
+
const composer = document.createElement("div");
|
|
2044
|
+
composer.className = "composer";
|
|
2045
|
+
composer.setAttribute("part", "composer-surface");
|
|
2046
|
+
|
|
2047
|
+
const tools = document.createElement("div");
|
|
2048
|
+
tools.className = "composer-tools";
|
|
2049
|
+
tools.setAttribute("part", "composer-tools");
|
|
2050
|
+
|
|
1779
2051
|
this.#input.className = "input";
|
|
1780
2052
|
this.#input.setAttribute("part", "input");
|
|
1781
2053
|
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
1782
|
-
this.#input.rows =
|
|
2054
|
+
this.#input.rows = 1;
|
|
1783
2055
|
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
1784
2056
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
1785
2057
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
1786
2058
|
|
|
2059
|
+
// Icon-only, with both glyphs mounted at once and CSS showing the one the
|
|
2060
|
+
// state calls for — swapping a single glyph would leave a host that slotted
|
|
2061
|
+
// its own Send mark holding a stop icon mid-run.
|
|
1787
2062
|
this.#send.className = "send";
|
|
1788
2063
|
this.#send.type = "button";
|
|
1789
2064
|
this.#send.setAttribute("part", "send");
|
|
1790
|
-
this.#send.
|
|
2065
|
+
this.#send.append(
|
|
2066
|
+
this.#glyphSlot("icon-send", "send-send", ICON_SEND),
|
|
2067
|
+
this.#glyphSlot("icon-stop", "send-stop", ICON_STOP),
|
|
2068
|
+
);
|
|
2069
|
+
this.#send.title = this.#strings.send;
|
|
1791
2070
|
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
1792
2071
|
this.#send.dataset["state"] = "idle";
|
|
1793
2072
|
this.#send.addEventListener("click", () => {
|
|
@@ -1804,13 +2083,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1804
2083
|
this.#skillHint.setAttribute("part", "skill-hint");
|
|
1805
2084
|
this.#skillHint.hidden = true;
|
|
1806
2085
|
|
|
1807
|
-
// File-upload affordance: a
|
|
1808
|
-
// is wired) opening a hidden multi-file input.
|
|
1809
|
-
// whole shell (wired in #enableDragAndDrop).
|
|
2086
|
+
// File-upload affordance: a paperclip button (hidden until
|
|
2087
|
+
// `data-attachments-url` is wired) opening a hidden multi-file input.
|
|
2088
|
+
// Drag-and-drop covers the whole shell (wired in #enableDragAndDrop).
|
|
1810
2089
|
this.#attachButton.className = "attach-btn";
|
|
1811
2090
|
this.#attachButton.type = "button";
|
|
1812
2091
|
this.#attachButton.setAttribute("part", "attach-button");
|
|
1813
|
-
this.#attachButton.
|
|
2092
|
+
this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
1814
2093
|
this.#attachButton.title = this.#strings.attachFiles;
|
|
1815
2094
|
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
1816
2095
|
this.#attachButton.hidden = true;
|
|
@@ -1831,7 +2110,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1831
2110
|
const footer = document.createElement("slot");
|
|
1832
2111
|
footer.name = "footer";
|
|
1833
2112
|
|
|
1834
|
-
|
|
2113
|
+
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
2114
|
+
composer.append(this.#input, tools);
|
|
2115
|
+
inputRow.append(composer, this.#fileInput);
|
|
1835
2116
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
1836
2117
|
// the missing-placeholder hint, and the pending-attachments tray.
|
|
1837
2118
|
this.#chat.append(
|
|
@@ -1847,15 +2128,24 @@ export class AgUiChat extends HTMLElement {
|
|
|
1847
2128
|
this.#checkpoints.element,
|
|
1848
2129
|
);
|
|
1849
2130
|
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
1852
|
-
//
|
|
1853
|
-
this.#
|
|
1854
|
-
this.#
|
|
1855
|
-
this.#
|
|
1856
|
-
this.#
|
|
1857
|
-
this.#
|
|
1858
|
-
this.#
|
|
2131
|
+
// What a collapsed widget shrinks to: a round floating button, or the slim
|
|
2132
|
+
// edge rail under `placement="sidebar"` — one element, shaped by CSS.
|
|
2133
|
+
// A sibling of the panel, so it survives the panel being hidden.
|
|
2134
|
+
this.#launcher.className = "launcher";
|
|
2135
|
+
this.#launcher.type = "button";
|
|
2136
|
+
this.#launcher.setAttribute("part", "launcher");
|
|
2137
|
+
this.#launcher.setAttribute("aria-label", this.#strings.expand);
|
|
2138
|
+
this.#badge.className = "launcher-badge";
|
|
2139
|
+
this.#badge.setAttribute("part", "launcher-badge");
|
|
2140
|
+
// The count is announced through the launcher's own label, so the badge is
|
|
2141
|
+
// decoration to a screen reader rather than a second, context-free number.
|
|
2142
|
+
this.#badge.setAttribute("aria-hidden", "true");
|
|
2143
|
+
this.#badge.hidden = true;
|
|
2144
|
+
this.#launcher.append(
|
|
2145
|
+
this.#iconElement("launcher", "launcher-icon", ICON_LAUNCHER, this.#launcherIconUrl()),
|
|
2146
|
+
this.#badge,
|
|
2147
|
+
);
|
|
2148
|
+
this.#launcher.addEventListener("click", () => this.setCollapsed(false));
|
|
1859
2149
|
|
|
1860
2150
|
this.#chat.append(
|
|
1861
2151
|
createResizeHandle({
|
|
@@ -1873,7 +2163,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1873
2163
|
label: this.#strings.resizePanel,
|
|
1874
2164
|
}),
|
|
1875
2165
|
);
|
|
1876
|
-
this.#root.append(style, this.#chat, this.#
|
|
2166
|
+
this.#root.append(style, this.#chat, this.#launcher);
|
|
1877
2167
|
}
|
|
1878
2168
|
|
|
1879
2169
|
/**
|
|
@@ -1902,16 +2192,44 @@ export class AgUiChat extends HTMLElement {
|
|
|
1902
2192
|
}
|
|
1903
2193
|
|
|
1904
2194
|
/**
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
2195
|
+
* A `<slot>` a host can project its own mark into, falling back to one of the
|
|
2196
|
+
* built-in glyphs. The markup is an author-written constant, never user or
|
|
2197
|
+
* server data, so it is assigned directly rather than sanitised.
|
|
2198
|
+
*/
|
|
2199
|
+
#glyphSlot(slotName: string, className: string, markup: string): HTMLSlotElement {
|
|
2200
|
+
const slot = document.createElement("slot");
|
|
2201
|
+
slot.name = slotName;
|
|
2202
|
+
slot.className = className;
|
|
2203
|
+
slot.innerHTML = markup;
|
|
2204
|
+
return slot;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* The launcher's own image URL. `data-launcher-icon-url` lets the collapsed
|
|
2209
|
+
* button carry a different mark from the header's — a product logo reads at
|
|
2210
|
+
* 22px in a header bar but rarely at 26px in a circle — and falls back to the
|
|
2211
|
+
* header icon so a single `data-icon-url` still feeds both.
|
|
2212
|
+
*/
|
|
2213
|
+
#launcherIconUrl(): string | null {
|
|
2214
|
+
return this.getAttribute("data-launcher-icon-url") ?? this.getAttribute("data-icon-url");
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup;
|
|
2219
|
+
* with an `<img>` as the slot's fallback when an icon URL is configured, or
|
|
2220
|
+
* the given glyph markup when it is not.
|
|
1907
2221
|
*/
|
|
1908
|
-
#iconElement(
|
|
2222
|
+
#iconElement(
|
|
2223
|
+
slotName: string,
|
|
2224
|
+
part: string,
|
|
2225
|
+
fallbackGlyph: string | null,
|
|
2226
|
+
iconUrl: string | null = this.getAttribute("data-icon-url"),
|
|
2227
|
+
): HTMLSpanElement {
|
|
1909
2228
|
const holder = document.createElement("span");
|
|
1910
2229
|
holder.className = "icon-holder";
|
|
1911
2230
|
holder.setAttribute("part", part);
|
|
1912
2231
|
const slot = document.createElement("slot");
|
|
1913
2232
|
slot.name = slotName;
|
|
1914
|
-
const iconUrl = this.getAttribute("data-icon-url");
|
|
1915
2233
|
if (iconUrl !== null) {
|
|
1916
2234
|
const img = document.createElement("img");
|
|
1917
2235
|
img.className = "icon-img";
|
|
@@ -1919,15 +2237,71 @@ export class AgUiChat extends HTMLElement {
|
|
|
1919
2237
|
img.alt = "";
|
|
1920
2238
|
slot.append(img);
|
|
1921
2239
|
} else if (fallbackGlyph !== null) {
|
|
1922
|
-
slot.
|
|
2240
|
+
slot.innerHTML = fallbackGlyph;
|
|
1923
2241
|
}
|
|
1924
2242
|
holder.append(slot);
|
|
1925
2243
|
return holder;
|
|
1926
2244
|
}
|
|
1927
2245
|
|
|
1928
|
-
/**
|
|
1929
|
-
|
|
1930
|
-
|
|
2246
|
+
/**
|
|
2247
|
+
* Reflect the collapsed state and the unread count on the launcher.
|
|
2248
|
+
*
|
|
2249
|
+
* The count is also the launcher's accessible name: a badge that only exists
|
|
2250
|
+
* as a coloured dot says nothing to a screen reader, and "Expand" alone would
|
|
2251
|
+
* be a lie once answers are waiting behind it.
|
|
2252
|
+
*/
|
|
2253
|
+
#syncLauncher(): void {
|
|
2254
|
+
this.#launcher.setAttribute("aria-expanded", String(!this.collapsed));
|
|
2255
|
+
const unread = this.#unread;
|
|
2256
|
+
// Past 9 the exact number stops being information and starts being a
|
|
2257
|
+
// layout problem — the badge is a circle, not a field.
|
|
2258
|
+
this.#badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
2259
|
+
this.#badge.hidden = unread === 0 || !this.#badgeEnabled();
|
|
2260
|
+
const label = this.#badge.hidden
|
|
2261
|
+
? this.#strings.expand
|
|
2262
|
+
: this.#strings.expandUnread.replace("{count}", String(unread));
|
|
2263
|
+
this.#launcher.setAttribute("aria-label", label);
|
|
2264
|
+
this.#launcher.title = label;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* The unread badge, unlike every other affordance here, is on by default:
|
|
2269
|
+
* a collapsed widget is the one state where an answer can arrive with nothing
|
|
2270
|
+
* on screen to say so. `data-unread-badge="false"` turns it off for a host
|
|
2271
|
+
* that drives its own chrome from the `ag-ui-unread` event.
|
|
2272
|
+
*/
|
|
2273
|
+
#badgeEnabled(): boolean {
|
|
2274
|
+
return this.getAttribute("data-unread-badge") !== "false";
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Set the unread count, repaint the badge, and tell the host.
|
|
2279
|
+
*
|
|
2280
|
+
* The count is kept whether or not the badge renders it, so `unread` stays
|
|
2281
|
+
* truthful for a host chrome and switching the badge on mid-session doesn't
|
|
2282
|
+
* start from a number that was never counted.
|
|
2283
|
+
*/
|
|
2284
|
+
#setUnread(count: number): void {
|
|
2285
|
+
this.#unread = count;
|
|
2286
|
+
this.#syncLauncher();
|
|
2287
|
+
this.dispatchEvent(
|
|
2288
|
+
new CustomEvent<UnreadDetail>(UNREAD_EVENT, {
|
|
2289
|
+
detail: { unread: count },
|
|
2290
|
+
bubbles: true,
|
|
2291
|
+
composed: true,
|
|
2292
|
+
}),
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* Count an answer the user cannot have seen: one that finished while the
|
|
2298
|
+
* widget was collapsed. Expanding is what marks them read.
|
|
2299
|
+
*/
|
|
2300
|
+
#noteUnread(): void {
|
|
2301
|
+
if (!this.collapsed) {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
this.#setUnread(this.#unread + 1);
|
|
1931
2305
|
}
|
|
1932
2306
|
|
|
1933
2307
|
/** Hide the empty-state region once the message list holds anything else. */
|
|
@@ -1939,6 +2313,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1939
2313
|
#onInput(): void {
|
|
1940
2314
|
this.#skillsMenu.onInput(this.#input.value);
|
|
1941
2315
|
this.#skillHint.hidden = true;
|
|
2316
|
+
this.#autoGrow();
|
|
1942
2317
|
}
|
|
1943
2318
|
|
|
1944
2319
|
#onKeydown(event: KeyboardEvent): void {
|
|
@@ -1971,15 +2346,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
1971
2346
|
this.#client?.cancel();
|
|
1972
2347
|
}
|
|
1973
2348
|
|
|
1974
|
-
/**
|
|
2349
|
+
/**
|
|
2350
|
+
* Swap the composer button between Send (idle) and Stop (running).
|
|
2351
|
+
*
|
|
2352
|
+
* The glyph is swapped by CSS from `data-state` — both are mounted — so this
|
|
2353
|
+
* only has to move the accessible name, which is the button's whole label now
|
|
2354
|
+
* that it carries no text.
|
|
2355
|
+
*/
|
|
1975
2356
|
#setRunning(running: boolean): void {
|
|
1976
2357
|
this.#running = running;
|
|
1977
2358
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
1978
|
-
this.#send.
|
|
2359
|
+
this.#send.title = label;
|
|
1979
2360
|
this.#send.setAttribute("aria-label", label);
|
|
1980
2361
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
1981
2362
|
}
|
|
1982
2363
|
|
|
2364
|
+
/**
|
|
2365
|
+
* Size the field to its content: one row when empty, growing with what is
|
|
2366
|
+
* typed until the CSS ceiling takes over and it scrolls.
|
|
2367
|
+
*
|
|
2368
|
+
* Resetting to `auto` first is what makes it shrink again — `scrollHeight`
|
|
2369
|
+
* never reports less than the current height, so measuring without the reset
|
|
2370
|
+
* would ratchet the composer taller and never back down.
|
|
2371
|
+
*/
|
|
2372
|
+
#autoGrow(): void {
|
|
2373
|
+
this.#input.style.height = "auto";
|
|
2374
|
+
this.#input.style.height = `${this.#input.scrollHeight}px`;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
1983
2377
|
async #submit(): Promise<void> {
|
|
1984
2378
|
// Ignore a submit while a run is in flight — the single choke point for
|
|
1985
2379
|
// both Enter and the Send button. The button already turns into Stop, but
|
|
@@ -1996,6 +2390,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1996
2390
|
return;
|
|
1997
2391
|
}
|
|
1998
2392
|
this.#input.value = "";
|
|
2393
|
+
this.#autoGrow();
|
|
1999
2394
|
// A file still uploading does not ride along — `readyRefs()` returns only
|
|
2000
2395
|
// settled ones, and `clearReady()` deliberately keeps the rest for a
|
|
2001
2396
|
// follow-up. Nothing said so, which is the whole defect: attachments are
|
|
@@ -2098,11 +2493,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2098
2493
|
if (this.#client === null) {
|
|
2099
2494
|
const agent = this.agentFactory({
|
|
2100
2495
|
endpoint: this.endpoint,
|
|
2101
|
-
headers: this
|
|
2496
|
+
headers: this.#requestHeaders(),
|
|
2102
2497
|
// Live getter: the client is built once and cached, but a rotated
|
|
2103
2498
|
// token must still reach every request — the factory's fetch wrapper
|
|
2104
2499
|
// re-reads this on each call.
|
|
2105
|
-
getHeaders: () => this
|
|
2500
|
+
getHeaders: () => this.#requestHeaders(),
|
|
2501
|
+
...this.#credentialsOption(),
|
|
2106
2502
|
threadId: this.#threadId,
|
|
2107
2503
|
initialMessages: this.#initialMessages,
|
|
2108
2504
|
initialState: this.#sharedState,
|
|
@@ -2349,6 +2745,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2349
2745
|
}
|
|
2350
2746
|
attachCopyButtons(bubble, this.#strings);
|
|
2351
2747
|
this.#streamingBubble = null;
|
|
2748
|
+
this.#noteUnread();
|
|
2352
2749
|
},
|
|
2353
2750
|
onToolCall: (call) => {
|
|
2354
2751
|
this.#hidePending();
|