@runtypelabs/persona 3.9.1 → 3.10.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.
Files changed (38) hide show
  1. package/dist/index.cjs +46 -44
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +119 -0
  4. package/dist/index.d.ts +119 -0
  5. package/dist/index.global.js +67 -65
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +46 -44
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +828 -212
  10. package/dist/theme-editor.d.cts +128 -3
  11. package/dist/theme-editor.d.ts +128 -3
  12. package/dist/theme-editor.js +824 -212
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +8 -0
  15. package/dist/theme-reference.d.ts +8 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +124 -0
  18. package/package.json +1 -1
  19. package/src/client.test.ts +312 -1
  20. package/src/client.ts +247 -24
  21. package/src/components/messages.ts +1 -1
  22. package/src/components/reasoning-bubble.ts +117 -28
  23. package/src/components/tool-bubble.ts +162 -28
  24. package/src/defaults.ts +13 -1
  25. package/src/styles/widget.css +124 -0
  26. package/src/theme-editor/index.ts +5 -0
  27. package/src/theme-editor/preview-utils.test.ts +58 -0
  28. package/src/theme-editor/preview-utils.ts +220 -4
  29. package/src/theme-editor/sections.test.ts +20 -0
  30. package/src/theme-editor/sections.ts +10 -0
  31. package/src/theme-reference.ts +8 -3
  32. package/src/tool-call-display-defaults.test.ts +23 -0
  33. package/src/types.ts +126 -0
  34. package/src/ui.scroll.test.ts +104 -0
  35. package/src/ui.tool-display.test.ts +204 -0
  36. package/src/ui.ts +103 -3
  37. package/src/utils/message-fingerprint.test.ts +17 -0
  38. package/src/utils/message-fingerprint.ts +13 -1
