@runtypelabs/persona 3.11.0 → 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.
@@ -218,9 +218,12 @@ export const THEME_TOKEN_DOCS = {
218
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.',
219
219
  },
220
220
  reasoning: {
221
- description: 'Reasoning/thinking row rendering hooks.',
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.',
222
225
  properties:
223
- 'renderCollapsedSummary, renderCollapsedPreview.',
226
+ 'renderCollapsedSummary, renderCollapsedPreview, activeTextTemplate, completeTextTemplate, loadingAnimationColor, loadingAnimationSecondaryColor, loadingAnimationDuration.',
224
227
  },
225
228
  approval: {
226
229
  description:
@@ -285,7 +288,7 @@ export const THEME_TOKEN_DOCS = {
285
288
  features: {
286
289
  description: 'Feature flags.',
287
290
  properties:
288
- 'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped, expandable, loadingAnimation), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines, expandable), 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).',
289
292
  },
290
293
  },
291
294
  }
@@ -19,6 +19,7 @@ describe("tool call display defaults", () => {
19
19
  activePreview: false,
20
20
  previewMaxLines: 3,
21
21
  expandable: true,
22
+ loadingAnimation: "none",
22
23
  });
23
24
  });
24
25
  });
package/src/types.ts CHANGED
@@ -656,6 +656,17 @@ export type AgentWidgetReasoningDisplayFeature = {
656
656
  * @default true
657
657
  */
658
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;
659
670
  };
660
671
 
661
672
  export type AgentWidgetFeatureFlags = {
@@ -1390,6 +1401,14 @@ export type AgentWidgetReasoningConfig = {
1390
1401
  previewText: string;
1391
1402
  isActive: boolean;
1392
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;
1393
1412
  }) => HTMLElement | string | null;
