@runtypelabs/persona 4.7.0 → 4.9.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 (185) hide show
  1. package/README.md +1 -0
  2. package/dist/animations/glyph-cycle.cjs +2 -2
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/glyph-cycle.js +2 -2
  6. package/dist/animations/{types-CSmiKRVa.d.cts → types-4ROVJ1gA.d.cts} +85 -3
  7. package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -3
  8. package/dist/animations/wipe.cjs +2 -2
  9. package/dist/animations/wipe.d.cts +1 -1
  10. package/dist/animations/wipe.d.ts +1 -1
  11. package/dist/chunk-5EIIHQLQ.js +1 -0
  12. package/dist/chunk-IO5VVUKP.js +3 -0
  13. package/dist/chunk-IPVK3KOM.js +1 -0
  14. package/dist/chunk-UPO4GUFC.js +1 -0
  15. package/dist/codegen.cjs +13 -13
  16. package/dist/codegen.js +14 -14
  17. package/dist/context-mentions-7S5KVUTG.js +169 -0
  18. package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
  19. package/dist/context-mentions-inline.cjs +4 -0
  20. package/dist/context-mentions-inline.d.cts +203 -0
  21. package/dist/context-mentions-inline.d.ts +203 -0
  22. package/dist/context-mentions-inline.js +4 -0
  23. package/dist/context-mentions.cjs +295 -0
  24. package/dist/context-mentions.d.cts +7025 -0
  25. package/dist/context-mentions.d.ts +7025 -0
  26. package/dist/context-mentions.js +295 -0
  27. package/dist/index.cjs +99 -68
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1507 -182
  30. package/dist/index.d.ts +1507 -182
  31. package/dist/index.global.js +88 -56
  32. package/dist/index.global.js.map +1 -1
  33. package/dist/index.js +99 -68
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.global.js +1 -1
  36. package/dist/install.global.js.map +1 -1
  37. package/dist/launcher.global.js +3 -2
  38. package/dist/launcher.global.js.map +1 -1
  39. package/dist/markdown-parsers.js +24 -24
  40. package/dist/plugin-kit.cjs +1 -1
  41. package/dist/plugin-kit.d.cts +17 -0
  42. package/dist/plugin-kit.d.ts +17 -0
  43. package/dist/plugin-kit.js +1 -1
  44. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  45. package/dist/runtype-tts.js +1 -1
  46. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  47. package/dist/smart-dom-reader.cjs +18 -17
  48. package/dist/smart-dom-reader.d.cts +1260 -16
  49. package/dist/smart-dom-reader.d.ts +1260 -16
  50. package/dist/smart-dom-reader.js +18 -17
  51. package/dist/testing.cjs +3 -3
  52. package/dist/testing.js +3 -3
  53. package/dist/theme-editor-preview.cjs +260 -58
  54. package/dist/theme-editor-preview.d.cts +1234 -15
  55. package/dist/theme-editor-preview.d.ts +1234 -15
  56. package/dist/theme-editor-preview.js +87 -58
  57. package/dist/theme-editor.cjs +10 -10
  58. package/dist/theme-editor.d.cts +1226 -15
  59. package/dist/theme-editor.d.ts +1226 -15
  60. package/dist/theme-editor.js +10 -10
  61. package/dist/theme-reference.cjs +1 -1
  62. package/dist/theme-reference.d.cts +76 -0
  63. package/dist/theme-reference.d.ts +76 -0
  64. package/dist/theme-reference.js +1 -1
  65. package/dist/voice-worklet-player.cjs +2 -2
  66. package/dist/voice-worklet-player.js +2 -2
  67. package/dist/webmcp-polyfill.js +2 -2
  68. package/dist/widget.css +1 -1
  69. package/package.json +16 -5
  70. package/src/artifacts-session.test.ts +178 -0
  71. package/src/client.test.ts +255 -1
  72. package/src/client.ts +157 -60
  73. package/src/components/artifact-card.test.ts +333 -0
  74. package/src/components/artifact-card.ts +75 -28
  75. package/src/components/artifact-inline.test.ts +1328 -0
  76. package/src/components/artifact-inline.ts +920 -0
  77. package/src/components/artifact-pane.test.ts +1042 -0
  78. package/src/components/artifact-pane.ts +440 -131
  79. package/src/components/artifact-preview.test.ts +1155 -0
  80. package/src/components/artifact-preview.ts +994 -0
  81. package/src/components/composer-parts.ts +3 -12
  82. package/src/components/context-mention-button.test.ts +70 -0
  83. package/src/components/context-mention-button.ts +82 -0
  84. package/src/components/context-mention-chip.ts +134 -0
  85. package/src/components/context-mention-menu.test.ts +508 -0
  86. package/src/components/context-mention-menu.ts +0 -0
  87. package/src/components/message-bubble.test.ts +175 -0
  88. package/src/components/message-bubble.ts +177 -19
  89. package/src/components/panel.ts +7 -10
  90. package/src/components/pill-composer-builder.test.ts +6 -2
  91. package/src/components/pill-composer-builder.ts +6 -6
  92. package/src/components/reasoning-bubble.ts +1 -13
  93. package/src/components/registry.ts +38 -3
  94. package/src/components/tool-bubble.ts +1 -13
  95. package/src/context-mentions-bundle.test.ts +163 -0
  96. package/src/context-mentions-entry.ts +185 -0
  97. package/src/context-mentions-inline-entry.test.ts +136 -0
  98. package/src/context-mentions-inline-entry.ts +226 -0
  99. package/src/context-mentions-inline-loader.test.ts +30 -0
  100. package/src/context-mentions-inline-loader.ts +36 -0
  101. package/src/context-mentions-inline.ts +15 -0
  102. package/src/context-mentions-loader.ts +32 -0
  103. package/src/context-mentions.ts +16 -0
  104. package/src/defaults.ts +1 -0
  105. package/src/generated/runtype-openapi-contract.ts +55 -3
  106. package/src/index-core.ts +46 -1
  107. package/src/index-global.ts +51 -0
  108. package/src/index.ts +8 -0
  109. package/src/markdown-parsers-loader.test.ts +158 -0
  110. package/src/markdown-parsers-loader.ts +83 -27
  111. package/src/plugin-kit.test.ts +40 -0
  112. package/src/plugin-kit.ts +39 -5
  113. package/src/runtime/host-layout.test.ts +163 -0
  114. package/src/runtime/host-layout.ts +110 -7
  115. package/src/runtime/init.ts +18 -61
  116. package/src/session.mentions.test.ts +175 -0
  117. package/src/session.test.ts +52 -4
  118. package/src/session.ts +197 -27
  119. package/src/smart-dom-reader.test.ts +129 -2
  120. package/src/smart-dom-reader.ts +127 -1
  121. package/src/styles/context-mention-menu-css.ts +176 -0
  122. package/src/styles/widget.css +1002 -152
  123. package/src/theme-editor/preview.ts +2 -0
  124. package/src/theme-editor/sections.test.ts +26 -1
  125. package/src/theme-editor/sections.ts +13 -5
  126. package/src/theme-reference.ts +2 -2
  127. package/src/tool-call-display-defaults.test.ts +1 -0
  128. package/src/types/theme.ts +79 -0
  129. package/src/types.ts +1041 -17
  130. package/src/ui.artifact-pane-gating.test.ts +636 -0
  131. package/src/ui.component-directive.test.ts +104 -0
  132. package/src/ui.composer-bar.test.ts +60 -2
  133. package/src/ui.detached-panel.test.ts +1049 -0
  134. package/src/ui.mention-submit.test.ts +235 -0
  135. package/src/ui.tool-display.test.ts +51 -0
  136. package/src/ui.ts +998 -159
  137. package/src/utils/artifact-custom-actions.ts +128 -0
  138. package/src/utils/artifact-display.test.ts +42 -0
  139. package/src/utils/artifact-display.ts +84 -0
  140. package/src/utils/artifact-file.test.ts +116 -0
  141. package/src/utils/artifact-file.ts +117 -0
  142. package/src/utils/artifact-gate.test.ts +112 -5
  143. package/src/utils/artifact-gate.ts +39 -14
  144. package/src/utils/artifact-loading-status.ts +55 -0
  145. package/src/utils/artifact-status-label.ts +190 -0
  146. package/src/utils/buttons.ts +7 -1
  147. package/src/utils/chunk-loader.test.ts +97 -0
  148. package/src/utils/chunk-loader.ts +88 -0
  149. package/src/utils/code-highlight.test.ts +186 -0
  150. package/src/utils/code-highlight.ts +400 -0
  151. package/src/utils/composer-contenteditable.test.ts +507 -0
  152. package/src/utils/composer-contenteditable.ts +626 -0
  153. package/src/utils/composer-document.test.ts +280 -0
  154. package/src/utils/composer-document.ts +293 -0
  155. package/src/utils/composer-history.test.ts +35 -7
  156. package/src/utils/composer-history.ts +30 -20
  157. package/src/utils/composer-input.ts +159 -0
  158. package/src/utils/context-mention-controller.test.ts +1215 -0
  159. package/src/utils/context-mention-controller.ts +1186 -0
  160. package/src/utils/context-mention-manager.test.ts +422 -0
  161. package/src/utils/context-mention-manager.ts +410 -0
  162. package/src/utils/context-mention-orchestrator.test.ts +538 -0
  163. package/src/utils/context-mention-orchestrator.ts +348 -0
  164. package/src/utils/icons.ts +2 -0
  165. package/src/utils/live-region.test.ts +108 -0
  166. package/src/utils/live-region.ts +94 -0
  167. package/src/utils/mention-channels.ts +63 -0
  168. package/src/utils/mention-llm-format.test.ts +91 -0
  169. package/src/utils/mention-llm-format.ts +79 -0
  170. package/src/utils/mention-matcher.test.ts +86 -0
  171. package/src/utils/mention-matcher.ts +221 -0
  172. package/src/utils/mention-token.ts +72 -0
  173. package/src/utils/mention-trigger.test.ts +155 -0
  174. package/src/utils/mention-trigger.ts +156 -0
  175. package/src/utils/roving-tablist.test.ts +152 -0
  176. package/src/utils/roving-tablist.ts +111 -0
  177. package/src/utils/spinner.ts +45 -0
  178. package/src/utils/theme.test.ts +76 -4
  179. package/src/utils/theme.ts +7 -0
  180. package/src/utils/tokens.ts +118 -11
  181. package/src/utils/tool-loading-animation.test.ts +32 -0
  182. package/src/utils/tool-loading-animation.ts +24 -0
  183. package/dist/chunk-DFBSCFYN.js +0 -1
  184. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  185. package/dist/session-reconnect-U77QFUR7.js +0 -1
