@runtypelabs/persona 4.10.0 → 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 (61) 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-4ROVJ1gA.d.cts → types-DveIaNx6.d.cts} +11 -316
  4. package/dist/animations/{types-4ROVJ1gA.d.ts → types-DveIaNx6.d.ts} +11 -316
  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 +35 -316
  12. package/dist/context-mentions.d.ts +35 -316
  13. package/dist/index.cjs +67 -67
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -317
  16. package/dist/index.d.ts +40 -317
  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 +35 -316
  22. package/dist/smart-dom-reader.d.ts +35 -316
  23. package/dist/theme-editor-preview.cjs +66 -66
  24. package/dist/theme-editor-preview.d.cts +35 -316
  25. package/dist/theme-editor-preview.d.ts +35 -316
  26. package/dist/theme-editor-preview.js +55 -55
  27. package/dist/theme-editor.cjs +5 -5
  28. package/dist/theme-editor.d.cts +35 -316
  29. package/dist/theme-editor.d.ts +35 -316
  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 +11 -317
  44. package/src/index-core.ts +7 -1
  45. package/src/runtime/host-layout.test.ts +94 -0
  46. package/src/runtime/host-layout.ts +9 -1
  47. package/src/runtime/init.test.ts +33 -0
  48. package/src/runtime/init.ts +4 -0
  49. package/src/styles/widget.css +118 -58
  50. package/src/theme-editor/sections.test.ts +33 -0
  51. package/src/theme-editor/sections.ts +10 -1
  52. package/src/types.ts +25 -0
  53. package/src/ui.attachments-drop.test.ts +2 -1
  54. package/src/ui.message-width.test.ts +407 -0
  55. package/src/ui.mount-fill-min-width.test.ts +81 -0
  56. package/src/ui.ts +158 -238
  57. package/src/utils/table-scroll-fade.test.ts +123 -0
  58. package/src/utils/table-scroll-fade.ts +75 -0
  59. package/src/utils/tooltip.test.ts +226 -0
  60. package/src/utils/tooltip.ts +245 -0
  61. 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
+ });
@@ -0,0 +1,81 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeAll, describe, expect, it } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+
7
+ // applyFullHeightStyles resets mount.style.cssText and re-applies the fill
8
+ // styles on every update, so the mount's shrinkable min-width has to be owned
9
+ // here (not only in the host layer, which it clobbers). Without it a wide
10
+ // artifact split forces the mount past its content-based minimum width. These
11
+ // tests drive the REAL controller so the reset/reapply actually runs (the
12
+ // init.ts handle tests use a mock controller and can't catch this).
13
+
14
+ beforeAll(() => {
15
+ window.matchMedia = ((query: string) => ({
16
+ matches: false,
17
+ media: query,
18
+ onchange: null,
19
+ addListener: () => {},
20
+ removeListener: () => {},
21
+ addEventListener: () => {},
22
+ removeEventListener: () => {},
23
+ dispatchEvent: () => false,
24
+ })) as unknown as typeof window.matchMedia;
25
+ });
26
+
27
+ const createMount = () => {
28
+ const mount = document.createElement("div");
29
+ document.body.appendChild(mount);
30
+ return mount;
31
+ };
32
+
33
+ describe("mount fill min-width (fullHeight embed)", () => {
34
+ afterEach(() => {
35
+ document.body.innerHTML = "";
36
+ });
37
+
38
+ it("keeps min-width:0 on the mount for a launcher-disabled fullHeight embed", () => {
39
+ const mount = createMount();
40
+ const controller = createAgentExperience(mount, {
41
+ launcher: { enabled: false, fullHeight: true },
42
+ });
43
+ expect(mount.style.minWidth).toBe("0px");
44
+ controller.destroy();
45
+ });
46
+
47
+ it("restores min-width:0 after update() resets the mount cssText", () => {
48
+ const mount = createMount();
49
+ const controller = createAgentExperience(mount, {
50
+ launcher: { enabled: false, fullHeight: true },
51
+ });
52
+ expect(mount.style.minWidth).toBe("0px");
53
+
54
+ // update() runs applyFullHeightStyles, which wipes mount.style.cssText and
55
+ // re-applies the fill styles: min-width must come back, not stay cleared.
56
+ controller.update({ colorScheme: "dark" });
57
+ expect(mount.style.minWidth).toBe("0px");
58
+
59
+ controller.destroy();
60
+ });
61
+
62
+ it("keeps min-width:0 on a launcher-disabled embed without fullHeight", () => {
63
+ const mount = createMount();
64
+ const controller = createAgentExperience(mount, {
65
+ launcher: { enabled: false },
66
+ });
67
+ // min-width baseline applies to every fill mount, not only the fullHeight
68
+ // branch, so the launcher-off embed stays shrinkable after the cssText reset.
69
+ expect(mount.style.minWidth).toBe("0px");
70
+ controller.destroy();
71
+ });
72
+
73
+ it("keeps min-width:0 on the mount in docked mode", () => {
74
+ const mount = createMount();
75
+ const controller = createAgentExperience(mount, {
76
+ launcher: { enabled: false, mountMode: "docked" },
77
+ });
78
+ expect(mount.style.minWidth).toBe("0px");
79
+ controller.destroy();
80
+ });
81
+ });