@artooi/ag-ui-web-component 0.21.0 → 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 +81 -1
- package/README.md +127 -12
- package/dist/ag-ui-web-component.bundle.js +404 -99
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +29 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +11 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -106
- package/dist/index.js.map +2 -2
- 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 +197 -32
- package/src/index.ts +2 -0
- package/src/ui/styles.ts +383 -78
- 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";
|
|
@@ -105,6 +110,11 @@ export interface ToggleDetail {
|
|
|
105
110
|
readonly collapsed: boolean;
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
/** `detail` shape of the {@link UNREAD_EVENT} CustomEvent. */
|
|
114
|
+
export interface UnreadDetail {
|
|
115
|
+
readonly unread: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
/**
|
|
109
119
|
* Attributes read once while connecting, to decide what chrome exists at all.
|
|
110
120
|
*
|
|
@@ -404,8 +414,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
404
414
|
readonly #attachSlot: HTMLDivElement;
|
|
405
415
|
/** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
|
|
406
416
|
readonly #themeToggle: HTMLButtonElement;
|
|
407
|
-
/**
|
|
408
|
-
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;
|
|
409
423
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
410
424
|
readonly #emptyWrap: HTMLDivElement;
|
|
411
425
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
@@ -481,7 +495,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
481
495
|
this.#attachSlot = document.createElement("div");
|
|
482
496
|
this.#voiceSlot = document.createElement("span");
|
|
483
497
|
this.#themeToggle = document.createElement("button");
|
|
484
|
-
this.#
|
|
498
|
+
this.#launcher = document.createElement("button");
|
|
499
|
+
this.#badge = document.createElement("span");
|
|
485
500
|
this.#emptyWrap = document.createElement("div");
|
|
486
501
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
487
502
|
this.#drawer = new ThreadDrawer({
|
|
@@ -546,6 +561,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
546
561
|
return;
|
|
547
562
|
}
|
|
548
563
|
this.#input.value = "";
|
|
564
|
+
this.#autoGrow();
|
|
549
565
|
const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
|
|
550
566
|
const agent = this.agentFactory({
|
|
551
567
|
endpoint,
|
|
@@ -964,7 +980,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
964
980
|
if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
|
|
965
981
|
this.setAttribute("collapsed", "");
|
|
966
982
|
}
|
|
967
|
-
this.#
|
|
983
|
+
this.#syncLauncher();
|
|
968
984
|
this.#initSkills();
|
|
969
985
|
// Namespace the built-in default store too (a host-injected store is used
|
|
970
986
|
// verbatim). Must precede #wireThreadStore, which wraps the current store.
|
|
@@ -1370,12 +1386,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1370
1386
|
.replace("{fields}", missing.join(", "));
|
|
1371
1387
|
this.#skillHint.hidden = false;
|
|
1372
1388
|
this.#input.value = text;
|
|
1389
|
+
this.#autoGrow();
|
|
1373
1390
|
this.#input.focus();
|
|
1374
1391
|
this.#selectFirstPlaceholder(text);
|
|
1375
1392
|
return;
|
|
1376
1393
|
}
|
|
1377
1394
|
this.#skillHint.hidden = true;
|
|
1378
1395
|
this.#input.value = text;
|
|
1396
|
+
this.#autoGrow();
|
|
1379
1397
|
if (skill.sendImmediately === false) {
|
|
1380
1398
|
this.#input.focus();
|
|
1381
1399
|
return;
|
|
@@ -1420,7 +1438,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1420
1438
|
this.removeAttribute("collapsed");
|
|
1421
1439
|
}
|
|
1422
1440
|
sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
1423
|
-
|
|
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);
|
|
1424
1444
|
this.dispatchEvent(
|
|
1425
1445
|
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
1426
1446
|
detail: { collapsed },
|
|
@@ -1430,6 +1450,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1430
1450
|
);
|
|
1431
1451
|
}
|
|
1432
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
|
+
|
|
1433
1463
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
1434
1464
|
toggleCollapsed(): void {
|
|
1435
1465
|
this.setCollapsed(!this.collapsed);
|
|
@@ -1644,6 +1674,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1644
1674
|
this.#resetState();
|
|
1645
1675
|
this.#threadId = this.conversationStore.threadId();
|
|
1646
1676
|
this.#setRunning(false);
|
|
1677
|
+
this.#setUnread(0);
|
|
1647
1678
|
}
|
|
1648
1679
|
|
|
1649
1680
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
@@ -2007,18 +2038,35 @@ export class AgUiChat extends HTMLElement {
|
|
|
2007
2038
|
inputRow.className = "input-row";
|
|
2008
2039
|
inputRow.setAttribute("part", "composer");
|
|
2009
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
|
+
|
|
2010
2051
|
this.#input.className = "input";
|
|
2011
2052
|
this.#input.setAttribute("part", "input");
|
|
2012
2053
|
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
2013
|
-
this.#input.rows =
|
|
2054
|
+
this.#input.rows = 1;
|
|
2014
2055
|
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
2015
2056
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
2016
2057
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
2017
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.
|
|
2018
2062
|
this.#send.className = "send";
|
|
2019
2063
|
this.#send.type = "button";
|
|
2020
2064
|
this.#send.setAttribute("part", "send");
|
|
2021
|
-
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;
|
|
2022
2070
|
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
2023
2071
|
this.#send.dataset["state"] = "idle";
|
|
2024
2072
|
this.#send.addEventListener("click", () => {
|
|
@@ -2035,13 +2083,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2035
2083
|
this.#skillHint.setAttribute("part", "skill-hint");
|
|
2036
2084
|
this.#skillHint.hidden = true;
|
|
2037
2085
|
|
|
2038
|
-
// File-upload affordance: a
|
|
2039
|
-
// is wired) opening a hidden multi-file input.
|
|
2040
|
-
// 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).
|
|
2041
2089
|
this.#attachButton.className = "attach-btn";
|
|
2042
2090
|
this.#attachButton.type = "button";
|
|
2043
2091
|
this.#attachButton.setAttribute("part", "attach-button");
|
|
2044
|
-
this.#attachButton.
|
|
2092
|
+
this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
2045
2093
|
this.#attachButton.title = this.#strings.attachFiles;
|
|
2046
2094
|
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
2047
2095
|
this.#attachButton.hidden = true;
|
|
@@ -2062,7 +2110,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2062
2110
|
const footer = document.createElement("slot");
|
|
2063
2111
|
footer.name = "footer";
|
|
2064
2112
|
|
|
2065
|
-
|
|
2113
|
+
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
2114
|
+
composer.append(this.#input, tools);
|
|
2115
|
+
inputRow.append(composer, this.#fileInput);
|
|
2066
2116
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
2067
2117
|
// the missing-placeholder hint, and the pending-attachments tray.
|
|
2068
2118
|
this.#chat.append(
|
|
@@ -2078,15 +2128,24 @@ export class AgUiChat extends HTMLElement {
|
|
|
2078
2128
|
this.#checkpoints.element,
|
|
2079
2129
|
);
|
|
2080
2130
|
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
this.#
|
|
2085
|
-
this.#
|
|
2086
|
-
this.#
|
|
2087
|
-
this.#
|
|
2088
|
-
this.#
|
|
2089
|
-
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));
|
|
2090
2149
|
|
|
2091
2150
|
this.#chat.append(
|
|
2092
2151
|
createResizeHandle({
|
|
@@ -2104,7 +2163,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2104
2163
|
label: this.#strings.resizePanel,
|
|
2105
2164
|
}),
|
|
2106
2165
|
);
|
|
2107
|
-
this.#root.append(style, this.#chat, this.#
|
|
2166
|
+
this.#root.append(style, this.#chat, this.#launcher);
|
|
2108
2167
|
}
|
|
2109
2168
|
|
|
2110
2169
|
/**
|
|
@@ -2133,16 +2192,44 @@ export class AgUiChat extends HTMLElement {
|
|
|
2133
2192
|
}
|
|
2134
2193
|
|
|
2135
2194
|
/**
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
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.
|
|
2138
2221
|
*/
|
|
2139
|
-
#iconElement(
|
|
2222
|
+
#iconElement(
|
|
2223
|
+
slotName: string,
|
|
2224
|
+
part: string,
|
|
2225
|
+
fallbackGlyph: string | null,
|
|
2226
|
+
iconUrl: string | null = this.getAttribute("data-icon-url"),
|
|
2227
|
+
): HTMLSpanElement {
|
|
2140
2228
|
const holder = document.createElement("span");
|
|
2141
2229
|
holder.className = "icon-holder";
|
|
2142
2230
|
holder.setAttribute("part", part);
|
|
2143
2231
|
const slot = document.createElement("slot");
|
|
2144
2232
|
slot.name = slotName;
|
|
2145
|
-
const iconUrl = this.getAttribute("data-icon-url");
|
|
2146
2233
|
if (iconUrl !== null) {
|
|
2147
2234
|
const img = document.createElement("img");
|
|
2148
2235
|
img.className = "icon-img";
|
|
@@ -2150,15 +2237,71 @@ export class AgUiChat extends HTMLElement {
|
|
|
2150
2237
|
img.alt = "";
|
|
2151
2238
|
slot.append(img);
|
|
2152
2239
|
} else if (fallbackGlyph !== null) {
|
|
2153
|
-
slot.
|
|
2240
|
+
slot.innerHTML = fallbackGlyph;
|
|
2154
2241
|
}
|
|
2155
2242
|
holder.append(slot);
|
|
2156
2243
|
return holder;
|
|
2157
2244
|
}
|
|
2158
2245
|
|
|
2159
|
-
/**
|
|
2160
|
-
|
|
2161
|
-
|
|
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);
|
|
2162
2305
|
}
|
|
2163
2306
|
|
|
2164
2307
|
/** Hide the empty-state region once the message list holds anything else. */
|
|
@@ -2170,6 +2313,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2170
2313
|
#onInput(): void {
|
|
2171
2314
|
this.#skillsMenu.onInput(this.#input.value);
|
|
2172
2315
|
this.#skillHint.hidden = true;
|
|
2316
|
+
this.#autoGrow();
|
|
2173
2317
|
}
|
|
2174
2318
|
|
|
2175
2319
|
#onKeydown(event: KeyboardEvent): void {
|
|
@@ -2202,15 +2346,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
2202
2346
|
this.#client?.cancel();
|
|
2203
2347
|
}
|
|
2204
2348
|
|
|
2205
|
-
/**
|
|
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
|
+
*/
|
|
2206
2356
|
#setRunning(running: boolean): void {
|
|
2207
2357
|
this.#running = running;
|
|
2208
2358
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
2209
|
-
this.#send.
|
|
2359
|
+
this.#send.title = label;
|
|
2210
2360
|
this.#send.setAttribute("aria-label", label);
|
|
2211
2361
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
2212
2362
|
}
|
|
2213
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
|
+
|
|
2214
2377
|
async #submit(): Promise<void> {
|
|
2215
2378
|
// Ignore a submit while a run is in flight — the single choke point for
|
|
2216
2379
|
// both Enter and the Send button. The button already turns into Stop, but
|
|
@@ -2227,6 +2390,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2227
2390
|
return;
|
|
2228
2391
|
}
|
|
2229
2392
|
this.#input.value = "";
|
|
2393
|
+
this.#autoGrow();
|
|
2230
2394
|
// A file still uploading does not ride along — `readyRefs()` returns only
|
|
2231
2395
|
// settled ones, and `clearReady()` deliberately keeps the rest for a
|
|
2232
2396
|
// follow-up. Nothing said so, which is the whole defect: attachments are
|
|
@@ -2581,6 +2745,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2581
2745
|
}
|
|
2582
2746
|
attachCopyButtons(bubble, this.#strings);
|
|
2583
2747
|
this.#streamingBubble = null;
|
|
2748
|
+
this.#noteUnread();
|
|
2584
2749
|
},
|
|
2585
2750
|
onToolCall: (call) => {
|
|
2586
2751
|
this.#hidePending();
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
TOGGLE_EVENT,
|
|
13
13
|
TOOL_CALL_STATUS,
|
|
14
14
|
TOOL_DISPLAY,
|
|
15
|
+
UNREAD_EVENT,
|
|
15
16
|
X_CONFIRM_KEY,
|
|
16
17
|
X_DESTRUCTIVE_KEY,
|
|
17
18
|
X_NAVIGATES_KEY,
|
|
@@ -24,6 +25,7 @@ export {
|
|
|
24
25
|
type StateDetail,
|
|
25
26
|
type SubmitDetail,
|
|
26
27
|
type ToggleDetail,
|
|
28
|
+
type UnreadDetail,
|
|
27
29
|
} from "./core/ag_ui_chat.js";
|
|
28
30
|
export {
|
|
29
31
|
AgUiClient,
|