@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Bx_rvjff.d.cts → types-DveIaNx6.d.cts} +2 -0
- package/dist/animations/{types-Bx_rvjff.d.ts → types-DveIaNx6.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-MMUPR2JW.js +1 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
- package/dist/context-mentions.d.cts +28 -0
- package/dist/context-mentions.d.ts +28 -0
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.global.js +56 -56
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +78 -78
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +28 -0
- package/dist/smart-dom-reader.d.ts +28 -0
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +28 -0
- package/dist/theme-editor-preview.d.ts +28 -0
- package/dist/theme-editor-preview.js +55 -55
- package/dist/theme-editor.cjs +5 -5
- package/dist/theme-editor.d.cts +28 -0
- package/dist/theme-editor.d.ts +28 -0
- package/dist/theme-editor.js +9 -9
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/approval-bubble.ts +0 -1
- package/src/components/composer-parts.ts +19 -19
- package/src/components/context-mention-button.test.ts +5 -1
- package/src/components/context-mention-button.ts +6 -4
- package/src/components/header-builder.test.ts +3 -3
- package/src/components/header-parts.ts +14 -108
- package/src/components/message-bubble.test.ts +49 -0
- package/src/components/message-bubble.ts +8 -7
- package/src/components/reasoning-bubble.ts +0 -2
- package/src/components/tool-bubble.ts +0 -2
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/index-core.ts +1 -0
- package/src/styles/widget.css +118 -58
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +10 -1
- package/src/types.ts +27 -0
- package/src/ui.attachments-drop.test.ts +2 -1
- package/src/ui.message-width.test.ts +407 -0
- package/src/ui.tool-display.test.ts +177 -8
- package/src/ui.ts +159 -244
- package/src/utils/table-scroll-fade.test.ts +123 -0
- package/src/utils/table-scroll-fade.ts +75 -0
- package/src/utils/tooltip.test.ts +226 -0
- package/src/utils/tooltip.ts +245 -0
- 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
|
-
|
|
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("
|
|
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
|
-
|
|
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, {
|
|
246
|
-
|
|
348
|
+
injectToolMessage(controller, {
|
|
349
|
+
id: "tool-1",
|
|
350
|
+
name: "Inspect data",
|
|
351
|
+
status: "pending",
|
|
352
|
+
});
|
|
247
353
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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
|
});
|