@runtypelabs/persona 4.17.1 → 4.18.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 (158) hide show
  1. package/dist/chunk-3I2S3I5P.js +1 -0
  2. package/dist/chunk-76IELU7F.js +1 -0
  3. package/dist/chunk-EFJBUH5F.js +1 -0
  4. package/dist/chunk-PDPVORUF.js +1 -0
  5. package/dist/chunk-Q735PSZL.js +1 -0
  6. package/dist/chunk-VK6JOIYJ.js +1 -0
  7. package/dist/codegen.cjs +4 -4
  8. package/dist/codegen.js +4 -4
  9. package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
  10. package/dist/context-mentions.cjs +9 -8
  11. package/dist/context-mentions.d.cts +1212 -14
  12. package/dist/context-mentions.d.ts +1212 -14
  13. package/dist/context-mentions.js +9 -8
  14. package/dist/event-stream-view-4EZ7BGFK.js +1 -0
  15. package/dist/event-stream-view.cjs +129 -0
  16. package/dist/event-stream-view.d.cts +8384 -0
  17. package/dist/event-stream-view.d.ts +8384 -0
  18. package/dist/event-stream-view.js +129 -0
  19. package/dist/history-view-BD6OXLNA.js +898 -0
  20. package/dist/history-view.cjs +898 -0
  21. package/dist/history-view.d.cts +1461 -0
  22. package/dist/history-view.d.ts +1461 -0
  23. package/dist/history-view.js +898 -0
  24. package/dist/index.cjs +77 -77
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1987 -17
  27. package/dist/index.d.ts +1987 -17
  28. package/dist/index.global.js +67 -66
  29. package/dist/index.global.js.map +1 -1
  30. package/dist/index.js +77 -77
  31. package/dist/index.js.map +1 -1
  32. package/dist/launcher.global.js +5 -4
  33. package/dist/launcher.global.js.map +1 -1
  34. package/dist/plugin-kit.cjs +1 -1
  35. package/dist/plugin-kit.d.cts +0 -8
  36. package/dist/plugin-kit.d.ts +0 -8
  37. package/dist/plugin-kit.js +1 -1
  38. package/dist/smart-dom-reader.d.cts +1212 -14
  39. package/dist/smart-dom-reader.d.ts +1212 -14
  40. package/dist/theme-editor-preview.cjs +970 -73
  41. package/dist/theme-editor-preview.d.cts +1331 -15
  42. package/dist/theme-editor-preview.d.ts +1331 -15
  43. package/dist/theme-editor-preview.js +81 -81
  44. package/dist/theme-editor.cjs +7 -7
  45. package/dist/theme-editor.d.cts +1215 -15
  46. package/dist/theme-editor.d.ts +1215 -15
  47. package/dist/theme-editor.js +9 -9
  48. package/dist/theme-reference.cjs +1 -1
  49. package/dist/theme-reference.d.cts +226 -7
  50. package/dist/theme-reference.d.ts +226 -7
  51. package/dist/theme-reference.js +1 -1
  52. package/dist/widget.css +1 -1
  53. package/package.json +16 -4
  54. package/src/client-history-rest.test.ts +1034 -0
  55. package/src/client-visitor-history.test.ts +596 -0
  56. package/src/client.ts +1040 -24
  57. package/src/components/artifact-inline.ts +8 -1
  58. package/src/components/artifact-pane.test.ts +278 -1
  59. package/src/components/artifact-pane.ts +135 -67
  60. package/src/components/artifact-preview.test.ts +14 -1
  61. package/src/components/artifact-preview.ts +9 -0
  62. package/src/components/header-builder.ts +3 -0
  63. package/src/components/header-layouts.test.ts +171 -7
  64. package/src/components/header-layouts.ts +83 -53
  65. package/src/components/header-parts.test.ts +154 -1
  66. package/src/components/header-parts.ts +266 -138
  67. package/src/components/history-confirm-dialog.ts +177 -0
  68. package/src/components/history-shell-copy.ts +99 -0
  69. package/src/components/history-view/announcer.ts +40 -0
  70. package/src/components/history-view/copy.ts +105 -0
  71. package/src/components/history-view/css.ts +1078 -0
  72. package/src/components/history-view/grouping.test.ts +142 -0
  73. package/src/components/history-view/grouping.ts +142 -0
  74. package/src/components/history-view/icons.ts +73 -0
  75. package/src/components/history-view/rows.ts +341 -0
  76. package/src/components/history-view/state.ts +309 -0
  77. package/src/components/history-view.test.ts +2660 -0
  78. package/src/components/history-view.ts +1891 -0
  79. package/src/components/launcher-surface.test.ts +133 -0
  80. package/src/components/launcher.test.ts +18 -0
  81. package/src/components/launcher.ts +7 -7
  82. package/src/components/panel.ts +17 -7
  83. package/src/components/welcome.ts +72 -2
  84. package/src/defaults.ts +36 -3
  85. package/src/event-stream-view-bundle.test.ts +69 -0
  86. package/src/event-stream-view-entry.ts +19 -0
  87. package/src/event-stream-view-loader.ts +31 -0
  88. package/src/event-stream-view.ts +19 -0
  89. package/src/generated/runtype-openapi-contract.ts +1 -0
  90. package/src/history-render.ts +325 -0
  91. package/src/history-view-bundle.test.ts +78 -0
  92. package/src/history-view-entry.ts +41 -0
  93. package/src/history-view-loader.test.ts +30 -0
  94. package/src/history-view-loader.ts +32 -0
  95. package/src/history-view.ts +28 -0
  96. package/src/index-core.ts +50 -2
  97. package/src/index-global.ts +55 -0
  98. package/src/internal/demo-history-provider.test.ts +402 -0
  99. package/src/internal/demo-history-provider.ts +689 -0
  100. package/src/internal/history-provider-registry.ts +25 -0
  101. package/src/internal/history-provider.ts +150 -0
  102. package/src/internal/runtype-history-provider.test.ts +313 -0
  103. package/src/internal/runtype-history-provider.ts +240 -0
  104. package/src/plugin-kit.test.ts +16 -0
  105. package/src/plugin-kit.ts +13 -5
  106. package/src/plugins/types.ts +177 -1
  107. package/src/session-history.test.ts +1397 -0
  108. package/src/session.ts +1200 -26
  109. package/src/styles/widget-styles.test.ts +133 -0
  110. package/src/styles/widget.css +227 -8
  111. package/src/theme-editor/preview-utils.test.ts +19 -0
  112. package/src/theme-editor/preview-utils.ts +15 -1
  113. package/src/theme-editor/preview.ts +7 -1
  114. package/src/theme-editor/role-mappings.ts +4 -1
  115. package/src/theme-editor/sections.test.ts +168 -1
  116. package/src/theme-editor/sections.ts +136 -7
  117. package/src/theme-editor/webmcp/summary.ts +16 -2
  118. package/src/theme-reference.ts +20 -6
  119. package/src/types/theme.ts +213 -7
  120. package/src/types.ts +927 -7
  121. package/src/ui.artifact-rail-resize.test.ts +235 -0
  122. package/src/ui.attachments-drop.test.ts +1 -0
  123. package/src/ui.header-update-stability.test.ts +2 -2
  124. package/src/ui.history-chunk.test.ts +72 -0
  125. package/src/ui.history-config-provider.test.ts +318 -0
  126. package/src/ui.history-controller.test.ts +711 -0
  127. package/src/ui.history-lifecycle.test.ts +456 -0
  128. package/src/ui.history-render-hooks.test.ts +904 -0
  129. package/src/ui.history-shell.test.ts +1887 -0
  130. package/src/ui.rail-overlay.test.ts +573 -0
  131. package/src/ui.rail-resize.test.ts +284 -0
  132. package/src/ui.rebuild-composer.test.ts +21 -0
  133. package/src/ui.render-welcome.test.ts +42 -0
  134. package/src/ui.shortcuts.test.ts +166 -0
  135. package/src/ui.ts +3071 -240
  136. package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
  137. package/src/utils/artifact-file.ts +15 -0
  138. package/src/utils/artifact-gate.test.ts +4 -0
  139. package/src/utils/artifact-gate.ts +13 -17
  140. package/src/utils/artifact-resize.test.ts +28 -0
  141. package/src/utils/artifact-resize.ts +9 -2
  142. package/src/utils/buttons.ts +188 -1
  143. package/src/utils/config-merge.test.ts +91 -0
  144. package/src/utils/dropdown.ts +10 -1
  145. package/src/utils/history-messages.test.ts +284 -0
  146. package/src/utils/history-messages.ts +284 -0
  147. package/src/utils/icons.ts +3 -0
  148. package/src/utils/shortcuts.test.ts +288 -0
  149. package/src/utils/shortcuts.ts +221 -0
  150. package/src/utils/theme.test.ts +501 -4
  151. package/src/utils/theme.ts +42 -3
  152. package/src/utils/tokens.ts +153 -15
  153. package/src/utils/tooltip-timing.ts +4 -0
  154. package/src/utils/tooltip.test.ts +249 -7
  155. package/src/utils/tooltip.ts +146 -4
  156. package/src/utils/visitor-store.test.ts +425 -0
  157. package/src/utils/visitor-store.ts +337 -0
  158. package/dist/chunk-QJ4LV2KW.js +0 -1
