@runtypelabs/persona 4.7.0 → 4.8.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 (117) hide show
  1. package/dist/animations/glyph-cycle.cjs +2 -2
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/glyph-cycle.js +2 -2
  5. package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
  6. package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
  7. package/dist/animations/wipe.cjs +2 -2
  8. package/dist/animations/wipe.d.cts +1 -1
  9. package/dist/animations/wipe.d.ts +1 -1
  10. package/dist/chunk-5EIIHQLQ.js +1 -0
  11. package/dist/codegen.cjs +12 -12
  12. package/dist/codegen.js +14 -14
  13. package/dist/index.cjs +91 -68
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +877 -172
  16. package/dist/index.d.ts +877 -172
  17. package/dist/index.global.js +81 -57
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +91 -68
  20. package/dist/index.js.map +1 -1
  21. package/dist/install.global.js +1 -1
  22. package/dist/install.global.js.map +1 -1
  23. package/dist/launcher.global.js +3 -2
  24. package/dist/launcher.global.js.map +1 -1
  25. package/dist/markdown-parsers.js +24 -24
  26. package/dist/plugin-kit.cjs +1 -1
  27. package/dist/plugin-kit.js +1 -1
  28. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  29. package/dist/runtype-tts.js +1 -1
  30. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  31. package/dist/smart-dom-reader.cjs +17 -17
  32. package/dist/smart-dom-reader.d.cts +753 -15
  33. package/dist/smart-dom-reader.d.ts +753 -15
  34. package/dist/smart-dom-reader.js +17 -17
  35. package/dist/testing.cjs +3 -3
  36. package/dist/testing.js +3 -3
  37. package/dist/theme-editor-preview.cjs +81 -58
  38. package/dist/theme-editor-preview.d.cts +761 -15
  39. package/dist/theme-editor-preview.d.ts +761 -15
  40. package/dist/theme-editor-preview.js +81 -58
  41. package/dist/theme-editor.cjs +6 -6
  42. package/dist/theme-editor.d.cts +753 -15
  43. package/dist/theme-editor.d.ts +753 -15
  44. package/dist/theme-editor.js +10 -10
  45. package/dist/theme-reference.cjs +1 -1
  46. package/dist/theme-reference.d.cts +74 -0
  47. package/dist/theme-reference.d.ts +74 -0
  48. package/dist/theme-reference.js +1 -1
  49. package/dist/voice-worklet-player.cjs +2 -2
  50. package/dist/voice-worklet-player.js +2 -2
  51. package/dist/webmcp-polyfill.js +2 -2
  52. package/dist/widget.css +1 -1
  53. package/package.json +2 -3
  54. package/src/artifacts-session.test.ts +178 -0
  55. package/src/client.test.ts +186 -1
  56. package/src/client.ts +92 -9
  57. package/src/components/artifact-card.test.ts +333 -0
  58. package/src/components/artifact-card.ts +75 -28
  59. package/src/components/artifact-inline.test.ts +1328 -0
  60. package/src/components/artifact-inline.ts +920 -0
  61. package/src/components/artifact-pane.test.ts +1042 -0
  62. package/src/components/artifact-pane.ts +440 -131
  63. package/src/components/artifact-preview.test.ts +1155 -0
  64. package/src/components/artifact-preview.ts +994 -0
  65. package/src/components/pill-composer-builder.test.ts +6 -2
  66. package/src/components/pill-composer-builder.ts +6 -6
  67. package/src/components/reasoning-bubble.ts +1 -13
  68. package/src/components/registry.ts +38 -3
  69. package/src/components/tool-bubble.ts +1 -13
  70. package/src/defaults.ts +1 -0
  71. package/src/generated/runtype-openapi-contract.ts +55 -3
  72. package/src/index-core.ts +20 -1
  73. package/src/index.ts +8 -0
  74. package/src/markdown-parsers-loader.test.ts +158 -0
  75. package/src/markdown-parsers-loader.ts +74 -9
  76. package/src/runtime/host-layout.test.ts +163 -0
  77. package/src/runtime/host-layout.ts +110 -7
  78. package/src/runtime/init.ts +18 -61
  79. package/src/session.ts +76 -22
  80. package/src/styles/widget.css +773 -26
  81. package/src/theme-editor/preview.ts +2 -0
  82. package/src/theme-editor/sections.test.ts +26 -1
  83. package/src/theme-editor/sections.ts +10 -2
  84. package/src/theme-reference.ts +2 -2
  85. package/src/tool-call-display-defaults.test.ts +1 -0
  86. package/src/types/theme.ts +77 -0
  87. package/src/types.ts +516 -17
  88. package/src/ui.artifact-pane-gating.test.ts +636 -0
  89. package/src/ui.component-directive.test.ts +104 -0
  90. package/src/ui.composer-bar.test.ts +60 -2
  91. package/src/ui.detached-panel.test.ts +1049 -0
  92. package/src/ui.tool-display.test.ts +51 -0
  93. package/src/ui.ts +698 -109
  94. package/src/utils/artifact-custom-actions.ts +128 -0
  95. package/src/utils/artifact-display.test.ts +42 -0
  96. package/src/utils/artifact-display.ts +84 -0
  97. package/src/utils/artifact-file.test.ts +116 -0
  98. package/src/utils/artifact-file.ts +117 -0
  99. package/src/utils/artifact-gate.test.ts +112 -5
  100. package/src/utils/artifact-gate.ts +39 -14
  101. package/src/utils/artifact-loading-status.ts +55 -0
  102. package/src/utils/artifact-status-label.ts +190 -0
  103. package/src/utils/buttons.ts +7 -1
  104. package/src/utils/code-highlight.test.ts +186 -0
  105. package/src/utils/code-highlight.ts +400 -0
  106. package/src/utils/icons.ts +2 -0
  107. package/src/utils/roving-tablist.test.ts +152 -0
  108. package/src/utils/roving-tablist.ts +111 -0
  109. package/src/utils/spinner.ts +45 -0
  110. package/src/utils/theme.test.ts +48 -0
  111. package/src/utils/theme.ts +7 -0
  112. package/src/utils/tokens.ts +91 -0
  113. package/src/utils/tool-loading-animation.test.ts +32 -0
  114. package/src/utils/tool-loading-animation.ts +24 -0
  115. package/dist/chunk-DFBSCFYN.js +0 -1
  116. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  117. 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 };
