@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
@@ -527,4 +527,167 @@ describe("createWidgetHostLayout docked", () => {
527
527
  layout.destroy();
528
528
  });
529
529
  });
530
+
531
+ describe("detached docked", () => {
532
+ it("leaves the dock slot flush when detachedPanel is unset", () => {
533
+ const parent = document.createElement("div");
534
+ document.body.appendChild(parent);
535
+ const target = document.createElement("div");
536
+ parent.appendChild(target);
537
+
538
+ const layout = createWidgetHostLayout(target, {
539
+ launcher: {
540
+ mountMode: "docked",
541
+ autoExpand: true,
542
+ dock: { width: "320px" },
543
+ },
544
+ });
545
+
546
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
547
+ expect(dockSlot?.style.padding).toBe("");
548
+ expect(dockSlot?.style.background).toBe("");
549
+
550
+ layout.destroy();
551
+ });
552
+
553
+ it("pads the dock slot with the default inset and canvas when detached", () => {
554
+ const parent = document.createElement("div");
555
+ document.body.appendChild(parent);
556
+ const target = document.createElement("div");
557
+ parent.appendChild(target);
558
+
559
+ const layout = createWidgetHostLayout(target, {
560
+ launcher: {
561
+ mountMode: "docked",
562
+ autoExpand: true,
563
+ detachedPanel: true,
564
+ dock: { width: "320px" },
565
+ },
566
+ });
567
+
568
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
569
+ expect(dockSlot?.style.padding).toBe("16px");
570
+ expect(dockSlot?.style.background).toBe("transparent");
571
+ expect(dockSlot?.style.boxSizing).toBe("border-box");
572
+
573
+ layout.destroy();
574
+ });
575
+
576
+ it("resolves detached inset and canvas from panel theme tokens", () => {
577
+ const parent = document.createElement("div");
578
+ document.body.appendChild(parent);
579
+ const target = document.createElement("div");
580
+ parent.appendChild(target);
581
+
582
+ const layout = createWidgetHostLayout(target, {
583
+ launcher: {
584
+ mountMode: "docked",
585
+ autoExpand: true,
586
+ detachedPanel: true,
587
+ dock: { width: "320px" },
588
+ },
589
+ theme: {
590
+ components: { panel: { inset: "24px", canvasBackground: "#eee" } },
591
+ },
592
+ });
593
+
594
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
595
+ expect(dockSlot?.style.padding).toBe("24px");
596
+ expect(dockSlot?.style.background).toBe("rgb(238, 238, 238)");
597
+
598
+ layout.destroy();
599
+ });
600
+
601
+ it("shrinks the emerge host by the inset so it fits the padded slot", () => {
602
+ const parent = document.createElement("div");
603
+ document.body.appendChild(parent);
604
+ const target = document.createElement("div");
605
+ parent.appendChild(target);
606
+
607
+ const layout = createWidgetHostLayout(target, {
608
+ launcher: {
609
+ mountMode: "docked",
610
+ autoExpand: true,
611
+ detachedPanel: true,
612
+ dock: { width: "320px", reveal: "emerge" },
613
+ },
614
+ });
615
+
616
+ const host = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="host"]');
617
+ // jsdom simplifies the calc expression (320px - 2 * 16px).
618
+ expect(host?.style.width).toBe("calc(288px)");
619
+
620
+ layout.destroy();
621
+ });
622
+
623
+ it("resolves a percentage emerge width to px before subtracting the inset", () => {
624
+ const parent = document.createElement("div");
625
+ document.body.appendChild(parent);
626
+ const target = document.createElement("div");
627
+ parent.appendChild(target);
628
+
629
+ const layout = createWidgetHostLayout(target, {
630
+ launcher: {
631
+ mountMode: "docked",
632
+ autoExpand: true,
633
+ detachedPanel: true,
634
+ dock: { width: "50%", reveal: "emerge" },
635
+ },
636
+ });
637
+ const shell = layout.shell!;
638
+ Object.defineProperty(shell, "clientWidth", { get: () => 800, configurable: true });
639
+ layout.updateConfig({
640
+ launcher: {
641
+ mountMode: "docked",
642
+ autoExpand: true,
643
+ detachedPanel: true,
644
+ dock: { width: "50%", reveal: "emerge" },
645
+ },
646
+ });
647
+
648
+ const host = shell.querySelector<HTMLElement>('[data-persona-dock-role="host"]');
649
+ // 50% of 800px resolved to px, then jsdom simplifies (400px - 2 * 16px).
650
+ expect(host?.style.width).toBe("calc(368px)");
651
+
652
+ layout.destroy();
653
+ });
654
+
655
+ it("clears detached chrome and pads nothing while the panel is collapsed", () => {
656
+ for (const reveal of ["resize", "emerge"] as const) {
657
+ const parent = document.createElement("div");
658
+ document.body.appendChild(parent);
659
+ const target = document.createElement("div");
660
+ parent.appendChild(target);
661
+
662
+ const layout = createWidgetHostLayout(target, {
663
+ launcher: {
664
+ mountMode: "docked",
665
+ autoExpand: false,
666
+ detachedPanel: true,
667
+ dock: { width: "320px", reveal },
668
+ },
669
+ });
670
+
671
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
672
+ // Collapsed: truly 0-width slot with no padding/background/box-sizing residue.
673
+ expect(dockSlot?.style.width, reveal).toBe("0px");
674
+ expect(dockSlot?.style.padding, reveal).toBe("");
675
+ expect(dockSlot?.style.background, reveal).toBe("");
676
+ expect(dockSlot?.style.boxSizing, reveal).toBe("");
677
+ // Padding rides the width transition so the canvas gutter shrinks with the column.
678
+ expect(dockSlot?.style.transition, reveal).toContain("padding");
679
+
680
+ // Opening restores the chrome; closing clears it again.
681
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
682
+ expect(dockSlot?.style.padding, reveal).toBe("16px");
683
+ expect(dockSlot?.style.boxSizing, reveal).toBe("border-box");
684
+ layout.syncWidgetState({ open: false, launcherEnabled: true });
685
+ expect(dockSlot?.style.padding, reveal).toBe("");
686
+ expect(dockSlot?.style.boxSizing, reveal).toBe("");
687
+
688
+ layout.destroy();
689
+ document.body.innerHTML = "";
690
+ }
691
+ });
692
+ });
530
693
  });
