@runtypelabs/persona 4.10.1 → 4.11.1

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 (57) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-Bx_rvjff.d.cts → types-DveIaNx6.d.cts} +2 -0
  4. package/dist/animations/{types-Bx_rvjff.d.ts → types-DveIaNx6.d.ts} +2 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/chunk-MMUPR2JW.js +1 -0
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
  11. package/dist/context-mentions.d.cts +28 -0
  12. package/dist/context-mentions.d.ts +28 -0
  13. package/dist/index.cjs +67 -67
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +29 -1
  16. package/dist/index.d.ts +29 -1
  17. package/dist/index.global.js +56 -56
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +78 -78
  20. package/dist/index.js.map +1 -1
  21. package/dist/smart-dom-reader.d.cts +28 -0
  22. package/dist/smart-dom-reader.d.ts +28 -0
  23. package/dist/theme-editor-preview.cjs +66 -66
  24. package/dist/theme-editor-preview.d.cts +28 -0
  25. package/dist/theme-editor-preview.d.ts +28 -0
  26. package/dist/theme-editor-preview.js +55 -55
  27. package/dist/theme-editor.cjs +5 -5
  28. package/dist/theme-editor.d.cts +28 -0
  29. package/dist/theme-editor.d.ts +28 -0
  30. package/dist/theme-editor.js +9 -9
  31. package/dist/widget.css +1 -1
  32. package/package.json +1 -1
  33. package/src/components/approval-bubble.ts +0 -1
  34. package/src/components/composer-parts.ts +19 -19
  35. package/src/components/context-mention-button.test.ts +5 -1
  36. package/src/components/context-mention-button.ts +6 -4
  37. package/src/components/header-builder.test.ts +3 -3
  38. package/src/components/header-parts.ts +14 -108
  39. package/src/components/message-bubble.test.ts +49 -0
  40. package/src/components/message-bubble.ts +8 -7
  41. package/src/components/reasoning-bubble.ts +0 -2
  42. package/src/components/tool-bubble.ts +0 -2
  43. package/src/generated/runtype-openapi-contract.ts +2 -0
  44. package/src/index-core.ts +1 -0
  45. package/src/styles/widget.css +118 -58
  46. package/src/theme-editor/sections.test.ts +33 -0
  47. package/src/theme-editor/sections.ts +10 -1
  48. package/src/types.ts +27 -0
  49. package/src/ui.attachments-drop.test.ts +2 -1
  50. package/src/ui.message-width.test.ts +407 -0
  51. package/src/ui.tool-display.test.ts +177 -8
  52. package/src/ui.ts +159 -244
  53. package/src/utils/table-scroll-fade.test.ts +123 -0
  54. package/src/utils/table-scroll-fade.ts +75 -0
  55. package/src/utils/tooltip.test.ts +226 -0
  56. package/src/utils/tooltip.ts +245 -0
  57. package/dist/chunk-IPVK3KOM.js +0 -1
