@runtypelabs/persona 3.10.1 → 3.12.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 +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.global.js +68 -68
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/theme-editor.cjs +328 -26
- package/dist/theme-editor.d.cts +143 -0
- package/dist/theme-editor.d.ts +143 -0
- package/dist/theme-editor.js +328 -26
- 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/reasoning-bubble.ts +139 -5
- package/src/components/tool-bubble.ts +121 -1
- package/src/defaults.ts +2 -0
- package/src/install.ts +4 -1
- package/src/styles/widget.css +80 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +2 -0
- package/src/types.ts +149 -0
- package/src/ui.scroll.test.ts +45 -2
- package/src/ui.ts +48 -2
- package/src/utils/formatting.test.ts +99 -1
- package/src/utils/formatting.ts +145 -0
- package/src/utils/message-fingerprint.test.ts +12 -0
- package/src/utils/message-fingerprint.ts +1 -0
- package/src/utils/morph.test.ts +86 -0
- package/src/utils/morph.ts +17 -3
package/src/theme-reference.ts
CHANGED
|
@@ -210,14 +210,20 @@ 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
|
-
description:
|
|
221
|
+
description:
|
|
222
|
+
'Reasoning/thinking row rendering hooks, text templates, and loading animations. ' +
|
|
223
|
+
'Text templates support {duration} placeholder and inline formatting (~dim~, *italic*, **bold**). ' +
|
|
224
|
+
'renderCollapsedSummary receives elapsed (static string) and createElapsedElement() (live-updating span) in its context.',
|
|
219
225
|
properties:
|
|
220
|
-
'renderCollapsedSummary, renderCollapsedPreview.',
|
|
226
|
+
'renderCollapsedSummary, renderCollapsedPreview, activeTextTemplate, completeTextTemplate, loadingAnimationColor, loadingAnimationSecondaryColor, loadingAnimationDuration.',
|
|
221
227
|
},
|
|
222
228
|
approval: {
|
|
223
229
|
description:
|
|
@@ -282,7 +288,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
282
288
|
features: {
|
|
283
289
|
description: 'Feature flags.',
|
|
284
290
|
properties:
|
|
285
|
-
'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines), artifacts (sidebar config).',
|
|
291
|
+
'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped, expandable, loadingAnimation), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines, expandable, loadingAnimation), artifacts (sidebar config).',
|
|
286
292
|
},
|
|
287
293
|
},
|
|
288
294
|
}
|
|
@@ -10,6 +10,7 @@ describe("tool call display defaults", () => {
|
|
|
10
10
|
grouped: false,
|
|
11
11
|
previewMaxLines: 3,
|
|
12
12
|
expandable: true,
|
|
13
|
+
loadingAnimation: "none",
|
|
13
14
|
});
|
|
14
15
|
});
|
|
15
16
|
|
|
@@ -18,6 +19,7 @@ describe("tool call display defaults", () => {
|
|
|
18
19
|
activePreview: false,
|
|
19
20
|
previewMaxLines: 3,
|
|
20
21
|
expandable: true,
|
|
22
|
+
loadingAnimation: "none",
|
|
21
23
|
});
|
|
22
24
|
});
|
|
23
25
|
});
|
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 = {
|
|
@@ -631,6 +656,17 @@ export type AgentWidgetReasoningDisplayFeature = {
|
|
|
631
656
|
* @default true
|
|
632
657
|
*/
|
|
633
658
|
expandable?: boolean;
|
|
659
|
+
/**
|
|
660
|
+
* Animation mode applied to the reasoning header text while reasoning is active.
|
|
661
|
+
* Reuses the same modes as tool call animations.
|
|
662
|
+
* - "none" — static text, no animation
|
|
663
|
+
* - "pulse" — opacity pulse on the entire header text
|
|
664
|
+
* - "shimmer" — monochrome opacity sweep per character
|
|
665
|
+
* - "shimmer-color" — color gradient sweep per character
|
|
666
|
+
* - "rainbow" — rainbow color cycle per character
|
|
667
|
+
* @default "none"
|
|
668
|
+
*/
|
|
669
|
+
loadingAnimation?: AgentWidgetToolCallLoadingAnimation;
|
|
634
670
|
};
|
|
635
671
|
|
|
636
672
|
export type AgentWidgetFeatureFlags = {
|
|
@@ -1234,22 +1270,39 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1234
1270
|
export type AgentWidgetToolCallConfig = {
|
|
1235
1271
|
/** Box-shadow for tool-call bubbles; overrides `theme.toolBubbleShadow` when set. */
|
|
1236
1272
|
shadow?: string;
|
|
1273
|
+
/** Background color of the tool call bubble container. */
|
|
1237
1274
|
backgroundColor?: string;
|
|
1275
|
+
/** Border color of the tool call bubble container. */
|
|
1238
1276
|
borderColor?: string;
|
|
1277
|
+
/** Border width of the tool call bubble container (CSS value, e.g. `"1px"`). */
|
|
1239
1278
|
borderWidth?: string;
|
|
1279
|
+
/** Border radius of the tool call bubble container (CSS value, e.g. `"12px"`). */
|
|
1240
1280
|
borderRadius?: string;
|
|
1281
|
+
/** Background color of the collapsed header row. */
|
|
1241
1282
|
headerBackgroundColor?: string;
|
|
1283
|
+
/** Text color of the collapsed header row (tool name / summary). */
|
|
1242
1284
|
headerTextColor?: string;
|
|
1285
|
+
/** Horizontal padding of the collapsed header row (CSS value). */
|
|
1243
1286
|
headerPaddingX?: string;
|
|
1287
|
+
/** Vertical padding of the collapsed header row (CSS value). */
|
|
1244
1288
|
headerPaddingY?: string;
|
|
1289
|
+
/** Background color of the expanded content area. */
|
|
1245
1290
|
contentBackgroundColor?: string;
|
|
1291
|
+
/** Text color of the expanded content area. */
|
|
1246
1292
|
contentTextColor?: string;
|
|
1293
|
+
/** Horizontal padding of the expanded content area (CSS value). */
|
|
1247
1294
|
contentPaddingX?: string;
|
|
1295
|
+
/** Vertical padding of the expanded content area (CSS value). */
|
|
1248
1296
|
contentPaddingY?: string;
|
|
1297
|
+
/** Background color of code blocks (arguments / result) in the expanded area. */
|
|
1249
1298
|
codeBlockBackgroundColor?: string;
|
|
1299
|
+
/** Border color of code blocks in the expanded area. */
|
|
1250
1300
|
codeBlockBorderColor?: string;
|
|
1301
|
+
/** Text color of code blocks in the expanded area. */
|
|
1251
1302
|
codeBlockTextColor?: string;
|
|
1303
|
+
/** Color of the expand/collapse toggle icon. */
|
|
1252
1304
|
toggleTextColor?: string;
|
|
1305
|
+
/** Color of section labels ("Arguments", "Result", "Activity") in the expanded area. */
|
|
1253
1306
|
labelTextColor?: string;
|
|
1254
1307
|
/**
|
|
1255
1308
|
* Override the collapsed summary row content for a tool call bubble.
|
|
@@ -1263,6 +1316,14 @@ export type AgentWidgetToolCallConfig = {
|
|
|
1263
1316
|
collapsedMode: AgentWidgetToolCallCollapsedMode;
|
|
1264
1317
|
isActive: boolean;
|
|
1265
1318
|
config: AgentWidgetConfig;
|
|
1319
|
+
/** Static elapsed time snapshot, e.g. "2.6s". */
|
|
1320
|
+
elapsed: string;
|
|
1321
|
+
/**
|
|
1322
|
+
* Returns a `<span>` whose text content is automatically updated every
|
|
1323
|
+
* 100ms by the widget's global timer. Place it anywhere in your returned
|
|
1324
|
+
* HTMLElement to get a live-ticking duration display.
|
|
1325
|
+
*/
|
|
1326
|
+
createElapsedElement: () => HTMLElement;
|
|
1266
1327
|
}) => HTMLElement | string | null;
|
|
1267
1328
|
/**
|
|
1268
1329
|
* Override the lightweight collapsed preview content shown for active tool rows.
|
|
@@ -1285,6 +1346,47 @@ export type AgentWidgetToolCallConfig = {
|
|
|
1285
1346
|
defaultSummary: string;
|
|
1286
1347
|
config: AgentWidgetConfig;
|
|
1287
1348
|
}) => HTMLElement | string | null;
|
|
1349
|
+
/**
|
|
1350
|
+
* Template string for the header text while a tool call is active (running).
|
|
1351
|
+
*
|
|
1352
|
+
* **Placeholders:** `{toolName}` (tool name), `{duration}` (live-updating elapsed time).
|
|
1353
|
+
*
|
|
1354
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — parsed at render time and
|
|
1355
|
+
* applied as styled `<span>` elements. Works with all animation modes.
|
|
1356
|
+
*
|
|
1357
|
+
* When not set, falls back to the current `collapsedMode` behavior.
|
|
1358
|
+
* @example "Calling {toolName}... ~{duration}~"
|
|
1359
|
+
* @example "**Searching** *{toolName}*..."
|
|
1360
|
+
*/
|
|
1361
|
+
activeTextTemplate?: string;
|
|
1362
|
+
/**
|
|
1363
|
+
* Template string for the header text when a tool call is complete.
|
|
1364
|
+
*
|
|
1365
|
+
* **Placeholders:** `{toolName}` (tool name), `{duration}` (final elapsed time).
|
|
1366
|
+
*
|
|
1367
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — same syntax as `activeTextTemplate`.
|
|
1368
|
+
*
|
|
1369
|
+
* When not set, falls back to the existing "Used tool for X seconds" text.
|
|
1370
|
+
* @example "Finished {toolName} ~{duration}~"
|
|
1371
|
+
*/
|
|
1372
|
+
completeTextTemplate?: string;
|
|
1373
|
+
/**
|
|
1374
|
+
* Primary color for shimmer-color animation mode.
|
|
1375
|
+
* Defaults to the current text color.
|
|
1376
|
+
*/
|
|
1377
|
+
loadingAnimationColor?: string;
|
|
1378
|
+
/**
|
|
1379
|
+
* Secondary/end color for shimmer-color animation mode.
|
|
1380
|
+
* Creates a gradient sweep between `loadingAnimationColor` and this color.
|
|
1381
|
+
* @default "#3b82f6"
|
|
1382
|
+
*/
|
|
1383
|
+
loadingAnimationSecondaryColor?: string;
|
|
1384
|
+
/**
|
|
1385
|
+
* Duration of one full animation cycle in milliseconds.
|
|
1386
|
+
* Applies to pulse, shimmer, shimmer-color, and rainbow modes.
|
|
1387
|
+
* @default 2000
|
|
1388
|
+
*/
|
|
1389
|
+
loadingAnimationDuration?: number;
|
|
1288
1390
|
};
|
|
1289
1391
|
|
|
1290
1392
|
export type AgentWidgetReasoningConfig = {
|
|
@@ -1299,6 +1401,14 @@ export type AgentWidgetReasoningConfig = {
|
|
|
1299
1401
|
previewText: string;
|
|
1300
1402
|
isActive: boolean;
|
|
1301
1403
|
config: AgentWidgetConfig;
|
|
1404
|
+
/** Static elapsed time snapshot, e.g. "2.6s". */
|
|
1405
|
+
elapsed: string;
|
|
1406
|
+
/**
|
|
1407
|
+
* Returns a `<span>` whose text content is automatically updated every
|
|
1408
|
+
* 100ms by the widget's global timer. Place it anywhere in your returned
|
|
1409
|
+
* HTMLElement to get a live-ticking duration display.
|
|
1410
|
+
*/
|
|
1411
|
+
createElapsedElement: () => HTMLElement;
|
|
1302
1412
|
}) => HTMLElement | string | null;
|
|
1303
1413
|
/**
|
|
1304
1414
|
* Override the lightweight collapsed preview content shown for active reasoning rows.
|
|
@@ -1311,6 +1421,45 @@ export type AgentWidgetReasoningConfig = {
|
|
|
1311
1421
|
isActive: boolean;
|
|
1312
1422
|
config: AgentWidgetConfig;
|
|
1313
1423
|
}) => HTMLElement | string | null;
|
|
1424
|
+
/**
|
|
1425
|
+
* Template string for the header text while reasoning is active (streaming).
|
|
1426
|
+
*
|
|
1427
|
+
* **Placeholders:** `{duration}` (live-updating elapsed time).
|
|
1428
|
+
*
|
|
1429
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — parsed at render time.
|
|
1430
|
+
*
|
|
1431
|
+
* When not set, falls back to the default "Thinking..." text.
|
|
1432
|
+
* @example "Thinking... ~{duration}~"
|
|
1433
|
+
*/
|
|
1434
|
+
activeTextTemplate?: string;
|
|
1435
|
+
/**
|
|
1436
|
+
* Template string for the header text when reasoning is complete.
|
|
1437
|
+
*
|
|
1438
|
+
* **Placeholders:** `{duration}` (final elapsed time).
|
|
1439
|
+
*
|
|
1440
|
+
* **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — same syntax as `activeTextTemplate`.
|
|
1441
|
+
*
|
|
1442
|
+
* When not set, falls back to the default "Thought for X seconds" text.
|
|
1443
|
+
* @example "Thought for ~{duration}~"
|
|
1444
|
+
*/
|
|
1445
|
+
completeTextTemplate?: string;
|
|
1446
|
+
/**
|
|
1447
|
+
* Primary color for shimmer-color animation mode.
|
|
1448
|
+
* Defaults to the current text color.
|
|
1449
|
+
*/
|
|
1450
|
+
loadingAnimationColor?: string;
|
|
1451
|
+
/**
|
|
1452
|
+
* Secondary/end color for shimmer-color animation mode.
|
|
1453
|
+
* Creates a gradient sweep between `loadingAnimationColor` and this color.
|
|
1454
|
+
* @default "#3b82f6"
|
|
1455
|
+
*/
|
|
1456
|
+
loadingAnimationSecondaryColor?: string;
|
|
1457
|
+
/**
|
|
1458
|
+
* Duration of one full animation cycle in milliseconds.
|
|
1459
|
+
* Applies to pulse, shimmer, shimmer-color, and rainbow modes.
|
|
1460
|
+
* @default 2000
|
|
1461
|
+
*/
|
|
1462
|
+
loadingAnimationDuration?: number;
|
|
1314
1463
|
};
|
|
1315
1464
|
|
|
1316
1465
|
export type AgentWidgetSuggestionChipsConfig = {
|
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();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createJsonStreamParser } from "./formatting";
|
|
2
|
+
import { createJsonStreamParser, parseFormattedTemplate, computeReasoningElapsed } from "./formatting";
|
|
3
3
|
|
|
4
4
|
describe("JSON Stream Parser", () => {
|
|
5
5
|
it("should extract text field incrementally as JSON streams in", () => {
|
|
@@ -170,3 +170,101 @@ describe("JSON Stream Parser", () => {
|
|
|
170
170
|
expect(finalResult).toBe("You're welcome! Enjoy your browsing, and I'm here if you need anything!");
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
describe("parseFormattedTemplate", () => {
|
|
175
|
+
it("returns plain text segments when no formatting markers are present", () => {
|
|
176
|
+
const segments = parseFormattedTemplate("Calling {toolName}...", "Get Weather");
|
|
177
|
+
expect(segments).toEqual([
|
|
178
|
+
{ text: "Calling Get Weather...", styles: [] },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("resolves {toolName} placeholder", () => {
|
|
183
|
+
const segments = parseFormattedTemplate("{toolName} running", "Search Catalog");
|
|
184
|
+
expect(segments).toEqual([
|
|
185
|
+
{ text: "Search Catalog running", styles: [] },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("parses ~dim~ markers", () => {
|
|
190
|
+
const segments = parseFormattedTemplate("Finished {toolName} ~{duration}~", "Get Weather");
|
|
191
|
+
expect(segments).toEqual([
|
|
192
|
+
{ text: "Finished Get Weather ", styles: [] },
|
|
193
|
+
{ text: "{duration}", styles: ["dim"], isDuration: true },
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("parses *italic* markers", () => {
|
|
198
|
+
const segments = parseFormattedTemplate("*{toolName}* completed", "Search");
|
|
199
|
+
expect(segments).toEqual([
|
|
200
|
+
{ text: "Search", styles: ["italic"] },
|
|
201
|
+
{ text: " completed", styles: [] },
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("parses **bold** markers", () => {
|
|
206
|
+
const segments = parseFormattedTemplate("**Calling** {toolName}", "Lookup");
|
|
207
|
+
expect(segments).toEqual([
|
|
208
|
+
{ text: "Calling", styles: ["bold"] },
|
|
209
|
+
{ text: " Lookup", styles: [] },
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("handles multiple formatting markers in one template", () => {
|
|
214
|
+
const segments = parseFormattedTemplate("**Done** *{toolName}* ~{duration}~", "API");
|
|
215
|
+
expect(segments).toEqual([
|
|
216
|
+
{ text: "Done", styles: ["bold"] },
|
|
217
|
+
{ text: " ", styles: [] },
|
|
218
|
+
{ text: "API", styles: ["italic"] },
|
|
219
|
+
{ text: " ", styles: [] },
|
|
220
|
+
{ text: "{duration}", styles: ["dim"], isDuration: true },
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("handles {duration} without formatting markers", () => {
|
|
225
|
+
const segments = parseFormattedTemplate("Ran for {duration}", "Tool");
|
|
226
|
+
expect(segments).toEqual([
|
|
227
|
+
{ text: "Ran for ", styles: [] },
|
|
228
|
+
{ text: "{duration}", styles: [], isDuration: true },
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("handles template with no placeholders", () => {
|
|
233
|
+
const segments = parseFormattedTemplate("Running...", "Ignored");
|
|
234
|
+
expect(segments).toEqual([
|
|
235
|
+
{ text: "Running...", styles: [] },
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("handles empty tool name fallback in template", () => {
|
|
240
|
+
const segments = parseFormattedTemplate("{toolName}", " ");
|
|
241
|
+
// toolName is resolved before parsing, so whitespace stays
|
|
242
|
+
expect(segments).toEqual([
|
|
243
|
+
{ text: " ", styles: [] },
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("computeReasoningElapsed", () => {
|
|
249
|
+
it("uses durationMs when provided", () => {
|
|
250
|
+
const result = computeReasoningElapsed({
|
|
251
|
+
id: "r1", status: "complete", chunks: [], durationMs: 2600,
|
|
252
|
+
});
|
|
253
|
+
expect(result).toBe("2.6s");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("computes from startedAt/completedAt when durationMs is undefined", () => {
|
|
257
|
+
const result = computeReasoningElapsed({
|
|
258
|
+
id: "r2", status: "complete", chunks: [],
|
|
259
|
+
startedAt: 1000, completedAt: 16000,
|
|
260
|
+
});
|
|
261
|
+
expect(result).toBe("15s");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("returns <0.1s for very short durations", () => {
|
|
265
|
+
const result = computeReasoningElapsed({
|
|
266
|
+
id: "r3", status: "complete", chunks: [], durationMs: 50,
|
|
267
|
+
});
|
|
268
|
+
expect(result).toBe("<0.1s");
|
|
269
|
+
});
|
|
270
|
+
});
|