@artooi/ag-ui-web-component 0.6.0 → 0.8.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 +180 -9
- package/dist/ag-ui-web-component.bundle.js +268 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +20 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +16 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +847 -237
- package/dist/index.js.map +3 -3
- 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_tray.d.ts +3 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- 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 +19 -8
- 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 +5 -0
- package/src/core/ag_ui_chat.ts +267 -46
- package/src/core/agui_client.ts +33 -2
- package/src/index.ts +7 -0
- package/src/tools/page_action_tools.ts +130 -0
- package/src/ui/attachment_tray.ts +13 -7
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/relative_time.ts +15 -8
- package/src/ui/styles.ts +221 -0
- package/src/ui/thread_drawer.ts +53 -25
- package/src/ui/tool_call_card.ts +63 -25
- package/src/ui/ui_strings.ts +208 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var ATTACHMENT_STATUS = {
|
|
|
24
24
|
};
|
|
25
25
|
var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
26
26
|
var TOOL_DISPLAY = {
|
|
27
|
+
INLINE: "inline",
|
|
27
28
|
MINIMAL: "minimal",
|
|
28
29
|
COMPACT: "compact",
|
|
29
30
|
FULL: "full"
|
|
@@ -104,6 +105,211 @@ function isNavigates(parameters) {
|
|
|
104
105
|
return parameters[X_NAVIGATES_KEY] === true;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// src/dom/native_setter.ts
|
|
109
|
+
function prototypeSetter(proto, prop) {
|
|
110
|
+
const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
|
|
111
|
+
return setter;
|
|
112
|
+
}
|
|
113
|
+
var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
|
|
114
|
+
var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
|
|
115
|
+
var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
|
|
116
|
+
var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
|
|
117
|
+
function setNativeValue(el, value) {
|
|
118
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
119
|
+
setTextareaValue.call(el, value);
|
|
120
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
121
|
+
setSelectValue.call(el, value);
|
|
122
|
+
} else {
|
|
123
|
+
setInputValue.call(el, value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function setNativeChecked(el, checked) {
|
|
127
|
+
setInputChecked.call(el, checked);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/dom/animations.ts
|
|
131
|
+
var ACCENT = "#4f46e5";
|
|
132
|
+
function delay(ms) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
setTimeout(resolve, ms);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async function typeInto(el, value, options = {}) {
|
|
138
|
+
const charDelayMs = options.charDelayMs ?? 35;
|
|
139
|
+
setNativeValue(el, "");
|
|
140
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
141
|
+
for (const char of value) {
|
|
142
|
+
setNativeValue(el, el.value + char);
|
|
143
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
144
|
+
if (charDelayMs > 0) {
|
|
145
|
+
await delay(charDelayMs);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
149
|
+
}
|
|
150
|
+
async function highlightThenClick(el, options = {}) {
|
|
151
|
+
const highlightMs = options.highlightMs ?? 280;
|
|
152
|
+
const previousOutline = el.style.outline;
|
|
153
|
+
const previousOffset = el.style.outlineOffset;
|
|
154
|
+
el.style.outline = `2px solid ${ACCENT}`;
|
|
155
|
+
el.style.outlineOffset = "2px";
|
|
156
|
+
await delay(highlightMs);
|
|
157
|
+
el.style.outline = previousOutline;
|
|
158
|
+
el.style.outlineOffset = previousOffset;
|
|
159
|
+
el.click();
|
|
160
|
+
}
|
|
161
|
+
function scrollIntoCenterView(el) {
|
|
162
|
+
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
163
|
+
}
|
|
164
|
+
async function focusWithFlash(el, options = {}) {
|
|
165
|
+
const flashMs = options.flashMs ?? 200;
|
|
166
|
+
el.focus();
|
|
167
|
+
const previousShadow = el.style.boxShadow;
|
|
168
|
+
el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
|
|
169
|
+
await delay(flashMs);
|
|
170
|
+
el.style.boxShadow = previousShadow;
|
|
171
|
+
}
|
|
172
|
+
var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
|
|
173
|
+
function prefersReducedMotion() {
|
|
174
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
175
|
+
}
|
|
176
|
+
function motionDelay(ms) {
|
|
177
|
+
if (ms <= 0 || prefersReducedMotion()) {
|
|
178
|
+
return Promise.resolve();
|
|
179
|
+
}
|
|
180
|
+
return delay(ms);
|
|
181
|
+
}
|
|
182
|
+
async function pressThenClick(el, options = {}) {
|
|
183
|
+
const pressMs = options.pressMs ?? 140;
|
|
184
|
+
const previousTransform = el.style.transform;
|
|
185
|
+
const previousTransition = el.style.transition;
|
|
186
|
+
const previousShadow = el.style.boxShadow;
|
|
187
|
+
el.style.transition = "transform 80ms ease";
|
|
188
|
+
el.style.transform = "scale(0.96)";
|
|
189
|
+
el.style.boxShadow = RING;
|
|
190
|
+
await motionDelay(pressMs);
|
|
191
|
+
el.style.transform = previousTransform;
|
|
192
|
+
el.style.transition = previousTransition;
|
|
193
|
+
el.style.boxShadow = previousShadow;
|
|
194
|
+
el.click();
|
|
195
|
+
}
|
|
196
|
+
function findOption(el, value) {
|
|
197
|
+
for (const option of Array.from(el.options)) {
|
|
198
|
+
if (option.value === value || option.text === value) {
|
|
199
|
+
return option;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
async function selectOption(el, value, options = {}) {
|
|
205
|
+
const option = findOption(el, value);
|
|
206
|
+
if (option === null) {
|
|
207
|
+
throw new Error(`no <option> matching "${value}"`);
|
|
208
|
+
}
|
|
209
|
+
const highlightMs = options.highlightMs ?? 220;
|
|
210
|
+
const previousOutline = el.style.outline;
|
|
211
|
+
const previousOffset = el.style.outlineOffset;
|
|
212
|
+
el.style.outline = `2px solid ${ACCENT}`;
|
|
213
|
+
el.style.outlineOffset = "2px";
|
|
214
|
+
await motionDelay(highlightMs);
|
|
215
|
+
setNativeValue(el, option.value);
|
|
216
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
217
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
218
|
+
el.style.outline = previousOutline;
|
|
219
|
+
el.style.outlineOffset = previousOffset;
|
|
220
|
+
}
|
|
221
|
+
async function toggleControl(el, checked, options = {}) {
|
|
222
|
+
const flashMs = options.flashMs ?? 200;
|
|
223
|
+
const previousShadow = el.style.boxShadow;
|
|
224
|
+
el.style.boxShadow = RING;
|
|
225
|
+
await motionDelay(flashMs);
|
|
226
|
+
setNativeChecked(el, checked);
|
|
227
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
228
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
229
|
+
el.style.boxShadow = previousShadow;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/tools/page_action_tools.ts
|
|
233
|
+
var PAGE_ACTIONS = {
|
|
234
|
+
SCROLL: "scroll",
|
|
235
|
+
DRAG: "drag"
|
|
236
|
+
};
|
|
237
|
+
function createPageActionTools(enabled, resolveTarget) {
|
|
238
|
+
const tools = [];
|
|
239
|
+
if (enabled.has(PAGE_ACTIONS.SCROLL)) {
|
|
240
|
+
tools.push(scrollTool(resolveTarget));
|
|
241
|
+
}
|
|
242
|
+
if (enabled.has(PAGE_ACTIONS.DRAG)) {
|
|
243
|
+
tools.push(dragTool(resolveTarget));
|
|
244
|
+
}
|
|
245
|
+
return tools;
|
|
246
|
+
}
|
|
247
|
+
function scrollTool(resolveTarget) {
|
|
248
|
+
return {
|
|
249
|
+
name: "scroll_to",
|
|
250
|
+
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.",
|
|
251
|
+
parameters: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: { target: { type: "string" } },
|
|
254
|
+
required: ["target"],
|
|
255
|
+
[X_SUMMARY_KEY]: "Scroll into view"
|
|
256
|
+
},
|
|
257
|
+
handler: (args) => {
|
|
258
|
+
const target = String(args["target"] ?? "");
|
|
259
|
+
if (target === "top" || target === "bottom") {
|
|
260
|
+
const top = target === "top" ? 0 : document.body.scrollHeight;
|
|
261
|
+
window.scrollTo({ top, behavior: "smooth" });
|
|
262
|
+
return { scrolled: true, target };
|
|
263
|
+
}
|
|
264
|
+
const element = resolveTarget(target);
|
|
265
|
+
if (element === null) {
|
|
266
|
+
throw new Error(`no element matching "${target}"`);
|
|
267
|
+
}
|
|
268
|
+
scrollIntoCenterView(element);
|
|
269
|
+
return { scrolled: true, target };
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function dragTool(resolveTarget) {
|
|
274
|
+
return {
|
|
275
|
+
name: "drag_and_drop",
|
|
276
|
+
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.",
|
|
277
|
+
parameters: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: { from: { type: "string" }, to: { type: "string" } },
|
|
280
|
+
required: ["from", "to"],
|
|
281
|
+
[X_SUMMARY_KEY]: "Drag and drop"
|
|
282
|
+
},
|
|
283
|
+
handler: (args) => {
|
|
284
|
+
const fromTarget = String(args["from"] ?? "");
|
|
285
|
+
const toTarget = String(args["to"] ?? "");
|
|
286
|
+
const from = resolveTarget(fromTarget);
|
|
287
|
+
if (from === null) {
|
|
288
|
+
throw new Error(`no element matching "${fromTarget}"`);
|
|
289
|
+
}
|
|
290
|
+
const to = resolveTarget(toTarget);
|
|
291
|
+
if (to === null) {
|
|
292
|
+
throw new Error(`no element matching "${toTarget}"`);
|
|
293
|
+
}
|
|
294
|
+
dispatchDragSequence(from, to);
|
|
295
|
+
return { dragged: true, from: fromTarget, to: toTarget };
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function dispatchDragSequence(from, to) {
|
|
300
|
+
const dataTransfer = new DataTransfer();
|
|
301
|
+
fire(from, "dragstart", dataTransfer);
|
|
302
|
+
fire(to, "dragenter", dataTransfer);
|
|
303
|
+
fire(to, "dragover", dataTransfer);
|
|
304
|
+
fire(to, "drop", dataTransfer);
|
|
305
|
+
fire(from, "dragend", dataTransfer);
|
|
306
|
+
}
|
|
307
|
+
function fire(target, type, dataTransfer) {
|
|
308
|
+
const event = new Event(type, { bubbles: true, cancelable: true });
|
|
309
|
+
event.dataTransfer = dataTransfer;
|
|
310
|
+
target.dispatchEvent(event);
|
|
311
|
+
}
|
|
312
|
+
|
|
107
313
|
// src/tools/page_map.ts
|
|
108
314
|
function createPageMapContext(getPageMap, autoInject) {
|
|
109
315
|
if (!autoInject || getPageMap === null) {
|
|
@@ -292,15 +498,83 @@ function formatBytes(bytes) {
|
|
|
292
498
|
|
|
293
499
|
// src/ui/attachment_tray.ts
|
|
294
500
|
import { randomUUID } from "@ag-ui/client";
|
|
501
|
+
|
|
502
|
+
// src/ui/ui_strings.ts
|
|
503
|
+
var DEFAULT_UI_STRINGS = {
|
|
504
|
+
title: "Assistant",
|
|
505
|
+
chatHistory: "Chat history",
|
|
506
|
+
newChat: "New chat",
|
|
507
|
+
collapse: "Collapse",
|
|
508
|
+
expand: "Expand",
|
|
509
|
+
conversation: "Conversation",
|
|
510
|
+
thinking: "Assistant is thinking\u2026",
|
|
511
|
+
stopped: "\u23F9 Stopped",
|
|
512
|
+
connectionLost: "Connection lost",
|
|
513
|
+
noResult: "No result returned.",
|
|
514
|
+
declinedAction: "User declined the action.",
|
|
515
|
+
navigating: "Navigating\u2026",
|
|
516
|
+
skillNeeds: "\u201C{title}\u201D needs: {fields}",
|
|
517
|
+
message: "Message",
|
|
518
|
+
inputPlaceholder: "Ask anything\u2026",
|
|
519
|
+
send: "Send",
|
|
520
|
+
stop: "Stop",
|
|
521
|
+
attachFiles: "Attach files",
|
|
522
|
+
toolRunning: "running\u2026",
|
|
523
|
+
toolDone: "\u2713 done",
|
|
524
|
+
toolError: "\u26A0 error",
|
|
525
|
+
toolDeclined: "\u2298 declined",
|
|
526
|
+
resultLabel: "Result",
|
|
527
|
+
errorLabel: "Error",
|
|
528
|
+
declinedLabel: "Declined",
|
|
529
|
+
details: "Details",
|
|
530
|
+
confirmAction: "Confirm action",
|
|
531
|
+
confirmRun: "Run \u201C{tool}\u201D?",
|
|
532
|
+
confirm: "Confirm",
|
|
533
|
+
cancel: "Cancel",
|
|
534
|
+
chats: "Chats",
|
|
535
|
+
noConversations: "No conversations yet.",
|
|
536
|
+
rename: "Rename",
|
|
537
|
+
renameConversation: "Rename conversation",
|
|
538
|
+
delete: "Delete",
|
|
539
|
+
deleteConversation: "Delete conversation",
|
|
540
|
+
deletePrompt: "Delete?",
|
|
541
|
+
tooLarge: "Too large (max {size})",
|
|
542
|
+
fileTypeNotAllowed: "File type not allowed",
|
|
543
|
+
uploadFailed: "upload failed",
|
|
544
|
+
retry: "Retry",
|
|
545
|
+
retryUpload: "Retry upload",
|
|
546
|
+
remove: "Remove",
|
|
547
|
+
removeAttachment: "Remove attachment",
|
|
548
|
+
justNow: "just now",
|
|
549
|
+
minutesAgo: "{n}m ago",
|
|
550
|
+
hoursAgo: "{n}h ago",
|
|
551
|
+
daysAgo: "{n}d ago",
|
|
552
|
+
weeksAgo: "{n}w ago"
|
|
553
|
+
};
|
|
554
|
+
function mergeUiStrings(overrides) {
|
|
555
|
+
const merged = { ...DEFAULT_UI_STRINGS };
|
|
556
|
+
for (const key of Object.keys(overrides)) {
|
|
557
|
+
const value = overrides[key];
|
|
558
|
+
if (value !== void 0) {
|
|
559
|
+
merged[key] = value;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return merged;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/ui/attachment_tray.ts
|
|
295
566
|
var AttachmentTray = class {
|
|
296
567
|
/** The tray root; append above the input row. Hidden while empty. */
|
|
297
568
|
element;
|
|
298
569
|
#config;
|
|
570
|
+
#strings;
|
|
299
571
|
#items = [];
|
|
300
572
|
constructor(config) {
|
|
301
573
|
this.#config = config;
|
|
574
|
+
this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
|
|
302
575
|
this.element = document.createElement("div");
|
|
303
576
|
this.element.className = "attachment-tray";
|
|
577
|
+
this.element.setAttribute("part", "attachment-tray");
|
|
304
578
|
this.element.hidden = true;
|
|
305
579
|
}
|
|
306
580
|
/** Queue a file: reject oversize/disallowed into an error chip, else upload. */
|
|
@@ -357,10 +631,10 @@ var AttachmentTray = class {
|
|
|
357
631
|
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
358
632
|
#reject(file) {
|
|
359
633
|
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
360
|
-
return
|
|
634
|
+
return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
|
|
361
635
|
}
|
|
362
636
|
if (!accepts(this.#config.accept, file)) {
|
|
363
|
-
return
|
|
637
|
+
return this.#strings.fileTypeNotAllowed;
|
|
364
638
|
}
|
|
365
639
|
return null;
|
|
366
640
|
}
|
|
@@ -377,7 +651,7 @@ var AttachmentTray = class {
|
|
|
377
651
|
item.ref = ref;
|
|
378
652
|
}).catch((error) => {
|
|
379
653
|
item.status = ATTACHMENT_STATUS.ERROR;
|
|
380
|
-
item.error = error instanceof Error ? error.message :
|
|
654
|
+
item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
|
|
381
655
|
}).finally(() => {
|
|
382
656
|
this.#render();
|
|
383
657
|
this.#config.onChange?.();
|
|
@@ -423,8 +697,8 @@ var AttachmentTray = class {
|
|
|
423
697
|
const retry = document.createElement("button");
|
|
424
698
|
retry.type = "button";
|
|
425
699
|
retry.className = "attachment-chip-retry";
|
|
426
|
-
retry.title =
|
|
427
|
-
retry.setAttribute("aria-label",
|
|
700
|
+
retry.title = this.#strings.retry;
|
|
701
|
+
retry.setAttribute("aria-label", this.#strings.retryUpload);
|
|
428
702
|
retry.textContent = "\u21BB";
|
|
429
703
|
retry.addEventListener("click", () => this.#upload(item));
|
|
430
704
|
chip.appendChild(retry);
|
|
@@ -432,8 +706,8 @@ var AttachmentTray = class {
|
|
|
432
706
|
const remove = document.createElement("button");
|
|
433
707
|
remove.type = "button";
|
|
434
708
|
remove.className = "attachment-chip-remove";
|
|
435
|
-
remove.title =
|
|
436
|
-
remove.setAttribute("aria-label",
|
|
709
|
+
remove.title = this.#strings.remove;
|
|
710
|
+
remove.setAttribute("aria-label", this.#strings.removeAttachment);
|
|
437
711
|
remove.textContent = "\u2715";
|
|
438
712
|
remove.addEventListener("click", () => this.#remove(item));
|
|
439
713
|
chip.appendChild(remove);
|
|
@@ -463,26 +737,32 @@ function actionButton(modifier, label) {
|
|
|
463
737
|
const button = document.createElement("button");
|
|
464
738
|
button.type = "button";
|
|
465
739
|
button.className = `confirm-btn confirm-btn--${modifier}`;
|
|
740
|
+
button.setAttribute("part", `confirm-button confirm-${modifier}`);
|
|
466
741
|
button.textContent = label;
|
|
467
742
|
return button;
|
|
468
743
|
}
|
|
469
744
|
function requestConfirmation(host, request, options = {}) {
|
|
745
|
+
const strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
470
746
|
return new Promise((resolve) => {
|
|
471
747
|
const card = document.createElement("div");
|
|
472
748
|
card.className = "confirm";
|
|
749
|
+
card.setAttribute("part", "confirm");
|
|
473
750
|
card.setAttribute("data-tool-name", request.toolName);
|
|
474
751
|
card.setAttribute("role", "group");
|
|
475
|
-
card.setAttribute("aria-label",
|
|
752
|
+
card.setAttribute("aria-label", strings.confirmAction);
|
|
476
753
|
const body = document.createElement("div");
|
|
477
754
|
body.className = "confirm-body";
|
|
478
|
-
body.
|
|
755
|
+
body.setAttribute("part", "confirm-body");
|
|
756
|
+
body.textContent = request.message ?? strings.confirmRun.replace("{tool}", request.toolName);
|
|
479
757
|
const args = document.createElement("pre");
|
|
480
758
|
args.className = "confirm-args";
|
|
759
|
+
args.setAttribute("part", "confirm-args");
|
|
481
760
|
args.textContent = JSON.stringify(request.args, null, 2);
|
|
482
761
|
const actions = document.createElement("div");
|
|
483
762
|
actions.className = "confirm-actions";
|
|
484
|
-
|
|
485
|
-
const
|
|
763
|
+
actions.setAttribute("part", "confirm-actions");
|
|
764
|
+
const cancel = actionButton("cancel", strings.cancel);
|
|
765
|
+
const confirm = actionButton("confirm", strings.confirm);
|
|
486
766
|
let settled = false;
|
|
487
767
|
const close = (accepted) => {
|
|
488
768
|
if (settled) {
|
|
@@ -3209,6 +3489,17 @@ var STYLES = `
|
|
|
3209
3489
|
--ag-ui-danger: #b91c1c;
|
|
3210
3490
|
--ag-ui-muted: #6b7280;
|
|
3211
3491
|
|
|
3492
|
+
/* Tool-call status icon glyphs (override to re-theme) + spinner speed.
|
|
3493
|
+
The pending state is the animated ring; the settled states use these. */
|
|
3494
|
+
--ag-ui-tool-icon-done: "\u2713";
|
|
3495
|
+
--ag-ui-tool-icon-error: "\u2715";
|
|
3496
|
+
--ag-ui-tool-icon-declined: "\u2298";
|
|
3497
|
+
--ag-ui-tool-spin-duration: 0.7s;
|
|
3498
|
+
|
|
3499
|
+
/* Answer well (opt-in via data-answer-well) \u2014 boxes a whole assistant turn. */
|
|
3500
|
+
--ag-ui-well-bg: transparent;
|
|
3501
|
+
--ag-ui-well-border: var(--ag-ui-border);
|
|
3502
|
+
|
|
3212
3503
|
/* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */
|
|
3213
3504
|
--ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
|
|
3214
3505
|
--ag-ui-font: inherit;
|
|
@@ -3231,6 +3522,8 @@ var STYLES = `
|
|
|
3231
3522
|
--ag-ui-inset: auto 24px 24px auto;
|
|
3232
3523
|
--ag-ui-max-width: calc(100vw - 48px);
|
|
3233
3524
|
--ag-ui-max-height: calc(100vh - 48px);
|
|
3525
|
+
/* Reading-column width for placement="page" (full-bleed, centred content). */
|
|
3526
|
+
--ag-ui-content-max-width: 820px;
|
|
3234
3527
|
|
|
3235
3528
|
position: var(--ag-ui-position);
|
|
3236
3529
|
inset: var(--ag-ui-inset);
|
|
@@ -3318,6 +3611,99 @@ var STYLES = `
|
|
|
3318
3611
|
--ag-ui-radius: 0;
|
|
3319
3612
|
}
|
|
3320
3613
|
|
|
3614
|
+
/* Page (PAGE-1): full-bleed background with a centred reading column. Unlike
|
|
3615
|
+
"full" (edge-to-edge, left-aligned messages) the content sits in a column
|
|
3616
|
+
capped at --ag-ui-content-max-width. The column is produced by symmetric auto
|
|
3617
|
+
padding on the scroll area + composer (no per-row wrapper), so user pills
|
|
3618
|
+
still right-align and the assistant well spans the column. */
|
|
3619
|
+
:host([placement="page"]) {
|
|
3620
|
+
--ag-ui-inset: 0;
|
|
3621
|
+
--ag-ui-width: 100vw;
|
|
3622
|
+
--ag-ui-height: 100vh;
|
|
3623
|
+
--ag-ui-max-width: 100vw;
|
|
3624
|
+
--ag-ui-max-height: 100vh;
|
|
3625
|
+
--ag-ui-radius: 0;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
:host([placement="page"]) .messages {
|
|
3629
|
+
padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
:host([placement="page"]) .input-row {
|
|
3633
|
+
padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
/* In the reading column the assistant well uses the full width; the user
|
|
3637
|
+
message stays a right-aligned pill (its default align-self + max-width). */
|
|
3638
|
+
:host([placement="page"]) .message--assistant {
|
|
3639
|
+
max-width: 100%;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
/* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
|
|
3643
|
+
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
3644
|
+
default; data-side="left" docks it left. Overlay by default \u2014 set
|
|
3645
|
+
--ag-ui-position: static (and place this element in your own layout) for a
|
|
3646
|
+
host-managed push instead. */
|
|
3647
|
+
:host([placement="sidebar"]) {
|
|
3648
|
+
--ag-ui-inset: 0 0 0 auto;
|
|
3649
|
+
--ag-ui-width: 420px;
|
|
3650
|
+
--ag-ui-height: 100vh;
|
|
3651
|
+
--ag-ui-max-height: 100vh;
|
|
3652
|
+
--ag-ui-radius: 0;
|
|
3653
|
+
--ag-ui-rail-width: 52px;
|
|
3654
|
+
transition: width 0.28s ease;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
:host([placement="sidebar"][data-side="left"]) {
|
|
3658
|
+
--ag-ui-inset: 0 auto 0 0;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
:host([placement="sidebar"]) .chat {
|
|
3662
|
+
transition: transform 0.28s ease;
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
/* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
|
|
3666
|
+
reveal the rail. Higher specificity than the generic collapse rules, so it
|
|
3667
|
+
wins regardless of source order. */
|
|
3668
|
+
:host([placement="sidebar"][collapsed]) {
|
|
3669
|
+
width: var(--ag-ui-rail-width);
|
|
3670
|
+
height: 100vh;
|
|
3671
|
+
max-height: 100vh;
|
|
3672
|
+
bottom: 0;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
:host([placement="sidebar"][collapsed]) .chat {
|
|
3676
|
+
display: none;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
/* The rail is a sibling of the panel (so it survives the panel being hidden);
|
|
3680
|
+
shown only for a collapsed sidebar. */
|
|
3681
|
+
.rail {
|
|
3682
|
+
display: none;
|
|
3683
|
+
border: none;
|
|
3684
|
+
font: inherit;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
:host([placement="sidebar"][collapsed]) .rail {
|
|
3688
|
+
display: flex;
|
|
3689
|
+
position: absolute;
|
|
3690
|
+
inset: 0;
|
|
3691
|
+
align-items: flex-start;
|
|
3692
|
+
justify-content: center;
|
|
3693
|
+
padding-top: 16px;
|
|
3694
|
+
border: 1px solid var(--ag-ui-border);
|
|
3695
|
+
background: var(--ag-ui-header-bg);
|
|
3696
|
+
color: var(--ag-ui-header-fg);
|
|
3697
|
+
cursor: pointer;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3701
|
+
:host([placement="sidebar"]),
|
|
3702
|
+
:host([placement="sidebar"]) .chat {
|
|
3703
|
+
transition: none;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3321
3707
|
/* Embedded: drop the floating chrome and the high z-index stacking context so
|
|
3322
3708
|
the widget lives in the host's own layout (fixes overlay/z-index clashes). */
|
|
3323
3709
|
:host([placement="embedded"]) {
|
|
@@ -3355,12 +3741,33 @@ var STYLES = `
|
|
|
3355
3741
|
}
|
|
3356
3742
|
|
|
3357
3743
|
.header-title {
|
|
3744
|
+
flex: 1;
|
|
3745
|
+
min-width: 0;
|
|
3358
3746
|
font-weight: 600;
|
|
3359
3747
|
overflow: hidden;
|
|
3360
3748
|
text-overflow: ellipsis;
|
|
3361
3749
|
white-space: nowrap;
|
|
3362
3750
|
}
|
|
3363
3751
|
|
|
3752
|
+
/* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
|
|
3753
|
+
fallback, sized via --ag-ui-icon-size. */
|
|
3754
|
+
.icon-holder {
|
|
3755
|
+
display: inline-flex;
|
|
3756
|
+
align-items: center;
|
|
3757
|
+
justify-content: center;
|
|
3758
|
+
flex: none;
|
|
3759
|
+
width: var(--ag-ui-icon-size, 22px);
|
|
3760
|
+
height: var(--ag-ui-icon-size, 22px);
|
|
3761
|
+
line-height: 1;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
.icon-img {
|
|
3765
|
+
width: 100%;
|
|
3766
|
+
height: 100%;
|
|
3767
|
+
object-fit: contain;
|
|
3768
|
+
border-radius: var(--ag-ui-icon-radius, 4px);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3364
3771
|
.header-controls {
|
|
3365
3772
|
display: flex;
|
|
3366
3773
|
gap: 2px;
|
|
@@ -3416,6 +3823,39 @@ var STYLES = `
|
|
|
3416
3823
|
gap: var(--ag-ui-space);
|
|
3417
3824
|
}
|
|
3418
3825
|
|
|
3826
|
+
/* Empty-state region (CUST-1 slot): centred while it's the only thing in the
|
|
3827
|
+
list, hidden as soon as a message, card, or pending indicator renders. */
|
|
3828
|
+
.empty {
|
|
3829
|
+
margin: auto;
|
|
3830
|
+
text-align: center;
|
|
3831
|
+
color: var(--ag-ui-muted);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
.empty[hidden] {
|
|
3835
|
+
display: none;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
/* \u2500\u2500 Answer group / well (WELL-1) \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
|
|
3839
|
+
One .answer per assistant turn wraps the streamed text, its tool cards,
|
|
3840
|
+
and the pending indicator so a whole answer reads (and can be boxed) as one
|
|
3841
|
+
unit. A flex column on the message-list gap, stretched to the list width so
|
|
3842
|
+
its children keep their own left/right alignment. data-answer-well opts into
|
|
3843
|
+
the bordered "well"; without it the layout is today's flat stack. */
|
|
3844
|
+
.answer {
|
|
3845
|
+
display: flex;
|
|
3846
|
+
flex-direction: column;
|
|
3847
|
+
gap: var(--ag-ui-space);
|
|
3848
|
+
align-self: stretch;
|
|
3849
|
+
min-width: 0;
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
:host([data-answer-well]) .answer {
|
|
3853
|
+
padding: var(--ag-ui-pad);
|
|
3854
|
+
background: var(--ag-ui-well-bg);
|
|
3855
|
+
border: 1px solid var(--ag-ui-well-border);
|
|
3856
|
+
border-radius: var(--ag-ui-radius);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3419
3859
|
.message {
|
|
3420
3860
|
max-width: 80%;
|
|
3421
3861
|
padding: var(--ag-ui-msg-pad);
|
|
@@ -3588,10 +4028,71 @@ var STYLES = `
|
|
|
3588
4028
|
}
|
|
3589
4029
|
|
|
3590
4030
|
.tool-call-name {
|
|
4031
|
+
flex: 1;
|
|
4032
|
+
min-width: 0;
|
|
3591
4033
|
font-weight: 600;
|
|
3592
4034
|
word-break: break-word;
|
|
3593
4035
|
}
|
|
3594
4036
|
|
|
4037
|
+
/* Leading status icon (CARD-1). Empty in the DOM \u2014 the glyph/spinner is drawn
|
|
4038
|
+
here from the card's data-status, so it stays themeable. */
|
|
4039
|
+
.tool-call-icon {
|
|
4040
|
+
flex: none;
|
|
4041
|
+
box-sizing: border-box;
|
|
4042
|
+
display: inline-flex;
|
|
4043
|
+
align-items: center;
|
|
4044
|
+
justify-content: center;
|
|
4045
|
+
width: 14px;
|
|
4046
|
+
height: 14px;
|
|
4047
|
+
font-size: 12px;
|
|
4048
|
+
line-height: 1;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
/* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
|
|
4052
|
+
.tool-call[data-status="pending"] .tool-call-icon {
|
|
4053
|
+
border: 2px solid var(--ag-ui-muted);
|
|
4054
|
+
border-top-color: transparent;
|
|
4055
|
+
border-radius: 50%;
|
|
4056
|
+
animation: ag-ui-tool-spin var(--ag-ui-tool-spin-duration) linear infinite;
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
@keyframes ag-ui-tool-spin {
|
|
4060
|
+
to { transform: rotate(360deg); }
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
/* Settled: a themeable glyph coloured by outcome. */
|
|
4064
|
+
.tool-call[data-status="done"] .tool-call-icon::before {
|
|
4065
|
+
content: var(--ag-ui-tool-icon-done);
|
|
4066
|
+
color: var(--ag-ui-success);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
.tool-call[data-status="error"] .tool-call-icon::before {
|
|
4070
|
+
content: var(--ag-ui-tool-icon-error);
|
|
4071
|
+
color: var(--ag-ui-danger);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
.tool-call[data-status="declined"] .tool-call-icon::before {
|
|
4075
|
+
content: var(--ag-ui-tool-icon-declined);
|
|
4076
|
+
color: var(--ag-ui-muted);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4080
|
+
.tool-call[data-status="pending"] .tool-call-icon {
|
|
4081
|
+
animation: none;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
/* Inline display mode (CARD-1): the lightest card \u2014 drop the box chrome so the
|
|
4086
|
+
status row reads as one line of the answer; the result toggle still expands
|
|
4087
|
+
below it. */
|
|
4088
|
+
.tool-call[data-display="inline"] {
|
|
4089
|
+
max-width: 100%;
|
|
4090
|
+
background: transparent;
|
|
4091
|
+
border: none;
|
|
4092
|
+
padding: 2px 0;
|
|
4093
|
+
gap: 2px;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
3595
4096
|
.tool-call-status {
|
|
3596
4097
|
flex: none;
|
|
3597
4098
|
padding: 1px 8px;
|
|
@@ -4126,24 +4627,24 @@ var STYLES = `
|
|
|
4126
4627
|
`;
|
|
4127
4628
|
|
|
4128
4629
|
// src/ui/relative_time.ts
|
|
4129
|
-
function relativeTime(timestamp, now = Date.now()) {
|
|
4630
|
+
function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
|
|
4130
4631
|
const seconds = Math.round((now - timestamp) / 1e3);
|
|
4131
4632
|
if (seconds < 60) {
|
|
4132
|
-
return
|
|
4633
|
+
return strings.justNow;
|
|
4133
4634
|
}
|
|
4134
4635
|
const minutes = Math.round(seconds / 60);
|
|
4135
4636
|
if (minutes < 60) {
|
|
4136
|
-
return
|
|
4637
|
+
return strings.minutesAgo.replace("{n}", String(minutes));
|
|
4137
4638
|
}
|
|
4138
4639
|
const hours = Math.round(minutes / 60);
|
|
4139
4640
|
if (hours < 24) {
|
|
4140
|
-
return
|
|
4641
|
+
return strings.hoursAgo.replace("{n}", String(hours));
|
|
4141
4642
|
}
|
|
4142
4643
|
const days = Math.round(hours / 24);
|
|
4143
4644
|
if (days < 7) {
|
|
4144
|
-
return
|
|
4645
|
+
return strings.daysAgo.replace("{n}", String(days));
|
|
4145
4646
|
}
|
|
4146
|
-
return
|
|
4647
|
+
return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
|
|
4147
4648
|
}
|
|
4148
4649
|
|
|
4149
4650
|
// src/ui/thread_drawer.ts
|
|
@@ -4151,39 +4652,59 @@ var ThreadDrawer = class {
|
|
|
4151
4652
|
/** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
|
|
4152
4653
|
element;
|
|
4153
4654
|
#callbacks;
|
|
4655
|
+
#panel;
|
|
4656
|
+
#heading;
|
|
4657
|
+
#newButton;
|
|
4154
4658
|
#list;
|
|
4659
|
+
#strings;
|
|
4155
4660
|
#threads = [];
|
|
4156
4661
|
#activeId = "";
|
|
4157
|
-
constructor(callbacks) {
|
|
4662
|
+
constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
|
|
4158
4663
|
this.#callbacks = callbacks;
|
|
4664
|
+
this.#strings = strings;
|
|
4159
4665
|
this.element = document.createElement("div");
|
|
4160
4666
|
this.element.className = "drawer";
|
|
4667
|
+
this.element.setAttribute("part", "drawer");
|
|
4161
4668
|
this.element.hidden = true;
|
|
4162
4669
|
const backdrop = document.createElement("div");
|
|
4163
4670
|
backdrop.className = "drawer-backdrop";
|
|
4671
|
+
backdrop.setAttribute("part", "drawer-backdrop");
|
|
4164
4672
|
backdrop.addEventListener("click", () => this.close());
|
|
4165
|
-
|
|
4166
|
-
panel.className = "drawer-panel";
|
|
4167
|
-
panel.setAttribute("
|
|
4168
|
-
panel.setAttribute("
|
|
4673
|
+
this.#panel = document.createElement("div");
|
|
4674
|
+
this.#panel.className = "drawer-panel";
|
|
4675
|
+
this.#panel.setAttribute("part", "drawer-panel");
|
|
4676
|
+
this.#panel.setAttribute("role", "dialog");
|
|
4677
|
+
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
4169
4678
|
const header = document.createElement("div");
|
|
4170
4679
|
header.className = "drawer-header";
|
|
4171
|
-
|
|
4172
|
-
heading
|
|
4173
|
-
heading.
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
newButton
|
|
4177
|
-
newButton.
|
|
4178
|
-
newButton.
|
|
4680
|
+
header.setAttribute("part", "drawer-header");
|
|
4681
|
+
this.#heading = document.createElement("span");
|
|
4682
|
+
this.#heading.className = "drawer-title";
|
|
4683
|
+
this.#heading.setAttribute("part", "drawer-title");
|
|
4684
|
+
this.#heading.textContent = strings.chats;
|
|
4685
|
+
this.#newButton = document.createElement("button");
|
|
4686
|
+
this.#newButton.type = "button";
|
|
4687
|
+
this.#newButton.className = "drawer-new";
|
|
4688
|
+
this.#newButton.setAttribute("part", "drawer-new");
|
|
4689
|
+
this.#newButton.textContent = strings.newChat;
|
|
4690
|
+
this.#newButton.addEventListener("click", () => {
|
|
4179
4691
|
this.close();
|
|
4180
4692
|
this.#callbacks.onNew();
|
|
4181
4693
|
});
|
|
4182
|
-
header.append(heading, newButton);
|
|
4694
|
+
header.append(this.#heading, this.#newButton);
|
|
4183
4695
|
this.#list = document.createElement("div");
|
|
4184
4696
|
this.#list.className = "drawer-list";
|
|
4185
|
-
|
|
4186
|
-
this.
|
|
4697
|
+
this.#list.setAttribute("part", "drawer-list");
|
|
4698
|
+
this.#panel.append(header, this.#list);
|
|
4699
|
+
this.element.append(backdrop, this.#panel);
|
|
4700
|
+
}
|
|
4701
|
+
/** Re-localize the drawer's chrome and rows (the host calls this on connect). */
|
|
4702
|
+
setStrings(strings) {
|
|
4703
|
+
this.#strings = strings;
|
|
4704
|
+
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
4705
|
+
this.#heading.textContent = strings.chats;
|
|
4706
|
+
this.#newButton.textContent = strings.newChat;
|
|
4707
|
+
this.#renderList();
|
|
4187
4708
|
}
|
|
4188
4709
|
isOpen() {
|
|
4189
4710
|
return !this.element.hidden;
|
|
@@ -4208,7 +4729,8 @@ var ThreadDrawer = class {
|
|
|
4208
4729
|
if (this.#threads.length === 0) {
|
|
4209
4730
|
const empty = document.createElement("div");
|
|
4210
4731
|
empty.className = "drawer-empty";
|
|
4211
|
-
empty.
|
|
4732
|
+
empty.setAttribute("part", "drawer-empty");
|
|
4733
|
+
empty.textContent = this.#strings.noConversations;
|
|
4212
4734
|
this.#list.appendChild(empty);
|
|
4213
4735
|
return;
|
|
4214
4736
|
}
|
|
@@ -4219,18 +4741,20 @@ var ThreadDrawer = class {
|
|
|
4219
4741
|
#renderRow(meta) {
|
|
4220
4742
|
const row = document.createElement("div");
|
|
4221
4743
|
row.className = "drawer-row";
|
|
4744
|
+
row.setAttribute("part", "drawer-row");
|
|
4222
4745
|
if (meta.threadId === this.#activeId) {
|
|
4223
4746
|
row.classList.add("drawer-row--active");
|
|
4224
4747
|
}
|
|
4225
4748
|
const select = document.createElement("button");
|
|
4226
4749
|
select.type = "button";
|
|
4227
4750
|
select.className = "drawer-row-select";
|
|
4751
|
+
select.setAttribute("part", "drawer-row-select");
|
|
4228
4752
|
const title = document.createElement("span");
|
|
4229
4753
|
title.className = "drawer-row-title";
|
|
4230
4754
|
title.textContent = meta.title;
|
|
4231
4755
|
const time = document.createElement("span");
|
|
4232
4756
|
time.className = "drawer-row-time";
|
|
4233
|
-
time.textContent = relativeTime(meta.updatedAt);
|
|
4757
|
+
time.textContent = relativeTime(meta.updatedAt, void 0, this.#strings);
|
|
4234
4758
|
const preview = document.createElement("span");
|
|
4235
4759
|
preview.className = "drawer-row-preview";
|
|
4236
4760
|
preview.textContent = meta.preview;
|
|
@@ -4242,15 +4766,15 @@ var ThreadDrawer = class {
|
|
|
4242
4766
|
const rename = document.createElement("button");
|
|
4243
4767
|
rename.type = "button";
|
|
4244
4768
|
rename.className = "drawer-row-rename";
|
|
4245
|
-
rename.title =
|
|
4246
|
-
rename.setAttribute("aria-label",
|
|
4769
|
+
rename.title = this.#strings.rename;
|
|
4770
|
+
rename.setAttribute("aria-label", this.#strings.renameConversation);
|
|
4247
4771
|
rename.textContent = "\u270E";
|
|
4248
4772
|
rename.addEventListener("click", () => this.#startRename(row, meta));
|
|
4249
4773
|
const remove = document.createElement("button");
|
|
4250
4774
|
remove.type = "button";
|
|
4251
4775
|
remove.className = "drawer-row-delete";
|
|
4252
|
-
remove.title =
|
|
4253
|
-
remove.setAttribute("aria-label",
|
|
4776
|
+
remove.title = this.#strings.delete;
|
|
4777
|
+
remove.setAttribute("aria-label", this.#strings.deleteConversation);
|
|
4254
4778
|
remove.textContent = "\u{1F5D1}";
|
|
4255
4779
|
remove.addEventListener("click", () => this.#confirmDelete(row, meta));
|
|
4256
4780
|
const actions = document.createElement("div");
|
|
@@ -4287,16 +4811,16 @@ var ThreadDrawer = class {
|
|
|
4287
4811
|
confirm.className = "drawer-confirm";
|
|
4288
4812
|
const label = document.createElement("span");
|
|
4289
4813
|
label.className = "drawer-confirm-label";
|
|
4290
|
-
label.textContent =
|
|
4814
|
+
label.textContent = this.#strings.deletePrompt;
|
|
4291
4815
|
const yes = document.createElement("button");
|
|
4292
4816
|
yes.type = "button";
|
|
4293
4817
|
yes.className = "drawer-confirm-yes";
|
|
4294
|
-
yes.textContent =
|
|
4818
|
+
yes.textContent = this.#strings.delete;
|
|
4295
4819
|
yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
|
|
4296
4820
|
const no = document.createElement("button");
|
|
4297
4821
|
no.type = "button";
|
|
4298
4822
|
no.className = "drawer-confirm-no";
|
|
4299
|
-
no.textContent =
|
|
4823
|
+
no.textContent = this.#strings.cancel;
|
|
4300
4824
|
no.addEventListener("click", () => this.#renderList());
|
|
4301
4825
|
confirm.append(label, yes, no);
|
|
4302
4826
|
row.replaceChildren(confirm);
|
|
@@ -4304,73 +4828,96 @@ var ThreadDrawer = class {
|
|
|
4304
4828
|
};
|
|
4305
4829
|
|
|
4306
4830
|
// src/ui/tool_call_card.ts
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4831
|
+
function statusLabels(strings) {
|
|
4832
|
+
return {
|
|
4833
|
+
[TOOL_CALL_STATUS.PENDING]: strings.toolRunning,
|
|
4834
|
+
[TOOL_CALL_STATUS.DONE]: strings.toolDone,
|
|
4835
|
+
[TOOL_CALL_STATUS.ERROR]: strings.toolError,
|
|
4836
|
+
[TOOL_CALL_STATUS.DECLINED]: strings.toolDeclined
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4839
|
+
function resultLabels(strings) {
|
|
4840
|
+
return {
|
|
4841
|
+
[TOOL_CALL_STATUS.DONE]: strings.resultLabel,
|
|
4842
|
+
[TOOL_CALL_STATUS.ERROR]: strings.errorLabel,
|
|
4843
|
+
[TOOL_CALL_STATUS.DECLINED]: strings.declinedLabel
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4318
4846
|
var ToolCallCard = class {
|
|
4319
4847
|
/** The card's root element; append this into the message list. */
|
|
4320
4848
|
element;
|
|
4321
4849
|
#status;
|
|
4322
4850
|
#mode;
|
|
4323
4851
|
#args;
|
|
4324
|
-
|
|
4852
|
+
#strings;
|
|
4853
|
+
#settled = false;
|
|
4854
|
+
constructor(name, args, mode = TOOL_DISPLAY.FULL, summary, strings = DEFAULT_UI_STRINGS) {
|
|
4325
4855
|
this.#mode = mode;
|
|
4326
4856
|
this.#args = args;
|
|
4857
|
+
this.#strings = strings;
|
|
4327
4858
|
this.element = document.createElement("div");
|
|
4328
4859
|
this.element.className = "tool-call";
|
|
4860
|
+
this.element.setAttribute("part", "tool-card");
|
|
4329
4861
|
this.element.setAttribute("data-tool-name", name);
|
|
4330
4862
|
this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
|
|
4331
4863
|
this.element.setAttribute("data-display", mode);
|
|
4332
4864
|
const head = document.createElement("div");
|
|
4333
4865
|
head.className = "tool-call-head";
|
|
4866
|
+
head.setAttribute("part", "tool-card-head");
|
|
4867
|
+
const icon = document.createElement("span");
|
|
4868
|
+
icon.className = "tool-call-icon";
|
|
4869
|
+
icon.setAttribute("part", "tool-card-icon");
|
|
4870
|
+
icon.setAttribute("aria-hidden", "true");
|
|
4334
4871
|
const label = document.createElement("span");
|
|
4335
4872
|
label.className = "tool-call-name";
|
|
4336
|
-
label.
|
|
4873
|
+
label.setAttribute("part", "tool-card-name");
|
|
4874
|
+
label.textContent = summary ?? name;
|
|
4337
4875
|
this.#status = document.createElement("span");
|
|
4338
4876
|
this.#status.className = "tool-call-status";
|
|
4339
|
-
this.#status.
|
|
4340
|
-
|
|
4877
|
+
this.#status.setAttribute("part", "tool-card-status");
|
|
4878
|
+
this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
|
|
4879
|
+
head.append(icon, label, this.#status);
|
|
4341
4880
|
this.element.append(head);
|
|
4342
4881
|
if (mode === TOOL_DISPLAY.FULL) {
|
|
4343
4882
|
const argsEl = document.createElement("pre");
|
|
4344
4883
|
argsEl.className = "tool-call-args";
|
|
4884
|
+
argsEl.setAttribute("part", "tool-card-args");
|
|
4345
4885
|
argsEl.textContent = JSON.stringify(args, null, 2);
|
|
4346
4886
|
this.element.append(argsEl);
|
|
4347
4887
|
}
|
|
4348
4888
|
}
|
|
4889
|
+
/** Whether {@link settle} has already run (so a terminal sweep can skip it). */
|
|
4890
|
+
get settled() {
|
|
4891
|
+
return this.#settled;
|
|
4892
|
+
}
|
|
4349
4893
|
/**
|
|
4350
4894
|
* Flip the status pill to ``status`` and, unless in `minimal` mode, append a
|
|
4351
|
-
* collapsed body behind a click-to-expand toggle: the result alone (`full`
|
|
4352
|
-
* or the args + result together (`compact`).
|
|
4895
|
+
* collapsed body behind a click-to-expand toggle: the result alone (`full` /
|
|
4896
|
+
* `inline`), or the args + result together (`compact`).
|
|
4353
4897
|
*/
|
|
4354
4898
|
settle(status, text2) {
|
|
4899
|
+
this.#settled = true;
|
|
4355
4900
|
this.element.setAttribute("data-status", status);
|
|
4356
|
-
this.#status.textContent =
|
|
4901
|
+
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
4357
4902
|
if (this.#mode === TOOL_DISPLAY.MINIMAL) {
|
|
4358
4903
|
return;
|
|
4359
4904
|
}
|
|
4360
4905
|
const toggle = document.createElement("button");
|
|
4361
4906
|
toggle.type = "button";
|
|
4362
4907
|
toggle.className = "tool-call-toggle";
|
|
4908
|
+
toggle.setAttribute("part", "tool-card-toggle");
|
|
4363
4909
|
toggle.setAttribute("aria-expanded", "false");
|
|
4364
4910
|
const output = document.createElement("pre");
|
|
4365
4911
|
output.className = "tool-call-result";
|
|
4912
|
+
output.setAttribute("part", "tool-card-result");
|
|
4366
4913
|
output.hidden = true;
|
|
4367
4914
|
if (this.#mode === TOOL_DISPLAY.COMPACT) {
|
|
4368
|
-
toggle.textContent =
|
|
4915
|
+
toggle.textContent = this.#strings.details;
|
|
4369
4916
|
output.textContent = `args: ${JSON.stringify(this.#args)}
|
|
4370
4917
|
|
|
4371
4918
|
${text2}`;
|
|
4372
4919
|
} else {
|
|
4373
|
-
toggle.textContent =
|
|
4920
|
+
toggle.textContent = resultLabels(this.#strings)[status];
|
|
4374
4921
|
output.textContent = text2;
|
|
4375
4922
|
}
|
|
4376
4923
|
toggle.addEventListener("click", () => {
|
|
@@ -4384,6 +4931,12 @@ ${text2}`;
|
|
|
4384
4931
|
|
|
4385
4932
|
// src/core/agui_client.ts
|
|
4386
4933
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4934
|
+
var ConnectionLostError = class extends Error {
|
|
4935
|
+
constructor(message) {
|
|
4936
|
+
super(message);
|
|
4937
|
+
this.name = "ConnectionLostError";
|
|
4938
|
+
}
|
|
4939
|
+
};
|
|
4387
4940
|
var AgUiClient = class {
|
|
4388
4941
|
#agent;
|
|
4389
4942
|
#handlers;
|
|
@@ -4391,6 +4944,7 @@ var AgUiClient = class {
|
|
|
4391
4944
|
#getContext;
|
|
4392
4945
|
#executeTool;
|
|
4393
4946
|
#onPersist;
|
|
4947
|
+
#connectionLostMessage;
|
|
4394
4948
|
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
4395
4949
|
// a cancel between frontend-tool rounds doesn't start another round.
|
|
4396
4950
|
#cancelled = false;
|
|
@@ -4402,6 +4956,7 @@ var AgUiClient = class {
|
|
|
4402
4956
|
this.#executeTool = config.executeTool ?? null;
|
|
4403
4957
|
this.#onPersist = config.onPersist ?? (() => {
|
|
4404
4958
|
});
|
|
4959
|
+
this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
|
|
4405
4960
|
}
|
|
4406
4961
|
/** Whether a run is currently in flight. */
|
|
4407
4962
|
get running() {
|
|
@@ -4483,14 +5038,18 @@ var AgUiClient = class {
|
|
|
4483
5038
|
return;
|
|
4484
5039
|
}
|
|
4485
5040
|
const pending = [];
|
|
5041
|
+
const runState = { terminal: false };
|
|
4486
5042
|
await this.#agent.runAgent(
|
|
4487
5043
|
{ tools: this.#getTools(), context: this.#getContext() },
|
|
4488
|
-
this.#buildSubscriber(pending)
|
|
5044
|
+
this.#buildSubscriber(pending, runState)
|
|
4489
5045
|
);
|
|
4490
5046
|
this.#onPersist(this.#agent.messages);
|
|
4491
5047
|
if (this.#cancelled) {
|
|
4492
5048
|
return;
|
|
4493
5049
|
}
|
|
5050
|
+
if (!runState.terminal) {
|
|
5051
|
+
throw new ConnectionLostError(this.#connectionLostMessage);
|
|
5052
|
+
}
|
|
4494
5053
|
if (this.#executeTool === null || pending.length === 0) {
|
|
4495
5054
|
return;
|
|
4496
5055
|
}
|
|
@@ -4517,7 +5076,7 @@ var AgUiClient = class {
|
|
|
4517
5076
|
}
|
|
4518
5077
|
}
|
|
4519
5078
|
}
|
|
4520
|
-
#buildSubscriber(pending) {
|
|
5079
|
+
#buildSubscriber(pending, runState) {
|
|
4521
5080
|
const h = this.#handlers;
|
|
4522
5081
|
return {
|
|
4523
5082
|
onRunInitialized() {
|
|
@@ -4542,9 +5101,11 @@ var AgUiClient = class {
|
|
|
4542
5101
|
h.onToolResult(event.toolCallId, event.content);
|
|
4543
5102
|
},
|
|
4544
5103
|
onRunErrorEvent({ event }) {
|
|
5104
|
+
runState.terminal = true;
|
|
4545
5105
|
h.onError(event.message);
|
|
4546
5106
|
},
|
|
4547
5107
|
onRunFinalized() {
|
|
5108
|
+
runState.terminal = true;
|
|
4548
5109
|
h.onRunEnd();
|
|
4549
5110
|
}
|
|
4550
5111
|
};
|
|
@@ -4982,12 +5543,28 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4982
5543
|
* schema; this map is the seam for everything else.
|
|
4983
5544
|
*/
|
|
4984
5545
|
toolSummaries = {};
|
|
5546
|
+
/**
|
|
5547
|
+
* Localizable UI strings — a partial override merged over the English
|
|
5548
|
+
* {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
|
|
5549
|
+
* element is appended); the `data-strings` JSON attribute is the markup
|
|
5550
|
+
* equivalent, and this property wins key-by-key over it.
|
|
5551
|
+
*/
|
|
5552
|
+
strings = {};
|
|
5553
|
+
/**
|
|
5554
|
+
* Resolve a `scroll_to` / `drag_and_drop` target string to a host-page
|
|
5555
|
+
* element (or `null`). Defaults to a CSS-selector lookup; override to map
|
|
5556
|
+
* page-map element ids. The page-action tools are opt-in via the
|
|
5557
|
+
* `data-page-actions` attribute (`"scroll"` / `"drag"`).
|
|
5558
|
+
*/
|
|
5559
|
+
resolvePageTarget = (target) => document.querySelector(target);
|
|
4985
5560
|
/**
|
|
4986
5561
|
* Card labels fetched from a server tool catalog (`data-tools-url`), keyed by
|
|
4987
5562
|
* tool name. The base layer behind {@link toolSummaries}: an explicit entry in
|
|
4988
5563
|
* `toolSummaries` wins, this fills the rest. Populated once on connect.
|
|
4989
5564
|
*/
|
|
4990
5565
|
#toolCatalog = {};
|
|
5566
|
+
/** The resolved string table (defaults ← `data-strings` ← `strings`). */
|
|
5567
|
+
#strings = DEFAULT_UI_STRINGS;
|
|
4991
5568
|
#toolRegistry = new ClientToolRegistry();
|
|
4992
5569
|
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
4993
5570
|
#toolCards = /* @__PURE__ */ new Map();
|
|
@@ -5010,6 +5587,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5010
5587
|
#attachButton;
|
|
5011
5588
|
#fileInput;
|
|
5012
5589
|
#attachSlot;
|
|
5590
|
+
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
5591
|
+
#rail;
|
|
5592
|
+
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
5593
|
+
#emptyWrap;
|
|
5013
5594
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5014
5595
|
#attachTray = null;
|
|
5015
5596
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
@@ -5028,6 +5609,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5028
5609
|
// it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
|
|
5029
5610
|
#streamDeltas = 0;
|
|
5030
5611
|
#pending = null;
|
|
5612
|
+
// The current assistant turn's grouping container (WELL-1). One `.answer`
|
|
5613
|
+
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
5614
|
+
// pending indicator — so it can be boxed as one "well" by CSS. Opened on the
|
|
5615
|
+
// turn's first run start, closed at settle, so it spans the whole multi-round
|
|
5616
|
+
// frontend-tool loop (which is several AG-UI runs), not one run. `null`
|
|
5617
|
+
// between turns; user bubbles never enter it.
|
|
5618
|
+
#currentGroup = null;
|
|
5031
5619
|
#threadId = "";
|
|
5032
5620
|
#initialMessages = [];
|
|
5033
5621
|
// Skill catalog by source; merged backend → embed → client (later wins).
|
|
@@ -5046,6 +5634,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5046
5634
|
this.#attachButton = document.createElement("button");
|
|
5047
5635
|
this.#fileInput = document.createElement("input");
|
|
5048
5636
|
this.#attachSlot = document.createElement("div");
|
|
5637
|
+
this.#rail = document.createElement("button");
|
|
5638
|
+
this.#emptyWrap = document.createElement("div");
|
|
5049
5639
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
5050
5640
|
this.#drawer = new ThreadDrawer({
|
|
5051
5641
|
onSelect: (threadId) => {
|
|
@@ -5069,7 +5659,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5069
5659
|
return ["title-text"];
|
|
5070
5660
|
}
|
|
5071
5661
|
attributeChangedCallback(_name, _previous, value) {
|
|
5072
|
-
this.#title.textContent = value ??
|
|
5662
|
+
this.#title.textContent = value ?? this.#strings.title;
|
|
5073
5663
|
}
|
|
5074
5664
|
/** Declare a frontend tool the agent may call. */
|
|
5075
5665
|
registerTool(tool) {
|
|
@@ -5115,9 +5705,25 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5115
5705
|
}
|
|
5116
5706
|
];
|
|
5117
5707
|
}
|
|
5118
|
-
/**
|
|
5708
|
+
/**
|
|
5709
|
+
* Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
|
|
5710
|
+
* via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
|
|
5711
|
+
* through {@link resolvePageTarget} so a host controls the agent's interaction
|
|
5712
|
+
* surface; absent attribute ⇒ no tools registered.
|
|
5713
|
+
*/
|
|
5714
|
+
#pageActionTools() {
|
|
5715
|
+
const attr = this.getAttribute("data-page-actions");
|
|
5716
|
+
if (attr === null) {
|
|
5717
|
+
return [];
|
|
5718
|
+
}
|
|
5719
|
+
const enabled = new Set(
|
|
5720
|
+
attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
|
|
5721
|
+
);
|
|
5722
|
+
return createPageActionTools(enabled, (target) => this.resolvePageTarget(target));
|
|
5723
|
+
}
|
|
5724
|
+
/** All built-in (route + page + page-action) frontend tools. */
|
|
5119
5725
|
#builtinTools() {
|
|
5120
|
-
return [...this.#routeTools(), ...this.#pageTools()];
|
|
5726
|
+
return [...this.#routeTools(), ...this.#pageTools(), ...this.#pageActionTools()];
|
|
5121
5727
|
}
|
|
5122
5728
|
/** Resolve a tool by name: built-in tools first, then the registry. */
|
|
5123
5729
|
#resolveTool(name) {
|
|
@@ -5143,7 +5749,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5143
5749
|
*/
|
|
5144
5750
|
get toolDisplay() {
|
|
5145
5751
|
const attr = this.getAttribute("data-tool-display");
|
|
5146
|
-
if (attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
|
|
5752
|
+
if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
|
|
5147
5753
|
return attr;
|
|
5148
5754
|
}
|
|
5149
5755
|
return TOOL_DISPLAY.FULL;
|
|
@@ -5152,10 +5758,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5152
5758
|
this.setAttribute("data-tool-display", value);
|
|
5153
5759
|
}
|
|
5154
5760
|
connectedCallback() {
|
|
5761
|
+
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
5155
5762
|
this.#render();
|
|
5763
|
+
this.#drawer.setStrings(this.#strings);
|
|
5156
5764
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
5157
5765
|
this.setAttribute("collapsed", "");
|
|
5158
5766
|
}
|
|
5767
|
+
this.#syncRail();
|
|
5159
5768
|
this.#initSkills();
|
|
5160
5769
|
void this.#fetchToolCatalog();
|
|
5161
5770
|
this.#wireThreadStore();
|
|
@@ -5163,6 +5772,21 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5163
5772
|
this.#threadId = this.conversationStore.threadId();
|
|
5164
5773
|
void this.#rehydrate();
|
|
5165
5774
|
}
|
|
5775
|
+
/** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
|
|
5776
|
+
#readStringOverrides() {
|
|
5777
|
+
const raw = this.getAttribute("data-strings");
|
|
5778
|
+
if (raw === null) {
|
|
5779
|
+
return {};
|
|
5780
|
+
}
|
|
5781
|
+
try {
|
|
5782
|
+
const parsed = JSON.parse(raw);
|
|
5783
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
5784
|
+
return parsed;
|
|
5785
|
+
}
|
|
5786
|
+
} catch {
|
|
5787
|
+
}
|
|
5788
|
+
return {};
|
|
5789
|
+
}
|
|
5166
5790
|
/**
|
|
5167
5791
|
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
5168
5792
|
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
@@ -5180,7 +5804,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5180
5804
|
this.#attachTray = new AttachmentTray({
|
|
5181
5805
|
upload,
|
|
5182
5806
|
maxBytes: this.#attachmentMaxBytes(),
|
|
5183
|
-
accept
|
|
5807
|
+
accept,
|
|
5808
|
+
strings: this.#strings
|
|
5184
5809
|
});
|
|
5185
5810
|
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
5186
5811
|
this.#fileInput.accept = accept;
|
|
@@ -5330,7 +5955,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5330
5955
|
#applySkill(skill) {
|
|
5331
5956
|
const { text: text2, missing } = fillTemplate(skill.prompt, this.skillContext());
|
|
5332
5957
|
if (missing.length > 0) {
|
|
5333
|
-
this.#skillHint.textContent =
|
|
5958
|
+
this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
|
|
5334
5959
|
this.#skillHint.hidden = false;
|
|
5335
5960
|
return;
|
|
5336
5961
|
}
|
|
@@ -5363,6 +5988,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5363
5988
|
this.removeAttribute("collapsed");
|
|
5364
5989
|
}
|
|
5365
5990
|
sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
5991
|
+
this.#syncRail();
|
|
5366
5992
|
this.dispatchEvent(
|
|
5367
5993
|
new CustomEvent(TOGGLE_EVENT, {
|
|
5368
5994
|
detail: { collapsed },
|
|
@@ -5390,13 +6016,15 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5390
6016
|
#resetState() {
|
|
5391
6017
|
this.#client = null;
|
|
5392
6018
|
this.#streamingBubble = null;
|
|
6019
|
+
this.#currentGroup = null;
|
|
5393
6020
|
this.#hidePending();
|
|
5394
6021
|
this.#toolCards.clear();
|
|
5395
6022
|
this.#serverSettled.clear();
|
|
5396
6023
|
this.#initialMessages = [];
|
|
5397
6024
|
this.#runAttachments = [];
|
|
5398
6025
|
this.#attachTray?.clear();
|
|
5399
|
-
this.#messages.replaceChildren();
|
|
6026
|
+
this.#messages.replaceChildren(this.#emptyWrap);
|
|
6027
|
+
this.#updateEmptyState();
|
|
5400
6028
|
}
|
|
5401
6029
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
5402
6030
|
async #switchThread(threadId) {
|
|
@@ -5520,72 +6148,102 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5520
6148
|
* Assistant content is rendered as sanitised markdown/HTML; user content
|
|
5521
6149
|
* stays literal text (no need to parse what the user typed, and it avoids
|
|
5522
6150
|
* rendering user-authored markup).
|
|
6151
|
+
*
|
|
6152
|
+
* Assistant bubbles land in the current answer group (WELL-1), opening one if
|
|
6153
|
+
* needed; a user bubble closes the prior group and sits directly in the list
|
|
6154
|
+
* (the well wraps the *assistant* turn, the user message precedes it).
|
|
5523
6155
|
*/
|
|
5524
6156
|
appendMessage(role, content) {
|
|
5525
6157
|
const bubble = document.createElement("div");
|
|
5526
6158
|
bubble.className = `message message--${role}`;
|
|
6159
|
+
bubble.setAttribute("part", `message message-${role}`);
|
|
5527
6160
|
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
5528
6161
|
bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
|
|
6162
|
+
this.#ensureGroup().appendChild(bubble);
|
|
5529
6163
|
} else {
|
|
6164
|
+
this.#currentGroup = null;
|
|
5530
6165
|
bubble.textContent = content;
|
|
6166
|
+
this.#messages.appendChild(bubble);
|
|
5531
6167
|
}
|
|
5532
|
-
this.#
|
|
6168
|
+
this.#updateEmptyState();
|
|
5533
6169
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5534
6170
|
return bubble;
|
|
5535
6171
|
}
|
|
6172
|
+
/**
|
|
6173
|
+
* The open answer group, creating and appending it on first use. Everything a
|
|
6174
|
+
* single assistant turn renders (text, tool cards, the pending indicator)
|
|
6175
|
+
* goes inside it, so the opt-in `data-answer-well` styling can box the whole
|
|
6176
|
+
* turn. Idempotent across the turn's runs — it persists until {@link #handlers}'
|
|
6177
|
+
* `onSettled` nulls it.
|
|
6178
|
+
*/
|
|
6179
|
+
#ensureGroup() {
|
|
6180
|
+
if (this.#currentGroup === null) {
|
|
6181
|
+
const group = document.createElement("div");
|
|
6182
|
+
group.className = "answer";
|
|
6183
|
+
group.setAttribute("part", "answer");
|
|
6184
|
+
this.#currentGroup = group;
|
|
6185
|
+
this.#messages.appendChild(group);
|
|
6186
|
+
this.#updateEmptyState();
|
|
6187
|
+
}
|
|
6188
|
+
return this.#currentGroup;
|
|
6189
|
+
}
|
|
5536
6190
|
#render() {
|
|
5537
6191
|
const style = document.createElement("style");
|
|
5538
6192
|
style.textContent = STYLES;
|
|
5539
6193
|
this.#chat.className = "chat";
|
|
6194
|
+
this.#chat.setAttribute("part", "panel");
|
|
5540
6195
|
const header = document.createElement("div");
|
|
5541
6196
|
header.className = "header";
|
|
6197
|
+
header.setAttribute("part", "header");
|
|
5542
6198
|
const title = this.#title;
|
|
5543
6199
|
title.className = "header-title";
|
|
5544
|
-
title.
|
|
6200
|
+
title.setAttribute("part", "title");
|
|
6201
|
+
title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
|
|
6202
|
+
if (this.querySelector('[slot="icon"]') !== null || this.getAttribute("data-icon-url") !== null) {
|
|
6203
|
+
header.append(this.#iconElement("icon", "icon", null));
|
|
6204
|
+
}
|
|
6205
|
+
const headerActions = document.createElement("slot");
|
|
6206
|
+
headerActions.name = "header-actions";
|
|
5545
6207
|
const controls = document.createElement("div");
|
|
5546
6208
|
controls.className = "header-controls";
|
|
5547
|
-
|
|
5548
|
-
history
|
|
5549
|
-
history.className = "header-btn header-btn--history";
|
|
5550
|
-
history.title = "Chat history";
|
|
5551
|
-
history.setAttribute("aria-label", "Chat history");
|
|
5552
|
-
history.textContent = "\u2630";
|
|
6209
|
+
controls.setAttribute("part", "header-controls");
|
|
6210
|
+
const history = this.#headerButton("history", this.#strings.chatHistory, "\u2630");
|
|
5553
6211
|
history.addEventListener("click", () => {
|
|
5554
6212
|
void this.#refreshDrawer();
|
|
5555
6213
|
this.#drawer.open();
|
|
5556
6214
|
});
|
|
5557
|
-
const newChat =
|
|
5558
|
-
newChat.type = "button";
|
|
5559
|
-
newChat.className = "header-btn header-btn--new";
|
|
5560
|
-
newChat.title = "New chat";
|
|
5561
|
-
newChat.setAttribute("aria-label", "New chat");
|
|
5562
|
-
newChat.textContent = "\u271A";
|
|
6215
|
+
const newChat = this.#headerButton("new", this.#strings.newChat, "\u271A");
|
|
5563
6216
|
newChat.addEventListener("click", () => this.newChat());
|
|
5564
|
-
const collapse =
|
|
5565
|
-
collapse.type = "button";
|
|
5566
|
-
collapse.className = "header-btn header-btn--collapse";
|
|
5567
|
-
collapse.title = "Collapse";
|
|
5568
|
-
collapse.setAttribute("aria-label", "Collapse");
|
|
5569
|
-
collapse.textContent = "\u2014";
|
|
6217
|
+
const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
5570
6218
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
5571
6219
|
controls.append(history, newChat, collapse);
|
|
5572
|
-
header.append(title, controls);
|
|
6220
|
+
header.append(title, headerActions, controls);
|
|
5573
6221
|
this.#messages.className = "messages";
|
|
6222
|
+
this.#messages.setAttribute("part", "messages");
|
|
5574
6223
|
this.#messages.setAttribute("role", "log");
|
|
5575
6224
|
this.#messages.setAttribute("aria-live", "polite");
|
|
5576
|
-
this.#messages.setAttribute("aria-label",
|
|
6225
|
+
this.#messages.setAttribute("aria-label", this.#strings.conversation);
|
|
6226
|
+
this.#emptyWrap.className = "empty";
|
|
6227
|
+
this.#emptyWrap.setAttribute("part", "empty");
|
|
6228
|
+
const emptySlot = document.createElement("slot");
|
|
6229
|
+
emptySlot.name = "empty";
|
|
6230
|
+
this.#emptyWrap.append(emptySlot);
|
|
6231
|
+
this.#messages.append(this.#emptyWrap);
|
|
5577
6232
|
const inputRow = document.createElement("div");
|
|
5578
6233
|
inputRow.className = "input-row";
|
|
6234
|
+
inputRow.setAttribute("part", "composer");
|
|
5579
6235
|
this.#input.className = "input";
|
|
5580
|
-
this.#input.setAttribute("
|
|
6236
|
+
this.#input.setAttribute("part", "input");
|
|
6237
|
+
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
5581
6238
|
this.#input.rows = 2;
|
|
5582
|
-
this.#input.placeholder =
|
|
6239
|
+
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
5583
6240
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
5584
6241
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
5585
6242
|
this.#send.className = "send";
|
|
5586
6243
|
this.#send.type = "button";
|
|
5587
|
-
this.#send.
|
|
5588
|
-
this.#send.
|
|
6244
|
+
this.#send.setAttribute("part", "send");
|
|
6245
|
+
this.#send.textContent = this.#strings.send;
|
|
6246
|
+
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
5589
6247
|
this.#send.dataset["state"] = "idle";
|
|
5590
6248
|
this.#send.addEventListener("click", () => {
|
|
5591
6249
|
if (this.#running) {
|
|
@@ -5598,9 +6256,10 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5598
6256
|
this.#skillHint.hidden = true;
|
|
5599
6257
|
this.#attachButton.className = "attach-btn";
|
|
5600
6258
|
this.#attachButton.type = "button";
|
|
6259
|
+
this.#attachButton.setAttribute("part", "attach-button");
|
|
5601
6260
|
this.#attachButton.textContent = "\u{1F4CE}";
|
|
5602
|
-
this.#attachButton.title =
|
|
5603
|
-
this.#attachButton.setAttribute("aria-label",
|
|
6261
|
+
this.#attachButton.title = this.#strings.attachFiles;
|
|
6262
|
+
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
5604
6263
|
this.#attachButton.hidden = true;
|
|
5605
6264
|
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
5606
6265
|
this.#fileInput.className = "attach-input";
|
|
@@ -5609,6 +6268,8 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5609
6268
|
this.#fileInput.hidden = true;
|
|
5610
6269
|
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
5611
6270
|
this.#attachSlot.className = "attachment-slot";
|
|
6271
|
+
const footer = document.createElement("slot");
|
|
6272
|
+
footer.name = "footer";
|
|
5612
6273
|
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
5613
6274
|
this.#chat.append(
|
|
5614
6275
|
header,
|
|
@@ -5618,9 +6279,58 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5618
6279
|
this.#skillHint,
|
|
5619
6280
|
this.#attachSlot,
|
|
5620
6281
|
inputRow,
|
|
6282
|
+
footer,
|
|
5621
6283
|
this.#drawer.element
|
|
5622
6284
|
);
|
|
5623
|
-
this.#
|
|
6285
|
+
this.#rail.className = "rail";
|
|
6286
|
+
this.#rail.type = "button";
|
|
6287
|
+
this.#rail.setAttribute("part", "launcher");
|
|
6288
|
+
this.#rail.setAttribute("aria-label", this.#strings.expand);
|
|
6289
|
+
this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
|
|
6290
|
+
this.#rail.addEventListener("click", () => this.setCollapsed(false));
|
|
6291
|
+
this.#root.append(style, this.#chat, this.#rail);
|
|
6292
|
+
}
|
|
6293
|
+
/** Build a header control button (icon glyph + localized title/aria). */
|
|
6294
|
+
#headerButton(modifier, label, glyph) {
|
|
6295
|
+
const button = document.createElement("button");
|
|
6296
|
+
button.type = "button";
|
|
6297
|
+
button.className = `header-btn header-btn--${modifier}`;
|
|
6298
|
+
button.setAttribute("part", `header-button ${modifier}-button`);
|
|
6299
|
+
button.title = label;
|
|
6300
|
+
button.setAttribute("aria-label", label);
|
|
6301
|
+
button.textContent = glyph;
|
|
6302
|
+
return button;
|
|
6303
|
+
}
|
|
6304
|
+
/**
|
|
6305
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup; with
|
|
6306
|
+
* a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
|
|
6307
|
+
*/
|
|
6308
|
+
#iconElement(slotName, part, fallbackGlyph) {
|
|
6309
|
+
const holder = document.createElement("span");
|
|
6310
|
+
holder.className = "icon-holder";
|
|
6311
|
+
holder.setAttribute("part", part);
|
|
6312
|
+
const slot = document.createElement("slot");
|
|
6313
|
+
slot.name = slotName;
|
|
6314
|
+
const iconUrl = this.getAttribute("data-icon-url");
|
|
6315
|
+
if (iconUrl !== null) {
|
|
6316
|
+
const img = document.createElement("img");
|
|
6317
|
+
img.className = "icon-img";
|
|
6318
|
+
img.src = iconUrl;
|
|
6319
|
+
img.alt = "";
|
|
6320
|
+
slot.append(img);
|
|
6321
|
+
} else if (fallbackGlyph !== null) {
|
|
6322
|
+
slot.append(document.createTextNode(fallbackGlyph));
|
|
6323
|
+
}
|
|
6324
|
+
holder.append(slot);
|
|
6325
|
+
return holder;
|
|
6326
|
+
}
|
|
6327
|
+
/** Reflect the collapsed state on the rail's `aria-expanded`. */
|
|
6328
|
+
#syncRail() {
|
|
6329
|
+
this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
|
|
6330
|
+
}
|
|
6331
|
+
/** Hide the empty-state region once the message list holds anything else. */
|
|
6332
|
+
#updateEmptyState() {
|
|
6333
|
+
this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
|
|
5624
6334
|
}
|
|
5625
6335
|
/** Forward input changes to the skills palette and clear any stale hint. */
|
|
5626
6336
|
#onInput() {
|
|
@@ -5655,7 +6365,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5655
6365
|
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
5656
6366
|
#setRunning(running) {
|
|
5657
6367
|
this.#running = running;
|
|
5658
|
-
const label = running ?
|
|
6368
|
+
const label = running ? this.#strings.stop : this.#strings.send;
|
|
5659
6369
|
this.#send.textContent = label;
|
|
5660
6370
|
this.#send.setAttribute("aria-label", label);
|
|
5661
6371
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
@@ -5706,7 +6416,8 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5706
6416
|
getTools: () => this.getTools(),
|
|
5707
6417
|
getContext: () => this.getContext(),
|
|
5708
6418
|
executeTool: (call) => this.#executeTool(call),
|
|
5709
|
-
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
|
|
6419
|
+
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
6420
|
+
connectionLostMessage: this.#strings.connectionLost
|
|
5710
6421
|
});
|
|
5711
6422
|
}
|
|
5712
6423
|
return this.#client;
|
|
@@ -5727,7 +6438,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5727
6438
|
const tool = this.#resolveTool(call.name);
|
|
5728
6439
|
if (tool === null) {
|
|
5729
6440
|
if (!this.#serverSettled.has(call.id)) {
|
|
5730
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
6441
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
5731
6442
|
}
|
|
5732
6443
|
return null;
|
|
5733
6444
|
}
|
|
@@ -5739,13 +6450,15 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5739
6450
|
}
|
|
5740
6451
|
this.#confirmAbort = new AbortController();
|
|
5741
6452
|
const decision = requestConfirmation(this.#messages, request, {
|
|
5742
|
-
signal: this.#confirmAbort.signal
|
|
6453
|
+
signal: this.#confirmAbort.signal,
|
|
6454
|
+
strings: this.#strings
|
|
5743
6455
|
});
|
|
6456
|
+
this.#updateEmptyState();
|
|
5744
6457
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5745
6458
|
const accepted = await decision;
|
|
5746
6459
|
this.#confirmAbort = null;
|
|
5747
6460
|
if (!accepted) {
|
|
5748
|
-
const message =
|
|
6461
|
+
const message = this.#strings.declinedAction;
|
|
5749
6462
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
5750
6463
|
this.#showPending();
|
|
5751
6464
|
return { content: message };
|
|
@@ -5758,7 +6471,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5758
6471
|
try {
|
|
5759
6472
|
const result = await tool.handler(call.args);
|
|
5760
6473
|
if (navigates) {
|
|
5761
|
-
card.settle(TOOL_CALL_STATUS.DONE,
|
|
6474
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
5762
6475
|
return { content: "", halt: true };
|
|
5763
6476
|
}
|
|
5764
6477
|
const content = JSON.stringify(result ?? null);
|
|
@@ -5779,6 +6492,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5779
6492
|
return {
|
|
5780
6493
|
onRunStart: () => {
|
|
5781
6494
|
this.#setRunning(true);
|
|
6495
|
+
this.#ensureGroup();
|
|
5782
6496
|
this.#showPending();
|
|
5783
6497
|
},
|
|
5784
6498
|
onTextDelta: (buffer) => {
|
|
@@ -5824,6 +6538,16 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5824
6538
|
this.#setRunning(false);
|
|
5825
6539
|
this.#streamingBubble = null;
|
|
5826
6540
|
this.#runAttachments = [];
|
|
6541
|
+
for (const card of this.#toolCards.values()) {
|
|
6542
|
+
if (!card.settled) {
|
|
6543
|
+
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.noResult);
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
if (this.#currentGroup !== null && this.#currentGroup.childElementCount === 0) {
|
|
6547
|
+
this.#currentGroup.remove();
|
|
6548
|
+
this.#updateEmptyState();
|
|
6549
|
+
}
|
|
6550
|
+
this.#currentGroup = null;
|
|
5827
6551
|
}
|
|
5828
6552
|
};
|
|
5829
6553
|
}
|
|
@@ -5831,9 +6555,11 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5831
6555
|
#appendStoppedNote() {
|
|
5832
6556
|
const note = document.createElement("div");
|
|
5833
6557
|
note.className = "stopped-note";
|
|
6558
|
+
note.setAttribute("part", "stopped");
|
|
5834
6559
|
note.setAttribute("role", "status");
|
|
5835
|
-
note.textContent =
|
|
5836
|
-
this.#
|
|
6560
|
+
note.textContent = this.#strings.stopped;
|
|
6561
|
+
this.#ensureGroup().appendChild(note);
|
|
6562
|
+
this.#updateEmptyState();
|
|
5837
6563
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5838
6564
|
}
|
|
5839
6565
|
/**
|
|
@@ -5847,15 +6573,17 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5847
6573
|
}
|
|
5848
6574
|
const pending = document.createElement("div");
|
|
5849
6575
|
pending.className = "pending";
|
|
6576
|
+
pending.setAttribute("part", "pending");
|
|
5850
6577
|
pending.setAttribute("role", "status");
|
|
5851
|
-
pending.setAttribute("aria-label",
|
|
6578
|
+
pending.setAttribute("aria-label", this.#strings.thinking);
|
|
5852
6579
|
for (let i = 0; i < 3; i += 1) {
|
|
5853
6580
|
const dot = document.createElement("span");
|
|
5854
6581
|
dot.className = "pending-dot";
|
|
5855
6582
|
pending.appendChild(dot);
|
|
5856
6583
|
}
|
|
5857
6584
|
this.#pending = pending;
|
|
5858
|
-
this.#
|
|
6585
|
+
this.#ensureGroup().appendChild(pending);
|
|
6586
|
+
this.#updateEmptyState();
|
|
5859
6587
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5860
6588
|
}
|
|
5861
6589
|
/** Remove the pending indicator if shown. */
|
|
@@ -5885,9 +6613,10 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
5885
6613
|
}
|
|
5886
6614
|
const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
|
|
5887
6615
|
const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
|
|
5888
|
-
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
|
|
6616
|
+
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
|
|
5889
6617
|
this.#toolCards.set(call.id, card);
|
|
5890
|
-
this.#
|
|
6618
|
+
this.#ensureGroup().appendChild(card.element);
|
|
6619
|
+
this.#updateEmptyState();
|
|
5891
6620
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
5892
6621
|
return card;
|
|
5893
6622
|
}
|
|
@@ -5900,130 +6629,6 @@ function defineAgUiChat() {
|
|
|
5900
6629
|
}
|
|
5901
6630
|
}
|
|
5902
6631
|
|
|
5903
|
-
// src/dom/native_setter.ts
|
|
5904
|
-
function prototypeSetter(proto, prop) {
|
|
5905
|
-
const setter = Object.getOwnPropertyDescriptor(proto, prop).set;
|
|
5906
|
-
return setter;
|
|
5907
|
-
}
|
|
5908
|
-
var setInputValue = prototypeSetter(HTMLInputElement.prototype, "value");
|
|
5909
|
-
var setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value");
|
|
5910
|
-
var setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value");
|
|
5911
|
-
var setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked");
|
|
5912
|
-
function setNativeValue(el, value) {
|
|
5913
|
-
if (el instanceof HTMLTextAreaElement) {
|
|
5914
|
-
setTextareaValue.call(el, value);
|
|
5915
|
-
} else if (el instanceof HTMLSelectElement) {
|
|
5916
|
-
setSelectValue.call(el, value);
|
|
5917
|
-
} else {
|
|
5918
|
-
setInputValue.call(el, value);
|
|
5919
|
-
}
|
|
5920
|
-
}
|
|
5921
|
-
function setNativeChecked(el, checked) {
|
|
5922
|
-
setInputChecked.call(el, checked);
|
|
5923
|
-
}
|
|
5924
|
-
|
|
5925
|
-
// src/dom/animations.ts
|
|
5926
|
-
var ACCENT = "#4f46e5";
|
|
5927
|
-
function delay(ms) {
|
|
5928
|
-
return new Promise((resolve) => {
|
|
5929
|
-
setTimeout(resolve, ms);
|
|
5930
|
-
});
|
|
5931
|
-
}
|
|
5932
|
-
async function typeInto(el, value, options = {}) {
|
|
5933
|
-
const charDelayMs = options.charDelayMs ?? 35;
|
|
5934
|
-
setNativeValue(el, "");
|
|
5935
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5936
|
-
for (const char of value) {
|
|
5937
|
-
setNativeValue(el, el.value + char);
|
|
5938
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5939
|
-
if (charDelayMs > 0) {
|
|
5940
|
-
await delay(charDelayMs);
|
|
5941
|
-
}
|
|
5942
|
-
}
|
|
5943
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5944
|
-
}
|
|
5945
|
-
async function highlightThenClick(el, options = {}) {
|
|
5946
|
-
const highlightMs = options.highlightMs ?? 280;
|
|
5947
|
-
const previousOutline = el.style.outline;
|
|
5948
|
-
const previousOffset = el.style.outlineOffset;
|
|
5949
|
-
el.style.outline = `2px solid ${ACCENT}`;
|
|
5950
|
-
el.style.outlineOffset = "2px";
|
|
5951
|
-
await delay(highlightMs);
|
|
5952
|
-
el.style.outline = previousOutline;
|
|
5953
|
-
el.style.outlineOffset = previousOffset;
|
|
5954
|
-
el.click();
|
|
5955
|
-
}
|
|
5956
|
-
function scrollIntoCenterView(el) {
|
|
5957
|
-
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
5958
|
-
}
|
|
5959
|
-
async function focusWithFlash(el, options = {}) {
|
|
5960
|
-
const flashMs = options.flashMs ?? 200;
|
|
5961
|
-
el.focus();
|
|
5962
|
-
const previousShadow = el.style.boxShadow;
|
|
5963
|
-
el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
|
|
5964
|
-
await delay(flashMs);
|
|
5965
|
-
el.style.boxShadow = previousShadow;
|
|
5966
|
-
}
|
|
5967
|
-
var RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
|
|
5968
|
-
function prefersReducedMotion() {
|
|
5969
|
-
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5970
|
-
}
|
|
5971
|
-
function motionDelay(ms) {
|
|
5972
|
-
if (ms <= 0 || prefersReducedMotion()) {
|
|
5973
|
-
return Promise.resolve();
|
|
5974
|
-
}
|
|
5975
|
-
return delay(ms);
|
|
5976
|
-
}
|
|
5977
|
-
async function pressThenClick(el, options = {}) {
|
|
5978
|
-
const pressMs = options.pressMs ?? 140;
|
|
5979
|
-
const previousTransform = el.style.transform;
|
|
5980
|
-
const previousTransition = el.style.transition;
|
|
5981
|
-
const previousShadow = el.style.boxShadow;
|
|
5982
|
-
el.style.transition = "transform 80ms ease";
|
|
5983
|
-
el.style.transform = "scale(0.96)";
|
|
5984
|
-
el.style.boxShadow = RING;
|
|
5985
|
-
await motionDelay(pressMs);
|
|
5986
|
-
el.style.transform = previousTransform;
|
|
5987
|
-
el.style.transition = previousTransition;
|
|
5988
|
-
el.style.boxShadow = previousShadow;
|
|
5989
|
-
el.click();
|
|
5990
|
-
}
|
|
5991
|
-
function findOption(el, value) {
|
|
5992
|
-
for (const option of Array.from(el.options)) {
|
|
5993
|
-
if (option.value === value || option.text === value) {
|
|
5994
|
-
return option;
|
|
5995
|
-
}
|
|
5996
|
-
}
|
|
5997
|
-
return null;
|
|
5998
|
-
}
|
|
5999
|
-
async function selectOption(el, value, options = {}) {
|
|
6000
|
-
const option = findOption(el, value);
|
|
6001
|
-
if (option === null) {
|
|
6002
|
-
throw new Error(`no <option> matching "${value}"`);
|
|
6003
|
-
}
|
|
6004
|
-
const highlightMs = options.highlightMs ?? 220;
|
|
6005
|
-
const previousOutline = el.style.outline;
|
|
6006
|
-
const previousOffset = el.style.outlineOffset;
|
|
6007
|
-
el.style.outline = `2px solid ${ACCENT}`;
|
|
6008
|
-
el.style.outlineOffset = "2px";
|
|
6009
|
-
await motionDelay(highlightMs);
|
|
6010
|
-
setNativeValue(el, option.value);
|
|
6011
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6012
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
6013
|
-
el.style.outline = previousOutline;
|
|
6014
|
-
el.style.outlineOffset = previousOffset;
|
|
6015
|
-
}
|
|
6016
|
-
async function toggleControl(el, checked, options = {}) {
|
|
6017
|
-
const flashMs = options.flashMs ?? 200;
|
|
6018
|
-
const previousShadow = el.style.boxShadow;
|
|
6019
|
-
el.style.boxShadow = RING;
|
|
6020
|
-
await motionDelay(flashMs);
|
|
6021
|
-
setNativeChecked(el, checked);
|
|
6022
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6023
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
6024
|
-
el.style.boxShadow = previousShadow;
|
|
6025
|
-
}
|
|
6026
|
-
|
|
6027
6632
|
// src/dom/dom_driver.ts
|
|
6028
6633
|
async function fillField(el, value, options = {}) {
|
|
6029
6634
|
scrollIntoCenterView(el);
|
|
@@ -6057,14 +6662,17 @@ function setControlValue(el, value) {
|
|
|
6057
6662
|
}
|
|
6058
6663
|
|
|
6059
6664
|
// src/version.ts
|
|
6060
|
-
var VERSION = "0.
|
|
6665
|
+
var VERSION = "0.8.0";
|
|
6061
6666
|
export {
|
|
6062
6667
|
AgUiChat,
|
|
6063
6668
|
AgUiClient,
|
|
6064
6669
|
ClientToolRegistry,
|
|
6670
|
+
ConnectionLostError,
|
|
6671
|
+
DEFAULT_UI_STRINGS,
|
|
6065
6672
|
ELEMENT_TAG,
|
|
6066
6673
|
MAX_TOOL_ROUNDS,
|
|
6067
6674
|
MESSAGE_ROLE,
|
|
6675
|
+
PAGE_ACTIONS,
|
|
6068
6676
|
RemoteConversationStore,
|
|
6069
6677
|
SUBMIT_EVENT,
|
|
6070
6678
|
SessionStorageStore,
|
|
@@ -6079,6 +6687,7 @@ export {
|
|
|
6079
6687
|
X_SUMMARY_KEY,
|
|
6080
6688
|
clickElement,
|
|
6081
6689
|
createHttpAgent,
|
|
6690
|
+
createPageActionTools,
|
|
6082
6691
|
createPageMapContext,
|
|
6083
6692
|
createRouteTools,
|
|
6084
6693
|
createStateHookTools,
|
|
@@ -6088,6 +6697,7 @@ export {
|
|
|
6088
6697
|
highlightThenClick,
|
|
6089
6698
|
isDestructive,
|
|
6090
6699
|
isNavigates,
|
|
6700
|
+
mergeUiStrings,
|
|
6091
6701
|
messageAttachments,
|
|
6092
6702
|
parseToolCatalog,
|
|
6093
6703
|
prefersReducedMotion,
|