@runtypelabs/persona 4.10.1 → 4.11.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 (56) 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 +26 -0
  12. package/dist/context-mentions.d.ts +26 -0
  13. package/dist/index.cjs +67 -67
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +27 -1
  16. package/dist/index.d.ts +27 -1
  17. package/dist/index.global.js +55 -55
  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 +26 -0
  22. package/dist/smart-dom-reader.d.ts +26 -0
  23. package/dist/theme-editor-preview.cjs +66 -66
  24. package/dist/theme-editor-preview.d.cts +26 -0
  25. package/dist/theme-editor-preview.d.ts +26 -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 +26 -0
  29. package/dist/theme-editor.d.ts +26 -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 +25 -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.ts +145 -238
  52. package/src/utils/table-scroll-fade.test.ts +123 -0
  53. package/src/utils/table-scroll-fade.ts +75 -0
  54. package/src/utils/tooltip.test.ts +226 -0
  55. package/src/utils/tooltip.ts +245 -0
  56. 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
+ });