@@ -228,6 +228,71 @@ describe("composer spacing styles", () => {
228
228
  });
229
229
  });
230
230
 
231
+ describe("header control styles", () => {
232
+ it("sizes and strokes the glyph from the header control tokens", () => {
233
+ const start = widgetCss.indexOf(".persona-header-control--glyph > svg {");
234
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
235
+
236
+ expect(start).toBeGreaterThan(-1);
237
+ expect(rule).toContain(
238
+ "width: var(--persona-header-control-icon-size, 20px)",
239
+ );
240
+ // CSS presentation beats the SVG stroke-width attribute, which stays as
241
+ // the no-CSS fallback.
242
+ expect(rule).toContain(
243
+ "stroke-width: var(--persona-components-header-controlStrokeWidth, 1.5)",
244
+ );
245
+ });
246
+
247
+ it("thins the sparse glyph stroke by the same 0.7 the JS attributes use", () => {
248
+ const start = widgetCss.indexOf(".persona-header-control--sparse > svg {");
249
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
250
+
251
+ expect(start).toBeGreaterThan(-1);
252
+ expect(rule).toContain(
253
+ "width: calc(var(--persona-header-control-icon-size, 20px) * 1.4)",
254
+ );
255
+ // 0.7 x the 1.5 default is 1.05, the stroke the close X has always
256
+ // rendered at; SPARSE_GLYPHS in header-parts.ts carries the same pair.
257
+ expect(rule).toContain(
258
+ "stroke-width: calc(var(--persona-components-header-controlStrokeWidth, 1.5) * 0.7)",
259
+ );
260
+
261
+ // The sparse rule must stay after the glyph rule: same specificity, so
262
+ // source order is the only thing that lets it win.
263
+ expect(start).toBeGreaterThan(
264
+ widgetCss.indexOf(".persona-header-control--glyph > svg {"),
265
+ );
266
+ });
267
+ });
268
+
269
+ describe("launcher icon styles", () => {
270
+ it("strokes every launcher glyph from the stroke token, surface-neutrally", () => {
271
+ // Locate the rule by its declaration and read the SHIPPED selector back
272
+ // out of the file, so a rescope cannot slip past a hardcoded expectation.
273
+ const declaration =
274
+ "stroke-width: var(--persona-components-launcher-iconStrokeWidth, 1.5)";
275
+ const declarationAt = widgetCss.indexOf(declaration);
276
+ expect(declarationAt).toBeGreaterThan(-1);
277
+ const selectorStart = widgetCss.lastIndexOf("}", declarationAt) + 1;
278
+ const selector = widgetCss
279
+ .slice(selectorStart, widgetCss.indexOf("{", selectorStart))
280
+ .replace(/\/\*[\s\S]*?\*\//g, "")
281
+ .trim();
282
+
283
+ // One pill, one line weight: the agent icon and the call-to-action arrow
284
+ // both follow the token.
285
+ expect(selector).toContain('[data-role="launcher-icon"] > svg');
286
+ expect(selector).toContain(
287
+ '[data-role="launcher-call-to-action-icon"] > svg',
288
+ );
289
+ // Both launcher surfaces render under [data-persona-root]; naming either
290
+ // surface here would change stroke weight between the two bundles.
291
+ expect(selector).not.toContain("data-persona-launcher-critical");
292
+ expect(selector).not.toContain("persona-widget-container");
293
+ });
294
+ });
295
+
231
296
  describe("transcript layout styles", () => {
232
297
  it("drops the empty messages wrapper from the flex flow", () => {
233
298
  // A zero-height flex child still triggers the body's gap, so an empty
@@ -332,6 +397,21 @@ describe("plugin welcome styles", () => {
332
397
  ".persona-widget-body.persona-welcome-overlay-active {\n overflow: hidden;\n}",
333
398
  );
334
399
  });
400
+
401
+ it("takes the transcript out of layout so nothing scrolls under the overlay", () => {
402
+ // The rule above cannot win against the inline `overflow-y: auto` that
403
+ // fill/fullscreen layouts stamp on the body, so the transcript stayed
404
+ // scrollable underneath an `inset: 0` box whose containing block scrolls
405
+ // with it: home and the transcript then read as one column, with no
406
+ // composer, and scrolling past the overlay left the transcript bare.
407
+ const selector =
408
+ ".persona-widget-body.persona-welcome-overlay-active\n > :not([data-persona-welcome-overlay]) {";
409
+ const start = widgetCss.indexOf(selector);
410
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
411
+
412
+ expect(start).toBeGreaterThan(-1);
413
+ expect(rule).toContain("display: none !important");
414
+ });
335
415
  });
336
416
 
337
417
  describe("scrollbar policy styles", () => {
@@ -380,3 +460,56 @@ describe("scrollbar policy styles", () => {
380
460
  );
381
461
  });
382
462
  });
463
+
464
+ describe("message action button styles", () => {
465
+ it("rounds the hit target on the medium radius, not the near-square small one", () => {
466
+ const start = widgetCss.indexOf(".persona-message-action-btn {");
467
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
468
+
469
+ expect(start).toBeGreaterThan(-1);
470
+ expect(rule).toContain(
471
+ "border-radius: var(--persona-message-action-radius, var(--persona-radius-md, 0.375rem))",
472
+ );
473
+ expect(rule).not.toContain("--persona-radius-sm");
474
+ });
475
+
476
+ it("hovers with the scheme-aware ghost wash and text color, not surface or brand", () => {
477
+ const start = widgetCss.indexOf(".persona-message-action-btn:hover {");
478
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
479
+
480
+ expect(start).toBeGreaterThan(-1);
481
+ expect(rule).toContain(
482
+ "background-color: var(--persona-message-action-hover-bg, var(--persona-button-ghost-hover-bg, rgba(0, 0, 0, 0.05)))",
483
+ );
484
+ expect(rule).toContain(
485
+ "color: var(--persona-message-action-hover-fg, var(--persona-text, #111827))",
486
+ );
487
+ // A surface token is nearly invisible on light and paints a darker box on
488
+ // dark; the brand color is never an icon-hover color.
489
+ expect(rule).not.toContain("--persona-container");
490
+ expect(rule).not.toContain("--persona-primary");
491
+ });
492
+ });
493
+
494
+ describe("conversation-open skeleton styles", () => {
495
+ it("follows each role's real bubble radius chain", () => {
496
+ const leading = ".persona-conversation-loading-bubble {";
497
+ const start = widgetCss.indexOf(leading);
498
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
499
+ expect(start).toBeGreaterThan(-1);
500
+ expect(rule).toContain(
501
+ "border-radius: var(--persona-message-assistant-radius, var(--persona-radius-lg, 0.5rem))",
502
+ );
503
+
504
+ const trailing = ".persona-conversation-loading-bubble--trailing {";
505
+ const trailingStart = widgetCss.indexOf(trailing);
506
+ const trailingRule = widgetCss.slice(
507
+ trailingStart,
508
+ widgetCss.indexOf("\n}", trailingStart),
509
+ );
510
+ expect(trailingStart).toBeGreaterThan(-1);
511
+ expect(trailingRule).toContain(
512
+ "border-radius: var(--persona-message-user-radius, var(--persona-radius-lg, 0.5rem))",
513
+ );
514
+ });
515
+ });
@@ -325,6 +325,16 @@
325
325
  display: none;
326
326
  }