@@ -0,0 +1,204 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+ import type { AgentWidgetController } from "./ui";
7
+
8
+ const createMount = () => {
9
+ const mount = document.createElement("div");
10
+ document.body.appendChild(mount);
11
+ return mount;
12
+ };
13
+
14
+ const injectToolMessage = (
15
+ controller: AgentWidgetController,
16
+ {
17
+ id,
18
+ name,
19
+ status = "running",
20
+ chunks = [],
21
+ }: {
22
+ id: string;
23
+ name?: string;
24
+ status?: "pending" | "running" | "complete";
25
+ chunks?: string[];
26
+ }
27
+ ) => {
28
+ controller.injectTestMessage({
29
+ type: "message",
30
+ message: {
31
+ id,
32
+ role: "assistant",
33
+ content: "",
34
+ createdAt: new Date().toISOString(),
35
+ streaming: status !== "complete",
36
+ variant: "tool",
37
+ toolCall: {
38
+ id,
39
+ name,
40
+ status,
41
+ chunks,
42
+ },
43
+ },
44
+ });
45
+ };
46
+
47
+ const injectReasoningMessage = (
48
+ controller: AgentWidgetController,
49
+ {
50
+ id,
51
+ status = "streaming",
52
+ chunks = [],
53
+ }: {
54
+ id: string;
55
+ status?: "pending" | "streaming" | "complete";
56
+ chunks?: string[];
57
+ }
58
+ ) => {
59
+ controller.injectTestMessage({
60
+ type: "message",
61
+ message: {
62
+ id,
63
+ role: "assistant",
64
+ content: "",
65
+ createdAt: new Date().toISOString(),
66
+ streaming: status !== "complete",
67
+ variant: "reasoning",
68
+ reasoning: {
69
+ id,
70
+ status,
71
+ chunks,
72
+ },
73
+ },
74
+ });
75
+ };
76
+
77
+ describe("createAgentExperience tool call display modes", () => {
78
+ beforeEach(() => {
79
+ vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
80
+ cb(0);
81
+ return 1;
82
+ });
83
+ vi.stubGlobal("cancelAnimationFrame", () => {});
84
+ window.scrollTo = vi.fn();
85
+ });
86
+
87
+ afterEach(() => {
88
+ document.body.innerHTML = "";
89
+ vi.restoreAllMocks();
90
+ });
91
+
92
+ it("keeps collapsed tool rows on the generic summary by default", () => {
93
+ const mount = createMount();
94
+ const controller = createAgentExperience(mount, {
95
+ apiUrl: "https://api.example.com/chat",
96
+ launcher: { enabled: false },
97
+ });
98
+
99
+ injectToolMessage(controller, {
100
+ id: "tool-1",
101
+ name: "Get platform documentation",
102
+ chunks: ["Loaded tools, used Runtype integration"],
103
+ });
104
+
105
+ const header = mount.querySelector(".persona-tool-bubble button[data-expand-header='true']");
106
+ expect(header?.textContent).toContain("Using tool...");
107
+ expect(header?.textContent).not.toContain("Get platform documentation");
108
+
109
+ controller.destroy();
110
+ });
111
+
112
+ it("shows the tool name in collapsed rows when configured", () => {
113
+ const mount = createMount();
114
+ const controller = createAgentExperience(mount, {
115
+ apiUrl: "https://api.example.com/chat",
116
+ launcher: { enabled: false },
117
+ features: {
118
+ toolCallDisplay: {
119
+ collapsedMode: "tool-name",
120
+ },
121
+ },
122
+ } as any);
123
+
124
+ injectToolMessage(controller, {
125
+ id: "tool-1",
126
+ name: "Get platform documentation",
127
+ chunks: ["Loaded tools, used Runtype integration"],
128
+ });
129
+
130
+ const header = mount.querySelector(".persona-tool-bubble button[data-expand-header='true']");
131
+ expect(header?.textContent).toContain("Get platform documentation");
132
+
133
+ controller.destroy();
134
+ });
135
+
136
+ it("renders a collapsed preview for active tool rows when enabled", () => {
137
+ const mount = createMount();
138
+ const controller = createAgentExperience(mount, {
139
+ apiUrl: "https://api.example.com/chat",
140
+ launcher: { enabled: false },
141
+ features: {
142
+ toolCallDisplay: {
143
+ activePreview: true,
144
+ },
145
+ },
146
+ } as any);
147
+
148
+ injectToolMessage(controller, {
149
+ id: "tool-1",
150
+ name: "Get platform documentation",
151
+ chunks: ["Loaded tools, used Runtype integration"],
152
+ });
153
+
154
+ const preview = mount.querySelector("[data-persona-collapsed-preview='tool']");
155
+ expect(preview?.textContent).toContain("Loaded tools, used Runtype integration");
156
+
157
+ controller.destroy();
158
+ });
159
+
160
+ it("renders a collapsed preview for active reasoning rows when enabled", () => {
161
+ const mount = createMount();
162
+ const controller = createAgentExperience(mount, {
163
+ apiUrl: "https://api.example.com/chat",
164
+ launcher: { enabled: false },
165
+ features: {
166
+ reasoningDisplay: {
167
+ activePreview: true,
168
+ },
169
+ },
170
+ } as any);
171
+
172
+ injectReasoningMessage(controller, {
173
+ id: "reason-1",
174
+ chunks: ["Now let me get the Persona embed documentation and builtin tools catalog."],
175
+ });
176
+
177
+ const preview = mount.querySelector("[data-persona-collapsed-preview='reasoning']");
178
+ expect(preview?.textContent).toContain("Now let me get the Persona embed documentation");
179
+
180
+ controller.destroy();
181
+ });
182
+
183
+ it("groups consecutive tool calls when enabled", () => {
184
+ const mount = createMount();
185
+ const controller = createAgentExperience(mount, {
186
+ apiUrl: "https://api.example.com/chat",
187
+ launcher: { enabled: false },
188
+ features: {
189
+ toolCallDisplay: {
190
+ grouped: true,
191
+ },
192
+ },
193
+ } as any);
194
+
195
+ injectToolMessage(controller, { id: "tool-1", name: "Load tools", chunks: ["Loaded tools"] });
196
+ injectToolMessage(controller, { id: "tool-2", name: "Get docs", chunks: ["Fetched docs"] });
197
+
198
+ const group = mount.querySelector("[data-persona-tool-group='true']");
199
+ expect(group).not.toBeNull();
200
+ expect(group?.textContent).toContain("Called 2 tools");
201
+
202
+ controller.destroy();
203
+ });
204
+ });
package/src/ui.ts CHANGED
@@ -2213,6 +2213,18 @@ export const createAgentExperience = (
2213
2213
  };
2214
2214
 
2215
2215
  const inlineLoadingRenderer = getInlineLoadingIndicatorRenderer();
2216
+ const appendRenderedValue = (
2217
+ containerEl: HTMLElement,
2218
+ value: HTMLElement | string | null | undefined
2219
+ ): boolean => {
2220
+ if (value == null) return false;
2221
+ if (typeof value === "string") {
2222
+ containerEl.textContent = value;
2223
+ return true;
2224
+ }
2225
+ containerEl.appendChild(value);
2226
+ return true;
2227
+ };
2216
2228
 
2217
2229
  // Track active message IDs for cache pruning