1394
1413
  /**
1395
1414
  * Override the lightweight collapsed preview content shown for active reasoning rows.
@@ -1402,6 +1421,45 @@ export type AgentWidgetReasoningConfig = {
1402
1421
  isActive: boolean;
1403
1422
  config: AgentWidgetConfig;
1404
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;
1405
1463
  };
1406
1464
 
1407
1465
  export type AgentWidgetSuggestionChipsConfig = {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { createJsonStreamParser, parseFormattedTemplate } 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", () => {
@@ -244,3 +244,27 @@ describe("parseFormattedTemplate", () => {
244
244
  ]);
245
245
  });
246
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
+ });
@@ -115,6 +115,21 @@ export const computeToolElapsed = (tool: AgentWidgetToolCall): string => {
115
115
  return formatElapsedMs(durationMs);
116
116
  };
117
117
 
118
+ /**
119
+ * Computes the current elapsed time string for a reasoning block.
120
+ */
121
+ export const computeReasoningElapsed = (reasoning: AgentWidgetReasoning): string => {
122
+ const durationMs =
123
+ reasoning.durationMs !== undefined
124
+ ? reasoning.durationMs
125
+ : Math.max(
126
+ 0,
127
+ (reasoning.completedAt ?? Date.now()) -
128
+ (reasoning.startedAt ?? reasoning.completedAt ?? Date.now())
129
+ );
130
+ return formatElapsedMs(durationMs);
131
+ };
132
+
118
133
  /**
119
134
  * Resolves a text template with tool call placeholders.
120
135
  * Supported placeholders: {toolName}, {duration}
@@ -90,6 +90,18 @@ describe("computeMessageFingerprint", () => {
90
90
  expect(fp1).not.toBe(fp2);
91
91
  });
92
92
 
93
+ it("changes when toolCall name changes", () => {
94
+ const fp1 = computeMessageFingerprint(
95
+ makeMessage({ toolCall: { status: "running" } }),
96
+ 0
97
+ );
98
+ const fp2 = computeMessageFingerprint(
99
+ makeMessage({ toolCall: { status: "running", name: "UCP Search Catalog" } }),
100
+ 0
101
+ );
102
+ expect(fp1).not.toBe(fp2);
103
+ });
104
+
93
105
  it("changes when toolCall chunks change", () => {
94
106
  const fp1 = computeMessageFingerprint(
95
107
  makeMessage({ toolCall: { status: "running", chunks: ["Loaded tools"] } }),
@@ -53,6 +53,7 @@ export function computeMessageFingerprint(
53
53
  message.llmContent?.length ?? 0,
54
54
  message.approval?.status ?? "",
55
55
  message.toolCall?.status ?? "",
56
+ message.toolCall?.name ?? "",
56
57
  message.toolCall?.chunks?.length ?? 0,
57
58
  message.toolCall?.chunks?.[message.toolCall.chunks.length - 1]?.slice(-32) ?? "",
58
59
  typeof message.toolCall?.args === "string"
@@ -0,0 +1,86 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect } from "vitest";
3
+ import { morphMessages } from "./morph";
4
+
5
+ function makeContainer(html: string): HTMLElement {
6
+ const div = document.createElement("div");
7
+ div.innerHTML = html;
8
+ return div;
9
+ }
10
+
11
+ function makeNewContent(html: string): HTMLElement {
12
+ const div = document.createElement("div");
13
+ div.innerHTML = html;
14
+ return div;
15
+ }
16
+
17
+ describe("morphMessages", () => {
18
+ describe("data-preserve-animation", () => {
19
+ it("preserves animated element when old and new both have data-preserve-animation with same text", () => {
20
+ const container = makeContainer(
21
+ '<span data-preserve-animation="true">Calling tool... 0.1s</span>'
22
+ );
23
+ const oldSpan = container.querySelector("span")!;
24
+
25
+ morphMessages(
26
+ container,
27
+ makeNewContent(
28
+ '<span data-preserve-animation="true">Calling tool... 0.1s</span>'
29
+ )
30
+ );
31
+
32
+ expect(container.querySelector("span")).toBe(oldSpan);
33
+ });
34
+
35
+ it("allows morph when new node drops data-preserve-animation (tool completed)", () => {
36
+ const container = makeContainer(
37
+ '<span data-preserve-animation="true">Calling tool... 0.5s</span>'
38
+ );
39
+
40
+ morphMessages(
41
+ container,
42
+ makeNewContent("<span>Finished tool 0.5s</span>")
43
+ );
44
+
45
+ expect(container.querySelector("span")!.textContent).toBe(
46
+ "Finished tool 0.5s"
47
+ );
48
+ expect(
49
+ container.querySelector("span")!.hasAttribute("data-preserve-animation")
50
+ ).toBe(false);
51
+ });
52
+
53
+ it("allows morph when text content changes despite both having data-preserve-animation", () => {
54
+ const container = makeContainer(
55
+ '<span data-preserve-animation="true">Calling tool... 0.1s</span>'
56
+ );
57
+
58
+ morphMessages(
59
+ container,
60
+ makeNewContent(
61
+ '<span data-preserve-animation="true">Calling UCP Search Catalog... 0.2s</span>'
62
+ )
63
+ );
64
+
65
+ expect(container.querySelector("span")!.textContent).toBe(
66
+ "Calling UCP Search Catalog... 0.2s"
67
+ );
68
+ });
69
+
70
+ it("does not preserve when preserveTypingAnimation is false", () => {
71
+ const container = makeContainer(
72
+ '<span data-preserve-animation="true">Old text</span>'
73
+ );
74
+
75
+ morphMessages(
76
+ container,
77
+ makeNewContent(
78
+ '<span data-preserve-animation="true">New text</span>'
79
+ ),
80
+ { preserveTypingAnimation: false }
81
+ );
82
+
83
+ expect(container.querySelector("span")!.textContent).toBe("New text");
84
+ });
85
+ });
86
+ });
@@ -35,6 +35,14 @@ export const morphMessages = (
35
35
  if (newNode instanceof HTMLElement && !newNode.hasAttribute("data-preserve-animation")) {
36
36
  return;
37
37
  }
38
+ // Allow morph when content has meaningfully changed (e.g. tool name arrived)
39
+ if (newNode instanceof HTMLElement && newNode.hasAttribute("data-preserve-animation")) {
40
+ const oldText = oldNode.textContent ?? "";
41
+ const newText = newNode.textContent ?? "";
42
+ if (oldText !== newText) {
43
+ return;
44
+ }
45
+ }
38
46
  return false;
39
47
  }
40
48
  }