327
327
 
328
+ /* Launcher glyph weight, agent icon and call-to-action arrow alike: one pill
329
+ must never carry two line weights. CSS wins over the SVG stroke-width
330
+ attribute, which stays the no-CSS fallback. Both the critical launcher
331
+ bundle and the full widget render this markup, so one rule keeps the two
332
+ surfaces identical. */
333
+ [data-persona-root] [data-role="launcher-icon"] > svg,
334
+ [data-persona-root] [data-role="launcher-call-to-action-icon"] > svg {
335
+ stroke-width: var(--persona-components-launcher-iconStrokeWidth, 1.5);
336
+ }
337
+
328
338
  /* An empty transcript still counts as a flex child, so the body's gap
329
339
  inserts phantom space below the welcome and fakes scrollable height. */
330
340
  .persona-widget-messages:empty {
@@ -957,6 +967,16 @@
957
967
  overflow: hidden;
958
968
  }
959
969
 
970
+ /* The overlay is absolute inside the scroll body, so anything left scrollable
971
+ underneath strands it at the content origin instead of the viewport (the
972
+ body carries an inline `overflow-y: auto` in fill/fullscreen layouts, which
973
+ outranks the rule above). Hiding the transcript siblings leaves the body
974
+ with nothing to scroll, so the overlay stays over the visible area. */
975
+ .persona-widget-body.persona-welcome-overlay-active
976
+ > :not([data-persona-welcome-overlay]) {
977
+ display: none !important;
978
+ }
979
+
960
980
  /* Hero: auto block margins center the surface in the empty conversation
961
981
  without `justify-content`, which would make overflowing content
962
982
  unreachable in the scroll body. */