2218
2230
  const activeMessageIds = new Set<string>();
@@ -2255,7 +2267,7 @@ export const createAgentExperience = (
2255
2267
  if (!showReasoning) return;
2256
2268
  bubble = matchingPlugin.renderReasoning({
2257
2269
  message,
2258
- defaultRenderer: () => createReasoningBubble(message),
2270
+ defaultRenderer: () => createReasoningBubble(message, config),
2259
2271
  config
2260
2272
  });
2261
2273
  } else if (message.variant === "tool" && message.toolCall && matchingPlugin.renderToolCall) {
@@ -2371,7 +2383,7 @@ export const createAgentExperience = (
2371
2383
  if (!bubble) {
2372
2384
  if (message.variant === "reasoning" && message.reasoning) {
2373
2385
  if (!showReasoning) return;
2374
- bubble = createReasoningBubble(message);
2386
+ bubble = createReasoningBubble(message, config);
2375
2387
  } else if (message.variant === "tool" && message.toolCall) {
2376
2388
  if (!showToolCalls) return;
2377
2389
  bubble = createToolBubble(message, config);
@@ -2428,6 +2440,86 @@ export const createAgentExperience = (
2428
2440
  tempContainer.appendChild(wrapper);
2429
2441
  });
2430
2442
 
2443
+ if (config.features?.toolCallDisplay?.grouped) {
2444
+ const toolGroups: AgentWidgetMessage[][] = [];
2445
+ let currentGroup: AgentWidgetMessage[] = [];
2446
+
2447
+ messages.forEach((message) => {
2448
+ if (message.variant === "tool" && message.toolCall && showToolCalls) {
2449
+ currentGroup.push(message);
2450
+ return;
2451
+ }
2452
+ if (currentGroup.length > 1) {
2453
+ toolGroups.push(currentGroup);
2454
+ }
2455
+ currentGroup = [];
2456
+ });
2457
+ if (currentGroup.length > 1) {
2458
+ toolGroups.push(currentGroup);
2459
+ }
2460
+
2461
+ toolGroups.forEach((group, groupIndex) => {
2462
+ const wrappers = group
2463
+ .map((groupMessage) =>
2464
+ Array.from(tempContainer.children).find(
2465
+ (child) =>
2466
+ child instanceof HTMLElement &&
2467
+ child.getAttribute("data-wrapper-id") === groupMessage.id
2468
+ ) as HTMLElement | undefined
2469
+ )
2470
+ .filter((wrapper): wrapper is HTMLElement => Boolean(wrapper));
2471
+
2472
+ if (wrappers.length < 2) {
2473
+ return;
2474
+ }
2475
+
2476
+ const groupWrapper = document.createElement("div");
2477
+ groupWrapper.className = "persona-flex";
2478
+ groupWrapper.id = `wrapper-tool-group-${groupIndex}-${group[0].id}`;
2479
+ groupWrapper.setAttribute("data-wrapper-id", `tool-group-${groupIndex}-${group[0].id}`);
2480
+
2481
+ const groupContainer = document.createElement("div");
2482
+ groupContainer.className =
2483
+ "persona-tool-group persona-flex persona-w-full persona-flex-col persona-gap-2";
2484
+ groupContainer.setAttribute("data-persona-tool-group", "true");
2485
+
2486
+ const summary = document.createElement("div");
2487
+ summary.className =
2488
+ "persona-tool-group-summary persona-text-xs persona-text-persona-muted";
2489
+
2490
+ const defaultSummary = `Called ${group.length} tools`;
2491
+ const renderedSummary = config.toolCall?.renderGroupedSummary?.({
2492
+ messages: group,
2493
+ toolCalls: group
2494
+ .map((groupMessage) => groupMessage.toolCall)
2495
+ .filter((toolCall): toolCall is NonNullable<typeof group[number]["toolCall"]> => Boolean(toolCall)),
2496
+ defaultSummary,
2497
+ config,
2498
+ });
2499
+ if (!appendRenderedValue(summary, renderedSummary)) {
2500
+ summary.textContent = defaultSummary;
2501
+ }
2502
+
2503
+ const stack = document.createElement("div");
2504
+ stack.className = "persona-tool-group-stack persona-flex persona-flex-col";
2505
+
2506
+ groupContainer.append(summary, stack);
2507
+ groupWrapper.appendChild(groupContainer);
2508
+ wrappers[0].before(groupWrapper);
2509
+
2510
+ wrappers.forEach((wrapper, wrapperIndex) => {
2511
+ const item = document.createElement("div");
2512
+ item.className = "persona-tool-group-item persona-relative";
2513
+ item.setAttribute("data-persona-tool-group-item", "true");
2514
+ if (wrapperIndex < wrappers.length - 1) {
2515
+ item.setAttribute("data-persona-tool-group-connector", "true");
2516
+ }
2517
+ item.appendChild(wrapper);
2518
+ stack.appendChild(item);
2519
+ });
2520
+ });
2521
+ }
2522
+
2431
2523
  // Remove cache entries for messages that no longer exist
2432
2524
  pruneCache(messageCache, activeMessageIds);
2433
2525
 
@@ -3853,6 +3945,10 @@ export const createAgentExperience = (
3853
3945
  const previousColorScheme = config.colorScheme;
3854
3946
  const previousLoadingIndicator = config.loadingIndicator;
3855
3947
  const previousIterationDisplay = config.iterationDisplay;
3948
+ const previousShowReasoning = config.features?.showReasoning;
3949
+ const previousShowToolCalls = config.features?.showToolCalls;
3950
+ const previousToolCallDisplay = config.features?.toolCallDisplay;
3951
+ const previousReasoningDisplay = config.features?.reasoningDisplay;
3856
3952
  config = { ...config, ...nextConfig };
3857
3953
  // applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
3858
3954
  applyFullHeightStyles();
@@ -4093,8 +4189,12 @@ export const createAgentExperience = (
4093
4189
  || config.loadingIndicator?.renderIdle !== previousLoadingIndicator?.renderIdle
4094
4190
  || config.loadingIndicator?.showBubble !== previousLoadingIndicator?.showBubble;
4095
4191
  const iterationDisplayChanged = config.iterationDisplay !== previousIterationDisplay;
4192
+ const featuresChanged = (config.features?.showReasoning ?? true) !== (previousShowReasoning ?? true)
4193
+ || (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
4194
+ || JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
4195
+ || JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay);
4096
4196
  const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
4097
- || loadingIndicatorChanged || iterationDisplayChanged;
4197
+ || loadingIndicatorChanged || iterationDisplayChanged || featuresChanged;
4098
4198
  if (messagesConfigChanged && session) {
4099
4199
  configVersion++;
4100
4200
  renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
@@ -90,6 +90,23 @@ describe("computeMessageFingerprint", () => {
90
90
  expect(fp1).not.toBe(fp2);
91
91
  });
92
92
 
93
+ it("changes when toolCall chunks change", () => {
94
+ const fp1 = computeMessageFingerprint(
95
+ makeMessage({ toolCall: { status: "running", chunks: ["Loaded tools"] } }),
96
+ 0
97
+ );
98
+ const fp2 = computeMessageFingerprint(
99
+ makeMessage({
100
+ toolCall: {
101
+ status: "running",
102
+ chunks: ["Loaded tools", "\nFetched platform documentation"],
103
+ },
104
+ }),
105
+ 0
106
+ );
107
+ expect(fp1).not.toBe(fp2);
108
+ });
109
+
93
110
  it("changes when reasoning chunks change", () => {
94
111
  const fp1 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1"] } }), 0);
95
112
  const fp2 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1", "step 2"] } }), 0);
@@ -16,7 +16,12 @@ export type FingerprintableMessage = {
16
16
  rawContent?: string;
17
17
  llmContent?: string;
18
18
  approval?: { status?: string; [key: string]: unknown };
19
- toolCall?: { status?: string; [key: string]: unknown };
19
+ toolCall?: {
20
+ status?: string;
21
+ chunks?: string[];
22
+ args?: unknown;
23
+ [key: string]: unknown;
24
+ };
20
25
  reasoning?: { chunks?: string[]; status?: string; [key: string]: unknown };
21
26
  contentParts?: unknown[];
22
27
  };
@@ -48,6 +53,13 @@ export function computeMessageFingerprint(
48
53
  message.llmContent?.length ?? 0,
49
54
  message.approval?.status ?? "",
50
55
  message.toolCall?.status ?? "",
56
+ message.toolCall?.chunks?.length ?? 0,
57
+ message.toolCall?.chunks?.[message.toolCall.chunks.length - 1]?.slice(-32) ?? "",
58
+ typeof message.toolCall?.args === "string"
59
+ ? message.toolCall.args.length
60
+ : message.toolCall?.args
61
+ ? JSON.stringify(message.toolCall.args).length
62
+ : 0,
51
63
  message.reasoning?.chunks?.length ?? 0,
52
64
  message.contentParts?.length ?? 0,
53
65
  configVersion,