@@ -0,0 +1,1049 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+ import type { AgentWidgetConfig } from "./types";
7
+
8
+ const originalInnerWidth = window.innerWidth;
9
+
10
+ beforeAll(() => {
11
+ // jsdom does not implement matchMedia; the artifact pane's layout code touches
12
+ // it. Evaluate min-width/max-width px queries against the live innerWidth so
13
+ // the shim exercises the real drawer-vs-split gating instead of always false.
14
+ window.matchMedia = ((query: string) => {
15
+ const evaluate = () => {
16
+ const max = /max-width:\s*(\d+)px/.exec(query);
17
+ if (max) return window.innerWidth <= Number(max[1]);
18
+ const min = /min-width:\s*(\d+)px/.exec(query);
19
+ if (min) return window.innerWidth >= Number(min[1]);
20
+ return false;
21
+ };
22
+ return {
23
+ get matches() {
24
+ return evaluate();
25
+ },
26
+ media: query,
27
+ onchange: null,
28
+ addListener: () => {},
29
+ removeListener: () => {},
30
+ addEventListener: () => {},
31
+ removeEventListener: () => {},
32
+ dispatchEvent: () => false,
33
+ };
34
+ }) as unknown as typeof window.matchMedia;
35
+ });
36
+
37
+ const setInnerWidth = (value: number) => {
38
+ Object.defineProperty(window, "innerWidth", {
39
+ configurable: true,
40
+ writable: true,
41
+ value,
42
+ });
43
+ };
44
+
45
+ const createMount = () => {
46
+ const mount = document.createElement("div");
47
+ document.body.appendChild(mount);
48
+ return mount;
49
+ };
50
+
51
+ const queryPanel = (mount: HTMLElement) =>
52
+ mount.querySelector<HTMLElement>(".persona-widget-panel");
53
+
54
+ // Launcher-off (inline/docked) modes render wrapper > panel > container without
55
+ // the .persona-widget-panel class, so reach the panel via the container's parent.
56
+ const queryEmbedRefs = (mount: HTMLElement) => {
57
+ const container = mount.querySelector<HTMLElement>(".persona-widget-container");
58
+ return {
59
+ wrapper: mount.firstElementChild as HTMLElement | null,
60
+ panel: (container?.parentElement ?? null) as HTMLElement | null,
61
+ container,
62
+ };
63
+ };
64
+
65
+ describe("createAgentExperience detached panel", () => {
66
+ beforeEach(() => {
67
+ // Desktop viewport so mobile fullscreen does not override sidebar chrome.
68
+ setInnerWidth(1024);
69
+ });
70
+
71
+ afterEach(() => {
72
+ document.body.innerHTML = "";
73
+ setInnerWidth(originalInnerWidth);
74
+ });
75
+
76
+ it("keeps flush sidebar chrome when detachedPanel is unset", () => {
77
+ const mount = createMount();
78
+ const controller = createAgentExperience(mount, {
79
+ apiUrl: "https://api.example.com/chat",
80
+ launcher: {
81
+ sidebarMode: true,
82
+ position: "bottom-right",
83
+ },
84
+ });
85
+
86
+ const wrapper = mount.firstElementChild as HTMLElement;
87
+ const panel = queryPanel(mount);
88
+
89
+ expect(mount.hasAttribute("data-persona-panel-detached")).toBe(false);
90
+ expect(wrapper.style.top).toBe("0px");
91
+ expect(wrapper.style.bottom).toBe("0px");
92
+ expect(wrapper.style.right).toBe("0px");
93
+ expect(wrapper.style.height).toBe("100vh");
94
+ expect(panel?.style.borderRadius).toBe("0px");
95
+
96
+ controller.destroy();
97
+ });
98
+
99
+ it("insets the sidebar card and stamps the detached attribute when detached", () => {
100
+ const mount = createMount();
101
+ const controller = createAgentExperience(mount, {
102
+ apiUrl: "https://api.example.com/chat",
103
+ launcher: {
104
+ sidebarMode: true,
105
+ position: "bottom-right",
106
+ detachedPanel: true,
107
+ },
108
+ });
109
+
110
+ const wrapper = mount.firstElementChild as HTMLElement;
111
+ const panel = queryPanel(mount);
112
+
113
+ expect(mount.getAttribute("data-persona-panel-detached")).toBe("true");
114
+ expect(wrapper.style.top).toBe("var(--persona-panel-inset)");
115
+ expect(wrapper.style.bottom).toBe("var(--persona-panel-inset)");
116
+ expect(wrapper.style.right).toBe("var(--persona-panel-inset)");
117
+ expect(wrapper.style.left).toBe("auto");
118
+ expect(wrapper.style.height).toContain("var(--persona-panel-inset)");
119
+ expect(wrapper.style.height).toContain("calc");
120
+ // Card chrome restored: radius is no longer the flush 0.
121
+ expect(panel?.style.borderRadius).not.toBe("0");
122
+ expect(panel?.style.borderRadius).not.toBe("");
123
+
124
+ controller.destroy();
125
+ });
126
+
127
+ it("anchors the inset to the left for a left-positioned detached sidebar", () => {
128
+ const mount = createMount();
129
+ const controller = createAgentExperience(mount, {
130
+ apiUrl: "https://api.example.com/chat",
131
+ launcher: {
132
+ sidebarMode: true,
133
+ position: "bottom-left",
134
+ detachedPanel: true,
135
+ },
136
+ });
137
+
138
+ const wrapper = mount.firstElementChild as HTMLElement;
139
+
140
+ expect(wrapper.style.left).toBe("var(--persona-panel-inset)");
141
+ expect(wrapper.style.right).toBe("auto");
142
+
143
+ controller.destroy();
144
+ });
145
+
146
+ it("does not pad the wrapper for docked + launcher off + detached (host-layout owns the inset)", () => {
147
+ const mount = createMount();
148
+ const controller = createAgentExperience(mount, {
149
+ apiUrl: "https://api.example.com/chat",
150
+ launcher: {
151
+ mountMode: "docked",
152
+ enabled: false,
153
+ detachedPanel: true,
154
+ },
155
+ });
156
+
157
+ const { wrapper } = queryEmbedRefs(mount);
158
+
159
+ // Detached card still stamped, but the inset comes from host-layout, not here.
160
+ expect(mount.getAttribute("data-persona-panel-detached")).toBe("true");
161
+ expect(wrapper?.style.padding).toBe("");
162
+
163
+ controller.destroy();
164
+ });
165
+
166
+ it("drops card chrome for docked + launcher off + detached on a mobile viewport", () => {
167
+ setInnerWidth(480);
168
+ const mount = createMount();
169
+ const controller = createAgentExperience(mount, {
170
+ apiUrl: "https://api.example.com/chat",
171
+ launcher: {
172
+ mountMode: "docked",
173
+ enabled: false,
174
+ detachedPanel: true,
175
+ },
176
+ });
177
+
178
+ const { wrapper, panel } = queryEmbedRefs(mount);
179
+
180
+ // Host-layout goes flush fullscreen: no detached attribute, no card shadow or inset.
181
+ expect(mount.hasAttribute("data-persona-panel-detached")).toBe(false);
182
+ expect(panel?.style.boxShadow).toBe("none");
183
+ expect(wrapper?.style.padding).toBe("");
184
+
185
+ controller.destroy();
186
+ });
187
+
188
+ it("does not clip the card shadow with overflow:hidden for an inline detached embed", () => {
189
+ const mount = createMount();
190
+ const controller = createAgentExperience(mount, {
191
+ apiUrl: "https://api.example.com/chat",
192
+ launcher: {
193
+ enabled: false,
194
+ detachedPanel: true,
195
+ },
196
+ });
197
+
198
+ const { wrapper } = queryEmbedRefs(mount);
199
+
200
+ expect(mount.getAttribute("data-persona-panel-detached")).toBe("true");
201
+ // Canvas padding applied, but no overflow:hidden so the xl shadow can escape.
202
+ expect(wrapper?.style.padding).toBe("var(--persona-panel-inset)");
203
+ expect(wrapper?.style.overflow).not.toBe("hidden");
204
+
205
+ controller.destroy();
206
+ });
207
+
208
+ it("renders a flush inline embed with no shadow; detached opts elevation back in", () => {
209
+ const mount = createMount();
210
+ const controller = createAgentExperience(mount, {
211
+ apiUrl: "https://api.example.com/chat",
212
+ launcher: { enabled: false },
213
+ });
214
+
215
+ const { panel } = queryEmbedRefs(mount);
216
+ expect(panel?.style.boxShadow).toBe("none");
217
+ controller.destroy();
218
+
219
+ const detachedMount = createMount();
220
+ const detachedController = createAgentExperience(detachedMount, {
221
+ apiUrl: "https://api.example.com/chat",
222
+ launcher: { enabled: false, detachedPanel: true },
223
+ });
224
+
225
+ const detachedPanel = queryEmbedRefs(detachedMount).panel;
226
+ expect(detachedPanel?.style.boxShadow).toContain("--persona-panel-shadow");
227
+ detachedController.destroy();
228
+ });
229
+ });
230
+
231
+ describe("createAgentExperience detached artifact split chrome", () => {
232
+ beforeEach(() => {
233
+ // Desktop viewport so the split renders side-by-side (not the narrow drawer).
234
+ setInnerWidth(1024);
235
+ });
236
+
237
+ afterEach(() => {
238
+ document.body.innerHTML = "";
239
+ setInnerWidth(originalInnerWidth);
240
+ });
241
+
242
+ const artifactConfig = (
243
+ launcher: AgentWidgetConfig["launcher"]
244
+ ): AgentWidgetConfig => ({
245
+ apiUrl: "https://api.example.com/chat",
246
+ launcher,
247
+ features: {
248
+ artifacts: { enabled: true, allowedTypes: ["markdown"] },
249
+ },
250
+ });
251
+
252
+ // Artifacts nest the container in a split root, so the panel is the split
253
+ // root's parent (container.parentElement would be the chat column).
254
+ const splitRefs = (mount: HTMLElement) => {
255
+ const container = mount.querySelector<HTMLElement>(".persona-widget-container");
256
+ const split = mount.querySelector<HTMLElement>(".persona-artifact-split-root");
257
+ return {
258
+ panel: (split?.parentElement ?? container?.parentElement ?? null) as HTMLElement | null,
259
+ container,
260
+ };
261
+ };
262
+
263
+ const openArtifact = (controller: ReturnType<typeof createAgentExperience>) => {
264
+ controller.upsertArtifact({
265
+ id: "split-test",
266
+ title: "Split test",
267
+ artifactType: "markdown",
268
+ content: "# Hello",
269
+ });
270
+ };
271
+
272
+ it("suppresses the outer panel shadow and cards the chat column when a detached split is active", () => {
273
+ const mount = createMount();
274
+ const controller = createAgentExperience(
275
+ mount,
276
+ artifactConfig({ enabled: false, detachedPanel: true })
277
+ );
278
+ const { panel, container } = splitRefs(mount);
279
+
280
+ // No pane open yet: panel is the single card and keeps its shadow.
281
+ expect(panel?.style.boxShadow).toContain("--persona-panel-shadow");
282
+
283
+ openArtifact(controller);
284
+
285
+ // Detached split active: outer union shadow gone, chat column is its own card.
286
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
287
+ expect(panel?.style.boxShadow).toBe("none");
288
+ expect(container?.style.boxShadow).toContain("--persona-artifact-pane-shadow");
289
+ expect(container?.style.border).toContain("--persona-panel-border");
290
+
291
+ controller.destroy();
292
+ });
293
+
294
+ it("restores the panel card shadow when the pane closes", () => {
295
+ const mount = createMount();
296
+ const controller = createAgentExperience(
297
+ mount,
298
+ artifactConfig({ enabled: false, detachedPanel: true })
299
+ );
300
+ const { panel, container } = splitRefs(mount);
301
+
302
+ openArtifact(controller);
303
+ expect(panel?.style.boxShadow).toBe("none");
304
+
305
+ controller.hideArtifacts();
306
+
307
+ // Pane closed: the panel is the single visible card again and re-elevates.
308
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
309
+ expect(panel?.style.boxShadow).toContain("--persona-panel-shadow");
310
+ expect(container?.style.boxShadow).toBe("");
311
+
312
+ controller.destroy();
313
+ });
314
+
315
+ it("leaves the outer panel shadow intact for the default panel appearance", () => {
316
+ const mount = createMount();
317
+ // Floating launcher (default): the panel is an elevated card, so the panel
318
+ // appearance keeps the outer shadow instead of the flush inline embed's none.
319
+ const controller = createAgentExperience(
320
+ mount,
321
+ artifactConfig({ detachedPanel: false })
322
+ );
323
+ const { panel, container } = splitRefs(mount);
324
+
325
+ openArtifact(controller);
326
+
327
+ // Panel appearance: one shared card, so the outer shadow survives and the
328
+ // chat column does not get its own elevation.
329
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
330
+ expect(panel?.style.boxShadow).not.toBe("none");
331
+ expect(container?.style.boxShadow).toBe("");
332
+
333
+ controller.destroy();
334
+ });
335
+
336
+ it("flattens the chat card via layout.chatShadow while the pane stays raised", () => {
337
+ const mount = createMount();
338
+ const controller = createAgentExperience(mount, {
339
+ apiUrl: "https://api.example.com/chat",
340
+ launcher: { enabled: false, detachedPanel: true },
341
+ features: {
342
+ artifacts: {
343
+ enabled: true,
344
+ allowedTypes: ["markdown"],
345
+ layout: { chatShadow: "none" },
346
+ },
347
+ },
348
+ });
349
+ const { container } = splitRefs(mount);
350
+
351
+ openArtifact(controller);
352
+
353
+ // Chat card resolves through the new front lookup, and the mount pins it to none.
354
+ expect(container?.style.boxShadow).toContain("--persona-artifact-chat-shadow");
355
+ expect(mount.style.getPropertyValue("--persona-artifact-chat-shadow")).toBe("none");
356
+
357
+ controller.destroy();
358
+ });
359
+
360
+ it("does not set the chat shadow var by default and leaves the pane chain intact", () => {
361
+ const mount = createMount();
362
+ const controller = createAgentExperience(
363
+ mount,
364
+ artifactConfig({ enabled: false, detachedPanel: true })
365
+ );
366
+ const { container } = splitRefs(mount);
367
+
368
+ openArtifact(controller);
369
+
370
+ expect(mount.style.getPropertyValue("--persona-artifact-chat-shadow")).toBe("");
371
+ expect(container?.style.boxShadow).toContain("--persona-artifact-pane-shadow");
372
+
373
+ controller.destroy();
374
+ });
375
+
376
+ it("does not leak chatShadow onto the welded chat column", () => {
377
+ const mount = createMount();
378
+ // Panel appearance welds into one card, so the chat container shadow is blanked
379
+ // regardless of chatShadow; the var must not re-elevate it.
380
+ const controller = createAgentExperience(mount, {
381
+ apiUrl: "https://api.example.com/chat",
382
+ launcher: { detachedPanel: false },
383
+ features: {
384
+ artifacts: {
385
+ enabled: true,
386
+ allowedTypes: ["markdown"],
387
+ layout: { chatShadow: "0 0 0 red" },
388
+ },
389
+ },
390
+ });
391
+ const { container } = splitRefs(mount);
392
+
393
+ openArtifact(controller);
394
+
395
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
396
+ expect(container?.style.boxShadow).toBe("");
397
+
398
+ controller.destroy();
399
+ });
400
+
401
+ it("uses the hardcoded 640 split boundary, not launcher.mobileBreakpoint", () => {
402
+ // 800px is above the 641 desktop split boundary but below a custom
403
+ // mobileBreakpoint of 900: the split must stay active regardless, because
404
+ // the artifact split CSS is hardcoded at 640/641 and mobileBreakpoint must
405
+ // not move it (otherwise the predicate and the CSS disagree).
406
+ setInnerWidth(800);
407
+ const mount = createMount();
408
+ const controller = createAgentExperience(
409
+ mount,
410
+ artifactConfig({ enabled: false, detachedPanel: true, mobileBreakpoint: 900 })
411
+ );
412
+ const { panel } = splitRefs(mount);
413
+
414
+ openArtifact(controller);
415
+
416
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
417
+ expect(panel?.style.boxShadow).toBe("none");
418
+
419
+ controller.destroy();
420
+ });
421
+
422
+ it("does not clip the fullHeight panel overflow when a detached split is active", () => {
423
+ const mount = createMount();
424
+ const controller = createAgentExperience(
425
+ mount,
426
+ artifactConfig({ fullHeight: true, detachedPanel: true })
427
+ );
428
+ const { panel, container } = splitRefs(mount);
429
+
430
+ openArtifact(controller);
431
+
432
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
433
+ // Each card carries its own shadow that must escape the panel, so the panel
434
+ // must not set overflow:hidden.
435
+ expect(panel?.style.overflow).not.toBe("hidden");
436
+ // The chat card still clips its own content inside its rounded border.
437
+ expect(container?.style.overflow).toBe("hidden");
438
+
439
+ controller.destroy();
440
+ });
441
+
442
+ it("does not suppress the panel shadow on a mobile viewport", () => {
443
+ setInnerWidth(480);
444
+ const mount = createMount();
445
+ // Inline embed keeps the launcher off so mobile does not force fullscreen
446
+ // (which zeroes the shadow for its own reasons); the detached card is intact.
447
+ const controller = createAgentExperience(
448
+ mount,
449
+ artifactConfig({ enabled: false, detachedPanel: true })
450
+ );
451
+ const { panel } = splitRefs(mount);
452
+
453
+ openArtifact(controller);
454
+
455
+ // Below the breakpoint the pane is a drawer overlay, so the panel stays the
456
+ // single detached card and the detached-split suppression must not apply.
457
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
458
+ expect(panel?.style.boxShadow).toContain("--persona-panel-shadow");
459
+
460
+ controller.destroy();
461
+ });
462
+ });
463
+
464
+ describe("createAgentExperience detached appearance perimeter inset", () => {
465
+ beforeEach(() => {
466
+ // Desktop viewport so the split renders side-by-side (not the narrow drawer).
467
+ setInnerWidth(1024);
468
+ localStorage.clear();
469
+ });
470
+
471
+ afterEach(() => {
472
+ document.body.innerHTML = "";
473
+ setInnerWidth(originalInnerWidth);
474
+ localStorage.clear();
475
+ });
476
+
477
+ const wrapperOf = (mount: HTMLElement) => mount.firstElementChild as HTMLElement;
478
+
479
+ const openArtifact = (controller: ReturnType<typeof createAgentExperience>) => {
480
+ controller.upsertArtifact({
481
+ id: "inset-test",
482
+ title: "Inset test",
483
+ artifactType: "markdown",
484
+ content: "# Hello",
485
+ });
486
+ };
487
+
488
+ it("pads the wrapper for an inline detached-appearance split with detachedPanel unset", () => {
489
+ const mount = createMount();
490
+ const controller = createAgentExperience(mount, {
491
+ apiUrl: "https://api.example.com/chat",
492
+ launcher: { enabled: false },
493
+ features: {
494
+ artifacts: {
495
+ enabled: true,
496
+ allowedTypes: ["markdown"],
497
+ layout: { paneAppearance: "detached" },
498
+ },
499
+ },
500
+ });
501
+
502
+ openArtifact(controller);
503
+
504
+ const wrapper = wrapperOf(mount);
505
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
506
+ expect(wrapper.style.padding).toBe("var(--persona-panel-inset)");
507
+ expect(wrapper.style.background).toContain("--persona-panel-canvas-bg");
508
+
509
+ controller.destroy();
510
+ });
511
+
512
+ it("does not pad the wrapper for the default (welded) inline embed with a pane open", () => {
513
+ const mount = createMount();
514
+ const controller = createAgentExperience(mount, {
515
+ apiUrl: "https://api.example.com/chat",
516
+ launcher: { enabled: false },
517
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
518
+ });
519
+
520
+ openArtifact(controller);
521
+
522
+ const wrapper = wrapperOf(mount);
523
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
524
+ expect(wrapper.style.padding).toBe("");
525
+
526
+ controller.destroy();
527
+ });
528
+
529
+ it("clears the perimeter inset after the detached-appearance pane closes", () => {
530
+ const mount = createMount();
531
+ const controller = createAgentExperience(mount, {
532
+ apiUrl: "https://api.example.com/chat",
533
+ launcher: { enabled: false },
534
+ features: {
535
+ artifacts: {
536
+ enabled: true,
537
+ allowedTypes: ["markdown"],
538
+ layout: { paneAppearance: "detached" },
539
+ },
540
+ },
541
+ });
542
+
543
+ openArtifact(controller);
544
+ expect(wrapperOf(mount).style.padding).toBe("var(--persona-panel-inset)");
545
+
546
+ controller.hideArtifacts();
547
+
548
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
549
+ expect(wrapperOf(mount).style.padding).toBe("");
550
+
551
+ controller.destroy();
552
+ });
553
+
554
+ it("still pads an inline detachedPanel embed (unchanged detached-card path)", () => {
555
+ const mount = createMount();
556
+ const controller = createAgentExperience(mount, {
557
+ apiUrl: "https://api.example.com/chat",
558
+ launcher: { enabled: false, detachedPanel: true },
559
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
560
+ });
561
+
562
+ const wrapper = wrapperOf(mount);
563
+ expect(mount.getAttribute("data-persona-panel-detached")).toBe("true");
564
+ expect(wrapper.style.padding).toBe("var(--persona-panel-inset)");
565
+
566
+ controller.destroy();
567
+ });
568
+ });
569
+
570
+ describe("createAgentExperience detached split chat surface", () => {
571
+ beforeEach(() => {
572
+ // Desktop viewport so the split renders side-by-side (not the narrow drawer).
573
+ setInnerWidth(1024);
574
+ localStorage.clear();
575
+ });
576
+
577
+ afterEach(() => {
578
+ document.body.innerHTML = "";
579
+ setInnerWidth(originalInnerWidth);
580
+ localStorage.clear();
581
+ });
582
+
583
+ const wrapperOf = (mount: HTMLElement) => mount.firstElementChild as HTMLElement;
584
+ const containerOf = (mount: HTMLElement) =>
585
+ mount.querySelector<HTMLElement>(".persona-widget-container");
586
+ const bodyOf = (mount: HTMLElement) =>
587
+ mount.querySelector<HTMLElement>(".persona-widget-body");
588
+ const footerOf = (mount: HTMLElement) =>
589
+ mount.querySelector<HTMLElement>(".persona-widget-footer");
590
+
591
+ const openArtifact = (controller: ReturnType<typeof createAgentExperience>) => {
592
+ controller.upsertArtifact({
593
+ id: "surface-test",
594
+ title: "Surface test",
595
+ artifactType: "markdown",
596
+ content: "# Hello",
597
+ });
598
+ };
599
+
600
+ const flushConfig = (
601
+ chatSurface: "card" | "flush"
602
+ ): AgentWidgetConfig => ({
603
+ apiUrl: "https://api.example.com/chat",
604
+ launcher: { enabled: false },
605
+ features: {
606
+ artifacts: {
607
+ enabled: true,
608
+ allowedTypes: ["markdown"],
609
+ layout: { paneAppearance: "detached", chatSurface },
610
+ },
611
+ },
612
+ });
613
+
614
+ it("flushes the chat and insets only the pane when chatSurface is flush", () => {
615
+ const mount = createMount();
616
+ const controller = createAgentExperience(mount, flushConfig("flush"));
617
+
618
+ openArtifact(controller);
619
+
620
+ const container = containerOf(mount);
621
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
622
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
623
+ // Chat column chrome dropped: flat flush background beside the floating pane.
624
+ expect(container?.style.borderStyle).toBe("none");
625
+ expect(container?.style.borderRadius).toBe("0px");
626
+ expect(container?.style.boxShadow).toBe("");
627
+ // The chat paints no surface of its own; the wrapper's canvas shows through.
628
+ // Body and footer backdrops go too, so the transcript sits on the host page
629
+ // and only element surfaces (bubbles, composer input) keep their color.
630
+ expect(container?.style.background).toBe("transparent");
631
+ expect(bodyOf(mount)?.style.background).toBe("transparent");
632
+ expect(footerOf(mount)?.style.background).toBe("transparent");
633
+ expect(footerOf(mount)?.style.borderTopStyle).toBe("none");
634
+ expect(wrapperOf(mount).style.background).toContain("--persona-panel-canvas-bg");
635
+ // No whole-split perimeter inset: the chat is flush to the container edges.
636
+ expect(wrapperOf(mount).style.padding).toBe("");
637
+
638
+ controller.destroy();
639
+ });
640
+
641
+ it("keeps two matched cards and pads the wrapper in the default card surface", () => {
642
+ const mount = createMount();
643
+ const controller = createAgentExperience(mount, flushConfig("card"));
644
+
645
+ openArtifact(controller);
646
+
647
+ const container = containerOf(mount);
648
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
649
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(false);
650
+ // Chat column carries its own card chrome.
651
+ expect(container?.style.border).toContain("--persona-panel-border");
652
+ expect(container?.style.boxShadow).toContain("--persona-artifact-pane-shadow");
653
+ expect(container?.style.background).toBe("");
654
+ expect(bodyOf(mount)?.style.background).toBe("");
655
+ expect(footerOf(mount)?.style.background).toBe("");
656
+ // Perimeter inset still pads the whole split.
657
+ expect(wrapperOf(mount).style.padding).toBe("var(--persona-panel-inset)");
658
+
659
+ controller.destroy();
660
+ });
661
+
662
+ it("applies the flush chrome while idle, before any artifact opens", () => {
663
+ const mount = createMount();
664
+ const controller = createAgentExperience(mount, flushConfig("flush"));
665
+
666
+ const container = containerOf(mount);
667
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
668
+ expect(container?.style.borderStyle).toBe("none");
669
+ expect(container?.style.borderRadius).toBe("0px");
670
+ expect(container?.style.background).toBe("transparent");
671
+ expect(bodyOf(mount)?.style.background).toBe("transparent");
672
+ expect(footerOf(mount)?.style.background).toBe("transparent");
673
+ expect(wrapperOf(mount).style.background).toContain("--persona-panel-canvas-bg");
674
+ expect(wrapperOf(mount).style.padding).toBe("");
675
+
676
+ controller.destroy();
677
+ });
678
+
679
+ it("keeps the flush chrome after the pane closes", () => {
680
+ const mount = createMount();
681
+ const controller = createAgentExperience(mount, flushConfig("flush"));
682
+
683
+ openArtifact(controller);
684
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
685
+
686
+ controller.hideArtifacts();
687
+
688
+ const container = containerOf(mount);
689
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
690
+ // Flush is a steady state: closing the pane never flips the chat chrome.
691
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
692
+ expect(container?.style.borderRadius).toBe("0px");
693
+ expect(container?.style.background).toBe("transparent");
694
+ expect(bodyOf(mount)?.style.background).toBe("transparent");
695
+ expect(footerOf(mount)?.style.background).toBe("transparent");
696
+ expect(wrapperOf(mount).style.background).toContain("--persona-panel-canvas-bg");
697
+
698
+ controller.destroy();
699
+ });
700
+
701
+ it("is a no-op with a non-detached appearance (welded panel)", () => {
702
+ const mount = createMount();
703
+ const controller = createAgentExperience(mount, {
704
+ apiUrl: "https://api.example.com/chat",
705
+ launcher: { enabled: false },
706
+ features: {
707
+ artifacts: {
708
+ enabled: true,
709
+ allowedTypes: ["markdown"],
710
+ // Default panel appearance welds into one card; flush requires detached.
711
+ layout: { chatSurface: "flush" },
712
+ },
713
+ },
714
+ });
715
+
716
+ openArtifact(controller);
717
+
718
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
719
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(false);
720
+
721
+ controller.destroy();
722
+ });
723
+
724
+ it("falls back to the card look for a non-inline (docked) detached split", () => {
725
+ // Flush is gated to inline embeds, matching the card-mode perimeter inset:
726
+ // a docked launcher (isInlineEmbed false, dockedMode true) must ignore
727
+ // chatSurface: 'flush' and keep the chat's card chrome.
728
+ const mount = createMount();
729
+ const controller = createAgentExperience(mount, {
730
+ apiUrl: "https://api.example.com/chat",
731
+ launcher: { mountMode: "docked" },
732
+ features: {
733
+ artifacts: {
734
+ enabled: true,
735
+ allowedTypes: ["markdown"],
736
+ layout: { paneAppearance: "detached", chatSurface: "flush" },
737
+ },
738
+ },
739
+ });
740
+
741
+ openArtifact(controller);
742
+
743
+ const container = containerOf(mount);
744
+ // Detached split is active, but flush is suppressed off the inline path.
745
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(true);
746
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(false);
747
+ // Chat keeps its card chrome instead of the flat flush 'none'.
748
+ expect(container?.style.border).not.toBe("none");
749
+ expect(container?.style.border).toContain("--persona-panel-border");
750
+
751
+ controller.destroy();
752
+ });
753
+
754
+ const panelOf = (mount: HTMLElement) =>
755
+ mount.querySelector<HTMLElement>(".persona-artifact-split-root")
756
+ ?.parentElement as HTMLElement | null;
757
+
758
+ it("squares the outer panel in flush mode when panel.borderRadius is unset", () => {
759
+ const mount = createMount();
760
+ const controller = createAgentExperience(mount, flushConfig("flush"));
761
+
762
+ openArtifact(controller);
763
+
764
+ const panel = panelOf(mount);
765
+ // Panel fills the container flush, so it squares off; the pane keeps its radius.
766
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
767
+ expect(panel?.style.borderRadius).toBe("0px");
768
+
769
+ controller.destroy();
770
+ });
771
+
772
+ it("keeps an explicit panel.borderRadius in flush mode", () => {
773
+ const mount = createMount();
774
+ const controller = createAgentExperience(mount, {
775
+ apiUrl: "https://api.example.com/chat",
776
+ launcher: { enabled: false },
777
+ theme: { components: { panel: { borderRadius: "16px" } } },
778
+ features: {
779
+ artifacts: {
780
+ enabled: true,
781
+ allowedTypes: ["markdown"],
782
+ layout: { paneAppearance: "detached", chatSurface: "flush" },
783
+ },
784
+ },
785
+ });
786
+
787
+ openArtifact(controller);
788
+
789
+ const panel = panelOf(mount);
790
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(true);
791
+ // Explicit override wins over the auto-square default.
792
+ expect(panel?.style.borderRadius).toBe("16px");
793
+
794
+ controller.destroy();
795
+ });
796
+
797
+ it("does not square the panel for a card-surface detached split", () => {
798
+ const mount = createMount();
799
+ const controller = createAgentExperience(mount, flushConfig("card"));
800
+
801
+ openArtifact(controller);
802
+
803
+ const panel = panelOf(mount);
804
+ // Card mode keeps the floating panel radius, not the flush square.
805
+ expect(mount.classList.contains("persona-artifact-chat-flush")).toBe(false);
806
+ expect(panel?.style.borderRadius).not.toBe("0px");
807
+
808
+ controller.destroy();
809
+ });
810
+ });
811
+
812
+ describe("createAgentExperience welded artifact split chrome", () => {
813
+ beforeEach(() => {
814
+ // Desktop viewport so the split renders side-by-side (not the narrow drawer).
815
+ setInnerWidth(1024);
816
+ // Artifacts persist to the shared default history key; clear it so a fresh
817
+ // widget starts pane-closed instead of restoring a prior test's artifact.
818
+ localStorage.clear();
819
+ });
820
+
821
+ afterEach(() => {
822
+ document.body.innerHTML = "";
823
+ setInnerWidth(originalInnerWidth);
824
+ localStorage.clear();
825
+ });
826
+
827
+ const weldedConfig = (
828
+ layout: NonNullable<
829
+ NonNullable<AgentWidgetConfig["features"]>["artifacts"]
830
+ >["layout"] = {},
831
+ theme?: AgentWidgetConfig["theme"]
832
+ ): AgentWidgetConfig => ({
833
+ apiUrl: "https://api.example.com/chat",
834
+ // Floating launcher (default): the panel is an elevated card, so the outer
835
+ // union shadow is present and the welded border can move onto it.
836
+ ...(theme ? { theme } : {}),
837
+ features: {
838
+ artifacts: { enabled: true, allowedTypes: ["markdown"], layout },
839
+ },
840
+ });
841
+
842
+ const splitRefs = (mount: HTMLElement) => {
843
+ const container = mount.querySelector<HTMLElement>(".persona-widget-container");
844
+ const split = mount.querySelector<HTMLElement>(".persona-artifact-split-root");
845
+ return {
846
+ panel: (split?.parentElement ?? container?.parentElement ?? null) as HTMLElement | null,
847
+ container,
848
+ };
849
+ };
850
+
851
+ const openArtifact = (controller: ReturnType<typeof createAgentExperience>) => {
852
+ controller.upsertArtifact({
853
+ id: "welded-test",
854
+ title: "Welded test",
855
+ artifactType: "markdown",
856
+ content: "# Hello",
857
+ });
858
+ };
859
+
860
+ it("welds panel appearance: border + radius move onto the union, chat column goes borderless", () => {
861
+ const mount = createMount();
862
+ const controller = createAgentExperience(mount, weldedConfig());
863
+ const { panel, container } = splitRefs(mount);
864
+
865
+ // Pane closed: the chat container carries the card border, the panel does not.
866
+ expect(container?.style.border).toContain("--persona-panel-border");
867
+ expect(panel?.style.border).toBe("");
868
+
869
+ openArtifact(controller);
870
+
871
+ // Welded split active: one card. Border + radius wrap the union (on the
872
+ // panel); the chat column drops its own border; the outer shadow survives.
873
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
874
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
875
+ expect(panel?.style.border).toContain("--persona-panel-border");
876
+ expect(panel?.style.borderRadius).not.toBe("");
877
+ expect(panel?.style.boxShadow).not.toBe("none");
878
+ expect(container?.style.border).not.toContain("--persona-panel-border");
879
+ expect(mount.classList.contains("persona-artifact-appearance-panel")).toBe(true);
880
+
881
+ controller.destroy();
882
+ });
883
+
884
+ it("welds seamless appearance the same way (union chrome, no divider class differences aside)", () => {
885
+ const mount = createMount();
886
+ const controller = createAgentExperience(mount, weldedConfig({ paneAppearance: "seamless" }));
887
+ const { panel, container } = splitRefs(mount);
888
+
889
+ openArtifact(controller);
890
+
891
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
892
+ expect(panel?.style.border).toContain("--persona-panel-border");
893
+ expect(container?.style.border).not.toContain("--persona-panel-border");
894
+ // Seamless keeps the shared welded chrome; the divider is suppressed in CSS.
895
+ expect(mount.classList.contains("persona-artifact-appearance-seamless")).toBe(true);
896
+
897
+ controller.destroy();
898
+ });
899
+
900
+ it("restores the chat-column border and clears the welded class when the pane closes", () => {
901
+ const mount = createMount();
902
+ const controller = createAgentExperience(mount, weldedConfig());
903
+ const { panel, container } = splitRefs(mount);
904
+
905
+ openArtifact(controller);
906
+ expect(container?.style.border).not.toContain("--persona-panel-border");
907
+
908
+ controller.hideArtifacts();
909
+
910
+ // Byte-identical to pane-closed: container carries the border, panel does not.
911
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(false);
912
+ expect(container?.style.border).toContain("--persona-panel-border");
913
+ expect(panel?.style.border).toBe("");
914
+
915
+ controller.destroy();
916
+ });
917
+
918
+ it("lets an explicit paneBorder win over the welded default", () => {
919
+ const mount = createMount();
920
+ const controller = createAgentExperience(
921
+ mount,
922
+ weldedConfig({ paneBorder: "2px solid #abcabc" })
923
+ );
924
+ openArtifact(controller);
925
+
926
+ // The pane border theme class + var are still applied under the welded split,
927
+ // so a configured border overrides the hairline divider default.
928
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
929
+ expect(mount.classList.contains("persona-artifact-border-full")).toBe(true);
930
+ expect(
931
+ mount.style.getPropertyValue("--persona-artifact-pane-border").trim()
932
+ ).toBe("2px solid #abcabc");
933
+
934
+ controller.destroy();
935
+ });
936
+
937
+ it("does not weld on a mobile viewport (drawer overlay, not a split)", () => {
938
+ setInnerWidth(480);
939
+ const mount = createMount();
940
+ // Inline embed keeps the launcher off so mobile does not force fullscreen.
941
+ const controller = createAgentExperience(mount, {
942
+ apiUrl: "https://api.example.com/chat",
943
+ launcher: { enabled: false },
944
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
945
+ });
946
+ const { container } = splitRefs(mount);
947
+
948
+ openArtifact(controller);
949
+
950
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(false);
951
+ // Below the breakpoint the chat column keeps its own border.
952
+ expect(container?.style.border).not.toBe("none");
953
+
954
+ controller.destroy();
955
+ });
956
+
957
+ it("does not weld in mobile fullscreen driven by a custom mobileBreakpoint", () => {
958
+ // 700px is a desktop-split width by the 641 breakpoint, but a launcher with
959
+ // mobileBreakpoint:768 goes fullscreen here, so the split chrome must stand down.
960
+ setInnerWidth(700);
961
+ const mount = createMount();
962
+ const controller = createAgentExperience(mount, {
963
+ apiUrl: "https://api.example.com/chat",
964
+ launcher: { mobileBreakpoint: 768 },
965
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
966
+ });
967
+
968
+ openArtifact(controller);
969
+
970
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(false);
971
+ expect(mount.classList.contains("persona-artifact-detached-split")).toBe(false);
972
+
973
+ controller.destroy();
974
+ });
975
+
976
+ it("welds again after a custom-breakpoint fullscreen viewport widens", () => {
977
+ setInnerWidth(700);
978
+ const mount = createMount();
979
+ const controller = createAgentExperience(mount, {
980
+ apiUrl: "https://api.example.com/chat",
981
+ launcher: { mobileBreakpoint: 768 },
982
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
983
+ });
984
+
985
+ openArtifact(controller);
986
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(false);
987
+
988
+ // Cross above the custom breakpoint: the resize resync re-applies split chrome.
989
+ setInnerWidth(900);
990
+ window.dispatchEvent(new Event("resize"));
991
+
992
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
993
+
994
+ controller.destroy();
995
+ });
996
+
997
+ it("gives a docked welded split the dock-facing hairline on the outer panel", () => {
998
+ const mount = createMount();
999
+ const controller = createAgentExperience(mount, {
1000
+ apiUrl: "https://api.example.com/chat",
1001
+ launcher: { mountMode: "docked", dock: { side: "right" } },
1002
+ features: { artifacts: { enabled: true, allowedTypes: ["markdown"] } },
1003
+ });
1004
+ const { panel } = splitRefs(mount);
1005
+
1006
+ openArtifact(controller);
1007
+
1008
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
1009
+ // Right dock: the page sits to the left, so the hairline faces left.
1010
+ expect(panel?.style.borderLeft).toContain("var(--persona-border)");
1011
+
1012
+ controller.destroy();
1013
+ });
1014
+
1015
+ it("derives the welded outer radius from a custom components.panel.borderRadius", () => {
1016
+ const mount = createMount();
1017
+ const controller = createAgentExperience(
1018
+ mount,
1019
+ weldedConfig({}, { components: { panel: { borderRadius: "1.75rem" } } })
1020
+ );
1021
+
1022
+ openArtifact(controller);
1023
+
1024
+ expect(mount.classList.contains("persona-artifact-welded-split")).toBe(true);
1025
+ // Both the chat card left corners and the pane outer-right corners resolve
1026
+ // from the same panel radius, so the welded card is symmetric.
1027
+ expect(
1028
+ mount.style.getPropertyValue("--persona-artifact-welded-outer-radius").trim()
1029
+ ).toBe("1.75rem");
1030
+
1031
+ controller.destroy();
1032
+ });
1033
+
1034
+ it("lets unifiedSplitOuterRadius override the welded outer radius", () => {
1035
+ const mount = createMount();
1036
+ const controller = createAgentExperience(
1037
+ mount,
1038
+ weldedConfig({ unifiedSplitOuterRadius: "9px" })
1039
+ );
1040
+
1041
+ openArtifact(controller);
1042
+
1043
+ expect(
1044
+ mount.style.getPropertyValue("--persona-artifact-welded-outer-radius").trim()
1045
+ ).toBe("9px");
1046
+
1047
+ controller.destroy();
1048
+ });
1049
+ });