@runtypelabs/persona 3.10.1 → 3.11.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/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.global.js +61 -61
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +44 -44
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +195 -12
- package/dist/theme-editor.d.cts +85 -0
- package/dist/theme-editor.d.ts +85 -0
- package/dist/theme-editor.js +195 -12
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +80 -0
- package/package.json +1 -1
- package/src/components/tool-bubble.ts +121 -1
- package/src/defaults.ts +1 -0
- package/src/styles/widget.css +80 -0
- package/src/theme-reference.ts +6 -3
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types.ts +91 -0
- package/src/ui.scroll.test.ts +45 -2
- package/src/ui.ts +48 -2
- package/src/utils/formatting.test.ts +75 -1
- package/src/utils/formatting.ts +130 -0
- package/src/utils/morph.ts +9 -3
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
2
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
|
-
import { formatUnknownValue, describeToolTitle } from "../utils/formatting";
|
|
3
|
+
import { formatUnknownValue, describeToolTitle, resolveToolHeaderText, computeToolElapsed, parseFormattedTemplate } from "../utils/formatting";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
5
|
|
|
6
6
|
// Expansion state per widget instance
|
|
7
7
|
export const toolExpansionState = new Set<string>();
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
const appendRenderedValue = (
|
|
10
11
|
container: HTMLElement,
|
|
11
12
|
value: HTMLElement | string | null | undefined
|
|
@@ -59,6 +60,7 @@ const getToolSummaryText = (
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const isActive = tool.status !== "complete";
|
|
63
|
+
const toolCallConfig = config?.toolCall ?? {};
|
|
62
64
|
let summary = defaultSummary;
|
|
63
65
|
if (collapsedMode === "tool-name") {
|
|
64
66
|
summary = tool.name?.trim() || defaultSummary;
|
|
@@ -66,6 +68,13 @@ const getToolSummaryText = (
|
|
|
66
68
|
summary = previewText;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// Apply text templates if configured
|
|
72
|
+
if (isActive && toolCallConfig.activeTextTemplate) {
|
|
73
|
+
summary = resolveToolHeaderText(tool, toolCallConfig.activeTextTemplate, summary);
|
|
74
|
+
} else if (!isActive && toolCallConfig.completeTextTemplate) {
|
|
75
|
+
summary = resolveToolHeaderText(tool, toolCallConfig.completeTextTemplate, summary);
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
return { summary, previewText, isActive };
|
|
70
79
|
};
|
|
71
80
|
|
|
@@ -151,6 +160,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
151
160
|
const expandable = toolDisplayConfig.expandable !== false;
|
|
152
161
|
let expanded = expandable && toolExpansionState.has(message.id);
|
|
153
162
|
const { summary, previewText, isActive } = getToolSummaryText(message, config);
|
|
163
|
+
|
|
154
164
|
const header = createElement(
|
|
155
165
|
"button",
|
|
156
166
|
expandable
|
|
@@ -182,6 +192,18 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
182
192
|
if (toolCallConfig.headerTextColor) {
|
|
183
193
|
title.style.color = toolCallConfig.headerTextColor;
|
|
184
194
|
}
|
|
195
|
+
|
|
196
|
+
// Elapsed helpers — defined early so they're available to renderCollapsedSummary
|
|
197
|
+
const startedAt = String(tool.startedAt ?? Date.now());
|
|
198
|
+
|
|
199
|
+
// Helper: build a <span data-tool-elapsed> that the global timer in ui.ts updates
|
|
200
|
+
const createElapsedSpan = (): HTMLElement => {
|
|
201
|
+
const span = createElement("span", "");
|
|
202
|
+
span.setAttribute("data-tool-elapsed", startedAt);
|
|
203
|
+
span.textContent = computeToolElapsed(tool);
|
|
204
|
+
return span;
|
|
205
|
+
};
|
|
206
|
+
|
|
185
207
|
const customSummary = toolCallConfig.renderCollapsedSummary?.({
|
|
186
208
|
message,
|
|
187
209
|
toolCall: tool,
|
|
@@ -190,6 +212,8 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
190
212
|
collapsedMode: toolDisplayConfig.collapsedMode ?? "tool-call",
|
|
191
213
|
isActive,
|
|
192
214
|
config: config ?? {},
|
|
215
|
+
elapsed: computeToolElapsed(tool),
|
|
216
|
+
createElapsedElement: createElapsedSpan,
|
|
193
217
|
});
|
|
194
218
|
if (typeof customSummary === "string" && customSummary.trim()) {
|
|
195
219
|
title.textContent = customSummary;
|
|
@@ -201,6 +225,102 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
201
225
|
headerContent.appendChild(title);
|
|
202
226
|
}
|
|
203
227
|
|
|
228
|
+
// Apply loading animation when tool is active and no custom HTMLElement was provided
|
|
229
|
+
const loadingAnimation = toolDisplayConfig.loadingAnimation ?? "none";
|
|
230
|
+
const activeTemplate = toolCallConfig.activeTextTemplate;
|
|
231
|
+
const completeTemplate = toolCallConfig.completeTextTemplate;
|
|
232
|
+
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
233
|
+
const skipCustomElement = customSummary instanceof HTMLElement;
|
|
234
|
+
|
|
235
|
+
// Helper: append text as individual animated character spans
|
|
236
|
+
const appendCharSpans = (container: HTMLElement, text: string, startIndex: number): number => {
|
|
237
|
+
let idx = startIndex;
|
|
238
|
+
for (const char of text) {
|
|
239
|
+
const span = createElement("span", "persona-tool-char");
|
|
240
|
+
span.style.setProperty("--char-index", String(idx));
|
|
241
|
+
span.textContent = char === " " ? "\u00A0" : char;
|
|
242
|
+
container.appendChild(span);
|
|
243
|
+
idx++;
|
|
244
|
+
}
|
|
245
|
+
return idx;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Renders a template into the title element, handling:
|
|
250
|
+
* - Inline formatting markers: **bold**, *italic*, ~dim~
|
|
251
|
+
* - {duration} as a live-updating elapsed span (active) or static text (complete)
|
|
252
|
+
* - Character-by-character animation wrapping when `animated` is true
|
|
253
|
+
*/
|
|
254
|
+
const renderFormattedTitle = (template: string, animated: boolean) => {
|
|
255
|
+
title.textContent = "";
|
|
256
|
+
const toolName = tool.name?.trim() || "tool";
|
|
257
|
+
const segments = parseFormattedTemplate(template, toolName);
|
|
258
|
+
let charIndex = 0;
|
|
259
|
+
|
|
260
|
+
for (const seg of segments) {
|
|
261
|
+
// Determine parent: wrap in a styled span if formatting is present
|
|
262
|
+
const parent = seg.styles.length > 0
|
|
263
|
+
? (() => {
|
|
264
|
+
const w = createElement("span", seg.styles.map(s => `persona-tool-text-${s}`).join(" "));
|
|
265
|
+
title.appendChild(w);
|
|
266
|
+
return w;
|
|
267
|
+
})()
|
|
268
|
+
: title;
|
|
269
|
+
|
|
270
|
+
if (seg.isDuration && isActive) {
|
|
271
|
+
// Live-updating elapsed span for active tools
|
|
272
|
+
parent.appendChild(createElapsedSpan());
|
|
273
|
+
} else {
|
|
274
|
+
// Static text (or resolved duration for completed tools)
|
|
275
|
+
const text = seg.isDuration ? computeToolElapsed(tool) : seg.text;
|
|
276
|
+
if (animated) {
|
|
277
|
+
charIndex = appendCharSpans(parent, text, charIndex);
|
|
278
|
+
} else {
|
|
279
|
+
parent.appendChild(document.createTextNode(text));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (!skipCustomElement) {
|
|
286
|
+
if (isActive && loadingAnimation !== "none") {
|
|
287
|
+
const animDuration = toolCallConfig.loadingAnimationDuration ?? 2000;
|
|
288
|
+
title.setAttribute("data-preserve-animation", "true");
|
|
289
|
+
|
|
290
|
+
if (loadingAnimation === "pulse") {
|
|
291
|
+
title.classList.add("persona-tool-loading-pulse");
|
|
292
|
+
title.style.setProperty("--persona-tool-anim-duration", `${animDuration}ms`);
|
|
293
|
+
if (currentTemplate) {
|
|
294
|
+
renderFormattedTitle(currentTemplate, false);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
// Character-by-character modes: shimmer, shimmer-color, rainbow
|
|
298
|
+
title.classList.add(`persona-tool-loading-${loadingAnimation}`);
|
|
299
|
+
title.style.setProperty("--persona-tool-anim-duration", `${animDuration}ms`);
|
|
300
|
+
|
|
301
|
+
if (loadingAnimation === "shimmer-color") {
|
|
302
|
+
if (toolCallConfig.loadingAnimationColor) {
|
|
303
|
+
title.style.setProperty("--persona-tool-anim-color", toolCallConfig.loadingAnimationColor);
|
|
304
|
+
}
|
|
305
|
+
if (toolCallConfig.loadingAnimationSecondaryColor) {
|
|
306
|
+
title.style.setProperty("--persona-tool-anim-secondary-color", toolCallConfig.loadingAnimationSecondaryColor);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (currentTemplate) {
|
|
311
|
+
renderFormattedTitle(currentTemplate, true);
|
|
312
|
+
} else {
|
|
313
|
+
const text = title.textContent || summary;
|
|
314
|
+
title.textContent = "";
|
|
315
|
+
appendCharSpans(title, text, 0);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else if (currentTemplate) {
|
|
319
|
+
// Template with formatting but no animation (or completed tool)
|
|
320
|
+
renderFormattedTitle(currentTemplate, false);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
204
324
|
let toggleIcon: HTMLElement | null = null;
|
|
205
325
|
if (expandable) {
|
|
206
326
|
toggleIcon = createElement("div", "persona-flex persona-items-center");
|
package/src/defaults.ts
CHANGED
package/src/styles/widget.css
CHANGED
|
@@ -1220,6 +1220,86 @@
|
|
|
1220
1220
|
box-shadow: var(--persona-tool-bubble-shadow, 0 5px 15px rgba(15, 23, 42, 0.08));
|
|
1221
1221
|
}
|
|
1222
1222
|
|
|
1223
|
+
/* ==============================
|
|
1224
|
+
Tool call loading animations
|
|
1225
|
+
============================== */
|
|
1226
|
+
|
|
1227
|
+
/* Inline formatting classes for template text */
|
|
1228
|
+
[data-persona-root] .persona-tool-text-dim {
|
|
1229
|
+
opacity: 0.5;
|
|
1230
|
+
}
|
|
1231
|
+
[data-persona-root] .persona-tool-text-bold {
|
|
1232
|
+
font-weight: 600;
|
|
1233
|
+
}
|
|
1234
|
+
[data-persona-root] .persona-tool-text-italic {
|
|
1235
|
+
font-style: italic;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/* Pulse mode: entire text element pulses opacity */
|
|
1239
|
+
@keyframes persona-tool-loading-pulse {
|
|
1240
|
+
0%, 100% { opacity: 1; }
|
|
1241
|
+
50% { opacity: 0.4; }
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
[data-persona-root] .persona-tool-loading-pulse {
|
|
1245
|
+
animation: persona-tool-loading-pulse var(--persona-tool-anim-duration, 2s) ease-in-out infinite;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/* Shimmer mode: monochrome brightness sweep per character */
|
|
1249
|
+
@keyframes persona-tool-loading-shimmer {
|
|
1250
|
+
0%, 100% { opacity: 0.4; }
|
|
1251
|
+
50% { opacity: 1; }
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
[data-persona-root] .persona-tool-loading-shimmer .persona-tool-char {
|
|
1255
|
+
display: inline-block;
|
|
1256
|
+
animation: persona-tool-loading-shimmer var(--persona-tool-anim-duration, 2s) ease-in-out infinite;
|
|
1257
|
+
animation-delay: calc(var(--char-index, 0) * 60ms);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* Shimmer-color mode: color gradient sweep per character */
|
|
1261
|
+
@keyframes persona-tool-loading-shimmer-color {
|
|
1262
|
+
0%, 100% {
|
|
1263
|
+
color: var(--persona-tool-anim-color, currentColor);
|
|
1264
|
+
}
|
|
1265
|
+
50% {
|
|
1266
|
+
color: var(--persona-tool-anim-secondary-color, #3b82f6);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
[data-persona-root] .persona-tool-loading-shimmer-color .persona-tool-char {
|
|
1271
|
+
display: inline-block;
|
|
1272
|
+
animation: persona-tool-loading-shimmer-color var(--persona-tool-anim-duration, 2s) ease-in-out infinite;
|
|
1273
|
+
animation-delay: calc(var(--char-index, 0) * 60ms);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/* Rainbow mode: hue rotation per character */
|
|
1277
|
+
@keyframes persona-tool-loading-rainbow {
|
|
1278
|
+
0% { color: #ef4444; }
|
|
1279
|
+
16% { color: #f59e0b; }
|
|
1280
|
+
33% { color: #22c55e; }
|
|
1281
|
+
50% { color: #06b6d4; }
|
|
1282
|
+
66% { color: #3b82f6; }
|
|
1283
|
+
83% { color: #8b5cf6; }
|
|
1284
|
+
100% { color: #ef4444; }
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
[data-persona-root] .persona-tool-loading-rainbow .persona-tool-char {
|
|
1288
|
+
display: inline-block;
|
|
1289
|
+
animation: persona-tool-loading-rainbow var(--persona-tool-anim-duration, 2s) linear infinite;
|
|
1290
|
+
animation-delay: calc(var(--char-index, 0) * 80ms);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/* Prefers-reduced-motion: disable all tool loading animations */
|
|
1294
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1295
|
+
[data-persona-root] .persona-tool-loading-pulse,
|
|
1296
|
+
[data-persona-root] .persona-tool-loading-shimmer .persona-tool-char,
|
|
1297
|
+
[data-persona-root] .persona-tool-loading-shimmer-color .persona-tool-char,
|
|
1298
|
+
[data-persona-root] .persona-tool-loading-rainbow .persona-tool-char {
|
|
1299
|
+
animation: none !important;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1223
1303
|
[data-persona-root] .persona-reasoning-bubble.persona-shadow-sm {
|
|
1224
1304
|
box-shadow: var(--persona-reasoning-bubble-shadow, 0 5px 15px rgba(15, 23, 42, 0.08));
|
|
1225
1305
|
}
|
package/src/theme-reference.ts
CHANGED
|
@@ -210,9 +210,12 @@ export const THEME_TOKEN_DOCS = {
|
|
|
210
210
|
'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
|
|
211
211
|
},
|
|
212
212
|
toolCall: {
|
|
213
|
-
description:
|
|
213
|
+
description:
|
|
214
|
+
'Tool call display styling, text templates, loading animations, and rendering hooks. ' +
|
|
215
|
+
'Text templates support placeholders ({toolName}, {duration}) and inline formatting (~dim~, *italic*, **bold**). ' +
|
|
216
|
+
'renderCollapsedSummary receives elapsed (static string) and createElapsedElement() (live-updating span) in its context.',
|
|
214
217
|
properties:
|
|
215
|
-
'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor, renderCollapsedSummary, renderCollapsedPreview, renderGroupedSummary.',
|
|
218
|
+
'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor, activeTextTemplate, completeTextTemplate, loadingAnimationColor, loadingAnimationSecondaryColor, loadingAnimationDuration, renderCollapsedSummary, renderCollapsedPreview, renderGroupedSummary.',
|
|
216
219
|
},
|
|
217
220
|
reasoning: {
|
|
218
221
|
description: 'Reasoning/thinking row rendering hooks.',
|
|
@@ -282,7 +285,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
282
285
|
features: {
|
|
283
286
|
description: 'Feature flags.',
|
|
284
287
|
properties:
|
|
285
|
-
'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines), artifacts (sidebar config).',
|
|
288
|
+
'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped, expandable, loadingAnimation), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines, expandable), artifacts (sidebar config).',
|
|
286
289
|
},
|
|
287
290
|
},
|
|
288
291
|
}
|
package/src/types.ts
CHANGED
|
@@ -577,6 +577,19 @@ export type AgentWidgetToolCallCollapsedMode =
|
|
|
577
577
|
| "tool-name"
|
|
578
578
|
| "tool-preview";
|
|
579
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Animation mode applied to tool call header text while the tool is running.
|
|
582
|
+
* Character-by-character modes (`shimmer`, `shimmer-color`, `rainbow`) wrap each
|
|
583
|
+
* character in a span with staggered `animation-delay`. `pulse` applies to the
|
|
584
|
+
* entire text container. Honors `prefers-reduced-motion`.
|
|
585
|
+
*/
|
|
586
|
+
export type AgentWidgetToolCallLoadingAnimation =
|
|
587
|
+
| "none"
|
|
588
|
+
| "pulse"
|
|
589
|
+
| "shimmer"
|
|
590
|
+
| "shimmer-color"
|
|
591
|
+
| "rainbow";
|
|
592
|
+
|
|
580
593
|
export type AgentWidgetToolCallDisplayFeature = {
|
|
581
594
|
/**
|
|
582
595
|
* Controls what collapsed tool call rows show in their header/summary area.
|
|
@@ -590,6 +603,8 @@ export type AgentWidgetToolCallDisplayFeature = {
|
|
|
590
603
|
activePreview?: boolean;
|
|
591
604
|
/**
|
|
592
605
|
* Optional CSS min-height applied to active collapsed tool call rows.
|
|
606
|
+
* @default undefined (no min-height)
|
|
607
|
+
* @example "100px"
|
|
593
608
|
*/
|
|
594
609
|
activeMinHeight?: string;
|
|
595
610
|
/**
|
|
@@ -608,6 +623,16 @@ export type AgentWidgetToolCallDisplayFeature = {
|
|
|
608
623
|
* @default true
|
|
609
624
|
*/
|
|
610
625
|
expandable?: boolean;
|
|
626
|
+
/**
|
|
627
|
+
* Animation mode applied to the tool call header text while the tool is active.
|
|
628
|
+
* - "none" — static text, no animation
|
|
629
|
+
* - "pulse" — opacity pulse on the entire header text
|
|
630
|
+
* - "shimmer" — monochrome opacity sweep per character
|
|
631
|
+
* - "shimmer-color" — color gradient sweep per character
|
|
632
|
+
* - "rainbow" — rainbow color cycle per character
|
|
633
|
+
* @default "none"
|
|
634
|
+
*/
|
|
635
|
+
loadingAnimation?: AgentWidgetToolCallLoadingAnimation;
|
|
611
636
|
};
|
|
612
637
|
|
|
613
638
|
export type AgentWidgetReasoningDisplayFeature = {
|
|
@@ -1234,22 +1259,39 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1234
1259
|
export type AgentWidgetToolCallConfig = {
|
|
1235
1260
|
/** Box-shadow for tool-call bubbles; overrides `theme.toolBubbleShadow` when set. */
|
|
1236
1261
|
shadow?: string;
|
|
1262
|
+
/** Background color of the tool call bubble container. */
|
|
1237
1263
|
backgroundColor?: string;
|
|
1264
|
+
/** Border color of the tool call bubble container. */
|
|
1238
1265
|
borderColor?: string;
|
|
1266
|
+
/** Border width of the tool call bubble container (CSS value, e.g. `"1px"`). */
|
|
1239
1267
|
borderWidth?: string;
|
|
1268
|
+
/** Border radius of the tool call bubble container (CSS value, e.g. `"12px"`). */
|
|
1240
1269
|
borderRadius?: string;
|
|
1270
|
+
/** Background color of the collapsed header row. */
|
|
1241
1271
|
headerBackgroundColor?: string;
|
|
1272
|
+
/** Text color of the collapsed header row (tool name / summary). */
|
|
1242
1273
|
headerTextColor?: string;
|
|
1274
|
+
/** Horizontal padding of the collapsed header row (CSS value). */
|
|
1243
1275
|
headerPaddingX?: string;
|
|
1276
|
+
/** Vertical padding of the collapsed header row (CSS value). */
|
|
1244
1277
|
headerPaddingY?: string;
|
|
1278
|
+
/** Background color of the expanded content area. */
|
|
1245
1279
|
contentBackgroundColor?: string;
|
|
1280
|
+
/** Text color of the expanded content area. */
|
|
1246
1281
|
contentTextColor?: string;
|
|
1282
|
+
/** Horizontal padding of the expanded content area (CSS value). */
|
|
1247
1283
|
contentPaddingX?: string;
|
|
1284
|
+
/** Vertical padding of the expanded content area (CSS value). */
|
|
1248
1285
|
contentPaddingY?: string;
|
|
1286
|
+
/** Background color of code blocks (arguments / result) in the expanded area. */
|
|
1249
1287
|
codeBlockBackgroundColor?: string;
|
|
1288
|
+
/** Border color of code blocks in the expanded area. */
|
|
1250
1289
|
codeBlockBorderColor?: string;
|
|
1290
|
+
/** Text color of code blocks in the expanded area. */
|
|
1251
1291
|
codeBlockTextColor?: string;
|
|
1292
|
+
/** Color of the expand/collapse toggle icon. */
|
|
1252
1293
|
toggleTextColor?: string;
|
|
1294
|
+
/** Color of section labels ("Arguments", "Result", "Activity") in the expanded area. */
|
|
1253
1295
|
labelTextColor?: string;
|
|
1254
1296
|
/**
|
|
1255
1297
|
* Override the collapsed summary row content for a tool call bubble.
|
|
@@ -1263,6 +1305,14 @@ export type AgentWidgetToolCallConfig = {
|
|
|
1263
1305
|
collapsedMode: AgentWidgetToolCallCollapsedMode;
|
|
1264
1306
|
isActive: boolean;
|
|
1265
1307
|
config: AgentWidgetConfig;
|
|
1308
|
+
/** Static elapsed time snapshot, e.g. "2.6s". */
|
|
1309
|
+
elapsed: string;
|
|
1310
|
+
/**
|
|
1311
|
+
* Returns a `<span>` whose text content is automatically updated every
|
|
1312
|
+
* 100ms by the widget's global timer. Place it anywhere in your returned
|
|
1313
|
+
* HTMLElement to get a live-ticking duration display.
|
|
1314
|
+
*/
|
|
1315
|
+
createElapsedElement: () => HTMLElement;
|
|
1266
1316
|
}) => HTMLElement | string | null;
|
|
1267
1317
|
/**
|
|
1268
1318
|
* Override the lightweight collapsed preview content shown for active tool rows.
|
|
@@ -1285,6 +1335,47 @@ export type AgentWidgetToolCallConfig = {
|
|
|
1285
1335
|
defaultSummary: string;
|
|
1286
1336
|
config: AgentWidgetConfig;
|
|
1287
1337
|
}) => HTMLElement | string | null;
|
|
1338
|
+
/**
|
|
1339
|
+
* Template string for the header text while a tool call is active (running).
|
|
1340
|
+
*
|
|
1341
|
+
* **Placeholders:** `{toolName}` (tool name), `{duration}` (live-updating elapsed time).
|
|
1342
|
+
*
|
|
1343
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — parsed at render time and
|
|
1344
|
+
* applied as styled `<span>` elements. Works with all animation modes.
|
|
1345
|
+
*
|
|
1346
|
+
* When not set, falls back to the current `collapsedMode` behavior.
|
|
1347
|
+
* @example "Calling {toolName}... ~{duration}~"
|
|
1348
|
+
* @example "**Searching** *{toolName}*..."
|
|
1349
|
+
*/
|
|
1350
|
+
activeTextTemplate?: string;
|
|
1351
|
+
/**
|
|
1352
|
+
* Template string for the header text when a tool call is complete.
|
|
1353
|
+
*
|
|
1354
|
+
* **Placeholders:** `{toolName}` (tool name), `{duration}` (final elapsed time).
|
|
1355
|
+
*
|
|
1356
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — same syntax as `activeTextTemplate`.
|
|
1357
|
+
*
|
|
1358
|
+
* When not set, falls back to the existing "Used tool for X seconds" text.
|
|
1359
|
+
* @example "Finished {toolName} ~{duration}~"
|
|
1360
|
+
*/
|
|
1361
|
+
completeTextTemplate?: string;
|
|
1362
|
+
/**
|
|
1363
|
+
* Primary color for shimmer-color animation mode.
|
|
1364
|
+
* Defaults to the current text color.
|
|
1365
|
+
*/
|
|
1366
|
+
loadingAnimationColor?: string;
|
|
1367
|
+
/**
|
|
1368
|
+
* Secondary/end color for shimmer-color animation mode.
|
|
1369
|
+
* Creates a gradient sweep between `loadingAnimationColor` and this color.
|
|
1370
|
+
* @default "#3b82f6"
|
|
1371
|
+
*/
|
|
1372
|
+
loadingAnimationSecondaryColor?: string;
|
|
1373
|
+
/**
|
|
1374
|
+
* Duration of one full animation cycle in milliseconds.
|
|
1375
|
+
* Applies to pulse, shimmer, shimmer-color, and rainbow modes.
|
|
1376
|
+
* @default 2000
|
|
1377
|
+
*/
|
|
1378
|
+
loadingAnimationDuration?: number;
|
|
1288
1379
|
};
|
|
1289
1380
|
|
|
1290
1381
|
export type AgentWidgetReasoningConfig = {
|
package/src/ui.scroll.test.ts
CHANGED
|
@@ -34,6 +34,16 @@ const installRafMock = () => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
|
+
step(frameCount = 1) {
|
|
38
|
+
let frames = 0;
|
|
39
|
+
while (callbacks.size > 0 && frames < frameCount) {
|
|
40
|
+
const pending = [...callbacks.entries()];
|
|
41
|
+
callbacks.clear();
|
|
42
|
+
frames += 1;
|
|
43
|
+
now += 16;
|
|
44
|
+
pending.forEach(([, callback]) => callback(now));
|
|
45
|
+
}
|
|
46
|
+
},
|
|
37
47
|
flush(maxFrames = 80) {
|
|
38
48
|
let frames = 0;
|
|
39
49
|
while (callbacks.size > 0 && frames < maxFrames) {
|
|
@@ -215,14 +225,14 @@ describe("createAgentExperience streaming scroll", () => {
|
|
|
215
225
|
|
|
216
226
|
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
217
227
|
|
|
218
|
-
metrics.setScrollTop(metrics.getBottomScrollTop() -
|
|
228
|
+
metrics.setScrollTop(metrics.getBottomScrollTop() - 6);
|
|
219
229
|
scrollContainer!.dispatchEvent(new Event("scroll"));
|
|
220
230
|
|
|
221
231
|
metrics.setScrollHeight(1040);
|
|
222
232
|
emitStreamingMessage(controller, "Second chunk");
|
|
223
233
|
raf.flush();
|
|
224
234
|
|
|
225
|
-
expect(metrics.getScrollTop()).toBe(
|
|
235
|
+
expect(metrics.getScrollTop()).toBe(594);
|
|
226
236
|
|
|
227
237
|
controller.destroy();
|
|
228
238
|
});
|
|
@@ -364,6 +374,39 @@ describe("createAgentExperience streaming scroll", () => {
|
|
|
364
374
|
controller.destroy();
|
|
365
375
|
});
|
|
366
376
|
|
|
377
|
+
it("catches up immediately when a streamed update lands far behind", () => {
|
|
378
|
+
const raf = installRafMock();
|
|
379
|
+
const mount = createMount();
|
|
380
|
+
const controller = createAgentExperience(mount, {
|
|
381
|
+
apiUrl: "https://api.example.com/chat",
|
|
382
|
+
launcher: { enabled: false }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
386
|
+
expect(scrollContainer).not.toBeNull();
|
|
387
|
+
|
|
388
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
389
|
+
scrollHeight: 900,
|
|
390
|
+
clientHeight: 400
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
emitStreamingStatus(controller);
|
|
394
|
+
emitStreamingMessage(controller, "Chunk one");
|
|
395
|
+
raf.flush();
|
|
396
|
+
|
|
397
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
398
|
+
|
|
399
|
+
metrics.setScrollHeight(1080);
|
|
400
|
+
emitStreamingMessage(controller, "Chunk two");
|
|
401
|
+
|
|
402
|
+
// Only run the scheduled auto-scroll frame, not the whole animation.
|
|
403
|
+
raf.step(1);
|
|
404
|
+
|
|
405
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
406
|
+
|
|
407
|
+
controller.destroy();
|
|
408
|
+
});
|
|
409
|
+
|
|
367
410
|
it("lets the user break away during reasoning streaming", () => {
|
|
368
411
|
const raf = installRafMock();
|
|
369
412
|
const mount = createMount();
|
package/src/ui.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { MessageTransform, MessageActionCallbacks, LoadingIndicatorRenderer } fr
|
|
|
54
54
|
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
55
55
|
import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
56
56
|
import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
|
|
57
|
+
import { formatElapsedMs } from "./utils/formatting";
|
|
57
58
|
import { createApprovalBubble } from "./components/approval-bubble";
|
|
58
59
|
import { createSuggestions } from "./components/suggestions";
|
|
59
60
|
import { EventStreamBuffer } from "./utils/event-stream-buffer";
|
|
@@ -1989,8 +1990,13 @@ export const createAgentExperience = (
|
|
|
1989
1990
|
let isAutoScrolling = false;
|
|
1990
1991
|
let hasPendingAutoScroll = false;
|
|
1991
1992
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1993
|
+
// Scroll events caused by layout, scroll anchoring, and smooth-scroll
|
|
1994
|
+
// easing can easily move by a couple pixels. Keep manual wheel intent
|
|
1995
|
+
// responsive, but require a slightly larger raw scroll delta before we
|
|
1996
|
+
// treat a plain scroll event as the user breaking away.
|
|
1997
|
+
const USER_SCROLL_THRESHOLD = 4;
|
|
1998
|
+
const BOTTOM_THRESHOLD = 24;
|
|
1999
|
+
const AUTO_SCROLL_SNAP_THRESHOLD = 80;
|
|
1994
2000
|
const messageState = new Map<
|
|
1995
2001
|
string,
|
|
1996
2002
|
{ streaming?: boolean; role: AgentWidgetMessage["role"] }
|
|
@@ -2177,6 +2183,18 @@ export const createAgentExperience = (
|
|
|
2177
2183
|
return;
|
|
2178
2184
|
}
|
|
2179
2185
|
|
|
2186
|
+
// If the transcript has fallen noticeably behind, catch up immediately
|
|
2187
|
+
// instead of easing over multiple frames. This keeps fast streaming /
|
|
2188
|
+
// bursty tool and reasoning updates pinned to the bottom.
|
|
2189
|
+
if (Math.abs(distance) >= AUTO_SCROLL_SNAP_THRESHOLD) {
|
|
2190
|
+
cancelSmoothScroll();
|
|
2191
|
+
isAutoScrolling = true;
|
|
2192
|
+
element.scrollTop = target;
|
|
2193
|
+
lastScrollTop = element.scrollTop;
|
|
2194
|
+
isAutoScrolling = false;
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2180
2198
|
// Cancel any ongoing smooth scroll animation
|
|
2181
2199
|
cancelSmoothScroll();
|
|
2182
2200
|
|
|
@@ -2960,9 +2978,33 @@ export const createAgentExperience = (
|
|
|
2960
2978
|
};
|
|
2961
2979
|
}
|
|
2962
2980
|
|
|
2981
|
+
// Global timer for live-updating tool elapsed time spans.
|
|
2982
|
+
// Runs at 100ms while any [data-tool-elapsed] span exists in the message area,
|
|
2983
|
+
// auto-stops when none remain. Operates on real DOM after morph, not temp elements.
|
|
2984
|
+
let toolElapsedTimerId: ReturnType<typeof setInterval> | null = null;
|
|
2985
|
+
const ensureToolElapsedTimer = () => {
|
|
2986
|
+
if (toolElapsedTimerId != null) return;
|
|
2987
|
+
toolElapsedTimerId = setInterval(() => {
|
|
2988
|
+
const spans = messagesWrapper.querySelectorAll<HTMLElement>("[data-tool-elapsed]");
|
|
2989
|
+
if (spans.length === 0) {
|
|
2990
|
+
clearInterval(toolElapsedTimerId!);
|
|
2991
|
+
toolElapsedTimerId = null;
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const now = Date.now();
|
|
2995
|
+
spans.forEach((span) => {
|
|
2996
|
+
const startedAt = Number(span.getAttribute("data-tool-elapsed"));
|
|
2997
|
+
if (!startedAt) return;
|
|
2998
|
+
span.textContent = formatElapsedMs(now - startedAt);
|
|
2999
|
+
});
|
|
3000
|
+
}, 100);
|
|
3001
|
+
};
|
|
3002
|
+
|
|
2963
3003
|
session = new AgentWidgetSession(config, {
|
|
2964
3004
|
onMessagesChanged(messages) {
|
|
2965
3005
|
renderMessagesWithPlugins(messagesWrapper, messages, postprocess);
|
|
3006
|
+
// Start elapsed timer if any active tool has a live duration span
|
|
3007
|
+
ensureToolElapsedTimer();
|
|
2966
3008
|
// Re-render suggestions to hide them after first user message
|
|
2967
3009
|
// Pass messages directly to avoid calling session.getMessages() during construction
|
|
2968
3010
|
if (session) {
|
|
@@ -5716,6 +5758,10 @@ export const createAgentExperience = (
|
|
|
5716
5758
|
return session.submitNPSFeedback(rating, comment);
|
|
5717
5759
|
},
|
|
5718
5760
|
destroy() {
|
|
5761
|
+
if (toolElapsedTimerId != null) {
|
|
5762
|
+
clearInterval(toolElapsedTimerId);
|
|
5763
|
+
toolElapsedTimerId = null;
|
|
5764
|
+
}
|
|
5719
5765
|
destroyCallbacks.forEach((cb) => cb());
|
|
5720
5766
|
wrapper.remove();
|
|
5721
5767
|
launcherButtonInstance?.destroy();
|