@@ -5,6 +5,12 @@ import type {
5
5
  } from "../types";
6
6
  import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
7
7
  import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
8
+ import { createThemeObserver, getActiveTheme } from "../utils/theme";
9
+ import {
10
+ DEFAULT_PANEL_CANVAS_BACKGROUND,
11
+ DEFAULT_PANEL_INSET,
12
+ resolveTokenValue,
13
+ } from "../utils/tokens";
8
14
 
9
15
  export type WidgetHostLayoutMode = "direct" | "docked";
10
16
 
@@ -208,6 +214,51 @@ const orderDockChildren = (
208
214
  }
209
215
  };
210
216
 
217
+ /**
218
+ * Detached inset + canvas literals for the dockSlot. The panel CSS vars are set
219
+ * inline on the widget root inside the slot, so they cannot cascade up to the
220
+ * slot; resolve the token values here and apply literals.
221
+ */
222
+ const resolveDetachedSlotStyles = (
223
+ config: AgentWidgetConfig | undefined
224
+ ): { inset: string; canvasBackground: string } => {
225
+ const theme = getActiveTheme(config);
226
+ const panel = theme.components?.panel;
227
+ const resolve = (raw: string | undefined, fallback: string): string => {
228
+ if (raw == null || raw === "") return fallback;
229
+ return resolveTokenValue(theme, raw) ?? raw;
230
+ };
231
+ return {
232
+ inset: resolve(panel?.inset, DEFAULT_PANEL_INSET),
233
+ canvasBackground: resolve(panel?.canvasBackground, DEFAULT_PANEL_CANVAS_BACKGROUND),
234
+ };
235
+ };
236
+
237
+ type DetachedSlotStyles = { inset: string; canvasBackground: string };
238
+
239
+ /**
240
+ * Detached docked mode pads the slot so the canvas shows around the panel card.
241
+ * Padding, never margin, keeps parseDockWidthToPx push math truthful; box-sizing
242
+ * border-box keeps the slot outer width (and max-height) inclusive of the
243
+ * padding so the panel shrinks inside rather than overflowing.
244
+ */
245
+ const applyDetachedDockSlotChrome = (
246
+ dockSlot: HTMLElement,
247
+ detached: boolean,
248
+ inset: string,
249
+ canvasBackground: string
250
+ ): void => {
251
+ if (!detached) {
252
+ dockSlot.style.padding = "";
253
+ dockSlot.style.background = "";
254
+ dockSlot.style.boxSizing = "";
255
+ return;
256
+ }
257
+ dockSlot.style.boxSizing = "border-box";
258
+ dockSlot.style.padding = inset;
259
+ dockSlot.style.background = canvasBackground;
260
+ };
261
+
211
262
  const applyDockStyles = (
212
263
  shell: HTMLElement,
213
264
  pushTrack: HTMLElement,
@@ -215,10 +266,16 @@ const applyDockStyles = (
215
266
  dockSlot: HTMLElement,
216
267
  host: HTMLElement,
217
268
  config: AgentWidgetConfig | undefined,
218
- expanded: boolean
269
+ expanded: boolean,
270
+ // Cached slot styles; null when not detached. Resolved once per config/scheme
271
+ // change so getActiveTheme never runs on the resize/ResizeObserver path.
272
+ detachedSlotStyles: DetachedSlotStyles | null
219
273
  ): void => {
220
274
  const dock = resolveDockConfig(config);
221
275
  const usePush = dock.reveal === "push";
276
+ const detached = detachedSlotStyles != null;
277
+ const detachedInset = detachedSlotStyles?.inset ?? "";
278
+ const detachedCanvas = detachedSlotStyles?.canvasBackground ?? "";
222
279
 
223
280
  migrateDockChildren(shell, pushTrack, contentSlot, dockSlot, usePush);
224
281
  orderDockChildren(shell, pushTrack, contentSlot, dockSlot, dock.side, usePush);
@@ -260,6 +317,8 @@ const applyDockStyles = (
260
317
  clearMobileFullscreenDockSlotStyles(dockSlot);
261
318
  resetContentSlotFlexSizing(contentSlot);
262
319
  clearEmergeDockStyles(host, dockSlot);
320
+ // Mobile fullscreen is flush regardless of detachedPanel.
321
+ applyDetachedDockSlotChrome(dockSlot, false, "", "");
263
322
 
264
323
  shell.style.display = "flex";
265
324
  shell.style.flexDirection = "column";
@@ -314,6 +373,9 @@ const applyDockStyles = (
314
373
  shell.removeAttribute("data-persona-dock-mobile-fullscreen");
315
374
  clearMobileFullscreenDockSlotStyles(dockSlot);
316
375
  applyDockSlotMaxHeight(dockSlot, dock.maxHeight);
376
+ // Chrome only when open: a collapsed detached slot with padding + border-box
377
+ // floors at 2*inset, painting a permanent canvas strip past the 0px width.
378
+ applyDetachedDockSlotChrome(dockSlot, detached && expanded, detachedInset, detachedCanvas);
317
379
 
318
380
  if (dock.reveal === "overlay") {
319
381
  shell.style.display = "flex";
@@ -435,8 +497,10 @@ const applyDockStyles = (
435
497
  }
436
498
 
437
499
  const width = expanded ? dock.width : "0px";
500
+ // Detached collapse snaps padding off as width animates to 0; animate padding
501
+ // alongside so the canvas gutter shrinks with the column.
438
502
  const dockTransition = dock.animate
439
- ? "width 180ms ease, min-width 180ms ease, max-width 180ms ease, flex-basis 180ms ease"
503
+ ? `width 180ms ease, min-width 180ms ease, max-width 180ms ease, flex-basis 180ms ease${detached ? ", padding 180ms ease" : ""}`
440
504
  : "none";
441
505
  const collapsedClosed = !expanded;
442
506
 
@@ -454,9 +518,17 @@ const applyDockStyles = (
454
518
 
455
519
  if (isEmerge) {
456
520
  dockSlot.style.alignItems = dock.side === "right" ? "flex-start" : "flex-end";
457
- host.style.width = dock.width;
458
- host.style.minWidth = dock.width;
459
- host.style.maxWidth = dock.width;
521
+ // Emerge pins the host to a fixed width and reveals it as the slot grows.
522
+ // When detached, shrink that fixed width by the padding so the host fits
523
+ // the padded slot content-box instead of overflowing one side. Resolve a
524
+ // percentage dock.width to px first: a raw % re-resolves against the
525
+ // padding-reduced content box and un-pins as the slot animates open.
526
+ const emergeHostWidth = detached
527
+ ? `calc(${parseDockWidthToPx(dock.width, shell.clientWidth)}px - (2 * ${detachedInset}))`
528
+ : dock.width;
529
+ host.style.width = emergeHostWidth;
530
+ host.style.minWidth = emergeHostWidth;
531
+ host.style.maxWidth = emergeHostWidth;
460
532
  host.style.boxSizing = "border-box";
461
533
  }
462
534
  }
@@ -518,12 +590,38 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
518
590
  resizeObserver = null;
519
591
  };
520
592
 
593
+ // Resolved once per config/scheme change; the resize paths reuse this so
594
+ // getActiveTheme never runs per frame.
595
+ let detachedSlotStyles: DetachedSlotStyles | null = null;
596
+ const refreshDetachedSlotStyles = (): void => {
597
+ detachedSlotStyles =
598
+ config?.launcher?.detachedPanel === true ? resolveDetachedSlotStyles(config) : null;
599
+ };
600
+ refreshDetachedSlotStyles();
601
+
602
+ const reapplyDockStyles = (): void => {
603
+ applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded, detachedSlotStyles);
604
+ };
605
+
606
+ // Scheme-dependent canvas tokens are baked to literals at layout time, so an
607
+ // OS dark-mode flip leaves a stale canvas until we recompute and re-apply.
608
+ let cleanupSchemeObserver: (() => void) | null = null;
609
+ const syncSchemeObserver = (): void => {
610
+ cleanupSchemeObserver?.();
611
+ cleanupSchemeObserver = null;
612
+ if (config?.launcher?.detachedPanel !== true || config?.colorScheme !== "auto") return;
613
+ cleanupSchemeObserver = createThemeObserver(() => {
614
+ refreshDetachedSlotStyles();
615
+ reapplyDockStyles();
616
+ });
617
+ };
618
+
521
619
  const syncPushResizeObserver = (): void => {
522
620
  disconnectResizeObserver();
523
621
  if (resolveDockConfig(config).reveal !== "push") return;
524
622
  if (typeof ResizeObserver === "undefined") return;
525
623
  resizeObserver = new ResizeObserver(() => {
526
- applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
624
+ reapplyDockStyles();
527
625
  });
528
626
  resizeObserver.observe(shell);
529
627
  };
@@ -531,7 +629,7 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
531
629
  let heightChainChecked = false;
532
630
 
533
631
  const layout = (): void => {
534
- applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
632
+ reapplyDockStyles();
535
633
  syncPushResizeObserver();
536
634
  // Check the height chain once, the first time the panel is actually shown
537
635
  // in a layout that depends on it (mobile fullscreen is fixed-position and
@@ -562,6 +660,7 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
562
660
  }
563
661
 
564
662
  layout();
663
+ syncSchemeObserver();
565
664
 
566
665
  return {
567
666
  mode: "docked",
@@ -578,10 +677,14 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
578
677
  if ((config?.launcher?.enabled ?? true) === false) {
579
678
  expanded = true;
580
679
  }
680
+ refreshDetachedSlotStyles();
581
681
  layout();
682
+ syncSchemeObserver();
582
683
  },
583
684
  destroy() {
584
685
  ownerWindow?.removeEventListener("resize", onViewportResize);
686
+ cleanupSchemeObserver?.();
687
+ cleanupSchemeObserver = null;
585
688
  disconnectResizeObserver();
586
689
  if (originalParent.isConnected) {
587
690
  if (originalNextSibling && originalNextSibling.parentNode === originalParent) {
@@ -19,71 +19,28 @@ const ensureTarget = (target: string | HTMLElement): HTMLElement => {
19
19
  return target;
20
20
  };
21
21
 
22
- const widgetCssHref = (): string | null => {
23
- try {
24
- // This works in ESM builds but not in IIFE builds
25
- if (typeof import.meta !== "undefined" && import.meta.url) {
26
- return new URL("../widget.css", import.meta.url).href;
27
- }
28
- } catch {
29
- // Fallback for IIFE builds where CSS should be loaded separately
30
- }
31
- return null;
32
- };
33
-
22
+ // CSS arrives via the installer's link[data-persona] or a consumer import of
23
+ // @runtypelabs/persona/widget.css; the widget never self-locates its stylesheet.
24
+ // A new URL(..., import.meta.url) here breaks consumer bundlers (webpack
25
+ // resolves the literal at build time), so shadow roots only clone the head link.
34
26
  const mountStyles = (root: ShadowRoot | HTMLElement, ownerDocument: Document) => {
35
- const href = widgetCssHref();
36
-
37
- const adoptExistingStylesheet = () => {
38
- if (!(root instanceof ShadowRoot)) {
39
- return;
40
- }
41
-
42
- if (root.querySelector('link[data-persona]')) {
43
- return;
44
- }
45
-
46
- const globalLink = ownerDocument.head.querySelector<HTMLLinkElement>(
47
- 'link[data-persona]'
48
- );
49
- if (!globalLink) {
50
- return;
51
- }
27
+ if (!(root instanceof ShadowRoot)) {
28
+ return;
29
+ }
52
30
 
53
- const clonedLink = globalLink.cloneNode(true) as HTMLLinkElement;
54
- root.insertBefore(clonedLink, root.firstChild);
55
- };
31
+ if (root.querySelector('link[data-persona]')) {
32
+ return;
33
+ }
56
34
 
57
- if (root instanceof ShadowRoot) {
58
- // For shadow DOM, we need to load CSS into the shadow root
59
- if (href) {
60
- const link = ownerDocument.createElement("link");
61
- link.rel = "stylesheet";
62
- link.href = href;
63
- link.setAttribute("data-persona", "true");
64
- root.insertBefore(link, root.firstChild);
65
- } else {
66
- adoptExistingStylesheet();
67
- }
68
- // If href is null (IIFE build), CSS should already be loaded globally
69
- } else {
70
- // For non-shadow DOM, check if CSS is already loaded
71
- const existing = ownerDocument.head.querySelector<HTMLLinkElement>(
72
- "link[data-persona]"
73
- );
74
- if (!existing) {
75
- if (href) {
76
- // ESM build - load CSS dynamically
77
- const link = ownerDocument.createElement("link");
78
- link.rel = "stylesheet";
79
- link.href = href;
80
- link.setAttribute("data-persona", "true");
81
- ownerDocument.head.appendChild(link);
82
- }
83
- // IIFE build - CSS should be loaded via <link> tag before script
84
- // If not found, we'll assume it's loaded globally or warn in dev
85
- }
35
+ const globalLink = ownerDocument.head.querySelector<HTMLLinkElement>(
36
+ 'link[data-persona]'
37
+ );
38
+ if (!globalLink) {
39
+ return;
86
40
  }
41
+
42
+ const clonedLink = globalLink.cloneNode(true) as HTMLLinkElement;
43
+ root.insertBefore(clonedLink, root.firstChild);
87
44
  };
88
45
 
89
46
  export type AgentWidgetInitHandle = AgentWidgetController & { host: HTMLElement };
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AgentWidgetSession } from "./session";
3
+ import type {
4
+ AgentWidgetMessage,
5
+ AgentWidgetRequestPayload,
6
+ ContentPart,
7
+ } from "./types";
8
+ import type { MentionSubmitBundle } from "./context-mentions-entry";
9
+
10
+ /** A closed, empty SSE stream so `dispatch` completes with no events. */
11
+ function closedStream(): ReadableStream<Uint8Array> {
12
+ return new ReadableStream({ start: (c) => c.close() });
13
+ }
14
+
15
+ /**
16
+ * Regression coverage for the mention/command submit path:
17
+ * - an empty-text, chip-only submit must NOT render the "[Image]" fallback
18
+ * (that fallback is for image-only turns);
19
+ * - a `command: "server"` mention's `resolve().context` must ride to the
20
+ * backend under request `context.mentions` (the c1 dispatch channel).
21
+ */
22
+ describe("AgentWidgetSession — mention/command submit", () => {
23
+ function makeSession(captured: { payload?: AgentWidgetRequestPayload }) {
24
+ let messages: AgentWidgetMessage[] = [];
25
+ let status = "idle";
26
+ let streaming = false;
27
+ const session = new AgentWidgetSession(
28
+ {
29
+ apiUrl: "http://localhost:8000",
30
+ customFetch: async (_url, _init, payload) => {
31
+ captured.payload = payload;
32
+ return { ok: true, body: closedStream() } as unknown as Response;
33
+ },
34
+ },
35
+ {
36
+ onMessagesChanged: (m) => {
37
+ messages = m;
38
+ },
39
+ onStatusChanged: (s) => {
40
+ status = s;
41
+ },
42
+ onStreamingChanged: (s) => {
43
+ streaming = s;
44
+ },
45
+ onError: () => {},
46
+ }
47
+ );
48
+ return {
49
+ session,
50
+ user: () => messages.find((m) => m.role === "user"),
51
+ status: () => status,
52
+ streaming: () => streaming,
53
+ };
54
+ }
55
+
56
+ it("renders no [Image] fallback for a command-only submit and forwards context.mentions", async () => {
57
+ const captured: { payload?: AgentWidgetRequestPayload } = {};
58
+ const { session, user } = makeSession(captured);
59
+
60
+ // A server skill: `/lookup 1042` resolved to structured context, namespaced
61
+ // `{ [sourceId]: { [itemId]: context } }` exactly as the manager builds it.
62
+ const context = { commands: { lookup: { intent: "lookup-order", orderId: "1042" } } };
63
+ const refs = [{ sourceId: "commands", itemId: "lookup", label: "lookup" }];
64
+ const bundle: MentionSubmitBundle = {
65
+ blocks: [],
66
+ contentParts: [],
67
+ context,
68
+ };
69
+
70
+ await session.sendMessage("", {
71
+ mentions: {
72
+ refs,
73
+ finalize: async () => bundle,
74
+ },
75
+ });
76
+
77
+ const msg = user();
78
+ expect(msg).toBeDefined();
79
+ // The bug: an empty-text, mention-only submit used to display "[Image]".
80
+ expect(msg!.content).toBe("");
81
+ expect(msg!.content).not.toBe("[Image]");
82
+ // The command chip echoes immediately as a ref.
83
+ expect(msg!.contextMentions).toEqual(refs);
84
+ // Structured server-command data is merged onto the message…
85
+ expect(msg!.mentionContext).toEqual(context);
86
+ // …and rides to the backend under request `context.mentions`.
87
+ expect(captured.payload?.context).toMatchObject({ mentions: context });
88
+ });
89
+
90
+ it("merges resolved @-mention llmContent into the dispatched snapshot", async () => {
91
+ const captured: { payload?: AgentWidgetRequestPayload } = {};
92
+ const { session } = makeSession(captured);
93
+
94
+ await session.sendMessage("summarize this", {
95
+ mentions: {
96
+ refs: [{ sourceId: "files", itemId: "app", label: "App.tsx" }],
97
+ finalize: async (): Promise<MentionSubmitBundle> => ({
98
+ blocks: ["```App.tsx\nFILE BODY\n```"],
99
+ contentParts: [],
100
+ context: {},
101
+ }),
102
+ },
103
+ });
104
+
105
+ const dispatched = (captured.payload?.messages ?? []).find(
106
+ (m) => m.role === "user"
107
+ );
108
+ // The client collapses `llmContent` into the payload `content` field via the
109
+ // content-priority chain, so the model must see the resolved file body
110
+ // appended to the typed prose — not just the raw "summarize this".
111
+ const sent = dispatched?.content;
112
+ expect(typeof sent).toBe("string");
113
+ expect(sent as string).toContain("FILE BODY");
114
+ expect(sent as string).toContain("summarize this");
115
+ // Mention blocks lead; the typed prose lands last (joined by a blank line).
116
+ expect(sent as string).toBe("```App.tsx\nFILE BODY\n```\n\nsummarize this");
117
+ });
118
+
119
+ it("still renders the [Image] fallback for an image-only submit", async () => {
120
+ const captured: { payload?: AgentWidgetRequestPayload } = {};
121
+ const { session, user } = makeSession(captured);
122
+
123
+ const contentParts: ContentPart[] = [
124
+ { type: "image", image: "data:image/png;base64,abc123", mimeType: "image/png" },
125
+ ];
126
+ await session.sendMessage("", { contentParts });
127
+
128
+ expect(user()!.content).toBe("[Image]");
129
+ });
130
+
131
+ it("cancel() during a pending mention finalize aborts the turn without dispatching", async () => {
132
+ const captured: { payload?: AgentWidgetRequestPayload } = {};
133
+ const { session, status, streaming } = makeSession(captured);
134
+
135
+ let releaseFinalize!: (bundle: MentionSubmitBundle) => void;
136
+ const pending = new Promise<MentionSubmitBundle>((resolve) => {
137
+ releaseFinalize = resolve;
138
+ });
139
+
140
+ const sent = session.sendMessage("summarize this", {
141
+ mentions: {
142
+ refs: [{ sourceId: "files", itemId: "app", label: "App.tsx" }],
143
+ finalize: () => pending,
144
+ },
145
+ });
146
+
147
+ // Stop mid-resolution, then let finalize settle: dispatch must not fire.
148
+ session.cancel();
149
+ releaseFinalize({ blocks: ["```App.tsx\nBODY\n```"], contentParts: [], context: {} });
150
+ await sent;
151
+
152
+ expect(captured.payload).toBeUndefined();
153
+ expect(status()).toBe("idle");
154
+ expect(streaming()).toBe(false);
155
+ });
156
+
157
+ it("dispatches a normal mention submit once finalize resolves", async () => {
158
+ const captured: { payload?: AgentWidgetRequestPayload } = {};
159
+ const { session, streaming } = makeSession(captured);
160
+
161
+ await session.sendMessage("summarize this", {
162
+ mentions: {
163
+ refs: [{ sourceId: "files", itemId: "app", label: "App.tsx" }],
164
+ finalize: async (): Promise<MentionSubmitBundle> => ({
165
+ blocks: ["```App.tsx\nBODY\n```"],
166
+ contentParts: [],
167
+ context: {},
168
+ }),
169
+ },
170
+ });
171
+
172
+ expect(captured.payload).toBeDefined();
173
+ expect(streaming()).toBe(false);
174
+ });
175
+ });