package/src/session.ts CHANGED
@@ -28,6 +28,10 @@ import {
28
28
  generateAssistantMessageId
29
29
  } from "./utils/message-id";
30
30
  import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "./utils/content";
31
+ import {
32
+ buildArtifactRefRawContent,
33
+ resolveArtifactDisplayMode
34
+ } from "./utils/artifact-display";
31
35
  import type {
32
36
  VoiceProvider,
33
37
  VoiceStatus,
@@ -2607,33 +2611,82 @@ export class AgentWidgetSession {
2607
2611
  const id =
2608
2612
  manual.id ||
2609
2613
  `art_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
2610
- if (manual.artifactType === "markdown") {
2611
- const rec: PersonaArtifactRecord = {
2612
- id,
2613
- artifactType: "markdown",
2614
- title: manual.title,
2615
- status: "complete",
2616
- markdown: manual.content
2617
- };
2618
- this.artifacts.set(id, rec);
2619
- this.selectedArtifactId = id;
2620
- this.emitArtifactsState();
2621
- return rec;
2622
- }
2623
- const rec: PersonaArtifactRecord = {
2624
- id,
2625
- artifactType: "component",
2626
- title: manual.title,
2627
- status: "complete",
2628
- component: manual.component,
2629
- props: manual.props ?? {}
2630
- };
2614
+ const rec: PersonaArtifactRecord =
2615
+ manual.artifactType === "markdown"
2616
+ ? {
2617
+ id,
2618
+ artifactType: "markdown",
2619
+ title: manual.title,
2620
+ status: "complete",
2621
+ markdown: manual.content,
2622
+ ...(manual.file ? { file: manual.file } : {})
2623
+ }
2624
+ : {
2625
+ id,
2626
+ artifactType: "component",
2627
+ title: manual.title,
2628
+ status: "complete",
2629
+ component: manual.component,
2630
+ props: manual.props ?? {}
2631
+ };
2631
2632
  this.artifacts.set(id, rec);
2632
2633
  this.selectedArtifactId = id;
2633
2634
  this.emitArtifactsState();
2635
+ if (manual.transcript !== false) {
2636
+ this.injectArtifactRefBlock(rec);
2637
+ }
2634
2638
  return rec;
2635
2639
  }
2636
2640
 
2641
+ /**
2642
+ * Injects (or refreshes) the in-thread artifact block for a programmatically
2643
+ * upserted artifact, matching the streamed UX: the resolved display mode
2644
+ * picks the component ("card"/"panel" → reference card, "inline" → inline
2645
+ * preview).
2646
+ *
2647
+ * Unlike the streamed path (which embeds content on `artifact_complete`),
2648
+ * the programmatic record is complete up front, so the final markdown /
2649
+ * file meta / component name + props are embedded immediately and the block
2650
+ * hydrates after a refresh without the original registry state.
2651
+ *
2652
+ * A re-upsert of the same id rebuilds the existing block's persisted
2653
+ * rawContent in place (the registry is not persisted, so hydration reads
2654
+ * these props); without this the block would keep the first version's
2655
+ * content after a refresh.
2656
+ */
2657
+ private injectArtifactRefBlock(rec: PersonaArtifactRecord): void {
2658
+ const refId = `artifact-ref-${rec.id}`;
2659
+ const displayMode = resolveArtifactDisplayMode(
2660
+ this.config.features?.artifacts,
2661
+ rec.artifactType
2662
+ );
2663
+ const rawContent = buildArtifactRefRawContent(displayMode, {
2664
+ artifactId: rec.id,
2665
+ title: rec.title,
2666
+ artifactType: rec.artifactType,
2667
+ status: "complete",
2668
+ ...(rec.file ? { file: rec.file } : {}),
2669
+ ...(rec.component ? { component: rec.component } : {}),
2670
+ ...(rec.props ? { componentProps: rec.props } : {}),
2671
+ ...(rec.markdown !== undefined ? { markdown: rec.markdown } : {})
2672
+ });
2673
+ // Re-upsert: mutate the STORED message (inject stores a copy via
2674
+ // ensureSequence, so a local copy would be lost) and notify so the
2675
+ // transcript re-renders and persistence picks up the new version.
2676
+ const existing = this.messages.find((m) => m.id === refId);
2677
+ if (existing) {
2678
+ existing.rawContent = rawContent;
2679
+ existing.streaming = false;
2680
+ this.callbacks.onMessagesChanged([...this.messages]);
2681
+ return;
2682
+ }
2683
+ this.injectAssistantMessage({
2684
+ id: refId,
2685
+ content: "",
2686
+ rawContent
2687
+ });
2688
+ }
2689
+
2637
2690
  private clearArtifactState(): void {
2638
2691
  if (this.artifacts.size === 0 && this.selectedArtifactId === null) return;
2639
2692
  this.artifacts.clear();
@@ -2657,7 +2710,8 @@ export class AgentWidgetSession {
2657
2710
  artifactType: "markdown",
2658
2711
  title: ev.title,
2659
2712
  status: "streaming",
2660
- markdown: ""
2713
+ markdown: "",
2714
+ ...(ev.file ? { file: ev.file } : {})
2661
2715
  });
2662
2716
  } else {
2663
2717
  this.artifacts.set(ev.id, {