@@ -1016,6 +1036,10 @@
1016
1036
  background-color: var(--persona-surface, #ffffff);
1017
1037
  }
1018
1038
 
1039
+ .persona-bg-persona-launcher {
1040
+ background-color: var(--persona-launcher-bg, var(--persona-surface, #ffffff));
1041
+ }
1042
+
1019
1043
  .persona-bg-persona-container {
1020
1044
  background-color: var(--persona-container, #f8fafc);
1021
1045
  }
@@ -1052,6 +1076,14 @@
1052
1076
  color: var(--persona-muted, #6b7280);
1053
1077
  }
1054
1078
 
1079
+ .persona-text-persona-launcher {
1080
+ color: var(--persona-launcher-fg, var(--persona-primary, #171717));
1081
+ }
1082
+
1083
+ .persona-text-persona-launcher-muted {
1084
+ color: var(--persona-launcher-fg-muted, var(--persona-muted, #6b7280));
1085
+ }
1086
+
1055
1087
  .persona-text-white {
1056
1088
  color: #ffffff;
1057
1089
  }
@@ -1783,12 +1815,12 @@
1783
1815
  .persona-control-tooltip {
1784
1816
  position: fixed;
1785
1817
  box-sizing: border-box;
1786
- max-width: min(320px, calc(100vw - 16px));
1818
+ max-width: var(--persona-tooltip-max-width, min(320px, calc(100vw - 16px)));
1787
1819
  background-color: var(--persona-tooltip-background, #111827);
1788
1820
  color: var(--persona-tooltip-foreground, #ffffff);
1789
- padding: 6px 12px;
1790
- border-radius: var(--persona-radius-sm, 0.125rem);
1791
- font-size: 12px;
1821
+ padding: var(--persona-tooltip-padding, 6px 12px);
1822
+ border-radius: var(--persona-tooltip-radius, var(--persona-radius-sm, 0.125rem));
1823
+ font-size: var(--persona-tooltip-font-size, 12px);
1792
1824
  line-height: 1.35;
1793
1825
  white-space: normal;
1794
1826
  overflow-wrap: anywhere;
@@ -1803,8 +1835,16 @@
1803
1835
  opacity: 0;
1804
1836
  }
1805
1837
 
1838
+ /* Trailing shortcut hint, e.g. a key combo after the label. */
1839
+ .persona-control-tooltip__hint {
1840
+ margin-left: 6px;
1841
+ color: var(--persona-tooltip-hint-fg, rgba(255, 255, 255, 0.55));
1842
+ white-space: nowrap;
1843
+ }
1844
+
1806
1845
  .persona-control-tooltip__arrow {
1807
1846
  position: absolute;
1847
+ display: var(--persona-tooltip-arrow-display, block);
1808
1848
  left: var(--persona-tooltip-arrow-x, 50%);
1809
1849
  width: 8px;
1810
1850
  height: 8px;
@@ -1827,6 +1867,44 @@
1827
1867
  justify-content: center;
1828
1868
  }
1829
1869
 
1870
+ /* Shared header control box: close, clear chat, trailing actions, and the
1871
+ Messages toggle. A per-control config key (`launcher.closeButtonSize`,
1872
+ `launcher.clearChat.size`) writes inline styles, which win. */
1873
+ .persona-header-control {
1874
+ width: var(--persona-header-control-size, 32px);
1875
+ height: var(--persona-header-control-size, 32px);
1876
+ flex-shrink: 0;
1877
+ /* UA button padding would eat into the box and crush the glyph; a configured
1878
+ paddingX/Y is inline and still wins. */
1879
+ padding: 0;
1880
+ }
1881
+
1882
+ .persona-header-control--glyph > svg {
1883
+ width: var(--persona-header-control-icon-size, 20px);
1884
+ height: var(--persona-header-control-icon-size, 20px);
1885
+ /* Presentation wins over the SVG stroke-width attribute, which stays as the
1886
+ no-CSS fallback. Unitless, straight off the theme token. */
1887
+ stroke-width: var(--persona-components-header-controlStrokeWidth, 1.5);
1888
+ }
1889
+
1890
+ /* Sparse-viewBox glyphs (X) need more box to match the visible extent of their
1891
+ siblings, and the same factor thins the stroke so the weight still matches.
1892
+ Multipliers mirror SPARSE_GLYPHS in header-parts.ts. */
1893
+ .persona-header-control--sparse > svg {
1894
+ width: calc(var(--persona-header-control-icon-size, 20px) * 1.4);
1895
+ height: calc(var(--persona-header-control-icon-size, 20px) * 1.4);
1896
+ stroke-width: calc(var(--persona-components-header-controlStrokeWidth, 1.5) * 0.7);
1897
+ }
1898
+
1899
+ /* A smaller controlSize must never shrink a touch target below 40px. An
1900
+ explicit per-control size is inline, so it opts out of this floor. */
1901
+ @media (pointer: coarse) {
1902
+ .persona-header-control {
1903
+ min-width: 40px;
1904
+ min-height: 40px;
1905
+ }
1906
+ }
1907
+
1830
1908
  @media (prefers-reduced-motion: reduce) {
1831
1909
  .persona-control-tooltip {
1832
1910
  transition: none;
@@ -1849,6 +1927,91 @@
1849
1927
  animation: persona-typing 1s infinite;
1850
1928
  }
1851
1929
 
1930
+ /* Conversation-open stand-in: transcript skeleton shown while a conversation
1931
+ selected from the Messages list is fetched. The container carries the
1932
+ transcript's centered column so custom renderers land where messages will. */
1933
+ .persona-conversation-loading {
1934
+ width: 100%;
1935
+ max-width: var(--persona-content-max-width, 768px);
1936
+ margin: 0 auto;
1937
+ }
1938
+
1939
+ .persona-conversation-loading-body {
1940
+ display: flex;
1941
+ flex-direction: column;
1942
+ gap: 12px;
1943
+ padding: 20px;
1944
+ }
1945
+
1946
+ /* Leading bubbles stand in for assistant messages, trailing for user ones,
1947
+ so each follows its role's real radius chain. */
1948
+ .persona-conversation-loading-bubble {
1949
+ height: 44px;
1950
+ border-radius: var(--persona-message-assistant-radius, var(--persona-radius-lg, 0.5rem));
1951
+ background: var(--persona-divider, var(--persona-border, #e5e7eb));
1952
+ align-self: flex-start;
1953
+ animation: persona-conversation-loading-pulse 1400ms ease-in-out infinite;
1954
+ }
1955
+
1956
+ .persona-conversation-loading-bubble--trailing {
1957
+ align-self: flex-end;
1958
+ border-radius: var(--persona-message-user-radius, var(--persona-radius-lg, 0.5rem));
1959
+ }
1960
+
1961
+ @keyframes persona-conversation-loading-pulse {
1962
+ 0%,
1963
+ 100% {
1964
+ opacity: 1;
1965
+ }
1966
+ 50% {
1967
+ opacity: 0.55;
1968
+ }
1969
+ }
1970
+
1971
+ @media (prefers-reduced-motion: reduce) {
1972
+ .persona-conversation-loading-bubble {
1973
+ animation: none;
1974
+ }
1975
+ }
1976
+
1977
+ /* Failed conversation-open surface. The container carries the transcript
1978
+ column; the body classes style the default block only, so a plugin's
1979
+ renderHistoryOpenError content starts from a clean slate. */
1980
+ .persona-conversation-loading-error {
1981
+ width: 100%;
1982
+ max-width: var(--persona-content-max-width, 768px);
1983
+ margin: 0 auto;
1984
+ }
1985
+
1986
+ .persona-conversation-loading-error-body {
1987
+ display: flex;
1988
+ flex-direction: column;
1989
+ align-items: flex-start;
1990
+ gap: 12px;
1991
+ padding: 20px;
1992
+ color: var(--persona-text-muted, #6b7280);
1993
+ }
1994
+
1995
+ .persona-conversation-loading-error-title {
1996
+ margin: 0;
1997
+ }
1998
+
1999
+ .persona-conversation-loading-error-actions {
2000
+ display: flex;
2001
+ gap: 8px;
2002
+ flex-wrap: wrap;
2003
+ }
2004
+
2005
+ .persona-conversation-loading-error-action {
2006
+ padding: 8px 14px;
2007
+ border: 1px solid var(--persona-divider, var(--persona-border, #e5e7eb));
2008
+ border-radius: var(--persona-radius-md, 8px);
2009
+ background: transparent;
2010
+ color: inherit;
2011
+ font: inherit;
2012
+ cursor: pointer;
2013
+ }
2014
+
1852
2015
  .persona-space-x-1 > * + * {
1853
2016
  margin-left: 0.25rem;
1854
2017
  }
@@ -2675,16 +2838,18 @@
2675
2838
  height: 28px;
2676
2839
  padding: 0;
2677
2840
  border: none;
2678
- border-radius: var(--persona-radius-sm, 0.125rem);
2841
+ border-radius: var(--persona-message-action-radius, var(--persona-radius-md, 0.375rem));
2679
2842
  background-color: transparent;
2680
2843
  color: var(--persona-muted, #6b7280);
2681
2844
  cursor: pointer;
2682
2845
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
2683
2846
  }
2684
2847
 
2848
+ /* Hover reads the shared ghost wash (scheme-aware) and moves the icon toward
2849
+ text, never a surface fill or the brand color. */
2685
2850
  .persona-message-action-btn:hover {
2686
- background-color: var(--persona-container, #f8fafc);
2687
- color: var(--persona-primary, #111827);
2851
+ background-color: var(--persona-message-action-hover-bg, var(--persona-button-ghost-hover-bg, rgba(0, 0, 0, 0.05)));
2852
+ color: var(--persona-message-action-hover-fg, var(--persona-text, #111827));
2688
2853
  }
2689
2854
 
2690
2855
  .persona-message-action-btn:active {
@@ -3406,6 +3571,42 @@
3406
3571
  gap: var(--persona-artifact-split-gap, 0.5rem);
3407
3572
  }
3408
3573
 
3574
+ /* Split button: one bordered control, two invocable halves, internal hairline
3575
+ divider (Primer/Fluent anatomy). The wrap paints border, surface, and radius
3576
+ once; the halves stay borderless and transparent, with inner radii carved
3577
+ 1px inside the wrap so hover fills never poke past the rounded corners. The
3578
+ half selectors carry a `button.` tag so they outrank context skins' rest
3579
+ rules (0,3,1 vs 0,3,0) while context hover rules (0,4,0) still win. */
3580
+ [data-persona-root] .persona-split-btn {
3581
+ --persona-split-radius: var(--persona-radius-md, 0.375rem);
3582
+ position: relative;
3583
+ display: inline-flex;
3584
+ align-items: stretch;
3585
+ border: 1px solid var(--persona-border, #e5e7eb);
3586
+ border-radius: var(--persona-split-radius);
3587
+ background: var(--persona-surface, #ffffff);
3588
+ }
3589
+
3590
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-primary,
3591
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-chevron {
3592
+ border: 0;
3593
+ background: transparent;
3594
+ }
3595
+
3596
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-primary {
3597
+ border-radius: calc(var(--persona-split-radius) - 1px) 0 0 calc(var(--persona-split-radius) - 1px);
3598
+ }
3599
+
3600
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-chevron {
3601
+ border-left: 1px solid var(--persona-split-divider, var(--persona-border, #e5e7eb));
3602
+ border-radius: 0 calc(var(--persona-split-radius) - 1px) calc(var(--persona-split-radius) - 1px) 0;
3603
+ }
3604
+
3605
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-primary:where(:hover),
3606
+ [data-persona-root] .persona-split-btn > button.persona-split-btn-chevron:where(:hover) {
3607
+ background: var(--persona-icon-btn-hover-bg, var(--persona-container, #f3f4f6));
3608
+ }
3609
+
3409
3610
  /* Document toolbar preset: optional layout vars: --persona-artifact-doc-toolbar-icon-color, etc. */
3410
3611
  /* Document-toolbar icon/copy button styling is shared by the artifact pane's
3411
3612
  document preset and the inline chrome actions cluster, so both surfaces read
@@ -3435,6 +3636,7 @@
3435
3636
  [data-persona-root] .persona-artifact-inline-chrome-actions .persona-artifact-doc-icon-btn[aria-pressed="true"] {
3436
3637
  background: var(--persona-artifact-doc-toggle-active-bg, var(--persona-icon-btn-active-bg, var(--persona-container, #f3f4f6)));
3437
3638
  border-color: var(--persona-artifact-doc-toggle-active-border, var(--persona-icon-btn-active-border, var(--persona-border, #e5e7eb)));
3639
+ color: var(--persona-artifact-doc-toggle-active-color, var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827)));
3438
3640
  }
3439
3641
 
3440
3642
  [data-persona-root] .persona-artifact-toolbar-document .persona-artifact-doc-copy-btn,
@@ -3457,6 +3659,16 @@
3457
3659
  background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-icon-btn-hover-bg, var(--persona-container, #f3f4f6)));
3458
3660
  }
3459
3661
 
3662
+ /* Copy split button inside the document toolbar: the wrap takes over the copy
3663
+ pill's themed border/background/radius; the halves fall through to the base
3664
+ split-btn rules above, which outrank these context rest rules by tag. */
3665
+ [data-persona-root] .persona-artifact-toolbar-document .persona-split-btn,
3666
+ [data-persona-root] .persona-artifact-inline-chrome-actions .persona-split-btn {
3667
+ --persona-split-radius: var(--persona-artifact-toolbar-copy-radius, var(--persona-radius-md, 0.375rem));
3668
+ border: var(--persona-artifact-toolbar-copy-border, 1px solid var(--persona-border, #e5e7eb));
3669
+ background: var(--persona-artifact-toolbar-copy-bg, var(--persona-surface, #ffffff));
3670
+ }
3671
+
3460
3672
 
3461
3673
  /* Artifact tab theming */
3462
3674
  [data-persona-root] .persona-artifact-tab {
@@ -3745,6 +3957,9 @@
3745
3957
  /* Inset ring instead of a border so documentToolbarToggleActiveBorderColor
3746
3958
  still applies inside the borderless segment (transparent by default). */
3747
3959
  box-shadow: inset 0 0 0 1px var(--persona-artifact-doc-toggle-active-border, transparent);
3960
+ /* Survives the @supports thumb override below, which resets only the
3961
+ background and ring; the active glyph keeps this color either way. */
3962
+ color: var(--persona-artifact-doc-toggle-active-color, var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827)));
3748
3963
  }
3749
3964
 
3750
3965
  /* Sliding selection thumb: a container ::before that translates between the
@@ -3850,7 +4065,9 @@
3850
4065
  margin: 0.25rem 0;
3851
4066
  }
3852
4067
 
3853
- /* Draggable split handle (desktop split only; hidden in drawer / narrow host / small viewport) */
4068
+ /* Draggable split handles: the artifact seam (desktop split only; hidden in
4069
+ drawer / narrow host / small viewport) and the docked Messages rail's edge. */
4070
+ [data-persona-root] .persona-rail-resizer,
3854
4071
  [data-persona-root] .persona-artifact-split-handle {
3855
4072
  width: 6px;
3856
4073
  flex-shrink: 0;
@@ -3864,6 +4081,8 @@
3864
4081
  box-sizing: border-box;
3865
4082
  }
3866
4083
 
4084
+ [data-persona-root] .persona-rail-resizer:hover,
4085
+ [data-persona-root] .persona-rail-resizer:focus-visible,
3867
4086
  [data-persona-root] .persona-artifact-split-handle:hover,
3868
4087
  [data-persona-root] .persona-artifact-split-handle:focus-visible {
3869
4088
  background: color-mix(in srgb, var(--persona-border, #e5e7eb) 55%, transparent);
@@ -43,6 +43,25 @@ describe("theme editor preview demo data", () => {
43
43
  );
44
44
  });
45
45
 
46
+ it("enables the history feature for the messages scene", () => {
47
+ const config = buildPreviewConfig({ scene: "messages" });
48
+
49
+ expect(config.features?.history?.enabled).toBe(true);
50
+ expect(config.features?.history?.presentation).toBe("panel");
51
+ // A transcript still sits behind the Messages surface.
52
+ expect(config.initialMessages?.length).toBeGreaterThan(0);
53
+ });
54
+
55
+ it("keeps an explicit history presentation in the messages scene", () => {
56
+ const config = buildPreviewConfig({
57
+ scene: "messages",
58
+ config: { features: { history: { enabled: false, presentation: "rail" } } },
59
+ });
60
+
61
+ expect(config.features?.history?.enabled).toBe(true);
62
+ expect(config.features?.history?.presentation).toBe("rail");
63
+ });
64
+
46
65
  it("seeds tool call preview messages when advanced tool display modes are enabled", () => {
47
66
  const config = buildPreviewConfig({
48
67
  scene: "conversation",
@@ -200,7 +200,8 @@ export type PreviewScene =
200
200
  | 'conversation'
201
201
  | 'follow-ups'
202
202
  | 'minimized'
203
- | 'artifact';
203
+ | 'artifact'
204
+ | 'messages';
204
205
 
205
206
  export type PreviewTranscriptEntryPreset =
206
207
  | 'user-message'
@@ -660,6 +661,19 @@ export function applySceneConfig(
660
661
  if (scene === 'artifact') {
661
662
  config.features = { ...config.features, artifacts: { ...config.features?.artifacts, enabled: true } };
662
663
  }
664
+ if (scene === 'messages') {
665
+ // The host wires a provider (setHistoryProviderFactory) and opens the view
666
+ // after mount; without one the widget hides its history chrome and the
667
+ // scene degrades to a plain conversation.
668
+ config.features = {
669
+ ...config.features,
670
+ history: {
671
+ presentation: 'panel',
672
+ ...config.features?.history,
673
+ enabled: true,
674
+ },
675
+ };
676
+ }
663
677
  return config;
664
678
  }
665
679
 
@@ -308,8 +308,14 @@ export function createThemePreview(
308
308
  if (isMinimized) controller.close();
309
309
  }
310
310
 
311
- // Inject artifacts if needed
312
311
  const scene = options.scene ?? 'conversation';
312
+ // No-op without a history provider (see applySceneConfig): the host
313
+ // registers one via setHistoryProviderFactory before mounting.
314
+ if (scene === 'messages') {
315
+ for (const c of controllers) void c.showHistory({ focus: false });
316
+ }
317
+
318
+ // Inject artifacts if needed
313
319
  if (scene === 'artifact' || options.config?.features?.artifacts?.enabled) {
314
320
  for (const c of controllers) {
315
321
  c.upsertArtifact({
@@ -52,6 +52,9 @@ export const ROLE_HEADER: RoleAssignmentOptions = {
52
52
  intensities: ROLE_INTENSITIES,
53
53
  targets: [
54
54
  { path: 'components.header.background', kind: 'background' },
55
+ // Anchor of the widget's derivation; the explicit keys below still win,
56
+ // so role themes keep their exact per-key shades.
57
+ { path: 'components.header.foreground', kind: 'foreground' },
55
58
  { path: 'components.header.border', kind: 'border' },
56
59
  { path: 'components.header.iconBackground', kind: 'accent' },
57
60
  { path: 'components.header.iconForeground', kind: 'foreground' },
@@ -250,7 +253,7 @@ function resolveHeaderTarget(family: string, solid: boolean, target: RoleTarget)
250
253
  ? `palette.colors.${family}.50`
251
254
  : `palette.colors.${family}.50`;
252
255
  }
253
- if (path.endsWith('.titleForeground')) {
256
+ if (path.endsWith('.foreground') || path.endsWith('.titleForeground')) {
254
257
  return solid
255
258
  ? `palette.colors.${family}.50`
256
259
  : `palette.colors.${family}.${family === 'gray' ? '900' : '700'}`;