@artooi/ag-ui-web-component 0.5.0 → 0.7.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 +73 -1
- package/README.md +208 -7
- package/dist/ag-ui-web-component.bundle.js +268 -58
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +33 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +23 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +35 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +32 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1247 -245
- package/dist/index.js.map +4 -4
- package/dist/tools/page_action_tools.d.ts +31 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -0
- package/dist/ui/attachment_chips.d.ts +13 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +45 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -0
- package/dist/ui/confirmation_card.d.ts +4 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.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/thread_drawer.d.ts +7 -1
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +6 -2
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +126 -0
- package/dist/ui/ui_strings.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +18 -0
- package/src/core/ag_ui_chat.ts +389 -51
- package/src/core/agui_client.ts +48 -4
- package/src/core/attachment.ts +39 -0
- package/src/core/upload_attachment.ts +113 -0
- package/src/index.ts +13 -0
- package/src/tools/page_action_tools.ts +130 -0
- package/src/ui/attachment_chips.ts +68 -0
- package/src/ui/attachment_tray.ts +243 -0
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/relative_time.ts +15 -8
- package/src/ui/styles.ts +208 -0
- package/src/ui/thread_drawer.ts +53 -25
- package/src/ui/tool_call_card.ts +40 -17
- package/src/ui/ui_strings.ts +208 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,12 @@ var TOOL_CALL_STATUS = {
|
|
|
17
17
|
ERROR: "error",
|
|
18
18
|
DECLINED: "declined"
|
|
19
19
|
};
|
|
20
|
+
var ATTACHMENT_STATUS = {
|
|
21
|
+
UPLOADING: "uploading",
|
|
22
|
+
READY: "ready",
|
|
23
|
+
ERROR: "error"
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
20
26
|
var TOOL_DISPLAY = {
|
|
21
27
|
MINIMAL: "minimal",
|
|
22
28
|
COMPACT: "compact",
|
|
@@ -98,6 +104,211 @@ function isNavigates(parameters) {
|
|
|
98
104
|
return parameters[X_NAVIGATES_KEY] === true;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
// src/dom/native_setter.ts
|
|
108
|
+
function prototypeSetter(proto, prop) {
|
|
109
|
+
const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
|
|
110
|
+
return setter;
|
|
111
|
+
}
|
|
112
|
+
var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
|
|
113
|
+
var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
|
|
114
|
+
var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
|
|
115
|
+
var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
|
|
116
|
+
function setNativeValue(el, value) {
|
|
117
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
118
|
+
setTextareaValue.call(el, value);
|
|
119
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
120
|
+
setSelectValue.call(el, value);
|
|
121
|
+
} else {
|
|
122
|
+
setInputValue.call(el, value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function setNativeChecked(el, checked) {
|
|
126
|
+
setInputChecked.call(el, checked);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/dom/animations.ts
|
|
130
|
+
var ACCENT = "#4f46e5";
|
|
131
|
+
function delay(ms) {
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
setTimeout(resolve, ms);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function typeInto(el, value, options = {}) {
|
|
137
|
+
const charDelayMs = options.charDelayMs ?? 35;
|
|
138
|
+
setNativeValue(el, "");
|
|
139
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
140
|
+
for (const char of value) {
|
|
141
|
+
setNativeValue(el, el.value + char);
|
|
142
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
143
|
+
if (charDelayMs > 0) {
|
|
144
|
+
await delay(charDelayMs);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
148
|
+
}
|
|
149
|
+
async function highlightThenClick(el, options = {}) {
|
|
150
|
+
const highlightMs = options.highlightMs ?? 280;
|
|
151
|
+
const previousOutline = el.style.outline;
|
|
152
|
+
const previousOffset = el.style.outlineOffset;
|
|
153
|
+
el.style.outline = `2px solid ${ACCENT}`;
|
|
154
|
+
el.style.outlineOffset = "2px";
|
|
155
|
+
await delay(highlightMs);
|
|
156
|
+
el.style.outline = previousOutline;
|
|
157
|
+
el.style.outlineOffset = previousOffset;
|
|
158
|
+
el.click();
|
|
159
|
+
}
|
|
160
|
+
function scrollIntoCenterView(el) {
|
|
161
|
+
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
162
|
+
}
|
|
163
|
+
async function focusWithFlash(el, options = {}) {
|
|
164
|
+
const flashMs = options.flashMs ?? 200;
|
|
165
|
+
el.focus();
|
|
166
|
+
const previousShadow = el.style.boxShadow;
|
|
167
|
+
el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
|
|
168
|
+
await delay(flashMs);
|
|
169
|
+
el.style.boxShadow = previousShadow;
|
|
170
|
+
}
|
|
171
|
+
var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
|
|
172
|
+
function prefersReducedMotion() {
|
|
173
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
174
|
+
}
|
|
175
|
+
function motionDelay(ms) {
|
|
176
|
+
if (ms <= 0 || prefersReducedMotion()) {
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
}
|
|
179
|
+
return delay(ms);
|
|
180
|
+
}
|
|
181
|
+
async function pressThenClick(el, options = {}) {
|
|
182
|
+
const pressMs = options.pressMs ?? 140;
|
|
183
|
+
const previousTransform = el.style.transform;
|
|
184
|
+
const previousTransition = el.style.transition;
|
|
185
|
+
const previousShadow = el.style.boxShadow;
|
|
186
|
+
el.style.transition = "transform 80ms ease";
|
|
187
|
+
el.style.transform = "scale(0.96)";
|
|
188
|
+
el.style.boxShadow = RING;
|
|
189
|
+
await motionDelay(pressMs);
|
|
190
|
+
el.style.transform = previousTransform;
|
|
191
|
+
el.style.transition = previousTransition;
|
|
192
|
+
el.style.boxShadow = previousShadow;
|
|
193
|
+
el.click();
|
|
194
|
+
}
|
|
195
|
+
function findOption(el, value) {
|
|
196
|
+
for (const option of Array.from(el.options)) {
|
|
197
|
+
if (option.value === value || option.text === value) {
|
|
198
|
+
return option;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
async function selectOption(el, value, options = {}) {
|
|
204
|
+
const option = findOption(el, value);
|
|
205
|
+
if (option === null) {
|
|
206
|
+
throw new Error(`no <option> matching "${value}"`);
|
|
207
|
+
}
|
|
208
|
+
const highlightMs = options.highlightMs ?? 220;
|
|
209
|
+
const previousOutline = el.style.outline;
|
|
210
|
+
const previousOffset = el.style.outlineOffset;
|
|
211
|
+
el.style.outline = `2px solid ${ACCENT}`;
|
|
212
|
+
el.style.outlineOffset = "2px";
|
|
213
|
+
await motionDelay(highlightMs);
|
|
214
|
+
setNativeValue(el, option.value);
|
|
215
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
216
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
217
|
+
el.style.outline = previousOutline;
|
|
218
|
+
el.style.outlineOffset = previousOffset;
|
|
219
|
+
}
|
|
220
|
+
async function toggleControl(el, checked, options = {}) {
|
|
221
|
+
const flashMs = options.flashMs ?? 200;
|
|
222
|
+
const previousShadow = el.style.boxShadow;
|
|
223
|
+
el.style.boxShadow = RING;
|
|
224
|
+
await motionDelay(flashMs);
|
|
225
|
+
setNativeChecked(el, checked);
|
|
226
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
227
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
228
|
+
el.style.boxShadow = previousShadow;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/tools/page_action_tools.ts
|
|
232
|
+
var PAGE_ACTIONS = {
|
|
233
|
+
SCROLL: "scroll",
|
|
234
|
+
DRAG: "drag"
|
|
235
|
+
};
|
|
236
|
+
function createPageActionTools(enabled, resolveTarget) {
|
|
237
|
+
const tools = [];
|
|
238
|
+
if (enabled.has(PAGE_ACTIONS.SCROLL)) {
|
|
239
|
+
tools.push(scrollTool(resolveTarget));
|
|
240
|
+
}
|
|
241
|
+
if (enabled.has(PAGE_ACTIONS.DRAG)) {
|
|
242
|
+
tools.push(dragTool(resolveTarget));
|
|
243
|
+
}
|
|
244
|
+
return tools;
|
|
245
|
+
}
|
|
246
|
+
function scrollTool(resolveTarget) {
|
|
247
|
+
return {
|
|
248
|
+
name: "scroll_to",
|
|
249
|
+
description: "Scroll a target into view. `target` is `top`, `bottom`, or a CSS selector / page-map element id. Read-only: it changes nothing on the page.",
|
|
250
|
+
parameters: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: { target: { type: "string" } },
|
|
253
|
+
required: ["target"],
|
|
254
|
+
[X_SUMMARY_KEY]: "Scroll into view"
|
|
255
|
+
},
|
|
256
|
+
handler: (args) => {
|
|
257
|
+
const target = String(args["target"] ?? "");
|
|
258
|
+
if (target === "top" || target === "bottom") {
|
|
259
|
+
const top = target === "top" ? 0 : document.body.scrollHeight;
|
|
260
|
+
window.scrollTo({ top, behavior: "smooth" });
|
|
261
|
+
return { scrolled: true, target };
|
|
262
|
+
}
|
|
263
|
+
const element = resolveTarget(target);
|
|
264
|
+
if (element === null) {
|
|
265
|
+
throw new Error(`no element matching "${target}"`);
|
|
266
|
+
}
|
|
267
|
+
scrollIntoCenterView(element);
|
|
268
|
+
return { scrolled: true, target };
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function dragTool(resolveTarget) {
|
|
273
|
+
return {
|
|
274
|
+
name: "drag_and_drop",
|
|
275
|
+
description: "Drag the `from` element onto the `to` element (CSS selectors or page-map element ids), firing the page's native drag-and-drop. Use for reordering sortable lists. The page decides what the drop commits.",
|
|
276
|
+
parameters: {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: { from: { type: "string" }, to: { type: "string" } },
|
|
279
|
+
required: ["from", "to"],
|
|
280
|
+
[X_SUMMARY_KEY]: "Drag and drop"
|
|
281
|
+
},
|
|
282
|
+
handler: (args) => {
|
|
283
|
+
const fromTarget = String(args["from"] ?? "");
|
|
284
|
+
const toTarget = String(args["to"] ?? "");
|
|
285
|
+
const from = resolveTarget(fromTarget);
|
|
286
|
+
if (from === null) {
|
|
287
|
+
throw new Error(`no element matching "${fromTarget}"`);
|
|
288
|
+
}
|
|
289
|
+
const to = resolveTarget(toTarget);
|
|
290
|
+
if (to === null) {
|
|
291
|
+
throw new Error(`no element matching "${toTarget}"`);
|
|
292
|
+
}
|
|
293
|
+
dispatchDragSequence(from, to);
|
|
294
|
+
return { dragged: true, from: fromTarget, to: toTarget };
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function dispatchDragSequence(from, to) {
|
|
299
|
+
const dataTransfer = new DataTransfer();
|
|
300
|
+
fire(from, "dragstart", dataTransfer);
|
|
301
|
+
fire(to, "dragenter", dataTransfer);
|
|
302
|
+
fire(to, "dragover", dataTransfer);
|
|
303
|
+
fire(to, "drop", dataTransfer);
|
|
304
|
+
fire(from, "dragend", dataTransfer);
|
|
305
|
+
}
|
|
306
|
+
function fire(target, type, dataTransfer) {
|
|
307
|
+
const event = new Event(type, { bubbles: true, cancelable: true });
|
|
308
|
+
event.dataTransfer = dataTransfer;
|
|
309
|
+
target.dispatchEvent(event);
|
|
310
|
+
}
|
|
311
|
+
|
|
101
312
|
// src/tools/page_map.ts
|
|
102
313
|
function createPageMapContext(getPageMap, autoInject) {
|
|
103
314
|
if (!autoInject || getPageMap === null) {
|
|
@@ -231,31 +442,326 @@ function createStateHookTools(hook) {
|
|
|
231
442
|
return tools;
|
|
232
443
|
}
|
|
233
444
|
|
|
445
|
+
// src/ui/attachment_chips.ts
|
|
446
|
+
function renderAttachmentChips(refs) {
|
|
447
|
+
const list = document.createElement("div");
|
|
448
|
+
list.className = "attachment-chips";
|
|
449
|
+
for (const ref of refs) {
|
|
450
|
+
list.appendChild(renderChip(ref));
|
|
451
|
+
}
|
|
452
|
+
return list;
|
|
453
|
+
}
|
|
454
|
+
function renderChip(ref) {
|
|
455
|
+
const chip = document.createElement("div");
|
|
456
|
+
chip.className = "attachment-chip attachment-chip--ready";
|
|
457
|
+
const icon = document.createElement("span");
|
|
458
|
+
icon.className = "attachment-chip-icon";
|
|
459
|
+
icon.textContent = iconFor(ref.mime);
|
|
460
|
+
icon.setAttribute("aria-hidden", "true");
|
|
461
|
+
const name = document.createElement("span");
|
|
462
|
+
name.className = "attachment-chip-name";
|
|
463
|
+
name.textContent = ref.name;
|
|
464
|
+
name.title = ref.name;
|
|
465
|
+
const size = document.createElement("span");
|
|
466
|
+
size.className = "attachment-chip-size";
|
|
467
|
+
size.textContent = formatBytes(ref.size);
|
|
468
|
+
chip.append(icon, name, size);
|
|
469
|
+
return chip;
|
|
470
|
+
}
|
|
471
|
+
function iconFor(mime) {
|
|
472
|
+
if (mime.startsWith("image/")) {
|
|
473
|
+
return "\u{1F5BC}";
|
|
474
|
+
}
|
|
475
|
+
if (mime === "application/pdf") {
|
|
476
|
+
return "\u{1F4D5}";
|
|
477
|
+
}
|
|
478
|
+
if (mime.startsWith("text/")) {
|
|
479
|
+
return "\u{1F4C4}";
|
|
480
|
+
}
|
|
481
|
+
return "\u{1F4CE}";
|
|
482
|
+
}
|
|
483
|
+
function formatBytes(bytes) {
|
|
484
|
+
if (bytes < 1024) {
|
|
485
|
+
return `${bytes} B`;
|
|
486
|
+
}
|
|
487
|
+
const units = ["KB", "MB", "GB"];
|
|
488
|
+
let value = bytes / 1024;
|
|
489
|
+
let unit = 0;
|
|
490
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
491
|
+
value /= 1024;
|
|
492
|
+
unit += 1;
|
|
493
|
+
}
|
|
494
|
+
const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
|
|
495
|
+
return `${rounded} ${units[unit]}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/ui/attachment_tray.ts
|
|
499
|
+
import { randomUUID } from "@ag-ui/client";
|
|
500
|
+
|
|
501
|
+
// src/ui/ui_strings.ts
|
|
502
|
+
var DEFAULT_UI_STRINGS = {
|
|
503
|
+
title: "Assistant",
|
|
504
|
+
chatHistory: "Chat history",
|
|
505
|
+
newChat: "New chat",
|
|
506
|
+
collapse: "Collapse",
|
|
507
|
+
expand: "Expand",
|
|
508
|
+
conversation: "Conversation",
|
|
509
|
+
thinking: "Assistant is thinking\u2026",
|
|
510
|
+
stopped: "\u23F9 Stopped",
|
|
511
|
+
connectionLost: "Connection lost",
|
|
512
|
+
noResult: "No result returned.",
|
|
513
|
+
declinedAction: "User declined the action.",
|
|
514
|
+
navigating: "Navigating\u2026",
|
|
515
|
+
skillNeeds: "\u201C{title}\u201D needs: {fields}",
|
|
516
|
+
message: "Message",
|
|
517
|
+
inputPlaceholder: "Ask anything\u2026",
|
|
518
|
+
send: "Send",
|
|
519
|
+
stop: "Stop",
|
|
520
|
+
attachFiles: "Attach files",
|
|
521
|
+
toolRunning: "running\u2026",
|
|
522
|
+
toolDone: "\u2713 done",
|
|
523
|
+
toolError: "\u26A0 error",
|
|
524
|
+
toolDeclined: "\u2298 declined",
|
|
525
|
+
resultLabel: "Result",
|
|
526
|
+
errorLabel: "Error",
|
|
527
|
+
declinedLabel: "Declined",
|
|
528
|
+
details: "Details",
|
|
529
|
+
confirmAction: "Confirm action",
|
|
530
|
+
confirmRun: "Run \u201C{tool}\u201D?",
|
|
531
|
+
confirm: "Confirm",
|
|
532
|
+
cancel: "Cancel",
|
|
533
|
+
chats: "Chats",
|
|
534
|
+
noConversations: "No conversations yet.",
|
|
535
|
+
rename: "Rename",
|
|
536
|
+
renameConversation: "Rename conversation",
|
|
537
|
+
delete: "Delete",
|
|
538
|
+
deleteConversation: "Delete conversation",
|
|
539
|
+
deletePrompt: "Delete?",
|
|
540
|
+
tooLarge: "Too large (max {size})",
|
|
541
|
+
fileTypeNotAllowed: "File type not allowed",
|
|
542
|
+
uploadFailed: "upload failed",
|
|
543
|
+
retry: "Retry",
|
|
544
|
+
retryUpload: "Retry upload",
|
|
545
|
+
remove: "Remove",
|
|
546
|
+
removeAttachment: "Remove attachment",
|
|
547
|
+
justNow: "just now",
|
|
548
|
+
minutesAgo: "{n}m ago",
|
|
549
|
+
hoursAgo: "{n}h ago",
|
|
550
|
+
daysAgo: "{n}d ago",
|
|
551
|
+
weeksAgo: "{n}w ago"
|
|
552
|
+
};
|
|
553
|
+
function mergeUiStrings(overrides) {
|
|
554
|
+
const merged = { ...DEFAULT_UI_STRINGS };
|
|
555
|
+
for (const key of Object.keys(overrides)) {
|
|
556
|
+
const value = overrides[key];
|
|
557
|
+
if (value !== void 0) {
|
|
558
|
+
merged[key] = value;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return merged;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/ui/attachment_tray.ts
|
|
565
|
+
var AttachmentTray = class {
|
|
566
|
+
/** The tray root; append above the input row. Hidden while empty. */
|
|
567
|
+
element;
|
|
568
|
+
#config;
|
|
569
|
+
#strings;
|
|
570
|
+
#items = [];
|
|
571
|
+
constructor(config) {
|
|
572
|
+
this.#config = config;
|
|
573
|
+
this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
|
|
574
|
+
this.element = document.createElement("div");
|
|
575
|
+
this.element.className = "attachment-tray";
|
|
576
|
+
this.element.setAttribute("part", "attachment-tray");
|
|
577
|
+
this.element.hidden = true;
|
|
578
|
+
}
|
|
579
|
+
/** Queue a file: reject oversize/disallowed into an error chip, else upload. */
|
|
580
|
+
add(file) {
|
|
581
|
+
const item = {
|
|
582
|
+
localId: randomUUID(),
|
|
583
|
+
file,
|
|
584
|
+
status: ATTACHMENT_STATUS.UPLOADING,
|
|
585
|
+
progress: 0,
|
|
586
|
+
ref: null,
|
|
587
|
+
error: ""
|
|
588
|
+
};
|
|
589
|
+
this.#items.push(item);
|
|
590
|
+
const rejection = this.#reject(file);
|
|
591
|
+
if (rejection !== null) {
|
|
592
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
593
|
+
item.error = rejection;
|
|
594
|
+
this.#render();
|
|
595
|
+
this.#config.onChange?.();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.#render();
|
|
599
|
+
this.#config.onChange?.();
|
|
600
|
+
this.#upload(item);
|
|
601
|
+
}
|
|
602
|
+
/** The durable refs of every chip that finished uploading. */
|
|
603
|
+
readyRefs() {
|
|
604
|
+
const refs = [];
|
|
605
|
+
for (const item of this.#items) {
|
|
606
|
+
if (item.ref !== null) {
|
|
607
|
+
refs.push(item.ref);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return refs;
|
|
611
|
+
}
|
|
612
|
+
/** Whether any chip is still uploading (a send would drop nothing if false). */
|
|
613
|
+
hasPending() {
|
|
614
|
+
return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
615
|
+
}
|
|
616
|
+
/** Whether the tray holds no chips. */
|
|
617
|
+
isEmpty() {
|
|
618
|
+
return this.#items.length === 0;
|
|
619
|
+
}
|
|
620
|
+
/** Drop the settled (ready / error) chips, leaving any still uploading. */
|
|
621
|
+
clearReady() {
|
|
622
|
+
this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
623
|
+
this.#render();
|
|
624
|
+
}
|
|
625
|
+
/** Drop every chip (a reset / new-chat). */
|
|
626
|
+
clear() {
|
|
627
|
+
this.#items = [];
|
|
628
|
+
this.#render();
|
|
629
|
+
}
|
|
630
|
+
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
631
|
+
#reject(file) {
|
|
632
|
+
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
633
|
+
return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
|
|
634
|
+
}
|
|
635
|
+
if (!accepts(this.#config.accept, file)) {
|
|
636
|
+
return this.#strings.fileTypeNotAllowed;
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
#upload(item) {
|
|
641
|
+
item.status = ATTACHMENT_STATUS.UPLOADING;
|
|
642
|
+
item.progress = 0;
|
|
643
|
+
item.error = "";
|
|
644
|
+
this.#render();
|
|
645
|
+
this.#config.upload(item.file, (fraction) => {
|
|
646
|
+
item.progress = fraction;
|
|
647
|
+
this.#render();
|
|
648
|
+
}).then((ref) => {
|
|
649
|
+
item.status = ATTACHMENT_STATUS.READY;
|
|
650
|
+
item.ref = ref;
|
|
651
|
+
}).catch((error) => {
|
|
652
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
653
|
+
item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
|
|
654
|
+
}).finally(() => {
|
|
655
|
+
this.#render();
|
|
656
|
+
this.#config.onChange?.();
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
#remove(item) {
|
|
660
|
+
this.#items = this.#items.filter((other) => other !== item);
|
|
661
|
+
this.#render();
|
|
662
|
+
this.#config.onChange?.();
|
|
663
|
+
}
|
|
664
|
+
#render() {
|
|
665
|
+
this.element.replaceChildren();
|
|
666
|
+
this.element.hidden = this.#items.length === 0;
|
|
667
|
+
for (const item of this.#items) {
|
|
668
|
+
this.element.appendChild(this.#renderChip(item));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
#renderChip(item) {
|
|
672
|
+
const chip = document.createElement("div");
|
|
673
|
+
chip.className = `attachment-chip attachment-chip--${item.status}`;
|
|
674
|
+
const icon = document.createElement("span");
|
|
675
|
+
icon.className = "attachment-chip-icon";
|
|
676
|
+
icon.textContent = iconFor(item.file.type);
|
|
677
|
+
icon.setAttribute("aria-hidden", "true");
|
|
678
|
+
const name = document.createElement("span");
|
|
679
|
+
name.className = "attachment-chip-name";
|
|
680
|
+
name.textContent = item.file.name;
|
|
681
|
+
name.title = item.file.name;
|
|
682
|
+
const meta = document.createElement("span");
|
|
683
|
+
meta.className = "attachment-chip-size";
|
|
684
|
+
meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
|
|
685
|
+
chip.append(icon, name, meta);
|
|
686
|
+
if (item.status === ATTACHMENT_STATUS.UPLOADING) {
|
|
687
|
+
const bar = document.createElement("div");
|
|
688
|
+
bar.className = "attachment-chip-bar";
|
|
689
|
+
const fill = document.createElement("div");
|
|
690
|
+
fill.className = "attachment-chip-bar-fill";
|
|
691
|
+
fill.style.width = `${Math.round(item.progress * 100)}%`;
|
|
692
|
+
bar.appendChild(fill);
|
|
693
|
+
chip.appendChild(bar);
|
|
694
|
+
}
|
|
695
|
+
if (item.status === ATTACHMENT_STATUS.ERROR) {
|
|
696
|
+
const retry = document.createElement("button");
|
|
697
|
+
retry.type = "button";
|
|
698
|
+
retry.className = "attachment-chip-retry";
|
|
699
|
+
retry.title = this.#strings.retry;
|
|
700
|
+
retry.setAttribute("aria-label", this.#strings.retryUpload);
|
|
701
|
+
retry.textContent = "\u21BB";
|
|
702
|
+
retry.addEventListener("click", () => this.#upload(item));
|
|
703
|
+
chip.appendChild(retry);
|
|
704
|
+
}
|
|
705
|
+
const remove = document.createElement("button");
|
|
706
|
+
remove.type = "button";
|
|
707
|
+
remove.className = "attachment-chip-remove";
|
|
708
|
+
remove.title = this.#strings.remove;
|
|
709
|
+
remove.setAttribute("aria-label", this.#strings.removeAttachment);
|
|
710
|
+
remove.textContent = "\u2715";
|
|
711
|
+
remove.addEventListener("click", () => this.#remove(item));
|
|
712
|
+
chip.appendChild(remove);
|
|
713
|
+
return chip;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
function accepts(accept, file) {
|
|
717
|
+
const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
|
|
718
|
+
if (tokens.length === 0) {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
const mime = file.type.toLowerCase();
|
|
722
|
+
const name = file.name.toLowerCase();
|
|
723
|
+
return tokens.some((token) => {
|
|
724
|
+
if (token.startsWith(".")) {
|
|
725
|
+
return name.endsWith(token);
|
|
726
|
+
}
|
|
727
|
+
if (token.endsWith("/*")) {
|
|
728
|
+
return mime.startsWith(token.slice(0, -1));
|
|
729
|
+
}
|
|
730
|
+
return mime === token;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
234
734
|
// src/ui/confirmation_card.ts
|
|
235
735
|
function actionButton(modifier, label) {
|
|
236
736
|
const button = document.createElement("button");
|
|
237
737
|
button.type = "button";
|
|
238
738
|
button.className = `confirm-btn confirm-btn--${modifier}`;
|
|
739
|
+
button.setAttribute("part", `confirm-button confirm-${modifier}`);
|
|
239
740
|
button.textContent = label;
|
|
240
741
|
return button;
|
|
241
742
|
}
|
|
242
743
|
function requestConfirmation(host, request, options = {}) {
|
|
744
|
+
const strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
243
745
|
return new Promise((resolve) => {
|
|
244
746
|
const card = document.createElement("div");
|
|
245
747
|
card.className = "confirm";
|
|
748
|
+
card.setAttribute("part", "confirm");
|
|
246
749
|
card.setAttribute("data-tool-name", request.toolName);
|
|
247
750
|
card.setAttribute("role", "group");
|
|
248
|
-
card.setAttribute("aria-label",
|
|
751
|
+
card.setAttribute("aria-label", strings.confirmAction);
|
|
249
752
|
const body = document.createElement("div");
|
|
250
753
|
body.className = "confirm-body";
|
|
251
|
-
body.
|
|
754
|
+
body.setAttribute("part", "confirm-body");
|
|
755
|
+
body.textContent = request.message ?? strings.confirmRun.replace("{tool}", request.toolName);
|
|
252
756
|
const args = document.createElement("pre");
|
|
253
757
|
args.className = "confirm-args";
|
|
758
|
+
args.setAttribute("part", "confirm-args");
|
|
254
759
|
args.textContent = JSON.stringify(request.args, null, 2);
|
|
255
760
|
const actions = document.createElement("div");
|
|
256
761
|
actions.className = "confirm-actions";
|
|
257
|
-
|
|
258
|
-
const
|
|
762
|
+
actions.setAttribute("part", "confirm-actions");
|
|
763
|
+
const cancel = actionButton("cancel", strings.cancel);
|
|
764
|
+
const confirm = actionButton("confirm", strings.confirm);
|
|
259
765
|
let settled = false;
|
|
260
766
|
const close = (accepted) => {
|
|
261
767
|
if (settled) {
|
|
@@ -3091,6 +3597,71 @@ var STYLES = `
|
|
|
3091
3597
|
--ag-ui-radius: 0;
|
|
3092
3598
|
}
|
|
3093
3599
|
|
|
3600
|
+
/* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
|
|
3601
|
+
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
3602
|
+
default; data-side="left" docks it left. Overlay by default \u2014 set
|
|
3603
|
+
--ag-ui-position: static (and place this element in your own layout) for a
|
|
3604
|
+
host-managed push instead. */
|
|
3605
|
+
:host([placement="sidebar"]) {
|
|
3606
|
+
--ag-ui-inset: 0 0 0 auto;
|
|
3607
|
+
--ag-ui-width: 420px;
|
|
3608
|
+
--ag-ui-height: 100vh;
|
|
3609
|
+
--ag-ui-max-height: 100vh;
|
|
3610
|
+
--ag-ui-radius: 0;
|
|
3611
|
+
--ag-ui-rail-width: 52px;
|
|
3612
|
+
transition: width 0.28s ease;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
:host([placement="sidebar"][data-side="left"]) {
|
|
3616
|
+
--ag-ui-inset: 0 auto 0 0;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
:host([placement="sidebar"]) .chat {
|
|
3620
|
+
transition: transform 0.28s ease;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
/* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
|
|
3624
|
+
reveal the rail. Higher specificity than the generic collapse rules, so it
|
|
3625
|
+
wins regardless of source order. */
|
|
3626
|
+
:host([placement="sidebar"][collapsed]) {
|
|
3627
|
+
width: var(--ag-ui-rail-width);
|
|
3628
|
+
height: 100vh;
|
|
3629
|
+
max-height: 100vh;
|
|
3630
|
+
bottom: 0;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
:host([placement="sidebar"][collapsed]) .chat {
|
|
3634
|
+
display: none;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
/* The rail is a sibling of the panel (so it survives the panel being hidden);
|
|
3638
|
+
shown only for a collapsed sidebar. */
|
|
3639
|
+
.rail {
|
|
3640
|
+
display: none;
|
|
3641
|
+
border: none;
|
|
3642
|
+
font: inherit;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
:host([placement="sidebar"][collapsed]) .rail {
|
|
3646
|
+
display: flex;
|
|
3647
|
+
position: absolute;
|
|
3648
|
+
inset: 0;
|
|
3649
|
+
align-items: flex-start;
|
|
3650
|
+
justify-content: center;
|
|
3651
|
+
padding-top: 16px;
|
|
3652
|
+
border: 1px solid var(--ag-ui-border);
|
|
3653
|
+
background: var(--ag-ui-header-bg);
|
|
3654
|
+
color: var(--ag-ui-header-fg);
|
|
3655
|
+
cursor: pointer;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3659
|
+
:host([placement="sidebar"]),
|
|
3660
|
+
:host([placement="sidebar"]) .chat {
|
|
3661
|
+
transition: none;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3094
3665
|
/* Embedded: drop the floating chrome and the high z-index stacking context so
|
|
3095
3666
|
the widget lives in the host's own layout (fixes overlay/z-index clashes). */
|
|
3096
3667
|
:host([placement="embedded"]) {
|
|
@@ -3128,12 +3699,33 @@ var STYLES = `
|
|
|
3128
3699
|
}
|
|
3129
3700
|
|
|
3130
3701
|
.header-title {
|
|
3702
|
+
flex: 1;
|
|
3703
|
+
min-width: 0;
|
|
3131
3704
|
font-weight: 600;
|
|
3132
3705
|
overflow: hidden;
|
|
3133
3706
|
text-overflow: ellipsis;
|
|
3134
3707
|
white-space: nowrap;
|
|
3135
3708
|
}
|
|
3136
3709
|
|
|
3710
|
+
/* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
|
|
3711
|
+
fallback, sized via --ag-ui-icon-size. */
|
|
3712
|
+
.icon-holder {
|
|
3713
|
+
display: inline-flex;
|
|
3714
|
+
align-items: center;
|
|
3715
|
+
justify-content: center;
|
|
3716
|
+
flex: none;
|
|
3717
|
+
width: var(--ag-ui-icon-size, 22px);
|
|
3718
|
+
height: var(--ag-ui-icon-size, 22px);
|
|
3719
|
+
line-height: 1;
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
.icon-img {
|
|
3723
|
+
width: 100%;
|
|
3724
|
+
height: 100%;
|
|
3725
|
+
object-fit: contain;
|
|
3726
|
+
border-radius: var(--ag-ui-icon-radius, 4px);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3137
3729
|
.header-controls {
|
|
3138
3730
|
display: flex;
|
|
3139
3731
|
gap: 2px;
|
|
@@ -3189,6 +3781,18 @@ var STYLES = `
|
|
|
3189
3781
|
gap: var(--ag-ui-space);
|
|
3190
3782
|
}
|
|
3191
3783
|
|
|
3784
|
+
/* Empty-state region (CUST-1 slot): centred while it's the only thing in the
|
|
3785
|
+
list, hidden as soon as a message, card, or pending indicator renders. */
|
|
3786
|
+
.empty {
|
|
3787
|
+
margin: auto;
|
|
3788
|
+
text-align: center;
|
|
3789
|
+
color: var(--ag-ui-muted);
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
.empty[hidden] {
|
|
3793
|
+
display: none;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3192
3796
|
.message {
|
|
3193
3797
|
max-width: 80%;
|
|
3194
3798
|
padding: var(--ag-ui-msg-pad);
|
|
@@ -3464,14 +4068,124 @@ var STYLES = `
|
|
|
3464
4068
|
background: var(--ag-ui-muted);
|
|
3465
4069
|
}
|
|
3466
4070
|
|
|
3467
|
-
/*
|
|
3468
|
-
.
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
padding:
|
|
3473
|
-
|
|
3474
|
-
|
|
4071
|
+
/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4072
|
+
/* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
|
|
4073
|
+
.attach-btn {
|
|
4074
|
+
border: 1px solid var(--ag-ui-border);
|
|
4075
|
+
border-radius: 8px;
|
|
4076
|
+
padding: 0 10px;
|
|
4077
|
+
background: var(--ag-ui-input-bg);
|
|
4078
|
+
color: inherit;
|
|
4079
|
+
font: inherit;
|
|
4080
|
+
cursor: pointer;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
.attach-btn:hover {
|
|
4084
|
+
border-color: var(--ag-ui-accent);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
.attach-input {
|
|
4088
|
+
display: none;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
4092
|
+
.attachment-slot {
|
|
4093
|
+
display: contents;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
.attachment-tray {
|
|
4097
|
+
display: flex;
|
|
4098
|
+
flex-wrap: wrap;
|
|
4099
|
+
gap: 6px;
|
|
4100
|
+
padding: 8px 12px 0;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
.attachment-chips {
|
|
4104
|
+
display: flex;
|
|
4105
|
+
flex-wrap: wrap;
|
|
4106
|
+
gap: 6px;
|
|
4107
|
+
margin-top: 6px;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
.attachment-chip {
|
|
4111
|
+
display: inline-flex;
|
|
4112
|
+
align-items: center;
|
|
4113
|
+
gap: 6px;
|
|
4114
|
+
max-width: 100%;
|
|
4115
|
+
padding: 4px 8px;
|
|
4116
|
+
border: 1px solid var(--ag-ui-border);
|
|
4117
|
+
border-radius: 999px;
|
|
4118
|
+
background: var(--ag-ui-assistant-bg);
|
|
4119
|
+
font-size: 0.85em;
|
|
4120
|
+
position: relative;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
.attachment-chip--error {
|
|
4124
|
+
border-color: var(--ag-ui-danger);
|
|
4125
|
+
color: var(--ag-ui-danger);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
.attachment-chip-name {
|
|
4129
|
+
overflow: hidden;
|
|
4130
|
+
text-overflow: ellipsis;
|
|
4131
|
+
white-space: nowrap;
|
|
4132
|
+
max-width: 14ch;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
.attachment-chip-size {
|
|
4136
|
+
color: var(--ag-ui-muted);
|
|
4137
|
+
white-space: nowrap;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
.attachment-chip--error .attachment-chip-size {
|
|
4141
|
+
color: var(--ag-ui-danger);
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
/* The progress bar fills as the file uploads. */
|
|
4145
|
+
.attachment-chip-bar {
|
|
4146
|
+
flex-basis: 100%;
|
|
4147
|
+
height: 3px;
|
|
4148
|
+
border-radius: 2px;
|
|
4149
|
+
background: var(--ag-ui-border);
|
|
4150
|
+
overflow: hidden;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
.attachment-chip-bar-fill {
|
|
4154
|
+
height: 100%;
|
|
4155
|
+
background: var(--ag-ui-accent);
|
|
4156
|
+
transition: width 0.15s ease;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
.attachment-chip-remove,
|
|
4160
|
+
.attachment-chip-retry {
|
|
4161
|
+
border: none;
|
|
4162
|
+
background: none;
|
|
4163
|
+
color: inherit;
|
|
4164
|
+
cursor: pointer;
|
|
4165
|
+
padding: 0;
|
|
4166
|
+
line-height: 1;
|
|
4167
|
+
opacity: 0.7;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
.attachment-chip-remove:hover,
|
|
4171
|
+
.attachment-chip-retry:hover {
|
|
4172
|
+
opacity: 1;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
/* A subtle outline while a file is dragged over the shell. */
|
|
4176
|
+
.chat--dragover {
|
|
4177
|
+
outline: 2px dashed var(--ag-ui-accent);
|
|
4178
|
+
outline-offset: -4px;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
/* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
|
|
4182
|
+
.stopped-note {
|
|
4183
|
+
align-self: flex-start;
|
|
4184
|
+
color: var(--ag-ui-muted);
|
|
4185
|
+
font-size: 12px;
|
|
4186
|
+
padding: 2px 4px;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
3475
4189
|
/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */
|
|
3476
4190
|
.confirm {
|
|
3477
4191
|
align-self: stretch;
|
|
@@ -3789,24 +4503,24 @@ var STYLES = `
|
|
|
3789
4503
|
`;
|
|
3790
4504
|
|
|
3791
4505
|
// src/ui/relative_time.ts
|
|
3792
|
-
function relativeTime(timestamp, now = Date.now()) {
|
|
4506
|
+
function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
|
|
3793
4507
|
const seconds = Math.round((now - timestamp) / 1e3);
|
|
3794
4508
|
if (seconds < 60) {
|
|
3795
|
-
return
|
|
4509
|
+
return strings.justNow;
|
|
3796
4510
|
}
|
|
3797
4511
|
const minutes = Math.round(seconds / 60);
|
|
3798
4512
|
if (minutes < 60) {
|
|
3799
|
-
return
|
|
4513
|
+
return strings.minutesAgo.replace("{n}", String(minutes));
|
|
3800
4514
|
}
|
|
3801
4515
|
const hours = Math.round(minutes / 60);
|
|
3802
4516
|
if (hours < 24) {
|
|
3803
|
-
return
|
|
4517
|
+
return strings.hoursAgo.replace("{n}", String(hours));
|
|
3804
4518
|
}
|
|
3805
4519
|
const days = Math.round(hours / 24);
|
|
3806
4520
|
if (days < 7) {
|
|
3807
|
-
return
|
|
4521
|
+
return strings.daysAgo.replace("{n}", String(days));
|
|
3808
4522
|
}
|
|
3809
|
-
return
|
|
4523
|
+
return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
|
|
3810
4524
|
}
|
|
3811
4525
|
|
|
3812
4526
|
// src/ui/thread_drawer.ts
|
|
@@ -3814,39 +4528,59 @@ var ThreadDrawer = class {
|
|
|
3814
4528
|
/** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
|
|
3815
4529
|
element;
|
|
3816
4530
|
#callbacks;
|
|
4531
|
+
#panel;
|
|
4532
|
+
#heading;
|
|
4533
|
+
#newButton;
|
|
3817
4534
|
#list;
|
|
4535
|
+
#strings;
|
|
3818
4536
|
#threads = [];
|
|
3819
4537
|
#activeId = "";
|
|
3820
|
-
constructor(callbacks) {
|
|
4538
|
+
constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
|
|
3821
4539
|
this.#callbacks = callbacks;
|
|
4540
|
+
this.#strings = strings;
|
|
3822
4541
|
this.element = document.createElement("div");
|
|
3823
4542
|
this.element.className = "drawer";
|
|
4543
|
+
this.element.setAttribute("part", "drawer");
|
|
3824
4544
|
this.element.hidden = true;
|
|
3825
4545
|
const backdrop = document.createElement("div");
|
|
3826
4546
|
backdrop.className = "drawer-backdrop";
|
|
4547
|
+
backdrop.setAttribute("part", "drawer-backdrop");
|
|
3827
4548
|
backdrop.addEventListener("click", () => this.close());
|
|
3828
|
-
|
|
3829
|
-
panel.className = "drawer-panel";
|
|
3830
|
-
panel.setAttribute("
|
|
3831
|
-
panel.setAttribute("
|
|
4549
|
+
this.#panel = document.createElement("div");
|
|
4550
|
+
this.#panel.className = "drawer-panel";
|
|
4551
|
+
this.#panel.setAttribute("part", "drawer-panel");
|
|
4552
|
+
this.#panel.setAttribute("role", "dialog");
|
|
4553
|
+
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
3832
4554
|
const header = document.createElement("div");
|
|
3833
4555
|
header.className = "drawer-header";
|
|
3834
|
-
|
|
3835
|
-
heading
|
|
3836
|
-
heading.
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
newButton
|
|
3840
|
-
newButton.
|
|
3841
|
-
newButton.
|
|
4556
|
+
header.setAttribute("part", "drawer-header");
|
|
4557
|
+
this.#heading = document.createElement("span");
|
|
4558
|
+
this.#heading.className = "drawer-title";
|
|
4559
|
+
this.#heading.setAttribute("part", "drawer-title");
|
|
4560
|
+
this.#heading.textContent = strings.chats;
|
|
4561
|
+
this.#newButton = document.createElement("button");
|
|
4562
|
+
this.#newButton.type = "button";
|
|
4563
|
+
this.#newButton.className = "drawer-new";
|
|
4564
|
+
this.#newButton.setAttribute("part", "drawer-new");
|
|
4565
|
+
this.#newButton.textContent = strings.newChat;
|
|
4566
|
+
this.#newButton.addEventListener("click", () => {
|
|
3842
4567
|
this.close();
|
|
3843
4568
|
this.#callbacks.onNew();
|
|
3844
4569
|
});
|
|
3845
|
-
header.append(heading, newButton);
|
|
4570
|
+
header.append(this.#heading, this.#newButton);
|
|
3846
4571
|
this.#list = document.createElement("div");
|
|
3847
4572
|
this.#list.className = "drawer-list";
|
|
3848
|
-
|
|
3849
|
-
this.
|
|
4573
|
+
this.#list.setAttribute("part", "drawer-list");
|
|
4574
|
+
this.#panel.append(header, this.#list);
|
|
4575
|
+
this.element.append(backdrop, this.#panel);
|
|
4576
|
+
}
|
|
4577
|
+
/** Re-localize the drawer's chrome and rows (the host calls this on connect). */
|
|
4578
|
+
setStrings(strings) {
|
|
4579
|
+
this.#strings = strings;
|
|
4580
|
+
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
4581
|
+
this.#heading.textContent = strings.chats;
|
|
4582
|
+
this.#newButton.textContent = strings.newChat;
|
|
4583
|
+
this.#renderList();
|
|
3850
4584
|
}
|
|
3851
4585
|
isOpen() {
|
|
3852
4586
|
return !this.element.hidden;
|
|
@@ -3871,7 +4605,8 @@ var ThreadDrawer = class {
|
|
|
3871
4605
|
if (this.#threads.length === 0) {
|
|
3872
4606
|
const empty = document.createElement("div");
|
|
3873
4607
|
empty.className = "drawer-empty";
|
|
3874
|
-
empty.
|
|
4608
|
+
empty.setAttribute("part", "drawer-empty");
|
|
4609
|
+
empty.textContent = this.#strings.noConversations;
|
|
3875
4610
|
this.#list.appendChild(empty);
|
|
3876
4611
|
return;
|
|
3877
4612
|
}
|
|
@@ -3882,18 +4617,20 @@ var ThreadDrawer = class {
|
|
|
3882
4617
|
#renderRow(meta) {
|
|
3883
4618
|
const row = document.createElement("div");
|
|
3884
4619
|
row.className = "drawer-row";
|
|
4620
|
+
row.setAttribute("part", "drawer-row");
|
|
3885
4621
|
if (meta.threadId === this.#activeId) {
|
|
3886
4622
|
row.classList.add("drawer-row--active");
|
|
3887
4623
|
}
|
|
3888
4624
|
const select = document.createElement("button");
|
|
3889
4625
|
select.type = "button";
|
|
3890
4626
|
select.className = "drawer-row-select";
|
|
4627
|
+
select.setAttribute("part", "drawer-row-select");
|
|
3891
4628
|
const title = document.createElement("span");
|
|
3892
4629
|
title.className = "drawer-row-title";
|
|
3893
4630
|
title.textContent = meta.title;
|
|
3894
4631
|
const time = document.createElement("span");
|
|
3895
4632
|
time.className = "drawer-row-time";
|
|
3896
|
-
time.textContent = relativeTime(meta.updatedAt);
|
|
4633
|
+
time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
|
|
3897
4634
|
const preview = document.createElement("span");
|
|
3898
4635
|
preview.className = "drawer-row-preview";
|
|
3899
4636
|
preview.textContent = meta.preview;
|
|
@@ -3905,15 +4642,15 @@ var ThreadDrawer = class {
|
|
|
3905
4642
|
const rename = document.createElement("button");
|
|
3906
4643
|
rename.type = "button";
|
|
3907
4644
|
rename.className = "drawer-row-rename";
|
|
3908
|
-
rename.title =
|
|
3909
|
-
rename.setAttribute("aria-label",
|
|
4645
|
+
rename.title = this.#strings.rename;
|
|
4646
|
+
rename.setAttribute("aria-label", this.#strings.renameConversation);
|
|
3910
4647
|
rename.textContent = "\u270E";
|
|
3911
4648
|
rename.addEventListener("click", () => this.#startRename(row, meta));
|
|
3912
4649
|
const remove = document.createElement("button");
|
|
3913
4650
|
remove.type = "button";
|
|
3914
4651
|
remove.className = "drawer-row-delete";
|
|
3915
|
-
remove.title =
|
|
3916
|
-
remove.setAttribute("aria-label",
|
|
4652
|
+
remove.title = this.#strings.delete;
|
|
4653
|
+
remove.setAttribute("aria-label", this.#strings.deleteConversation);
|
|
3917
4654
|
remove.textContent = "\u{1F5D1}";
|
|
3918
4655
|
remove.addEventListener("click", () => this.#confirmDelete(row, meta));
|
|
3919
4656
|
const actions = document.createElement("div");
|
|
@@ -3950,16 +4687,16 @@ var ThreadDrawer = class {
|
|
|
3950
4687
|
confirm.className = "drawer-confirm";
|
|
3951
4688
|
const label = document.createElement("span");
|
|
3952
4689
|
label.className = "drawer-confirm-label";
|
|
3953
|
-
label.textContent =
|
|
4690
|
+
label.textContent = this.#strings.deletePrompt;
|
|
3954
4691
|
const yes = document.createElement("button");
|
|
3955
4692
|
yes.type = "button";
|
|
3956
4693
|
yes.className = "drawer-confirm-yes";
|
|
3957
|
-
yes.textContent =
|
|
4694
|
+
yes.textContent = this.#strings.delete;
|
|
3958
4695
|
yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
|
|
3959
4696
|
const no = document.createElement("button");
|
|
3960
4697
|
no.type = "button";
|
|
3961
4698
|
no.className = "drawer-confirm-no";
|
|
3962
|
-
no.textContent =
|
|
4699
|
+
no.textContent = this.#strings.cancel;
|
|
3963
4700
|
no.addEventListener("click", () => this.#renderList());
|
|
3964
4701
|
confirm.append(label, yes, no);
|
|
3965
4702
|
row.replaceChildren(confirm);
|
|
@@ -3967,73 +4704,92 @@ var ThreadDrawer = class {
|
|
|
3967
4704
|
};
|
|
3968
4705
|
|
|
3969
4706
|
// src/ui/tool_call_card.ts
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
4707
|
+
function statusLabels(strings) {
|
|
4708
|
+
return {
|
|
4709
|
+
[TOOL_CALL_STATUS.PENDING]: strings.toolRunning,
|
|
4710
|
+
[TOOL_CALL_STATUS.DONE]: strings.toolDone,
|
|
4711
|
+
[TOOL_CALL_STATUS.ERROR]: strings.toolError,
|
|
4712
|
+
[TOOL_CALL_STATUS.DECLINED]: strings.toolDeclined
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
function resultLabels(strings) {
|
|
4716
|
+
return {
|
|
4717
|
+
[TOOL_CALL_STATUS.DONE]: strings.resultLabel,
|
|
4718
|
+
[TOOL_CALL_STATUS.ERROR]: strings.errorLabel,
|
|
4719
|
+
[TOOL_CALL_STATUS.DECLINED]: strings.declinedLabel
|
|
4720
|
+
};
|
|
4721
|
+
}
|
|
3981
4722
|
var ToolCallCard = class {
|
|
3982
4723
|
/** The card's root element; append this into the message list. */
|
|
3983
4724
|
element;
|
|
3984
4725
|
#status;
|
|
3985
4726
|
#mode;
|
|
3986
4727
|
#args;
|
|
3987
|
-
|
|
4728
|
+
#strings;
|
|
4729
|
+
#settled = false;
|
|
4730
|
+
constructor(name, args, mode = TOOL_DISPLAY.FULL, summary, strings = DEFAULT_UI_STRINGS) {
|
|
3988
4731
|
this.#mode = mode;
|
|
3989
4732
|
this.#args = args;
|
|
4733
|
+
this.#strings = strings;
|
|
3990
4734
|
this.element = document.createElement("div");
|
|
3991
4735
|
this.element.className = "tool-call";
|
|
4736
|
+
this.element.setAttribute("part", "tool-card");
|
|
3992
4737
|
this.element.setAttribute("data-tool-name", name);
|
|
3993
4738
|
this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
|
|
3994
4739
|
this.element.setAttribute("data-display", mode);
|
|
3995
4740
|
const head = document.createElement("div");
|
|
3996
4741
|
head.className = "tool-call-head";
|
|
4742
|
+
head.setAttribute("part", "tool-card-head");
|
|
3997
4743
|
const label = document.createElement("span");
|
|
3998
4744
|
label.className = "tool-call-name";
|
|
4745
|
+
label.setAttribute("part", "tool-card-name");
|
|
3999
4746
|
label.textContent = `\u{1F527} ${summary ?? name}`;
|
|
4000
4747
|
this.#status = document.createElement("span");
|
|
4001
4748
|
this.#status.className = "tool-call-status";
|
|
4002
|
-
this.#status.
|
|
4749
|
+
this.#status.setAttribute("part", "tool-card-status");
|
|
4750
|
+
this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
|
|
4003
4751
|
head.append(label, this.#status);
|
|
4004
4752
|
this.element.append(head);
|
|
4005
4753
|
if (mode === TOOL_DISPLAY.FULL) {
|
|
4006
4754
|
const argsEl = document.createElement("pre");
|
|
4007
4755
|
argsEl.className = "tool-call-args";
|
|
4756
|
+
argsEl.setAttribute("part", "tool-card-args");
|
|
4008
4757
|
argsEl.textContent = JSON.stringify(args, null, 2);
|
|
4009
4758
|
this.element.append(argsEl);
|
|
4010
4759
|
}
|
|
4011
4760
|
}
|
|
4761
|
+
/** Whether {@link settle} has already run (so a terminal sweep can skip it). */
|
|
4762
|
+
get settled() {
|
|
4763
|
+
return this.#settled;
|
|
4764
|
+
}
|
|
4012
4765
|
/**
|
|
4013
4766
|
* Flip the status pill to ``status`` and, unless in `minimal` mode, append a
|
|
4014
4767
|
* collapsed body behind a click-to-expand toggle: the result alone (`full`),
|
|
4015
4768
|
* or the args + result together (`compact`).
|
|
4016
4769
|
*/
|
|
4017
4770
|
settle(status, text2) {
|
|
4771
|
+
this.#settled = true;
|
|
4018
4772
|
this.element.setAttribute("data-status", status);
|
|
4019
|
-
this.#status.textContent =
|
|
4773
|
+
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
4020
4774
|
if (this.#mode === TOOL_DISPLAY.MINIMAL) {
|
|
4021
4775
|
return;
|
|
4022
4776
|
}
|
|
4023
4777
|
const toggle = document.createElement("button");
|
|
4024
4778
|
toggle.type = "button";
|
|
4025
4779
|
toggle.className = "tool-call-toggle";
|
|
4780
|
+
toggle.setAttribute("part", "tool-card-toggle");
|
|
4026
4781
|
toggle.setAttribute("aria-expanded", "false");
|
|
4027
4782
|
const output = document.createElement("pre");
|
|
4028
4783
|
output.className = "tool-call-result";
|
|
4784
|
+
output.setAttribute("part", "tool-card-result");
|
|
4029
4785
|
output.hidden = true;
|
|
4030
4786
|
if (this.#mode === TOOL_DISPLAY.COMPACT) {
|
|
4031
|
-
toggle.textContent =
|
|
4787
|
+
toggle.textContent = this.#strings.details;
|
|
4032
4788
|
output.textContent = `args: ${JSON.stringify(this.#args)}
|
|
4033
4789
|
|
|
4034
4790
|
${text2}`;
|
|
4035
4791
|
} else {
|
|
4036
|
-
toggle.textContent =
|
|
4792
|
+
toggle.textContent = resultLabels(this.#strings)[status];
|
|
4037
4793
|
output.textContent = text2;
|
|
4038
4794
|
}
|
|
4039
4795
|
toggle.addEventListener("click", () => {
|
|
@@ -4046,7 +4802,13 @@ ${text2}`;
|
|
|
4046
4802
|
};
|
|
4047
4803
|
|
|
4048
4804
|
// src/core/agui_client.ts
|
|
4049
|
-
import { randomUUID } from "@ag-ui/client";
|
|
4805
|
+
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4806
|
+
var ConnectionLostError = class extends Error {
|
|
4807
|
+
constructor(message) {
|
|
4808
|
+
super(message);
|
|
4809
|
+
this.name = "ConnectionLostError";
|
|
4810
|
+
}
|
|
4811
|
+
};
|
|
4050
4812
|
var AgUiClient = class {
|
|
4051
4813
|
#agent;
|
|
4052
4814
|
#handlers;
|
|
@@ -4054,6 +4816,7 @@ var AgUiClient = class {
|
|
|
4054
4816
|
#getContext;
|
|
4055
4817
|
#executeTool;
|
|
4056
4818
|
#onPersist;
|
|
4819
|
+
#connectionLostMessage;
|
|
4057
4820
|
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
4058
4821
|
// a cancel between frontend-tool rounds doesn't start another round.
|
|
4059
4822
|
#cancelled = false;
|
|
@@ -4065,6 +4828,7 @@ var AgUiClient = class {
|
|
|
4065
4828
|
this.#executeTool = config.executeTool ?? null;
|
|
4066
4829
|
this.#onPersist = config.onPersist ?? (() => {
|
|
4067
4830
|
});
|
|
4831
|
+
this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
|
|
4068
4832
|
}
|
|
4069
4833
|
/** Whether a run is currently in flight. */
|
|
4070
4834
|
get running() {
|
|
@@ -4080,9 +4844,18 @@ var AgUiClient = class {
|
|
|
4080
4844
|
* When the agent calls frontend tools, this executes them and re-runs the
|
|
4081
4845
|
* agent with the results, looping until the agent stops calling frontend
|
|
4082
4846
|
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
4847
|
+
*
|
|
4848
|
+
* `attachments` ride on the user message as a non-standard field so the
|
|
4849
|
+
* default client store round-trips them for history replay; the agent learns
|
|
4850
|
+
* the ids from the run context (the server's strict validation ignores the
|
|
4851
|
+
* unknown message field), then reads bytes via the `read_attachment` tool.
|
|
4083
4852
|
*/
|
|
4084
|
-
async send(content) {
|
|
4085
|
-
|
|
4853
|
+
async send(content, attachments = []) {
|
|
4854
|
+
const message = { id: randomUUID2(), role: "user", content };
|
|
4855
|
+
if (attachments.length > 0) {
|
|
4856
|
+
message.attachments = attachments;
|
|
4857
|
+
}
|
|
4858
|
+
this.#agent.addMessage(message);
|
|
4086
4859
|
this.#onPersist(this.#agent.messages);
|
|
4087
4860
|
await this.#run();
|
|
4088
4861
|
}
|
|
@@ -4096,7 +4869,7 @@ var AgUiClient = class {
|
|
|
4096
4869
|
}
|
|
4097
4870
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
4098
4871
|
addToolResult(toolCallId, content) {
|
|
4099
|
-
this.#agent.addMessage({ id:
|
|
4872
|
+
this.#agent.addMessage({ id: randomUUID2(), role: "tool", content, toolCallId });
|
|
4100
4873
|
this.#onPersist(this.#agent.messages);
|
|
4101
4874
|
}
|
|
4102
4875
|
/**
|
|
@@ -4137,14 +4910,18 @@ var AgUiClient = class {
|
|
|
4137
4910
|
return;
|
|
4138
4911
|
}
|
|
4139
4912
|
const pending = [];
|
|
4913
|
+
const runState = { terminal: false };
|
|
4140
4914
|
await this.#agent.runAgent(
|
|
4141
4915
|
{ tools: this.#getTools(), context: this.#getContext() },
|
|
4142
|
-
this.#buildSubscriber(pending)
|
|
4916
|
+
this.#buildSubscriber(pending, runState)
|
|
4143
4917
|
);
|
|
4144
4918
|
this.#onPersist(this.#agent.messages);
|
|
4145
4919
|
if (this.#cancelled) {
|
|
4146
4920
|
return;
|
|
4147
4921
|
}
|
|
4922
|
+
if (!runState.terminal) {
|
|
4923
|
+
throw new ConnectionLostError(this.#connectionLostMessage);
|
|
4924
|
+
}
|
|
4148
4925
|
if (this.#executeTool === null || pending.length === 0) {
|
|
4149
4926
|
return;
|
|
4150
4927
|
}
|
|
@@ -4158,7 +4935,7 @@ var AgUiClient = class {
|
|
|
4158
4935
|
return;
|
|
4159
4936
|
}
|
|
4160
4937
|
this.#agent.addMessage({
|
|
4161
|
-
id:
|
|
4938
|
+
id: randomUUID2(),
|
|
4162
4939
|
role: "tool",
|
|
4163
4940
|
content: result.content,
|
|
4164
4941
|
toolCallId: call.id
|
|
@@ -4171,7 +4948,7 @@ var AgUiClient = class {
|
|
|
4171
4948
|
}
|
|
4172
4949
|
}
|
|
4173
4950
|
}
|
|
4174
|
-
#buildSubscriber(pending) {
|
|
4951
|
+
#buildSubscriber(pending, runState) {
|
|
4175
4952
|
const h = this.#handlers;
|
|
4176
4953
|
return {
|
|
4177
4954
|
onRunInitialized() {
|
|
@@ -4196,9 +4973,11 @@ var AgUiClient = class {
|
|
|
4196
4973
|
h.onToolResult(event.toolCallId, event.content);
|
|
4197
4974
|
},
|
|
4198
4975
|
onRunErrorEvent({ event }) {
|
|
4976
|
+
runState.terminal = true;
|
|
4199
4977
|
h.onError(event.message);
|
|
4200
4978
|
},
|
|
4201
4979
|
onRunFinalized() {
|
|
4980
|
+
runState.terminal = true;
|
|
4202
4981
|
h.onRunEnd();
|
|
4203
4982
|
}
|
|
4204
4983
|
};
|
|
@@ -4208,8 +4987,14 @@ function isAbortError(error) {
|
|
|
4208
4987
|
return error instanceof Error && error.name === "AbortError";
|
|
4209
4988
|
}
|
|
4210
4989
|
|
|
4990
|
+
// src/core/attachment.ts
|
|
4991
|
+
function messageAttachments(message) {
|
|
4992
|
+
const refs = message.attachments;
|
|
4993
|
+
return Array.isArray(refs) ? refs : [];
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4211
4996
|
// src/core/conversation_store.ts
|
|
4212
|
-
import { randomUUID as
|
|
4997
|
+
import { randomUUID as randomUUID3 } from "@ag-ui/client";
|
|
4213
4998
|
var THREAD_KEY = "ag-ui-chat:thread";
|
|
4214
4999
|
var THREADS_KEY = "ag-ui-chat:threads";
|
|
4215
5000
|
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
@@ -4223,7 +5008,7 @@ var SessionStorageStore = class {
|
|
|
4223
5008
|
if (existing !== null) {
|
|
4224
5009
|
return existing;
|
|
4225
5010
|
}
|
|
4226
|
-
const id =
|
|
5011
|
+
const id = randomUUID3();
|
|
4227
5012
|
sessionStorage.setItem(THREAD_KEY, id);
|
|
4228
5013
|
return id;
|
|
4229
5014
|
}
|
|
@@ -4463,6 +5248,70 @@ var RemoteConversationStore = class {
|
|
|
4463
5248
|
}
|
|
4464
5249
|
};
|
|
4465
5250
|
|
|
5251
|
+
// src/core/upload_attachment.ts
|
|
5252
|
+
function uploadAttachment(file, options) {
|
|
5253
|
+
return new Promise((resolve, reject) => {
|
|
5254
|
+
const form = new FormData();
|
|
5255
|
+
form.append("file", file);
|
|
5256
|
+
const xhr = new XMLHttpRequest();
|
|
5257
|
+
xhr.open("POST", options.url);
|
|
5258
|
+
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
5259
|
+
xhr.setRequestHeader(key, value);
|
|
5260
|
+
}
|
|
5261
|
+
const onProgress = options.onProgress;
|
|
5262
|
+
if (onProgress !== void 0) {
|
|
5263
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
5264
|
+
if (event.lengthComputable) {
|
|
5265
|
+
onProgress(event.total === 0 ? 0 : event.loaded / event.total);
|
|
5266
|
+
}
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5269
|
+
xhr.addEventListener("load", () => {
|
|
5270
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
5271
|
+
try {
|
|
5272
|
+
resolve(parseRef(JSON.parse(xhr.responseText)));
|
|
5273
|
+
} catch {
|
|
5274
|
+
reject(new Error("upload returned an unreadable response"));
|
|
5275
|
+
}
|
|
5276
|
+
} else {
|
|
5277
|
+
reject(new Error(errorMessage(xhr)));
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
5281
|
+
xhr.addEventListener("abort", () => reject(new Error("upload cancelled")));
|
|
5282
|
+
const signal = options.signal;
|
|
5283
|
+
if (signal !== void 0) {
|
|
5284
|
+
signal.addEventListener("abort", () => xhr.abort());
|
|
5285
|
+
}
|
|
5286
|
+
xhr.send(form);
|
|
5287
|
+
});
|
|
5288
|
+
}
|
|
5289
|
+
function parseRef(body) {
|
|
5290
|
+
if (typeof body !== "object" || body === null) {
|
|
5291
|
+
throw new Error("not an object");
|
|
5292
|
+
}
|
|
5293
|
+
const o = body;
|
|
5294
|
+
const id = o["id"];
|
|
5295
|
+
const name = o["name"];
|
|
5296
|
+
const mime = o["mime"];
|
|
5297
|
+
const size = o["size"];
|
|
5298
|
+
const url = o["url"];
|
|
5299
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof mime !== "string" || typeof size !== "number") {
|
|
5300
|
+
throw new Error("missing fields");
|
|
5301
|
+
}
|
|
5302
|
+
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
5303
|
+
}
|
|
5304
|
+
function errorMessage(xhr) {
|
|
5305
|
+
try {
|
|
5306
|
+
const body = JSON.parse(xhr.responseText);
|
|
5307
|
+
if (typeof body.error === "string") {
|
|
5308
|
+
return body.error;
|
|
5309
|
+
}
|
|
5310
|
+
} catch {
|
|
5311
|
+
}
|
|
5312
|
+
return `upload failed (${xhr.status})`;
|
|
5313
|
+
}
|
|
5314
|
+
|
|
4466
5315
|
// src/core/ag_ui_chat.ts
|
|
4467
5316
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
4468
5317
|
var AgUiChat = class extends HTMLElement {
|
|
@@ -4503,9 +5352,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4503
5352
|
];
|
|
4504
5353
|
/**
|
|
4505
5354
|
* Per-run context provider. Defaults to the compact page map (when a
|
|
4506
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
5355
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
5356
|
+
* plus a one-line manifest of the files attached to the message being sent,
|
|
5357
|
+
* so the agent knows which `read_attachment` ids are available.
|
|
4507
5358
|
*/
|
|
4508
|
-
getContext = () =>
|
|
5359
|
+
getContext = () => [
|
|
5360
|
+
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
5361
|
+
...this.#attachmentContext()
|
|
5362
|
+
];
|
|
4509
5363
|
/**
|
|
4510
5364
|
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
4511
5365
|
* A compact summary also rides in each run's context.
|
|
@@ -4527,6 +5381,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4527
5381
|
* server-backed store for cross-tab/device durability.
|
|
4528
5382
|
*/
|
|
4529
5383
|
conversationStore = new SessionStorageStore();
|
|
5384
|
+
/**
|
|
5385
|
+
* How attached files are uploaded. `null` (default) uses the built-in
|
|
5386
|
+
* multipart `POST` to `data-attachments-url`. Set a custom
|
|
5387
|
+
* {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
|
|
5388
|
+
* swap the transport (e.g. a `tus-js-client` resumable adapter or
|
|
5389
|
+
* direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
|
|
5390
|
+
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
5391
|
+
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
5392
|
+
*/
|
|
5393
|
+
uploadHandler = null;
|
|
4530
5394
|
/**
|
|
4531
5395
|
* Builds the tool result a navigating tool resumes with after the page
|
|
4532
5396
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -4551,12 +5415,28 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4551
5415
|
* schema; this map is the seam for everything else.
|
|
4552
5416
|
*/
|
|
4553
5417
|
toolSummaries = {};
|
|
5418
|
+
/**
|
|
5419
|
+
* Localizable UI strings — a partial override merged over the English
|
|
5420
|
+
* {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
|
|
5421
|
+
* element is appended); the `data-strings` JSON attribute is the markup
|
|
5422
|
+
* equivalent, and this property wins key-by-key over it.
|
|
5423
|
+
*/
|
|
5424
|
+
strings = {};
|
|
5425
|
+
/**
|
|
5426
|
+
* Resolve a `scroll_to` / `drag_and_drop` target string to a host-page
|
|
5427
|
+
* element (or `null`). Defaults to a CSS-selector lookup; override to map
|
|
5428
|
+
* page-map element ids. The page-action tools are opt-in via the
|
|
5429
|
+
* `data-page-actions` attribute (`"scroll"` / `"drag"`).
|
|
5430
|
+
*/
|
|
5431
|
+
resolvePageTarget = (target) => document.querySelector(target);
|
|
4554
5432
|
/**
|
|
4555
5433
|
* Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
|
|
4556
5434
|
* tool name. The base layer behind {@link toolSummaries}: an explicit entry in
|
|
4557
5435
|
* `toolSummaries` wins, this fills the rest. Populated once on connect.
|
|
4558
5436
|
*/
|
|
4559
5437
|
#toolCatalog = {};
|
|
5438
|
+
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
5439
|
+
#strings = DEFAULT_UI_STRINGS;
|
|
4560
5440
|
#toolRegistry = new ClientToolRegistry();
|
|
4561
5441
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
4562
5442
|
#toolCards = /* @__PURE__ */ new Map();
|
|
@@ -4575,6 +5455,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4575
5455
|
#skillsMenu;
|
|
4576
5456
|
#drawer;
|
|
4577
5457
|
#skillHint;
|
|
5458
|
+
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
5459
|
+
#attachButton;
|
|
5460
|
+
#fileInput;
|
|
5461
|
+
#attachSlot;
|
|
5462
|
+
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
5463
|
+
#rail;
|
|
5464
|
+
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
5465
|
+
#emptyWrap;
|
|
5466
|
+
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5467
|
+
#attachTray = null;
|
|
5468
|
+
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5469
|
+
#runAttachments = [];
|
|
4578
5470
|
#client = null;
|
|
4579
5471
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
4580
5472
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
@@ -4604,6 +5496,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4604
5496
|
this.#send = document.createElement("button");
|
|
4605
5497
|
this.#title = document.createElement("span");
|
|
4606
5498
|
this.#skillHint = document.createElement("div");
|
|
5499
|
+
this.#attachButton = document.createElement("button");
|
|
5500
|
+
this.#fileInput = document.createElement("input");
|
|
5501
|
+
this.#attachSlot = document.createElement("div");
|
|
5502
|
+
this.#rail = document.createElement("button");
|
|
5503
|
+
this.#emptyWrap = document.createElement("div");
|
|
4607
5504
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
4608
5505
|
this.#drawer = new ThreadDrawer({
|
|
4609
5506
|
onSelect: (threadId) => {
|
|
@@ -4627,7 +5524,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4627
5524
|
return ["title-text"];
|
|
4628
5525
|
}
|
|
4629
5526
|
attributeChangedCallback(_name, _previous, value) {
|
|
4630
|
-
this.#title.textContent = value ??
|
|
5527
|
+
this.#title.textContent = value ?? this.#strings.title;
|
|
4631
5528
|
}
|
|
4632
5529
|
/** Declare a frontend tool the agent may call. */
|
|
4633
5530
|
registerTool(tool) {
|
|
@@ -4673,9 +5570,25 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4673
5570
|
}
|
|
4674
5571
|
];
|
|
4675
5572
|
}
|
|
4676
|
-
/**
|
|
5573
|
+
/**
|
|
5574
|
+
* Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
|
|
5575
|
+
* via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
|
|
5576
|
+
* through {@link resolvePageTarget} so a host controls the agent's interaction
|
|
5577
|
+
* surface; absent attribute ⇒ no tools registered.
|
|
5578
|
+
*/
|
|
5579
|
+
#pageActionTools() {
|
|
5580
|
+
const attr = this.getAttribute("data-page-actions");
|
|
5581
|
+
if (attr === null) {
|
|
5582
|
+
return [];
|
|
5583
|
+
}
|
|
5584
|
+
const enabled = new Set(
|
|
5585
|
+
attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
|
|
5586
|
+
);
|
|
5587
|
+
return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
|
|
5588
|
+
}
|
|
5589
|
+
/** All built-in (route + page + page-action) frontend tools. */
|
|
4677
5590
|
#builtinTools() {
|
|
4678
|
-
return [...this.#routeTools(), ...this.#pageTools()];
|
|
5591
|
+
return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
|
|
4679
5592
|
}
|
|
4680
5593
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
4681
5594
|
#resolveTool(name) {
|
|
@@ -4710,16 +5623,122 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4710
5623
|
this.setAttribute("data-tool-display", value);
|
|
4711
5624
|
}
|
|
4712
5625
|
connectedCallback() {
|
|
5626
|
+
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
4713
5627
|
this.#render();
|
|
5628
|
+
this.#drawer.setStrings(this.#strings);
|
|
4714
5629
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
4715
5630
|
this.setAttribute("collapsed", "");
|
|
4716
5631
|
}
|
|
5632
|
+
this.#syncRail();
|
|
4717
5633
|
this.#initSkills();
|
|
4718
5634
|
void this.#fetchToolCatalog();
|
|
4719
5635
|
this.#wireThreadStore();
|
|
5636
|
+
this.#wireAttachments();
|
|
4720
5637
|
this.#threadId = this.conversationStore.threadId();
|
|
4721
5638
|
void this.#rehydrate();
|
|
4722
5639
|
}
|
|
5640
|
+
/** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
|
|
5641
|
+
#readStringOverrides() {
|
|
5642
|
+
const raw = this.getAttribute("data-strings");
|
|
5643
|
+
if (raw === null) {
|
|
5644
|
+
return {};
|
|
5645
|
+
}
|
|
5646
|
+
try {
|
|
5647
|
+
const parsed = JSON.parse(raw);
|
|
5648
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
5649
|
+
return parsed;
|
|
5650
|
+
}
|
|
5651
|
+
} catch {
|
|
5652
|
+
}
|
|
5653
|
+
return {};
|
|
5654
|
+
}
|
|
5655
|
+
/**
|
|
5656
|
+
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
5657
|
+
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
5658
|
+
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
5659
|
+
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
5660
|
+
* stays hidden and the chat degrades to text-only.
|
|
5661
|
+
*/
|
|
5662
|
+
#wireAttachments() {
|
|
5663
|
+
const url = this.getAttribute("data-attachments-url");
|
|
5664
|
+
const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
|
|
5665
|
+
if (upload === null) {
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
5669
|
+
this.#attachTray = new AttachmentTray({
|
|
5670
|
+
upload,
|
|
5671
|
+
maxBytes: this.#attachmentMaxBytes(),
|
|
5672
|
+
accept,
|
|
5673
|
+
strings: this.#strings
|
|
5674
|
+
});
|
|
5675
|
+
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
5676
|
+
this.#fileInput.accept = accept;
|
|
5677
|
+
this.#attachButton.hidden = false;
|
|
5678
|
+
this.#enableDragAndDrop();
|
|
5679
|
+
}
|
|
5680
|
+
/** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
|
|
5681
|
+
#defaultUploadHandler(url) {
|
|
5682
|
+
if (url === null) {
|
|
5683
|
+
return null;
|
|
5684
|
+
}
|
|
5685
|
+
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
5686
|
+
}
|
|
5687
|
+
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5688
|
+
#attachmentMaxBytes() {
|
|
5689
|
+
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
5690
|
+
if (attr === null) {
|
|
5691
|
+
return DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5692
|
+
}
|
|
5693
|
+
const parsed = Number.parseInt(attr, 10);
|
|
5694
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5695
|
+
}
|
|
5696
|
+
/** Queue every file from the picker into the tray, then reset the input. */
|
|
5697
|
+
#onFilesPicked() {
|
|
5698
|
+
const files = this.#fileInput.files;
|
|
5699
|
+
if (files !== null) {
|
|
5700
|
+
for (const file of Array.from(files)) {
|
|
5701
|
+
this.#attachTray?.add(file);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
this.#fileInput.value = "";
|
|
5705
|
+
}
|
|
5706
|
+
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
5707
|
+
#enableDragAndDrop() {
|
|
5708
|
+
this.#chat.addEventListener("dragover", (event) => {
|
|
5709
|
+
event.preventDefault();
|
|
5710
|
+
this.#chat.classList.add("chat--dragover");
|
|
5711
|
+
});
|
|
5712
|
+
this.#chat.addEventListener("dragleave", () => {
|
|
5713
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5714
|
+
});
|
|
5715
|
+
this.#chat.addEventListener("drop", (event) => {
|
|
5716
|
+
event.preventDefault();
|
|
5717
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5718
|
+
const files = event.dataTransfer?.files;
|
|
5719
|
+
if (files !== void 0) {
|
|
5720
|
+
for (const file of Array.from(files)) {
|
|
5721
|
+
this.#attachTray?.add(file);
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
});
|
|
5725
|
+
}
|
|
5726
|
+
/** The one-line manifest of the message's attachments, for the run context. */
|
|
5727
|
+
#attachmentContext() {
|
|
5728
|
+
if (this.#runAttachments.length === 0) {
|
|
5729
|
+
return [];
|
|
5730
|
+
}
|
|
5731
|
+
const lines = this.#runAttachments.map(
|
|
5732
|
+
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`
|
|
5733
|
+
);
|
|
5734
|
+
return [
|
|
5735
|
+
{
|
|
5736
|
+
description: "Files the user attached to this message",
|
|
5737
|
+
value: `${lines.join("\n")}
|
|
5738
|
+
Use the read_attachment tool with an id to read a file's contents.`
|
|
5739
|
+
}
|
|
5740
|
+
];
|
|
5741
|
+
}
|
|
4723
5742
|
/**
|
|
4724
5743
|
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
4725
5744
|
* delete through that server endpoint (wrapping the current store as the
|
|
@@ -4801,7 +5820,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4801
5820
|
#applySkill(skill) {
|
|
4802
5821
|
const { text: text2, missing } = fillTemplate(skill.prompt, this.skillContext());
|
|
4803
5822
|
if (missing.length > 0) {
|
|
4804
|
-
this.#skillHint.textContent =
|
|
5823
|
+
this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
|
|
4805
5824
|
this.#skillHint.hidden = false;
|
|
4806
5825
|
return;
|
|
4807
5826
|
}
|
|
@@ -4834,6 +5853,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4834
5853
|
this.removeAttribute("collapsed");
|
|
4835
5854
|
}
|
|
4836
5855
|
sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
5856
|
+
this.#syncRail();
|
|
4837
5857
|
this.dispatchEvent(
|
|
4838
5858
|
new CustomEvent(TOGGLE_EVENT, {
|
|
4839
5859
|
detail: { collapsed },
|
|
@@ -4865,7 +5885,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4865
5885
|
this.#toolCards.clear();
|
|
4866
5886
|
this.#serverSettled.clear();
|
|
4867
5887
|
this.#initialMessages = [];
|
|
4868
|
-
this.#
|
|
5888
|
+
this.#runAttachments = [];
|
|
5889
|
+
this.#attachTray?.clear();
|
|
5890
|
+
this.#messages.replaceChildren(this.#emptyWrap);
|
|
5891
|
+
this.#updateEmptyState();
|
|
4869
5892
|
}
|
|
4870
5893
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
4871
5894
|
async #switchThread(threadId) {
|
|
@@ -4924,8 +5947,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4924
5947
|
#renderHistoricMessage(message) {
|
|
4925
5948
|
const text2 = typeof message.content === "string" ? message.content : "";
|
|
4926
5949
|
if (message.role === MESSAGE_ROLE.USER) {
|
|
4927
|
-
|
|
4928
|
-
|
|
5950
|
+
const attachments = messageAttachments(message);
|
|
5951
|
+
if (text2 !== "" || attachments.length > 0) {
|
|
5952
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, text2);
|
|
5953
|
+
if (attachments.length > 0) {
|
|
5954
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
5955
|
+
}
|
|
4929
5956
|
}
|
|
4930
5957
|
return;
|
|
4931
5958
|
}
|
|
@@ -4989,12 +6016,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4989
6016
|
appendMessage(role, content) {
|
|
4990
6017
|
const bubble = document.createElement("div");
|
|
4991
6018
|
bubble.className = `message message--${role}`;
|
|
6019
|
+
bubble.setAttribute("part", `message message-${role}`);
|
|
4992
6020
|
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
4993
6021
|
bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
|
|
4994
6022
|
} else {
|
|
4995
6023
|
bubble.textContent = content;
|
|
4996
6024
|
}
|
|
4997
6025
|
this.#messages.appendChild(bubble);
|
|
6026
|
+
this.#updateEmptyState();
|
|
4998
6027
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
4999
6028
|
return bubble;
|
|
5000
6029
|
}
|
|
@@ -5002,55 +6031,59 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5002
6031
|
const style = document.createElement("style");
|
|
5003
6032
|
style.textContent = STYLES;
|
|
5004
6033
|
this.#chat.className = "chat";
|
|
6034
|
+
this.#chat.setAttribute("part", "panel");
|
|
5005
6035
|
const header = document.createElement("div");
|
|
5006
6036
|
header.className = "header";
|
|
6037
|
+
header.setAttribute("part", "header");
|
|
5007
6038
|
const title = this.#title;
|
|
5008
6039
|
title.className = "header-title";
|
|
5009
|
-
title.
|
|
6040
|
+
title.setAttribute("part", "title");
|
|
6041
|
+
title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
|
|
6042
|
+
if (this.querySelector('[slot="icon"]') !== null || this.getAttribute("data-icon-url") !== null) {
|
|
6043
|
+
header.append(this.#iconElement("icon", "icon", null));
|
|
6044
|
+
}
|
|
6045
|
+
const headerActions = document.createElement("slot");
|
|
6046
|
+
headerActions.name = "header-actions";
|
|
5010
6047
|
const controls = document.createElement("div");
|
|
5011
6048
|
controls.className = "header-controls";
|
|
5012
|
-
|
|
5013
|
-
history
|
|
5014
|
-
history.className = "header-btn header-btn--history";
|
|
5015
|
-
history.title = "Chat history";
|
|
5016
|
-
history.setAttribute("aria-label", "Chat history");
|
|
5017
|
-
history.textContent = "\u2630";
|
|
6049
|
+
controls.setAttribute("part", "header-controls");
|
|
6050
|
+
const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
|
|
5018
6051
|
history.addEventListener("click", () => {
|
|
5019
6052
|
void this.#refreshDrawer();
|
|
5020
6053
|
this.#drawer.open();
|
|
5021
6054
|
});
|
|
5022
|
-
const newChat =
|
|
5023
|
-
newChat.type = "button";
|
|
5024
|
-
newChat.className = "header-btn header-btn--new";
|
|
5025
|
-
newChat.title = "New chat";
|
|
5026
|
-
newChat.setAttribute("aria-label", "New chat");
|
|
5027
|
-
newChat.textContent = "\u271A";
|
|
6055
|
+
const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
|
|
5028
6056
|
newChat.addEventListener("click", () => this.newChat());
|
|
5029
|
-
const collapse =
|
|
5030
|
-
collapse.type = "button";
|
|
5031
|
-
collapse.className = "header-btn header-btn--collapse";
|
|
5032
|
-
collapse.title = "Collapse";
|
|
5033
|
-
collapse.setAttribute("aria-label", "Collapse");
|
|
5034
|
-
collapse.textContent = "\u2014";
|
|
6057
|
+
const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
5035
6058
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
5036
6059
|
controls.append(history, newChat, collapse);
|
|
5037
|
-
header.append(title, controls);
|
|
6060
|
+
header.append(title, headerActions, controls);
|
|
5038
6061
|
this.#messages.className = "messages";
|
|
6062
|
+
this.#messages.setAttribute("part", "messages");
|
|
5039
6063
|
this.#messages.setAttribute("role", "log");
|
|
5040
6064
|
this.#messages.setAttribute("aria-live", "polite");
|
|
5041
|
-
this.#messages.setAttribute("aria-label",
|
|
6065
|
+
this.#messages.setAttribute("aria-label", this.#strings.conversation);
|
|
6066
|
+
this.#emptyWrap.className = "empty";
|
|
6067
|
+
this.#emptyWrap.setAttribute("part", "empty");
|
|
6068
|
+
const emptySlot = document.createElement("slot");
|
|
6069
|
+
emptySlot.name = "empty";
|
|
6070
|
+
this.#emptyWrap.append(emptySlot);
|
|
6071
|
+
this.#messages.append(this.#emptyWrap);
|
|
5042
6072
|
const inputRow = document.createElement("div");
|
|
5043
6073
|
inputRow.className = "input-row";
|
|
6074
|
+
inputRow.setAttribute("part", "composer");
|
|
5044
6075
|
this.#input.className = "input";
|
|
5045
|
-
this.#input.setAttribute("
|
|
6076
|
+
this.#input.setAttribute("part", "input");
|
|
6077
|
+
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
5046
6078
|
this.#input.rows = 2;
|
|
5047
|
-
this.#input.placeholder =
|
|
6079
|
+
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
5048
6080
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
5049
6081
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
5050
6082
|
this.#send.className = "send";
|
|
5051
6083
|
this.#send.type = "button";
|
|
5052
|
-
this.#send.
|
|
5053
|
-
this.#send.
|
|
6084
|
+
this.#send.setAttribute("part", "send");
|
|
6085
|
+
this.#send.textContent = this.#strings.send;
|
|
6086
|
+
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
5054
6087
|
this.#send.dataset["state"] = "idle";
|
|
5055
6088
|
this.#send.addEventListener("click", () => {
|
|
5056
6089
|
if (this.#running) {
|
|
@@ -5061,17 +6094,83 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5061
6094
|
});
|
|
5062
6095
|
this.#skillHint.className = "skill-hint";
|
|
5063
6096
|
this.#skillHint.hidden = true;
|
|
5064
|
-
|
|
6097
|
+
this.#attachButton.className = "attach-btn";
|
|
6098
|
+
this.#attachButton.type = "button";
|
|
6099
|
+
this.#attachButton.setAttribute("part", "attach-button");
|
|
6100
|
+
this.#attachButton.textContent = "\u{1F4CE}";
|
|
6101
|
+
this.#attachButton.title = this.#strings.attachFiles;
|
|
6102
|
+
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
6103
|
+
this.#attachButton.hidden = true;
|
|
6104
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
6105
|
+
this.#fileInput.className = "attach-input";
|
|
6106
|
+
this.#fileInput.type = "file";
|
|
6107
|
+
this.#fileInput.multiple = true;
|
|
6108
|
+
this.#fileInput.hidden = true;
|
|
6109
|
+
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
6110
|
+
this.#attachSlot.className = "attachment-slot";
|
|
6111
|
+
const footer = document.createElement("slot");
|
|
6112
|
+
footer.name = "footer";
|
|
6113
|
+
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
5065
6114
|
this.#chat.append(
|
|
5066
6115
|
header,
|
|
5067
6116
|
this.#messages,
|
|
5068
6117
|
this.#skillsMenu.palette,
|
|
5069
6118
|
this.#skillsMenu.chips,
|
|
5070
6119
|
this.#skillHint,
|
|
6120
|
+
this.#attachSlot,
|
|
5071
6121
|
inputRow,
|
|
6122
|
+
footer,
|
|
5072
6123
|
this.#drawer.element
|
|
5073
6124
|
);
|
|
5074
|
-
this.#
|
|
6125
|
+
this.#rail.className = "rail";
|
|
6126
|
+
this.#rail.type = "button";
|
|
6127
|
+
this.#rail.setAttribute("part", "launcher");
|
|
6128
|
+
this.#rail.setAttribute("aria-label", this.#strings.expand);
|
|
6129
|
+
this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
|
|
6130
|
+
this.#rail.addEventListener("click", () => this.setCollapsed(false));
|
|
6131
|
+
this.#root.append(style, this.#chat, this.#rail);
|
|
6132
|
+
}
|
|
6133
|
+
/** Build a header control button (icon glyph + localized title/aria). */
|
|
6134
|
+
#headerButton(modifier, label, glyph) {
|
|
6135
|
+
const button = document.createElement("button");
|
|
6136
|
+
button.type = "button";
|
|
6137
|
+
button.className = `header-btn header-btn--${modifier}`;
|
|
6138
|
+
button.setAttribute("part", `header-button ${modifier}-button`);
|
|
6139
|
+
button.title = label;
|
|
6140
|
+
button.setAttribute("aria-label", label);
|
|
6141
|
+
button.textContent = glyph;
|
|
6142
|
+
return button;
|
|
6143
|
+
}
|
|
6144
|
+
/**
|
|
6145
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup; with
|
|
6146
|
+
* a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
|
|
6147
|
+
*/
|
|
6148
|
+
#iconElement(slotName, part, fallbackGlyph) {
|
|
6149
|
+
const holder = document.createElement("span");
|
|
6150
|
+
holder.className = "icon-holder";
|
|
6151
|
+
holder.setAttribute("part", part);
|
|
6152
|
+
const slot = document.createElement("slot");
|
|
6153
|
+
slot.name = slotName;
|
|
6154
|
+
const iconUrl = this.getAttribute("data-icon-url");
|
|
6155
|
+
if (iconUrl !== null) {
|
|
6156
|
+
const img = document.createElement("img");
|
|
6157
|
+
img.className = "icon-img";
|
|
6158
|
+
img.src = iconUrl;
|
|
6159
|
+
img.alt = "";
|
|
6160
|
+
slot.append(img);
|
|
6161
|
+
} else if (fallbackGlyph !== null) {
|
|
6162
|
+
slot.append(document.createTextNode(fallbackGlyph));
|
|
6163
|
+
}
|
|
6164
|
+
holder.append(slot);
|
|
6165
|
+
return holder;
|
|
6166
|
+
}
|
|
6167
|
+
/** Reflect the collapsed state on the rail's `aria-expanded`. */
|
|
6168
|
+
#syncRail() {
|
|
6169
|
+
this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
|
|
6170
|
+
}
|
|
6171
|
+
/** Hide the empty-state region once the message list holds anything else. */
|
|
6172
|
+
#updateEmptyState() {
|
|
6173
|
+
this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
|
|
5075
6174
|
}
|
|
5076
6175
|
/** Forward input changes to the skills palette and clear any stale hint. */
|
|
5077
6176
|
#onInput() {
|
|
@@ -5106,32 +6205,38 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5106
6205
|
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
5107
6206
|
#setRunning(running) {
|
|
5108
6207
|
this.#running = running;
|
|
5109
|
-
const label = running ?
|
|
6208
|
+
const label = running ? this.#strings.stop : this.#strings.send;
|
|
5110
6209
|
this.#send.textContent = label;
|
|
5111
6210
|
this.#send.setAttribute("aria-label", label);
|
|
5112
6211
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
5113
6212
|
}
|
|
5114
6213
|
async #submit() {
|
|
5115
6214
|
const content = this.#input.value.trim();
|
|
5116
|
-
|
|
6215
|
+
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
6216
|
+
if (content === "" && attachments.length === 0) {
|
|
5117
6217
|
return;
|
|
5118
6218
|
}
|
|
5119
|
-
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
6219
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
6220
|
+
if (attachments.length > 0) {
|
|
6221
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
6222
|
+
}
|
|
5120
6223
|
this.#input.value = "";
|
|
6224
|
+
this.#attachTray?.clearReady();
|
|
6225
|
+
this.#runAttachments = attachments;
|
|
5121
6226
|
this.dispatchEvent(
|
|
5122
6227
|
new CustomEvent(SUBMIT_EVENT, {
|
|
5123
|
-
detail: { content },
|
|
6228
|
+
detail: { content, attachments },
|
|
5124
6229
|
bubbles: true,
|
|
5125
6230
|
composed: true
|
|
5126
6231
|
})
|
|
5127
6232
|
);
|
|
5128
|
-
await this.#client_send(content);
|
|
6233
|
+
await this.#client_send(content, attachments);
|
|
5129
6234
|
}
|
|
5130
|
-
async #client_send(content) {
|
|
6235
|
+
async #client_send(content, attachments) {
|
|
5131
6236
|
if (this.endpoint === "") {
|
|
5132
6237
|
return;
|
|
5133
6238
|
}
|
|
5134
|
-
await this.#ensureClient().send(content);
|
|
6239
|
+
await this.#ensureClient().send(content, attachments);
|
|
5135
6240
|
}
|
|
5136
6241
|
#ensureClient() {
|
|
5137
6242
|
if (this.#client === null) {
|
|
@@ -5151,7 +6256,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5151
6256
|
getTools: () => this.getTools(),
|
|
5152
6257
|
getContext: () => this.getContext(),
|
|
5153
6258
|
executeTool: (call) => this.#executeTool(call),
|
|
5154
|
-
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
|
|
6259
|
+
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
6260
|
+
connectionLostMessage: this.#strings.connectionLost
|
|
5155
6261
|
});
|
|
5156
6262
|
}
|
|
5157
6263
|
return this.#client;
|
|
@@ -5172,7 +6278,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5172
6278
|
const tool = this.#resolveTool(call.name);
|
|
5173
6279
|
if (tool === null) {
|
|
5174
6280
|
if (!this.#serverSettled.has(call.id)) {
|
|
5175
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
6281
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
5176
6282
|
}
|
|
5177
6283
|
return null;
|
|
5178
6284
|
}
|
|
@@ -5184,13 +6290,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5184
6290
|
}
|
|
5185
6291
|
this.#confirmAbort = new AbortController();
|
|
5186
6292
|
const decision = requestConfirmation(this.#messages, request, {
|
|
5187
|
-
signal: this.#confirmAbort.signal
|
|
6293
|
+
signal: this.#confirmAbort.signal,
|
|
6294
|
+
strings: this.#strings
|
|
5188
6295
|
});
|
|
6296
|
+
this.#updateEmptyState();
|
|
5189
6297
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5190
6298
|
const accepted = await decision;
|
|
5191
6299
|
this.#confirmAbort = null;
|
|
5192
6300
|
if (!accepted) {
|
|
5193
|
-
const message =
|
|
6301
|
+
const message = this.#strings.declinedAction;
|
|
5194
6302
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
5195
6303
|
this.#showPending();
|
|
5196
6304
|
return { content: message };
|
|
@@ -5203,7 +6311,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5203
6311
|
try {
|
|
5204
6312
|
const result = await tool.handler(call.args);
|
|
5205
6313
|
if (navigates) {
|
|
5206
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
6314
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
5207
6315
|
return { content: "", halt: true };
|
|
5208
6316
|
}
|
|
5209
6317
|
const content = JSON.stringify(result ?? null);
|
|
@@ -5268,6 +6376,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5268
6376
|
this.#hidePending();
|
|
5269
6377
|
this.#setRunning(false);
|
|
5270
6378
|
this.#streamingBubble = null;
|
|
6379
|
+
this.#runAttachments = [];
|
|
6380
|
+
for (const card of this.#toolCards.values()) {
|
|
6381
|
+
if (!card.settled) {
|
|
6382
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
5271
6385
|
}
|
|
5272
6386
|
};
|
|
5273
6387
|
}
|
|
@@ -5275,9 +6389,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5275
6389
|
#appendStoppedNote() {
|
|
5276
6390
|
const note = document.createElement("div");
|
|
5277
6391
|
note.className = "stopped-note";
|
|
6392
|
+
note.setAttribute("part", "stopped");
|
|
5278
6393
|
note.setAttribute("role", "status");
|
|
5279
|
-
note.textContent =
|
|
6394
|
+
note.textContent = this.#strings.stopped;
|
|
5280
6395
|
this.#messages.appendChild(note);
|
|
6396
|
+
this.#updateEmptyState();
|
|
5281
6397
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5282
6398
|
}
|
|
5283
6399
|
/**
|
|
@@ -5291,8 +6407,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5291
6407
|
}
|
|
5292
6408
|
const pending = document.createElement("div");
|
|
5293
6409
|
pending.className = "pending";
|
|
6410
|
+
pending.setAttribute("part", "pending");
|
|
5294
6411
|
pending.setAttribute("role", "status");
|
|
5295
|
-
pending.setAttribute("aria-label",
|
|
6412
|
+
pending.setAttribute("aria-label", this.#strings.thinking);
|
|
5296
6413
|
for (let i = 0; i < 3; i += 1) {
|
|
5297
6414
|
const dot = document.createElement("span");
|
|
5298
6415
|
dot.className = "pending-dot";
|
|
@@ -5300,6 +6417,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5300
6417
|
}
|
|
5301
6418
|
this.#pending = pending;
|
|
5302
6419
|
this.#messages.appendChild(pending);
|
|
6420
|
+
this.#updateEmptyState();
|
|
5303
6421
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5304
6422
|
}
|
|
5305
6423
|
/** Remove the pending indicator if shown. */
|
|
@@ -5329,9 +6447,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5329
6447
|
}
|
|
5330
6448
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
5331
6449
|
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
|
|
5332
|
-
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
|
|
6450
|
+
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
|
|
5333
6451
|
this.#toolCards.set(call.id, card);
|
|
5334
6452
|
this.#messages.appendChild(card.element);
|
|
6453
|
+
this.#updateEmptyState();
|
|
5335
6454
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5336
6455
|
return card;
|
|
5337
6456
|
}
|
|
@@ -5344,130 +6463,6 @@ function defineAgUiChat() {
|
|
|
5344
6463
|
}
|
|
5345
6464
|
}
|
|
5346
6465
|
|
|
5347
|
-
// src/dom/native_setter.ts
|
|
5348
|
-
function prototypeSetter(proto, prop) {
|
|
5349
|
-
const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
|
|
5350
|
-
return setter;
|
|
5351
|
-
}
|
|
5352
|
-
var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
|
|
5353
|
-
var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
|
|
5354
|
-
var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
|
|
5355
|
-
var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
|
|
5356
|
-
function setNativeValue(el, value) {
|
|
5357
|
-
if (el instanceof HTMLTextAreaElement) {
|
|
5358
|
-
setTextareaValue.call(el, value);
|
|
5359
|
-
} else if (el instanceof HTMLSelectElement) {
|
|
5360
|
-
setSelectValue.call(el, value);
|
|
5361
|
-
} else {
|
|
5362
|
-
setInputValue.call(el, value);
|
|
5363
|
-
}
|
|
5364
|
-
}
|
|
5365
|
-
function setNativeChecked(el, checked) {
|
|
5366
|
-
setInputChecked.call(el, checked);
|
|
5367
|
-
}
|
|
5368
|
-
|
|
5369
|
-
// src/dom/animations.ts
|
|
5370
|
-
var ACCENT = "#4f46e5";
|
|
5371
|
-
function delay(ms) {
|
|
5372
|
-
return new Promise((resolve) => {
|
|
5373
|
-
setTimeout(resolve, ms);
|
|
5374
|
-
});
|
|
5375
|
-
}
|
|
5376
|
-
async function typeInto(el, value, options = {}) {
|
|
5377
|
-
const charDelayMs = options.charDelayMs ?? 35;
|
|
5378
|
-
setNativeValue(el, "");
|
|
5379
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5380
|
-
for (const char of value) {
|
|
5381
|
-
setNativeValue(el, el.value + char);
|
|
5382
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5383
|
-
if (charDelayMs > 0) {
|
|
5384
|
-
await delay(charDelayMs);
|
|
5385
|
-
}
|
|
5386
|
-
}
|
|
5387
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5388
|
-
}
|
|
5389
|
-
async function highlightThenClick(el, options = {}) {
|
|
5390
|
-
const highlightMs = options.highlightMs ?? 280;
|
|
5391
|
-
const previousOutline = el.style.outline;
|
|
5392
|
-
const previousOffset = el.style.outlineOffset;
|
|
5393
|
-
el.style.outline = `2px solid ${ACCENT}`;
|
|
5394
|
-
el.style.outlineOffset = "2px";
|
|
5395
|
-
await delay(highlightMs);
|
|
5396
|
-
el.style.outline = previousOutline;
|
|
5397
|
-
el.style.outlineOffset = previousOffset;
|
|
5398
|
-
el.click();
|
|
5399
|
-
}
|
|
5400
|
-
function scrollIntoCenterView(el) {
|
|
5401
|
-
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
5402
|
-
}
|
|
5403
|
-
async function focusWithFlash(el, options = {}) {
|
|
5404
|
-
const flashMs = options.flashMs ?? 200;
|
|
5405
|
-
el.focus();
|
|
5406
|
-
const previousShadow = el.style.boxShadow;
|
|
5407
|
-
el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
|
|
5408
|
-
await delay(flashMs);
|
|
5409
|
-
el.style.boxShadow = previousShadow;
|
|
5410
|
-
}
|
|
5411
|
-
var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
|
|
5412
|
-
function prefersReducedMotion() {
|
|
5413
|
-
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5414
|
-
}
|
|
5415
|
-
function motionDelay(ms) {
|
|
5416
|
-
if (ms <= 0 || prefersReducedMotion()) {
|
|
5417
|
-
return Promise.resolve();
|
|
5418
|
-
}
|
|
5419
|
-
return delay(ms);
|
|
5420
|
-
}
|
|
5421
|
-
async function pressThenClick(el, options = {}) {
|
|
5422
|
-
const pressMs = options.pressMs ?? 140;
|
|
5423
|
-
const previousTransform = el.style.transform;
|
|
5424
|
-
const previousTransition = el.style.transition;
|
|
5425
|
-
const previousShadow = el.style.boxShadow;
|
|
5426
|
-
el.style.transition = "transform 80ms ease";
|
|
5427
|
-
el.style.transform = "scale(0.96)";
|
|
5428
|
-
el.style.boxShadow = RING;
|
|
5429
|
-
await motionDelay(pressMs);
|
|
5430
|
-
el.style.transform = previousTransform;
|
|
5431
|
-
el.style.transition = previousTransition;
|
|
5432
|
-
el.style.boxShadow = previousShadow;
|
|
5433
|
-
el.click();
|
|
5434
|
-
}
|
|
5435
|
-
function findOption(el, value) {
|
|
5436
|
-
for (const option of Array.from(el.options)) {
|
|
5437
|
-
if (option.value === value || option.text === value) {
|
|
5438
|
-
return option;
|
|
5439
|
-
}
|
|
5440
|
-
}
|
|
5441
|
-
return null;
|
|
5442
|
-
}
|
|
5443
|
-
async function selectOption(el, value, options = {}) {
|
|
5444
|
-
const option = findOption(el, value);
|
|
5445
|
-
if (option === null) {
|
|
5446
|
-
throw new Error(`no <option> matching "${value}"`);
|
|
5447
|
-
}
|
|
5448
|
-
const highlightMs = options.highlightMs ?? 220;
|
|
5449
|
-
const previousOutline = el.style.outline;
|
|
5450
|
-
const previousOffset = el.style.outlineOffset;
|
|
5451
|
-
el.style.outline = `2px solid ${ACCENT}`;
|
|
5452
|
-
el.style.outlineOffset = "2px";
|
|
5453
|
-
await motionDelay(highlightMs);
|
|
5454
|
-
setNativeValue(el, option.value);
|
|
5455
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5456
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5457
|
-
el.style.outline = previousOutline;
|
|
5458
|
-
el.style.outlineOffset = previousOffset;
|
|
5459
|
-
}
|
|
5460
|
-
async function toggleControl(el, checked, options = {}) {
|
|
5461
|
-
const flashMs = options.flashMs ?? 200;
|
|
5462
|
-
const previousShadow = el.style.boxShadow;
|
|
5463
|
-
el.style.boxShadow = RING;
|
|
5464
|
-
await motionDelay(flashMs);
|
|
5465
|
-
setNativeChecked(el, checked);
|
|
5466
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5467
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5468
|
-
el.style.boxShadow = previousShadow;
|
|
5469
|
-
}
|
|
5470
|
-
|
|
5471
6466
|
// src/dom/dom_driver.ts
|
|
5472
6467
|
async function fillField(el, value, options = {}) {
|
|
5473
6468
|
scrollIntoCenterView(el);
|
|
@@ -5501,14 +6496,17 @@ function setControlValue(el, value) {
|
|
|
5501
6496
|
}
|
|
5502
6497
|
|
|
5503
6498
|
// src/version.ts
|
|
5504
|
-
var VERSION = "0.
|
|
6499
|
+
var VERSION = "0.7.0";
|
|
5505
6500
|
export {
|
|
5506
6501
|
AgUiChat,
|
|
5507
6502
|
AgUiClient,
|
|
5508
6503
|
ClientToolRegistry,
|
|
6504
|
+
ConnectionLostError,
|
|
6505
|
+
DEFAULT_UI_STRINGS,
|
|
5509
6506
|
ELEMENT_TAG,
|
|
5510
6507
|
MAX_TOOL_ROUNDS,
|
|
5511
6508
|
MESSAGE_ROLE,
|
|
6509
|
+
PAGE_ACTIONS,
|
|
5512
6510
|
RemoteConversationStore,
|
|
5513
6511
|
SUBMIT_EVENT,
|
|
5514
6512
|
SessionStorageStore,
|
|
@@ -5523,6 +6521,7 @@ export {
|
|
|
5523
6521
|
X_SUMMARY_KEY,
|
|
5524
6522
|
clickElement,
|
|
5525
6523
|
createHttpAgent,
|
|
6524
|
+
createPageActionTools,
|
|
5526
6525
|
createPageMapContext,
|
|
5527
6526
|
createRouteTools,
|
|
5528
6527
|
createStateHookTools,
|
|
@@ -5532,6 +6531,8 @@ export {
|
|
|
5532
6531
|
highlightThenClick,
|
|
5533
6532
|
isDestructive,
|
|
5534
6533
|
isNavigates,
|
|
6534
|
+
mergeUiStrings,
|
|
6535
|
+
messageAttachments,
|
|
5535
6536
|
parseToolCatalog,
|
|
5536
6537
|
prefersReducedMotion,
|
|
5537
6538
|
pressButton,
|
|
@@ -5547,7 +6548,8 @@ export {
|
|
|
5547
6548
|
setNativeValue,
|
|
5548
6549
|
toggleCheckbox,
|
|
5549
6550
|
toggleControl,
|
|
5550
|
-
typeInto
|
|
6551
|
+
typeInto,
|
|
6552
|
+
uploadAttachment
|
|
5551
6553
|
};
|
|
5552
6554
|
/*! Bundled license information:
|
|
5553
6555
|
|