@runtypelabs/persona 3.10.0 → 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 +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.global.js +61 -61
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +339 -27
- package/dist/theme-editor.d.cts +114 -0
- package/dist/theme-editor.d.ts +114 -0
- package/dist/theme-editor.js +339 -27
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +110 -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 +110 -0
- package/src/theme-reference.ts +6 -3
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types.ts +120 -0
- package/src/ui.attachments-drop.test.ts +188 -0
- package/src/ui.scroll.test.ts +91 -2
- package/src/ui.ts +221 -7
- package/src/utils/formatting.test.ts +75 -1
- package/src/utils/formatting.ts +130 -0
- package/src/utils/morph.ts +9 -3
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 = {
|
|
@@ -2081,6 +2172,35 @@ export type AgentWidgetAttachmentsConfig = {
|
|
|
2081
2172
|
* Callback when a file is rejected (wrong type or too large).
|
|
2082
2173
|
*/
|
|
2083
2174
|
onFileRejected?: (file: File, reason: 'type' | 'size' | 'count') => void;
|
|
2175
|
+
/**
|
|
2176
|
+
* Customize the drag-and-drop overlay that appears when files are dragged over the widget.
|
|
2177
|
+
*/
|
|
2178
|
+
dropOverlay?: {
|
|
2179
|
+
/** Background color/value of the overlay. @default 'rgba(59, 130, 246, 0.08)' */
|
|
2180
|
+
background?: string;
|
|
2181
|
+
/** Backdrop blur applied behind the overlay (CSS value). @default '8px' */
|
|
2182
|
+
backdropBlur?: string;
|
|
2183
|
+
/** Border style shown during drag. @default '2px dashed rgba(59, 130, 246, 0.4)' */
|
|
2184
|
+
border?: string;
|
|
2185
|
+
/** Border radius of the overlay. @default 'inherit' */
|
|
2186
|
+
borderRadius?: string;
|
|
2187
|
+
/** Inset/margin pulling the overlay away from the container edges (CSS value). @default '0' */
|
|
2188
|
+
inset?: string;
|
|
2189
|
+
/** Lucide icon name displayed in the center. @default 'upload' */
|
|
2190
|
+
iconName?: string;
|
|
2191
|
+
/** Icon size (CSS value). @default '48px' */
|
|
2192
|
+
iconSize?: string;
|
|
2193
|
+
/** Icon stroke color. @default 'rgba(59, 130, 246, 0.6)' */
|
|
2194
|
+
iconColor?: string;
|
|
2195
|
+
/** Icon stroke width. @default 0.5 */
|
|
2196
|
+
iconStrokeWidth?: number;
|
|
2197
|
+
/** Optional label text shown below the icon. */
|
|
2198
|
+
label?: string;
|
|
2199
|
+
/** Label font size. @default '0.875rem' */
|
|
2200
|
+
labelSize?: string;
|
|
2201
|
+
/** Label color. @default 'rgba(59, 130, 246, 0.8)' */
|
|
2202
|
+
labelColor?: string;
|
|
2203
|
+
};
|
|
2084
2204
|
};
|
|
2085
2205
|
|
|
2086
2206
|
/**
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
import { AttachmentManager } from "./utils/attachment-manager";
|
|
7
|
+
|
|
8
|
+
const createMount = () => {
|
|
9
|
+
const mount = document.createElement("div");
|
|
10
|
+
document.body.appendChild(mount);
|
|
11
|
+
return mount;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** jsdom does not expose `DataTransfer`; real browsers set `dropEffect` on dragover. */
|
|
15
|
+
function createFileDataTransfer(files: File[]): DataTransfer {
|
|
16
|
+
const list: File[] = [...files];
|
|
17
|
+
const fileList = list as unknown as FileList;
|
|
18
|
+
return {
|
|
19
|
+
dropEffect: "none",
|
|
20
|
+
effectAllowed: "all",
|
|
21
|
+
files: fileList,
|
|
22
|
+
items: {
|
|
23
|
+
add: () => {},
|
|
24
|
+
clear: () => {},
|
|
25
|
+
remove: () => {}
|
|
26
|
+
} as unknown as DataTransferItemList,
|
|
27
|
+
types: files.length > 0 ? ["Files"] : [],
|
|
28
|
+
clearData: () => {},
|
|
29
|
+
getData: () => "",
|
|
30
|
+
setData: () => {},
|
|
31
|
+
setDragImage: () => {}
|
|
32
|
+
} as unknown as DataTransfer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createDragEvent(type: string, dataTransfer: DataTransfer): DragEvent {
|
|
36
|
+
const ev = new Event(type, { bubbles: true, cancelable: true }) as unknown as DragEvent;
|
|
37
|
+
Object.defineProperty(ev, "dataTransfer", { value: dataTransfer, enumerable: true });
|
|
38
|
+
return ev;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("createAgentExperience attachment file drop", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
|
|
44
|
+
cb(0);
|
|
45
|
+
return 1;
|
|
46
|
+
});
|
|
47
|
+
vi.stubGlobal("cancelAnimationFrame", () => {});
|
|
48
|
+
window.scrollTo = vi.fn();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
document.body.innerHTML = "";
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("calls AttachmentManager.handleFiles when files are dropped on the mount", () => {
|
|
57
|
+
const handleFilesSpy = vi.spyOn(AttachmentManager.prototype, "handleFiles");
|
|
58
|
+
|
|
59
|
+
const mount = createMount();
|
|
60
|
+
const controller = createAgentExperience(mount, {
|
|
61
|
+
apiUrl: "https://api.example.com/chat",
|
|
62
|
+
launcher: { enabled: false },
|
|
63
|
+
attachments: { enabled: true, maxFiles: 4 },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const file = new File(["x"], "test.png", { type: "image/png" });
|
|
67
|
+
const dt = createFileDataTransfer([file]);
|
|
68
|
+
|
|
69
|
+
// dragover/drop are on mount so the browser default is suppressed everywhere
|
|
70
|
+
const dragOver = createDragEvent("dragover", dt);
|
|
71
|
+
mount.dispatchEvent(dragOver);
|
|
72
|
+
expect(dragOver.defaultPrevented).toBe(true);
|
|
73
|
+
expect(dt.dropEffect).toBe("copy");
|
|
74
|
+
|
|
75
|
+
const drop = createDragEvent("drop", dt);
|
|
76
|
+
mount.dispatchEvent(drop);
|
|
77
|
+
expect(drop.defaultPrevented).toBe(true);
|
|
78
|
+
|
|
79
|
+
expect(handleFilesSpy).toHaveBeenCalledTimes(1);
|
|
80
|
+
const passed = handleFilesSpy.mock.calls[0]?.[0] as File[];
|
|
81
|
+
expect(passed).toHaveLength(1);
|
|
82
|
+
expect(passed[0]?.name).toBe("test.png");
|
|
83
|
+
|
|
84
|
+
handleFilesSpy.mockRestore();
|
|
85
|
+
controller.destroy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("shows drop-active highlight on container during dragenter", () => {
|
|
89
|
+
const mount = createMount();
|
|
90
|
+
const controller = createAgentExperience(mount, {
|
|
91
|
+
apiUrl: "https://api.example.com/chat",
|
|
92
|
+
launcher: { enabled: false },
|
|
93
|
+
attachments: { enabled: true, maxFiles: 4 },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const container = mount.querySelector(".persona-widget-container")!;
|
|
97
|
+
const file = new File(["x"], "test.png", { type: "image/png" });
|
|
98
|
+
const dt = createFileDataTransfer([file]);
|
|
99
|
+
|
|
100
|
+
container.dispatchEvent(createDragEvent("dragenter", dt));
|
|
101
|
+
expect(container.classList.contains("persona-attachment-drop-active")).toBe(true);
|
|
102
|
+
|
|
103
|
+
container.dispatchEvent(createDragEvent("dragleave", dt));
|
|
104
|
+
expect(container.classList.contains("persona-attachment-drop-active")).toBe(false);
|
|
105
|
+
|
|
106
|
+
controller.destroy();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders drop overlay with icon inside container", () => {
|
|
110
|
+
const mount = createMount();
|
|
111
|
+
const controller = createAgentExperience(mount, {
|
|
112
|
+
apiUrl: "https://api.example.com/chat",
|
|
113
|
+
launcher: { enabled: false },
|
|
114
|
+
attachments: { enabled: true, maxFiles: 4 },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const overlay = mount.querySelector(".persona-attachment-drop-overlay");
|
|
118
|
+
expect(overlay).not.toBeNull();
|
|
119
|
+
expect(overlay!.querySelector("svg")).not.toBeNull();
|
|
120
|
+
|
|
121
|
+
controller.destroy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("applies custom dropOverlay config as CSS variables", () => {
|
|
125
|
+
const mount = createMount();
|
|
126
|
+
const controller = createAgentExperience(mount, {
|
|
127
|
+
apiUrl: "https://api.example.com/chat",
|
|
128
|
+
launcher: { enabled: false },
|
|
129
|
+
attachments: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
maxFiles: 4,
|
|
132
|
+
dropOverlay: {
|
|
133
|
+
background: "rgba(255, 0, 0, 0.1)",
|
|
134
|
+
backdropBlur: "12px",
|
|
135
|
+
border: "2px solid red",
|
|
136
|
+
inset: "8px",
|
|
137
|
+
iconName: "image-plus",
|
|
138
|
+
label: "Drop here",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const overlay = mount.querySelector<HTMLElement>(".persona-attachment-drop-overlay")!;
|
|
144
|
+
expect(overlay).not.toBeNull();
|
|
145
|
+
expect(overlay.style.getPropertyValue("--persona-drop-overlay-bg")).toBe("rgba(255, 0, 0, 0.1)");
|
|
146
|
+
expect(overlay.style.getPropertyValue("--persona-drop-overlay-blur")).toBe("12px");
|
|
147
|
+
expect(overlay.style.getPropertyValue("--persona-drop-overlay-border")).toBe("2px solid red");
|
|
148
|
+
expect(overlay.style.getPropertyValue("--persona-drop-overlay-inset")).toBe("8px");
|
|
149
|
+
|
|
150
|
+
const label = overlay.querySelector(".persona-drop-overlay-label");
|
|
151
|
+
expect(label).not.toBeNull();
|
|
152
|
+
expect(label!.textContent).toBe("Drop here");
|
|
153
|
+
|
|
154
|
+
controller.destroy();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("does not render drop overlay when attachments are disabled", () => {
|
|
158
|
+
const mount = createMount();
|
|
159
|
+
const controller = createAgentExperience(mount, {
|
|
160
|
+
apiUrl: "https://api.example.com/chat",
|
|
161
|
+
launcher: { enabled: false },
|
|
162
|
+
attachments: { enabled: false },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const overlay = mount.querySelector(".persona-attachment-drop-overlay");
|
|
166
|
+
expect(overlay).toBeNull();
|
|
167
|
+
|
|
168
|
+
controller.destroy();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("does not prevent dragover when attachments are disabled", () => {
|
|
172
|
+
const mount = createMount();
|
|
173
|
+
const controller = createAgentExperience(mount, {
|
|
174
|
+
apiUrl: "https://api.example.com/chat",
|
|
175
|
+
launcher: { enabled: false },
|
|
176
|
+
attachments: { enabled: false },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const file = new File(["x"], "test.png", { type: "image/png" });
|
|
180
|
+
const dt = createFileDataTransfer([file]);
|
|
181
|
+
|
|
182
|
+
const dragOver = createDragEvent("dragover", dt);
|
|
183
|
+
mount.dispatchEvent(dragOver);
|
|
184
|
+
expect(dragOver.defaultPrevented).toBe(false);
|
|
185
|
+
|
|
186
|
+
controller.destroy();
|
|
187
|
+
});
|
|
188
|
+
});
|
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();
|
|
@@ -472,6 +515,52 @@ describe("createAgentExperience streaming scroll", () => {
|
|
|
472
515
|
controller.destroy();
|
|
473
516
|
});
|
|
474
517
|
|
|
518
|
+
it("ignores layout-driven scroll events before a scheduled auto-scroll starts", () => {
|
|
519
|
+
const raf = installRafMock();
|
|
520
|
+
const mount = createMount();
|
|
521
|
+
const controller = createAgentExperience(mount, {
|
|
522
|
+
apiUrl: "https://api.example.com/chat",
|
|
523
|
+
launcher: { enabled: false },
|
|
524
|
+
features: {
|
|
525
|
+
toolCallDisplay: {
|
|
526
|
+
activePreview: true,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
} as any);
|
|
530
|
+
|
|
531
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
532
|
+
expect(scrollContainer).not.toBeNull();
|
|
533
|
+
|
|
534
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
535
|
+
scrollHeight: 960,
|
|
536
|
+
clientHeight: 400,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
emitStreamingStatus(controller);
|
|
540
|
+
emitToolMessage(controller, { id: "tool-1", chunks: ["Loaded tools"] });
|
|
541
|
+
emitToolMessage(controller, { id: "tool-2", chunks: ["Fetched docs"] });
|
|
542
|
+
raf.flush();
|
|
543
|
+
|
|
544
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
545
|
+
|
|
546
|
+
metrics.setScrollHeight(1035);
|
|
547
|
+
emitToolMessage(controller, {
|
|
548
|
+
id: "tool-3",
|
|
549
|
+
chunks: ["Compared layouts and noted launcher sizing"],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Simulate the browser emitting a scroll event caused by layout/scroll
|
|
553
|
+
// anchoring before the scheduled auto-scroll rAF has started.
|
|
554
|
+
metrics.setScrollTop(metrics.getScrollTop() - 2);
|
|
555
|
+
scrollContainer!.dispatchEvent(new Event("scroll"));
|
|
556
|
+
|
|
557
|
+
raf.flush();
|
|
558
|
+
|
|
559
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
560
|
+
|
|
561
|
+
controller.destroy();
|
|
562
|
+
});
|
|
563
|
+
|
|
475
564
|
it("uses icon-only arrow-down defaults for the transcript affordance", () => {
|
|
476
565
|
const raf = installRafMock();
|
|
477
566
|
const mount = createMount();
|