@artooi/ag-ui-web-component 0.21.0 → 0.23.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 +142 -1
- package/README.md +127 -12
- package/dist/ag-ui-web-component.bundle.js +459 -108
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +45 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +19 -4
- 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 +619 -141
- package/dist/index.js.map +2 -2
- package/dist/ui/attachment_chips.d.ts +12 -1
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- 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 +55 -0
- package/src/core/ag_ui_chat.ts +205 -62
- package/src/index.ts +2 -0
- package/src/ui/attachment_chips.ts +18 -6
- package/src/ui/attachment_tray.ts +2 -1
- package/src/ui/styles.ts +433 -80
- 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
|
*
|
|
@@ -267,14 +277,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
267
277
|
];
|
|
268
278
|
|
|
269
279
|
/**
|
|
270
|
-
* Per-run context provider. Defaults to the compact page map
|
|
271
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on
|
|
272
|
-
*
|
|
273
|
-
*
|
|
280
|
+
* Per-run context provider. Defaults to the compact page map, when a
|
|
281
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on.
|
|
282
|
+
*
|
|
283
|
+
* It used to append a one-line manifest of the message's attachments too.
|
|
284
|
+
* The server now derives that manifest from the refs riding the messages, so
|
|
285
|
+
* the client's copy only duplicated it, on exactly the turn a file was
|
|
286
|
+
* attached. Attachments still reach the agent; they reach it through the
|
|
287
|
+
* message, which is where they were already.
|
|
274
288
|
*/
|
|
275
289
|
getContext: () => Context[] = () => [
|
|
276
290
|
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
277
|
-
...this.#attachmentContext(),
|
|
278
291
|
];
|
|
279
292
|
|
|
280
293
|
/**
|
|
@@ -404,8 +417,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
404
417
|
readonly #attachSlot: HTMLDivElement;
|
|
405
418
|
/** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
|
|
406
419
|
readonly #themeToggle: HTMLButtonElement;
|
|
407
|
-
/**
|
|
408
|
-
readonly #
|
|
420
|
+
/** What the collapsed widget shrinks to: the floating launcher, or the sidebar rail. */
|
|
421
|
+
readonly #launcher: HTMLButtonElement;
|
|
422
|
+
/** The launcher's unread badge; hidden at zero, and when the host opts out. */
|
|
423
|
+
readonly #badge: HTMLSpanElement;
|
|
424
|
+
// Answers that finished while the widget was collapsed. Expanding clears it.
|
|
425
|
+
#unread = 0;
|
|
409
426
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
410
427
|
readonly #emptyWrap: HTMLDivElement;
|
|
411
428
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
@@ -417,9 +434,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
417
434
|
/** Whether the element is currently in the DOM; gates the connect-time warning. */
|
|
418
435
|
#connected = false;
|
|
419
436
|
|
|
420
|
-
/** Refs attached to the message currently being sent (the context manifest). */
|
|
421
|
-
#runAttachments: readonly AttachmentRef[] = [];
|
|
422
|
-
|
|
423
437
|
#client: AgUiClient | null = null;
|
|
424
438
|
// Seed for the next client. Once one exists it owns the live value (the
|
|
425
439
|
// agent applies STATE_SNAPSHOT / STATE_DELTA into it), so this is only the
|
|
@@ -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.
|
|
@@ -1226,22 +1242,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
1226
1242
|
});
|
|
1227
1243
|
}
|
|
1228
1244
|
|
|
1229
|
-
/** The one-line manifest of the message's attachments, for the run context. */
|
|
1230
|
-
#attachmentContext(): Context[] {
|
|
1231
|
-
if (this.#runAttachments.length === 0) {
|
|
1232
|
-
return [];
|
|
1233
|
-
}
|
|
1234
|
-
const lines = this.#runAttachments.map(
|
|
1235
|
-
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`,
|
|
1236
|
-
);
|
|
1237
|
-
return [
|
|
1238
|
-
{
|
|
1239
|
-
description: "Files the user attached to this message",
|
|
1240
|
-
value: `${lines.join("\n")}\nUse the read_attachment tool with an id to read a file's contents.`,
|
|
1241
|
-
},
|
|
1242
|
-
];
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
1245
|
/**
|
|
1246
1246
|
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
1247
1247
|
* delete through that server endpoint (wrapping the current store as the
|
|
@@ -1370,12 +1370,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1370
1370
|
.replace("{fields}", missing.join(", "));
|
|
1371
1371
|
this.#skillHint.hidden = false;
|
|
1372
1372
|
this.#input.value = text;
|
|
1373
|
+
this.#autoGrow();
|
|
1373
1374
|
this.#input.focus();
|
|
1374
1375
|
this.#selectFirstPlaceholder(text);
|
|
1375
1376
|
return;
|
|
1376
1377
|
}
|
|
1377
1378
|
this.#skillHint.hidden = true;
|
|
1378
1379
|
this.#input.value = text;
|
|
1380
|
+
this.#autoGrow();
|
|
1379
1381
|
if (skill.sendImmediately === false) {
|
|
1380
1382
|
this.#input.focus();
|
|
1381
1383
|
return;
|
|
@@ -1420,7 +1422,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1420
1422
|
this.removeAttribute("collapsed");
|
|
1421
1423
|
}
|
|
1422
1424
|
sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
1423
|
-
|
|
1425
|
+
// Expanding is what marks the waiting answers read; collapsing starts a
|
|
1426
|
+
// fresh count. Either way the badge is cleared and the host told.
|
|
1427
|
+
this.#setUnread(0);
|
|
1424
1428
|
this.dispatchEvent(
|
|
1425
1429
|
new CustomEvent<ToggleDetail>(TOGGLE_EVENT, {
|
|
1426
1430
|
detail: { collapsed },
|
|
@@ -1430,6 +1434,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1430
1434
|
);
|
|
1431
1435
|
}
|
|
1432
1436
|
|
|
1437
|
+
/**
|
|
1438
|
+
* Answers that finished while the widget was collapsed, and that the user has
|
|
1439
|
+
* therefore not seen. Expanding (or {@link newChat}) clears it. The launcher's
|
|
1440
|
+
* badge renders this; {@link UNREAD_EVENT} announces every change, so a host
|
|
1441
|
+
* chrome can render its own instead.
|
|
1442
|
+
*/
|
|
1443
|
+
get unread(): number {
|
|
1444
|
+
return this.#unread;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1433
1447
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
1434
1448
|
toggleCollapsed(): void {
|
|
1435
1449
|
this.setCollapsed(!this.collapsed);
|
|
@@ -1644,6 +1658,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1644
1658
|
this.#resetState();
|
|
1645
1659
|
this.#threadId = this.conversationStore.threadId();
|
|
1646
1660
|
this.#setRunning(false);
|
|
1661
|
+
this.#setUnread(0);
|
|
1647
1662
|
}
|
|
1648
1663
|
|
|
1649
1664
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
@@ -1656,7 +1671,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
1656
1671
|
this.#toolCards.clear();
|
|
1657
1672
|
this.#serverSettled.clear();
|
|
1658
1673
|
this.#initialMessages = [];
|
|
1659
|
-
this.#runAttachments = [];
|
|
1660
1674
|
this.#attachTray?.clear();
|
|
1661
1675
|
// Keep the empty-state region; everything else clears.
|
|
1662
1676
|
this.#messages.replaceChildren(this.#emptyWrap);
|
|
@@ -2007,18 +2021,35 @@ export class AgUiChat extends HTMLElement {
|
|
|
2007
2021
|
inputRow.className = "input-row";
|
|
2008
2022
|
inputRow.setAttribute("part", "composer");
|
|
2009
2023
|
|
|
2024
|
+
// One bordered surface holds the field and the tool row under it, so the
|
|
2025
|
+
// icon buttons stop competing with the field for weight.
|
|
2026
|
+
const composer = document.createElement("div");
|
|
2027
|
+
composer.className = "composer";
|
|
2028
|
+
composer.setAttribute("part", "composer-surface");
|
|
2029
|
+
|
|
2030
|
+
const tools = document.createElement("div");
|
|
2031
|
+
tools.className = "composer-tools";
|
|
2032
|
+
tools.setAttribute("part", "composer-tools");
|
|
2033
|
+
|
|
2010
2034
|
this.#input.className = "input";
|
|
2011
2035
|
this.#input.setAttribute("part", "input");
|
|
2012
2036
|
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
2013
|
-
this.#input.rows =
|
|
2037
|
+
this.#input.rows = 1;
|
|
2014
2038
|
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
2015
2039
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
2016
2040
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
2017
2041
|
|
|
2042
|
+
// Icon-only, with both glyphs mounted at once and CSS showing the one the
|
|
2043
|
+
// state calls for — swapping a single glyph would leave a host that slotted
|
|
2044
|
+
// its own Send mark holding a stop icon mid-run.
|
|
2018
2045
|
this.#send.className = "send";
|
|
2019
2046
|
this.#send.type = "button";
|
|
2020
2047
|
this.#send.setAttribute("part", "send");
|
|
2021
|
-
this.#send.
|
|
2048
|
+
this.#send.append(
|
|
2049
|
+
this.#glyphSlot("icon-send", "send-send", ICON_SEND),
|
|
2050
|
+
this.#glyphSlot("icon-stop", "send-stop", ICON_STOP),
|
|
2051
|
+
);
|
|
2052
|
+
this.#send.title = this.#strings.send;
|
|
2022
2053
|
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
2023
2054
|
this.#send.dataset["state"] = "idle";
|
|
2024
2055
|
this.#send.addEventListener("click", () => {
|
|
@@ -2035,13 +2066,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2035
2066
|
this.#skillHint.setAttribute("part", "skill-hint");
|
|
2036
2067
|
this.#skillHint.hidden = true;
|
|
2037
2068
|
|
|
2038
|
-
// File-upload affordance: a
|
|
2039
|
-
// is wired) opening a hidden multi-file input.
|
|
2040
|
-
// whole shell (wired in #enableDragAndDrop).
|
|
2069
|
+
// File-upload affordance: a paperclip button (hidden until
|
|
2070
|
+
// `data-attachments-url` is wired) opening a hidden multi-file input.
|
|
2071
|
+
// Drag-and-drop covers the whole shell (wired in #enableDragAndDrop).
|
|
2041
2072
|
this.#attachButton.className = "attach-btn";
|
|
2042
2073
|
this.#attachButton.type = "button";
|
|
2043
2074
|
this.#attachButton.setAttribute("part", "attach-button");
|
|
2044
|
-
this.#attachButton.
|
|
2075
|
+
this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
2045
2076
|
this.#attachButton.title = this.#strings.attachFiles;
|
|
2046
2077
|
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
2047
2078
|
this.#attachButton.hidden = true;
|
|
@@ -2062,7 +2093,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2062
2093
|
const footer = document.createElement("slot");
|
|
2063
2094
|
footer.name = "footer";
|
|
2064
2095
|
|
|
2065
|
-
|
|
2096
|
+
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
2097
|
+
composer.append(this.#input, tools);
|
|
2098
|
+
inputRow.append(composer, this.#fileInput);
|
|
2066
2099
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
2067
2100
|
// the missing-placeholder hint, and the pending-attachments tray.
|
|
2068
2101
|
this.#chat.append(
|
|
@@ -2078,15 +2111,24 @@ export class AgUiChat extends HTMLElement {
|
|
|
2078
2111
|
this.#checkpoints.element,
|
|
2079
2112
|
);
|
|
2080
2113
|
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
this.#
|
|
2085
|
-
this.#
|
|
2086
|
-
this.#
|
|
2087
|
-
this.#
|
|
2088
|
-
this.#
|
|
2089
|
-
this.#
|
|
2114
|
+
// What a collapsed widget shrinks to: a round floating button, or the slim
|
|
2115
|
+
// edge rail under `placement="sidebar"` — one element, shaped by CSS.
|
|
2116
|
+
// A sibling of the panel, so it survives the panel being hidden.
|
|
2117
|
+
this.#launcher.className = "launcher";
|
|
2118
|
+
this.#launcher.type = "button";
|
|
2119
|
+
this.#launcher.setAttribute("part", "launcher");
|
|
2120
|
+
this.#launcher.setAttribute("aria-label", this.#strings.expand);
|
|
2121
|
+
this.#badge.className = "launcher-badge";
|
|
2122
|
+
this.#badge.setAttribute("part", "launcher-badge");
|
|
2123
|
+
// The count is announced through the launcher's own label, so the badge is
|
|
2124
|
+
// decoration to a screen reader rather than a second, context-free number.
|
|
2125
|
+
this.#badge.setAttribute("aria-hidden", "true");
|
|
2126
|
+
this.#badge.hidden = true;
|
|
2127
|
+
this.#launcher.append(
|
|
2128
|
+
this.#iconElement("launcher", "launcher-icon", ICON_LAUNCHER, this.#launcherIconUrl()),
|
|
2129
|
+
this.#badge,
|
|
2130
|
+
);
|
|
2131
|
+
this.#launcher.addEventListener("click", () => this.setCollapsed(false));
|
|
2090
2132
|
|
|
2091
2133
|
this.#chat.append(
|
|
2092
2134
|
createResizeHandle({
|
|
@@ -2104,7 +2146,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2104
2146
|
label: this.#strings.resizePanel,
|
|
2105
2147
|
}),
|
|
2106
2148
|
);
|
|
2107
|
-
this.#root.append(style, this.#chat, this.#
|
|
2149
|
+
this.#root.append(style, this.#chat, this.#launcher);
|
|
2108
2150
|
}
|
|
2109
2151
|
|
|
2110
2152
|
/**
|
|
@@ -2133,16 +2175,44 @@ export class AgUiChat extends HTMLElement {
|
|
|
2133
2175
|
}
|
|
2134
2176
|
|
|
2135
2177
|
/**
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2178
|
+
* A `<slot>` a host can project its own mark into, falling back to one of the
|
|
2179
|
+
* built-in glyphs. The markup is an author-written constant, never user or
|
|
2180
|
+
* server data, so it is assigned directly rather than sanitised.
|
|
2181
|
+
*/
|
|
2182
|
+
#glyphSlot(slotName: string, className: string, markup: string): HTMLSlotElement {
|
|
2183
|
+
const slot = document.createElement("slot");
|
|
2184
|
+
slot.name = slotName;
|
|
2185
|
+
slot.className = className;
|
|
2186
|
+
slot.innerHTML = markup;
|
|
2187
|
+
return slot;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* The launcher's own image URL. `data-launcher-icon-url` lets the collapsed
|
|
2192
|
+
* button carry a different mark from the header's — a product logo reads at
|
|
2193
|
+
* 22px in a header bar but rarely at 26px in a circle — and falls back to the
|
|
2194
|
+
* header icon so a single `data-icon-url` still feeds both.
|
|
2195
|
+
*/
|
|
2196
|
+
#launcherIconUrl(): string | null {
|
|
2197
|
+
return this.getAttribute("data-launcher-icon-url") ?? this.getAttribute("data-icon-url");
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup;
|
|
2202
|
+
* with an `<img>` as the slot's fallback when an icon URL is configured, or
|
|
2203
|
+
* the given glyph markup when it is not.
|
|
2138
2204
|
*/
|
|
2139
|
-
#iconElement(
|
|
2205
|
+
#iconElement(
|
|
2206
|
+
slotName: string,
|
|
2207
|
+
part: string,
|
|
2208
|
+
fallbackGlyph: string | null,
|
|
2209
|
+
iconUrl: string | null = this.getAttribute("data-icon-url"),
|
|
2210
|
+
): HTMLSpanElement {
|
|
2140
2211
|
const holder = document.createElement("span");
|
|
2141
2212
|
holder.className = "icon-holder";
|
|
2142
2213
|
holder.setAttribute("part", part);
|
|
2143
2214
|
const slot = document.createElement("slot");
|
|
2144
2215
|
slot.name = slotName;
|
|
2145
|
-
const iconUrl = this.getAttribute("data-icon-url");
|
|
2146
2216
|
if (iconUrl !== null) {
|
|
2147
2217
|
const img = document.createElement("img");
|
|
2148
2218
|
img.className = "icon-img";
|
|
@@ -2150,15 +2220,71 @@ export class AgUiChat extends HTMLElement {
|
|
|
2150
2220
|
img.alt = "";
|
|
2151
2221
|
slot.append(img);
|
|
2152
2222
|
} else if (fallbackGlyph !== null) {
|
|
2153
|
-
slot.
|
|
2223
|
+
slot.innerHTML = fallbackGlyph;
|
|
2154
2224
|
}
|
|
2155
2225
|
holder.append(slot);
|
|
2156
2226
|
return holder;
|
|
2157
2227
|
}
|
|
2158
2228
|
|
|
2159
|
-
/**
|
|
2160
|
-
|
|
2161
|
-
|
|
2229
|
+
/**
|
|
2230
|
+
* Reflect the collapsed state and the unread count on the launcher.
|
|
2231
|
+
*
|
|
2232
|
+
* The count is also the launcher's accessible name: a badge that only exists
|
|
2233
|
+
* as a coloured dot says nothing to a screen reader, and "Expand" alone would
|
|
2234
|
+
* be a lie once answers are waiting behind it.
|
|
2235
|
+
*/
|
|
2236
|
+
#syncLauncher(): void {
|
|
2237
|
+
this.#launcher.setAttribute("aria-expanded", String(!this.collapsed));
|
|
2238
|
+
const unread = this.#unread;
|
|
2239
|
+
// Past 9 the exact number stops being information and starts being a
|
|
2240
|
+
// layout problem — the badge is a circle, not a field.
|
|
2241
|
+
this.#badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
2242
|
+
this.#badge.hidden = unread === 0 || !this.#badgeEnabled();
|
|
2243
|
+
const label = this.#badge.hidden
|
|
2244
|
+
? this.#strings.expand
|
|
2245
|
+
: this.#strings.expandUnread.replace("{count}", String(unread));
|
|
2246
|
+
this.#launcher.setAttribute("aria-label", label);
|
|
2247
|
+
this.#launcher.title = label;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
/**
|
|
2251
|
+
* The unread badge, unlike every other affordance here, is on by default:
|
|
2252
|
+
* a collapsed widget is the one state where an answer can arrive with nothing
|
|
2253
|
+
* on screen to say so. `data-unread-badge="false"` turns it off for a host
|
|
2254
|
+
* that drives its own chrome from the `ag-ui-unread` event.
|
|
2255
|
+
*/
|
|
2256
|
+
#badgeEnabled(): boolean {
|
|
2257
|
+
return this.getAttribute("data-unread-badge") !== "false";
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/**
|
|
2261
|
+
* Set the unread count, repaint the badge, and tell the host.
|
|
2262
|
+
*
|
|
2263
|
+
* The count is kept whether or not the badge renders it, so `unread` stays
|
|
2264
|
+
* truthful for a host chrome and switching the badge on mid-session doesn't
|
|
2265
|
+
* start from a number that was never counted.
|
|
2266
|
+
*/
|
|
2267
|
+
#setUnread(count: number): void {
|
|
2268
|
+
this.#unread = count;
|
|
2269
|
+
this.#syncLauncher();
|
|
2270
|
+
this.dispatchEvent(
|
|
2271
|
+
new CustomEvent<UnreadDetail>(UNREAD_EVENT, {
|
|
2272
|
+
detail: { unread: count },
|
|
2273
|
+
bubbles: true,
|
|
2274
|
+
composed: true,
|
|
2275
|
+
}),
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/**
|
|
2280
|
+
* Count an answer the user cannot have seen: one that finished while the
|
|
2281
|
+
* widget was collapsed. Expanding is what marks them read.
|
|
2282
|
+
*/
|
|
2283
|
+
#noteUnread(): void {
|
|
2284
|
+
if (!this.collapsed) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
this.#setUnread(this.#unread + 1);
|
|
2162
2288
|
}
|
|
2163
2289
|
|
|
2164
2290
|
/** Hide the empty-state region once the message list holds anything else. */
|
|
@@ -2170,6 +2296,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2170
2296
|
#onInput(): void {
|
|
2171
2297
|
this.#skillsMenu.onInput(this.#input.value);
|
|
2172
2298
|
this.#skillHint.hidden = true;
|
|
2299
|
+
this.#autoGrow();
|
|
2173
2300
|
}
|
|
2174
2301
|
|
|
2175
2302
|
#onKeydown(event: KeyboardEvent): void {
|
|
@@ -2202,15 +2329,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
2202
2329
|
this.#client?.cancel();
|
|
2203
2330
|
}
|
|
2204
2331
|
|
|
2205
|
-
/**
|
|
2332
|
+
/**
|
|
2333
|
+
* Swap the composer button between Send (idle) and Stop (running).
|
|
2334
|
+
*
|
|
2335
|
+
* The glyph is swapped by CSS from `data-state` — both are mounted — so this
|
|
2336
|
+
* only has to move the accessible name, which is the button's whole label now
|
|
2337
|
+
* that it carries no text.
|
|
2338
|
+
*/
|
|
2206
2339
|
#setRunning(running: boolean): void {
|
|
2207
2340
|
this.#running = running;
|
|
2208
2341
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
2209
|
-
this.#send.
|
|
2342
|
+
this.#send.title = label;
|
|
2210
2343
|
this.#send.setAttribute("aria-label", label);
|
|
2211
2344
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
2212
2345
|
}
|
|
2213
2346
|
|
|
2347
|
+
/**
|
|
2348
|
+
* Size the field to its content: one row when empty, growing with what is
|
|
2349
|
+
* typed until the CSS ceiling takes over and it scrolls.
|
|
2350
|
+
*
|
|
2351
|
+
* Resetting to `auto` first is what makes it shrink again — `scrollHeight`
|
|
2352
|
+
* never reports less than the current height, so measuring without the reset
|
|
2353
|
+
* would ratchet the composer taller and never back down.
|
|
2354
|
+
*/
|
|
2355
|
+
#autoGrow(): void {
|
|
2356
|
+
this.#input.style.height = "auto";
|
|
2357
|
+
this.#input.style.height = `${this.#input.scrollHeight}px`;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2214
2360
|
async #submit(): Promise<void> {
|
|
2215
2361
|
// Ignore a submit while a run is in flight — the single choke point for
|
|
2216
2362
|
// both Enter and the Send button. The button already turns into Stop, but
|
|
@@ -2227,6 +2373,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2227
2373
|
return;
|
|
2228
2374
|
}
|
|
2229
2375
|
this.#input.value = "";
|
|
2376
|
+
this.#autoGrow();
|
|
2230
2377
|
// A file still uploading does not ride along — `readyRefs()` returns only
|
|
2231
2378
|
// settled ones, and `clearReady()` deliberately keeps the rest for a
|
|
2232
2379
|
// follow-up. Nothing said so, which is the whole defect: attachments are
|
|
@@ -2275,8 +2422,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
2275
2422
|
if (attachments.length > 0) {
|
|
2276
2423
|
bubble.appendChild(renderAttachmentChips(attachments));
|
|
2277
2424
|
}
|
|
2278
|
-
// Surfaced to the run via the context manifest until the run settles.
|
|
2279
|
-
this.#runAttachments = attachments;
|
|
2280
2425
|
this.dispatchEvent(
|
|
2281
2426
|
new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
|
|
2282
2427
|
detail: { content, attachments },
|
|
@@ -2581,6 +2726,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2581
2726
|
}
|
|
2582
2727
|
attachCopyButtons(bubble, this.#strings);
|
|
2583
2728
|
this.#streamingBubble = null;
|
|
2729
|
+
this.#noteUnread();
|
|
2584
2730
|
},
|
|
2585
2731
|
onToolCall: (call) => {
|
|
2586
2732
|
this.#hidePending();
|
|
@@ -2639,9 +2785,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
2639
2785
|
this.#hidePending();
|
|
2640
2786
|
this.#setRunning(false);
|
|
2641
2787
|
this.#streamingBubble = null;
|
|
2642
|
-
// The attachment manifest was for this run only; the model has read what
|
|
2643
|
-
// it needed (results now live in history).
|
|
2644
|
-
this.#runAttachments = [];
|
|
2645
2788
|
// Belt-and-suspenders: a tool card still pending at settle (e.g. a
|
|
2646
2789
|
// server tool whose result never streamed because the connection
|
|
2647
2790
|
// dropped) would hang forever — settle it to the no-result fallback.
|
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,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ICON_FILE, ICON_FILE_IMAGE, ICON_FILE_PDF, ICON_FILE_TEXT } from "../constants.js";
|
|
1
2
|
import type { AttachmentRef } from "../core/attachment.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -24,7 +25,7 @@ function renderChip(ref: AttachmentRef): HTMLDivElement {
|
|
|
24
25
|
const icon = document.createElement("span");
|
|
25
26
|
icon.className = "attachment-chip-icon";
|
|
26
27
|
icon.setAttribute("part", "attachment-chip-icon");
|
|
27
|
-
icon.
|
|
28
|
+
icon.innerHTML = iconFor(ref.mime);
|
|
28
29
|
icon.setAttribute("aria-hidden", "true");
|
|
29
30
|
|
|
30
31
|
const name = document.createElement("span");
|
|
@@ -42,18 +43,29 @@ function renderChip(ref: AttachmentRef): HTMLDivElement {
|
|
|
42
43
|
return chip;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* A coarse type mark for a chip — image, PDF, text document, or generic file —
|
|
48
|
+
* as the inline SVG markup of one of the built-in glyphs.
|
|
49
|
+
*
|
|
50
|
+
* Returns markup rather than a character because chips sit beside the
|
|
51
|
+
* composer's SVG send, attach and mic buttons: emoji render at a different
|
|
52
|
+
* optical weight, vary by platform, and take neither the glyph size nor
|
|
53
|
+
* `currentColor`, so the two never matched.
|
|
54
|
+
*
|
|
55
|
+
* The MIME string only selects among author-written constants and is never
|
|
56
|
+
* interpolated into one, so the result is safe to assign as markup.
|
|
57
|
+
*/
|
|
46
58
|
export function iconFor(mime: string): string {
|
|
47
59
|
if (mime.startsWith("image/")) {
|
|
48
|
-
return
|
|
60
|
+
return ICON_FILE_IMAGE;
|
|
49
61
|
}
|
|
50
62
|
if (mime === "application/pdf") {
|
|
51
|
-
return
|
|
63
|
+
return ICON_FILE_PDF;
|
|
52
64
|
}
|
|
53
65
|
if (mime.startsWith("text/")) {
|
|
54
|
-
return
|
|
66
|
+
return ICON_FILE_TEXT;
|
|
55
67
|
}
|
|
56
|
-
return
|
|
68
|
+
return ICON_FILE;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** A compact human-readable byte size (e.g. `1.2 MB`). */
|
|
@@ -215,7 +215,8 @@ export class AttachmentTray {
|
|
|
215
215
|
const icon = document.createElement("span");
|
|
216
216
|
icon.className = "attachment-chip-icon";
|
|
217
217
|
icon.setAttribute("part", "attachment-chip-icon");
|
|
218
|
-
|
|
218
|
+
// Author-written glyph markup, selected by MIME family — see iconFor.
|
|
219
|
+
icon.innerHTML = iconFor(item.file.type);
|
|
219
220
|
icon.setAttribute("aria-hidden", "true");
|
|
220
221
|
|
|
221
222
|
const name = document.createElement("span");
|