@@ -0,0 +1,407 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import {
6
+ createAgentExperience,
7
+ type AgentWidgetController,
8
+ } from "./ui";
9
+ import type {
10
+ AgentWidgetConfig,
11
+ AgentWidgetMessage,
12
+ } from "./types";
13
+
14
+ const CREATED_AT = "2026-07-24T00:00:00.000Z";
15
+
16
+ const createMount = (): HTMLElement => {
17
+ const mount = document.createElement("div");
18
+ document.body.appendChild(mount);
19
+ return mount;
20
+ };
21
+
22
+ const createController = (
23
+ mount: HTMLElement,
24
+ config: Partial<AgentWidgetConfig> = {}
25
+ ): AgentWidgetController =>
26
+ createAgentExperience(mount, {
27
+ apiUrl: "https://api.example.com/chat",
28
+ launcher: { enabled: false },
29
+ ...config,
30
+ });
31
+
32
+ const injectMessage = (
33
+ controller: AgentWidgetController,
34
+ message: Partial<AgentWidgetMessage> &
35
+ Pick<AgentWidgetMessage, "id" | "role">
36
+ ): void => {
37
+ controller.injectTestMessage({
38
+ type: "message",
39
+ message: {
40
+ content: "",
41
+ createdAt: CREATED_AT,
42
+ streaming: false,
43
+ ...message,
44
+ },
45
+ });
46
+ };
47
+
48
+ const getRow = (mount: HTMLElement, id: string): HTMLElement => {
49
+ const row = mount.querySelector<HTMLElement>(`#wrapper-${id}`);
50
+ expect(row).not.toBeNull();
51
+ return row!;
52
+ };
53
+
54
+ const expectRowLayout = (
55
+ row: HTMLElement,
56
+ expected: {
57
+ role: "user" | "assistant" | "system";
58
+ width: "content" | "full";
59
+ maxWidth: string;
60
+ }
61
+ ): void => {
62
+ expect(row.classList.contains("persona-message-row")).toBe(true);
63
+ expect(row.classList.contains(`persona-message-row-${expected.role}`)).toBe(
64
+ true
65
+ );
66
+ expect(row.classList.contains(`persona-message-width-${expected.width}`)).toBe(
67
+ true
68
+ );
69
+ expect(row.getAttribute("data-message-role")).toBe(expected.role);
70
+ expect(row.getAttribute("data-message-width")).toBe(expected.width);
71
+ expect(
72
+ row.style.getPropertyValue("--persona-message-row-max-width")
73
+ ).toBe(expected.maxWidth);
74
+ };
75
+
76
+ describe("createAgentExperience: role-specific message width", () => {
77
+ beforeEach(() => {
78
+ vi.stubGlobal("requestAnimationFrame", (callback: (time: number) => void) => {
79
+ callback(0);
80
+ return 1;
81
+ });
82
+ vi.stubGlobal("cancelAnimationFrame", () => {});
83
+ window.scrollTo = vi.fn();
84
+ });
85
+
86
+ afterEach(() => {
87
+ document.body.innerHTML = "";
88
+ if (typeof localStorage !== "undefined") localStorage.clear();
89
+ vi.restoreAllMocks();
90
+ });
91
+
92
+ it("preserves content-sized 85% rows by default", () => {
93
+ const mount = createMount();
94
+ const controller = createController(mount);
95
+
96
+ injectMessage(controller, {
97
+ id: "user-default",
98
+ role: "user",
99
+ content: "Hello",
100
+ });
101
+ injectMessage(controller, {
102
+ id: "assistant-default",
103
+ role: "assistant",
104
+ content: "Hi there",
105
+ });
106
+
107
+ expectRowLayout(getRow(mount, "user-default"), {
108
+ role: "user",
109
+ width: "content",
110
+ maxWidth: "85%",
111
+ });
112
+ expect(getRow(mount, "user-default").classList.contains("persona-justify-end")).toBe(
113
+ true
114
+ );
115
+ expectRowLayout(getRow(mount, "assistant-default"), {
116
+ role: "assistant",
117
+ width: "content",
118
+ maxWidth: "85%",
119
+ });
120
+
121
+ controller.destroy();
122
+ });
123
+
124
+ it("configures user and assistant geometry independently", () => {
125
+ const mount = createMount();
126
+ const controller = createController(mount, {
127
+ layout: {
128
+ messages: {
129
+ user: { width: "content", maxWidth: "70%" },
130
+ assistant: { width: "full" },
131
+ },
132
+ },
133
+ });
134
+
135
+ injectMessage(controller, {
136
+ id: "user-narrow",
137
+ role: "user",
138
+ content: "Question",
139
+ });
140
+ injectMessage(controller, {
141
+ id: "assistant-full",
142
+ role: "assistant",
143
+ content: "A longer response",
144
+ });
145
+
146
+ expectRowLayout(getRow(mount, "user-narrow"), {
147
+ role: "user",
148
+ width: "content",
149
+ maxWidth: "70%",
150
+ });
151
+ expectRowLayout(getRow(mount, "assistant-full"), {
152
+ role: "assistant",
153
+ width: "full",
154
+ maxWidth: "100%",
155
+ });
156
+
157
+ controller.destroy();
158
+ });
159
+
160
+ it("sizes the avatar and bubble together as one full-width content track", () => {
161
+ const mount = createMount();
162
+ const controller = createController(mount, {
163
+ layout: {
164
+ messages: {
165
+ assistant: { width: "full", maxWidth: "72ch" },
166
+ avatar: {
167
+ show: true,
168
+ position: "left",
169
+ assistantAvatar: "A",
170
+ },
171
+ },
172
+ },
173
+ });
174
+
175
+ injectMessage(controller, {
176
+ id: "assistant-avatar",
177
+ role: "assistant",
178
+ content: "Avatar-backed response",
179
+ });
180
+
181
+ const row = getRow(mount, "assistant-avatar");
182
+ expectRowLayout(row, {
183
+ role: "assistant",
184
+ width: "full",
185
+ maxWidth: "72ch",
186
+ });
187
+ const track = row.querySelector<HTMLElement>(
188
+ ":scope > .persona-message-with-avatar"
189
+ );
190
+ expect(track).not.toBeNull();
191
+ expect(
192
+ track?.querySelector(":scope > .persona-message-bubble")
193
+ ).not.toBeNull();
194
+
195
+ controller.destroy();
196
+ });
197
+
198
+ it("applies assistant geometry to tools, reasoning, approvals, and idle UI", () => {
199
+ const mount = createMount();
200
+ const controller = createController(mount, {
201
+ layout: {
202
+ messages: {
203
+ assistant: { width: "full", maxWidth: "64rem" },
204
+ },
205
+ },
206
+ loadingIndicator: {
207
+ renderIdle: () => {
208
+ const idle = document.createElement("span");
209
+ idle.textContent = "Ready";
210
+ return idle;
211
+ },
212
+ },
213
+ });
214
+
215
+ injectMessage(controller, {
216
+ id: "tool-width",
217
+ role: "assistant",
218
+ variant: "tool",
219
+ toolCall: {
220
+ id: "tool-width",
221
+ name: "Read file",
222
+ status: "complete",
223
+ chunks: ["Done"],
224
+ },
225
+ });
226
+ injectMessage(controller, {
227
+ id: "reasoning-width",
228
+ role: "assistant",
229
+ variant: "reasoning",
230
+ reasoning: {
231
+ id: "reasoning-width",
232
+ status: "complete",
233
+ chunks: ["Considered the options"],
234
+ },
235
+ });
236
+ injectMessage(controller, {
237
+ id: "approval-width",
238
+ role: "assistant",
239
+ variant: "approval",
240
+ approval: {
241
+ id: "approval-1",
242
+ status: "pending",
243
+ agentId: "agent-1",
244
+ executionId: "execution-1",
245
+ toolName: "Write file",
246
+ description: "Write the generated file",
247
+ parameters: { path: "output.md" },
248
+ },
249
+ });
250
+
251
+ for (const id of ["tool-width", "reasoning-width", "approval-width"]) {
252
+ expectRowLayout(getRow(mount, id), {
253
+ role: "assistant",
254
+ width: "full",
255
+ maxWidth: "64rem",
256
+ });
257
+ }
258
+ expectRowLayout(getRow(mount, "idle-indicator"), {
259
+ role: "assistant",
260
+ width: "full",
261
+ maxWidth: "64rem",
262
+ });
263
+
264
+ controller.destroy();
265
+ });
266
+
267
+ it("wraps custom renderers in the resolved role row", () => {
268
+ const mount = createMount();
269
+ const controller = createController(mount, {
270
+ layout: {
271
+ messages: {
272
+ user: { width: "full" },
273
+ assistant: { width: "content", maxWidth: "42rem" },
274
+ renderUserMessage: ({ message }) => {
275
+ const element = document.createElement("article");
276
+ element.setAttribute("data-custom-user", "");
277
+ element.textContent = message.content;
278
+ return element;
279
+ },
280
+ renderAssistantMessage: ({ message }) => {
281
+ const element = document.createElement("article");
282
+ element.setAttribute("data-custom-assistant", "");
283
+ element.textContent = message.content;
284
+ return element;
285
+ },
286
+ },
287
+ },
288
+ });
289
+
290
+ injectMessage(controller, {
291
+ id: "custom-user",
292
+ role: "user",
293
+ content: "Custom question",
294
+ });
295
+ injectMessage(controller, {
296
+ id: "custom-assistant",
297
+ role: "assistant",
298
+ content: "Custom answer",
299
+ });
300
+
301
+ const userRow = getRow(mount, "custom-user");
302
+ expectRowLayout(userRow, {
303
+ role: "user",
304
+ width: "full",
305
+ maxWidth: "100%",
306
+ });
307
+ expect(userRow.querySelector(":scope > [data-custom-user]")).not.toBeNull();
308
+
309
+ const assistantRow = getRow(mount, "custom-assistant");
310
+ expectRowLayout(assistantRow, {
311
+ role: "assistant",
312
+ width: "content",
313
+ maxWidth: "42rem",
314
+ });
315
+ expect(
316
+ assistantRow.querySelector(":scope > [data-custom-assistant]")
317
+ ).not.toBeNull();
318
+
319
+ controller.destroy();
320
+ });
321
+
322
+ it("re-renders live rows when role width changes through update()", () => {
323
+ const mount = createMount();
324
+ const controller = createController(mount);
325
+
326
+ injectMessage(controller, {
327
+ id: "update-width",
328
+ role: "assistant",
329
+ content: "Update my row",
330
+ });
331
+ expectRowLayout(getRow(mount, "update-width"), {
332
+ role: "assistant",
333
+ width: "content",
334
+ maxWidth: "85%",
335
+ });
336
+
337
+ controller.update({
338
+ layout: {
339
+ messages: {
340
+ assistant: {
341
+ width: "full",
342
+ maxWidth: "68ch",
343
+ },
344
+ },
345
+ },
346
+ });
347
+
348
+ expectRowLayout(getRow(mount, "update-width"), {
349
+ role: "assistant",
350
+ width: "full",
351
+ maxWidth: "68ch",
352
+ });
353
+
354
+ controller.destroy();
355
+ });
356
+
357
+ it("lets a grouped tool row own the cap without applying it twice", () => {
358
+ const mount = createMount();
359
+ const controller = createController(mount, {
360
+ layout: {
361
+ messages: {
362
+ assistant: { width: "content", maxWidth: "80%" },
363
+ },
364
+ },
365
+ features: {
366
+ toolCallDisplay: {
367
+ grouped: true,
368
+ },
369
+ },
370
+ });
371
+
372
+ for (const id of ["group-tool-1", "group-tool-2"]) {
373
+ injectMessage(controller, {
374
+ id,
375
+ role: "assistant",
376
+ variant: "tool",
377
+ toolCall: {
378
+ id,
379
+ name: "Read file",
380
+ status: "complete",
381
+ chunks: ["Done"],
382
+ },
383
+ });
384
+ }
385
+
386
+ const groupRow = mount.querySelector<HTMLElement>(
387
+ "[data-persona-tool-group-row]"
388
+ );
389
+ expect(groupRow).not.toBeNull();
390
+ expectRowLayout(groupRow!, {
391
+ role: "assistant",
392
+ width: "content",
393
+ maxWidth: "80%",
394
+ });
395
+ const innerRows = groupRow!.querySelectorAll<HTMLElement>(
396
+ ".persona-message-row"
397
+ );
398
+ expect(innerRows.length).toBe(2);
399
+ innerRows.forEach((innerRow) => {
400
+ expect(
401
+ innerRow.style.getPropertyValue("--persona-message-row-max-width")
402
+ ).toBe("100%");
403
+ });
404
+
405
+ controller.destroy();
406
+ });
407
+ });
@@ -31,7 +31,10 @@ const injectToolMessage = (
31
31
  id,
32
32
  role: "assistant",
33
33
  content: "",
34
- createdAt: new Date().toISOString(),
34
+ // Real stream updates reuse the original tool message's timeline fields.
35
+ // Keep them stable here so an upsert does not reorder the group.
36
+ createdAt: "2026-01-01T00:00:00.000Z",
37
+ sequence: Number(id.match(/(\d+)$/)?.[1] ?? 0),
35
38
  streaming: status !== "complete",
36
39
  variant: "tool",
37
40
  toolCall: {
@@ -76,6 +79,7 @@ const injectReasoningMessage = (
76
79
 
77
80
  describe("createAgentExperience tool call display modes", () => {
78
81
  beforeEach(() => {
82
+ window.localStorage.clear();
79
83
  vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
80
84
  cb(0);
81
85
  return 1;
@@ -157,6 +161,48 @@ describe("createAgentExperience tool call display modes", () => {
157
161
  controller.destroy();
158
162
  });
159
163
 
164
+ it("preserves a custom collapsed summary across tool chunk updates", () => {
165
+ const mount = createMount();
166
+ const controller = createAgentExperience(mount, {
167
+ apiUrl: "https://api.example.com/chat",
168
+ launcher: { enabled: false },
169
+ features: {
170
+ toolCallDisplay: {
171
+ grouped: false,
172
+ expandable: false,
173
+ loadingAnimation: "none",
174
+ },
175
+ },
176
+ toolCall: {
177
+ renderCollapsedSummary: ({ toolCall }: any) => {
178
+ const summary = document.createElement("span");
179
+ summary.setAttribute("data-test-collapsed-summary", "true");
180
+ summary.textContent = `${toolCall.name}:${toolCall.status}`;
181
+ return summary;
182
+ },
183
+ },
184
+ } as any);
185
+
186
+ injectToolMessage(controller, {
187
+ id: "tool-1",
188
+ name: "Run SQL",
189
+ chunks: ["starting"],
190
+ });
191
+ const bubble = mount.querySelector("#bubble-tool-1");
192
+ const summary = mount.querySelector("[data-test-collapsed-summary='true']");
193
+
194
+ injectToolMessage(controller, {
195
+ id: "tool-1",
196
+ name: "Run SQL",
197
+ chunks: ["starting", "received rows"],
198
+ });
199
+
200
+ expect(mount.querySelector("#bubble-tool-1")).toBe(bubble);
201
+ expect(mount.querySelector("[data-test-collapsed-summary='true']")).toBe(summary);
202
+
203
+ controller.destroy();
204
+ });
205
+
160
206
  it("renders a collapsed preview for active reasoning rows when enabled", () => {
161
207
  const mount = createMount();
162
208
  const controller = createAgentExperience(mount, {
@@ -202,6 +248,41 @@ describe("createAgentExperience tool call display modes", () => {
202
248
  controller.destroy();
203
249
  });
204
250
 
251
+ it("preserves tool rows when a standalone row becomes a stacked group", () => {
252
+ const mount = createMount();
253
+ const controller = createAgentExperience(mount, {
254
+ apiUrl: "https://api.example.com/chat",
255
+ launcher: { enabled: false },
256
+ features: {
257
+ toolCallDisplay: {
258
+ grouped: true,
259
+ groupedMode: "stack",
260
+ },
261
+ },
262
+ } as any);
263
+
264
+ injectToolMessage(controller, { id: "tool-1", name: "Inspect data" });
265
+
266
+ const firstRow = mount.querySelector("#wrapper-tool-1");
267
+ const firstBubble = mount.querySelector("#bubble-tool-1");
268
+ expect(firstRow).not.toBeNull();
269
+ expect(firstBubble).not.toBeNull();
270
+
271
+ injectToolMessage(controller, { id: "tool-2", name: "Run analysis" });
272
+
273
+ const groupRow = mount.querySelector("[data-persona-tool-group-row='true']");
274
+ expect(groupRow).not.toBeNull();
275
+ expect(mount.querySelector("#wrapper-tool-1")).toBe(firstRow);
276
+ expect(mount.querySelector("#bubble-tool-1")).toBe(firstBubble);
277
+
278
+ injectToolMessage(controller, { id: "tool-3", name: "Build chart" });
279
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
280
+ expect(mount.querySelector("#wrapper-tool-1")).toBe(firstRow);
281
+ expect(mount.querySelector("#bubble-tool-1")).toBe(firstBubble);
282
+
283
+ controller.destroy();
284
+ });
285
+
205
286
  it("keeps tool calls grouped across reasoning that is hidden", () => {
206
287
  const mount = createMount();
207
288
  const controller = createAgentExperience(mount, {
@@ -226,30 +307,118 @@ describe("createAgentExperience tool call display modes", () => {
226
307
  controller.destroy();
227
308
  });
228
309
 
229
- it("can render a grouped tool sequence as one summary item", () => {
310
+ it("keeps a summary group's visible DOM stable from the first tool onward", () => {
230
311
  const mount = createMount();
312
+ const renderedSummaries: HTMLElement[] = [];
231
313
  const controller = createAgentExperience(mount, {
232
314
  apiUrl: "https://api.example.com/chat",
233
315
  launcher: { enabled: false },
234
316
  features: {
235
317
  toolCallDisplay: {
318
+ collapsedMode: "tool-name",
319
+ activePreview: false,
236
320
  grouped: true,
237
321
  groupedMode: "summary",
322
+ expandable: false,
323
+ loadingAnimation: "shimmer-color",
238
324
  },
239
325
  },
240
326
  toolCall: {
241
- renderGroupedSummary: () => "Analysis ready",
327
+ renderCollapsedSummary: ({ message, toolCall }: any) => {
328
+ const step = document.createElement("span");
329
+ step.setAttribute("data-test-collapsed-step", message.id);
330
+ step.textContent = `${toolCall.name}:${toolCall.status}`;
331
+ return step;
332
+ },
333
+ renderGroupedSummary: ({ toolCalls }: any) => {
334
+ const list = document.createElement("span");
335
+ list.setAttribute("data-test-activity-list", "true");
336
+ toolCalls.forEach((toolCall: { id: string; status: string }) => {
337
+ const step = document.createElement("span");
338
+ step.setAttribute("data-test-activity-step", toolCall.id);
339
+ step.textContent = `${toolCall.id}:${toolCall.status}`;
340
+ list.appendChild(step);
341
+ });
342
+ renderedSummaries.push(list);
343
+ return list;
344
+ },
242
345
  },
243
346
  } as any);
244
347
 
245
- injectToolMessage(controller, { id: "tool-1", name: "Inspect data", status: "complete" });
246
- injectToolMessage(controller, { id: "tool-2", name: "Run analysis", status: "complete" });
348
+ injectToolMessage(controller, {
349
+ id: "tool-1",
350
+ name: "Inspect data",
351
+ status: "pending",
352
+ });
247
353
 
248
- const group = mount.querySelector("[data-persona-tool-group='true']");
249
- expect(group?.textContent).toContain("Analysis ready");
250
- expect(group?.querySelector("[data-persona-tool-group-item]")).toBeNull();
354
+ const groupRow = mount.querySelector("[data-persona-tool-group-row='true']");
355
+ const activityList = mount.querySelector("[data-test-activity-list='true']");
356
+ const firstStep = mount.querySelector("[data-test-activity-step='tool-1']");
357
+ expect(groupRow).not.toBeNull();
358
+ expect(groupRow?.getAttribute("data-wrapper-id")).toBe("tool-group-tool-1");
359
+ expect(activityList).not.toBeNull();
360
+ expect(firstStep).not.toBeNull();
251
361
  expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
252
362
 
363
+ injectToolMessage(controller, {
364
+ id: "tool-1",
365
+ name: "Inspect data",
366
+ status: "running",
367
+ chunks: ["starting"],
368
+ });
369
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
370
+ expect(mount.querySelector("[data-test-activity-list='true']")).toBe(activityList);
371
+ expect(mount.querySelector("[data-test-activity-step='tool-1']")).toBe(firstStep);
372
+
373
+ injectToolMessage(controller, {
374
+ id: "tool-1",
375
+ name: "Inspect data",
376
+ chunks: ["starting", "received rows"],
377
+ });
378
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
379
+ expect(mount.querySelector("[data-test-activity-list='true']")).toBe(activityList);
380
+ expect(mount.querySelector("[data-test-activity-step='tool-1']")).toBe(firstStep);
381
+
382
+ injectToolMessage(controller, { id: "tool-2", name: "Run analysis" });
383
+
384
+ const secondStep = mount.querySelector("[data-test-activity-step='tool-2']");
385
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
386
+ expect(mount.querySelector("[data-test-activity-list='true']")).toBe(activityList);
387
+ expect(mount.querySelector("[data-test-activity-step='tool-1']")).toBe(firstStep);
388
+ expect(secondStep).not.toBeNull();
389
+ expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
390
+
391
+ injectToolMessage(controller, {
392
+ id: "tool-1",
393
+ name: "Inspect data",
394
+ status: "complete",
395
+ chunks: ["starting", "received rows"],
396
+ });
397
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
398
+ expect(mount.querySelector("[data-test-activity-list='true']")).toBe(activityList);
399
+ expect(mount.querySelector("[data-test-activity-step='tool-1']")).toBe(firstStep);
400
+ expect(mount.querySelector("[data-test-activity-step='tool-2']")).toBe(secondStep);
401
+ expect(firstStep?.textContent).toBe("tool-1:complete");
402
+ expect(secondStep?.textContent).toBe("tool-2:running");
403
+
404
+ injectToolMessage(controller, {
405
+ id: "tool-2",
406
+ name: "Run analysis",
407
+ status: "complete",
408
+ });
409
+ expect(mount.querySelector("[data-persona-tool-group-row='true']")).toBe(groupRow);
410
+ expect(mount.querySelector("[data-test-activity-list='true']")).toBe(activityList);
411
+ expect(mount.querySelector("[data-test-activity-step='tool-1']")).toBe(firstStep);
412
+ expect(mount.querySelector("[data-test-activity-step='tool-2']")).toBe(secondStep);
413
+ expect(secondStep?.textContent).toBe("tool-2:complete");
414
+ expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
415
+
416
+ // The callback can return a fresh tree on every status update; idiomorph
417
+ // must reconcile it into the existing visible summary rather than mounting
418
+ // those detached callback results.
419
+ expect(renderedSummaries.length).toBeGreaterThan(1);
420
+ expect(renderedSummaries).not.toContain(activityList);
421
+
253
422
  controller.destroy();
254
423
  });
255
424
  });