@runtypelabs/persona 3.34.0 → 3.35.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 (186) hide show
  1. package/README.md +11 -11
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
  5. package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +11 -11
  9. package/dist/codegen.js +10 -10
  10. package/dist/index.cjs +46 -44
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +514 -158
  13. package/dist/index.d.ts +514 -158
  14. package/dist/index.global.js +41 -89
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +46 -44
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.global.js +1 -1
  19. package/dist/install.global.js.map +1 -1
  20. package/dist/launcher.global.js +2 -2
  21. package/dist/launcher.global.js.map +1 -1
  22. package/dist/markdown-parsers.js +53 -0
  23. package/dist/plugin-kit.d.cts +4 -4
  24. package/dist/plugin-kit.d.ts +4 -4
  25. package/dist/runtype-tts.js +1 -0
  26. package/dist/smart-dom-reader.cjs +1 -1
  27. package/dist/smart-dom-reader.d.cts +352 -103
  28. package/dist/smart-dom-reader.d.ts +352 -103
  29. package/dist/smart-dom-reader.js +1 -1
  30. package/dist/theme-editor-preview.cjs +203 -0
  31. package/dist/theme-editor-preview.d.cts +5748 -0
  32. package/dist/theme-editor-preview.d.ts +5748 -0
  33. package/dist/theme-editor-preview.js +203 -0
  34. package/dist/theme-editor.cjs +14 -131
  35. package/dist/theme-editor.d.cts +362 -574
  36. package/dist/theme-editor.d.ts +362 -574
  37. package/dist/theme-editor.js +14 -131
  38. package/dist/theme-reference.cjs +1 -1
  39. package/dist/theme-reference.d.cts +1 -1
  40. package/dist/theme-reference.d.ts +1 -1
  41. package/dist/theme-reference.js +1 -1
  42. package/dist/voice-worklet-player.cjs +80 -0
  43. package/dist/voice-worklet-player.d.cts +215 -0
  44. package/dist/voice-worklet-player.d.ts +215 -0
  45. package/dist/voice-worklet-player.js +80 -0
  46. package/dist/widget.css +54 -36
  47. package/package.json +16 -2
  48. package/src/animations/glyph-cycle.ts +11 -14
  49. package/src/animations/wipe.ts +1 -1
  50. package/src/ask-user-question-tool.test.ts +1 -1
  51. package/src/ask-user-question-tool.ts +6 -8
  52. package/src/client.test.ts +67 -12
  53. package/src/client.ts +40 -32
  54. package/src/codegen.test.ts +1 -1
  55. package/src/codegen.ts +1 -1
  56. package/src/components/approval-bubble.test.ts +1 -1
  57. package/src/components/approval-bubble.ts +3 -3
  58. package/src/components/artifact-pane.ts +3 -3
  59. package/src/components/ask-user-question-bubble.test.ts +14 -14
  60. package/src/components/ask-user-question-bubble.ts +10 -10
  61. package/src/components/composer-builder.ts +17 -16
  62. package/src/components/composer-parts.test.ts +54 -0
  63. package/src/components/composer-parts.ts +95 -126
  64. package/src/components/demo-carousel.ts +1 -1
  65. package/src/components/event-stream-view.test.ts +7 -7
  66. package/src/components/event-stream-view.ts +5 -5
  67. package/src/components/header-builder.ts +33 -27
  68. package/src/components/header-layouts.ts +1 -1
  69. package/src/components/header-parts.test.ts +90 -0
  70. package/src/components/header-parts.ts +62 -93
  71. package/src/components/launcher.test.ts +11 -0
  72. package/src/components/launcher.ts +1 -1
  73. package/src/components/message-bubble.test.ts +74 -2
  74. package/src/components/message-bubble.ts +25 -11
  75. package/src/components/messages.ts +2 -2
  76. package/src/components/panel.test.ts +1 -1
  77. package/src/components/panel.ts +92 -97
  78. package/src/components/pill-composer-builder.ts +28 -24
  79. package/src/components/reasoning-bubble.ts +2 -2
  80. package/src/components/suggestions.ts +1 -1
  81. package/src/components/tool-bubble.ts +1 -1
  82. package/src/components/widget-view.test.ts +181 -0
  83. package/src/components/widget-view.ts +234 -0
  84. package/src/defaults.ts +54 -51
  85. package/src/generated/runtype-openapi-contract.ts +5 -0
  86. package/src/index-core.ts +30 -4
  87. package/src/index-global.ts +73 -6
  88. package/src/index.ts +7 -1
  89. package/src/install.test.ts +11 -11
  90. package/src/install.ts +28 -18
  91. package/src/launcher-global.ts +25 -14
  92. package/src/markdown-parsers-eager.ts +20 -0
  93. package/src/markdown-parsers-entry.ts +4 -0
  94. package/src/markdown-parsers-loader.ts +49 -0
  95. package/src/plugin-kit.test.ts +1 -1
  96. package/src/plugin-kit.ts +7 -7
  97. package/src/plugins/types.ts +5 -5
  98. package/src/postprocessors.ts +27 -14
  99. package/src/runtime/host-layout.test.ts +1 -1
  100. package/src/runtime/host-layout.ts +4 -5
  101. package/src/runtime/init.ts +2 -2
  102. package/src/session.test.ts +5 -6
  103. package/src/session.ts +333 -227
  104. package/src/session.voice.test.ts +146 -0
  105. package/src/session.webmcp.test.ts +24 -25
  106. package/src/smart-dom-reader.ts +3 -4
  107. package/src/styles/widget.css +54 -36
  108. package/src/suggest-replies-tool.test.ts +3 -3
  109. package/src/suggest-replies-tool.ts +5 -5
  110. package/src/testing/index.ts +1 -1
  111. package/src/theme-editor/color-utils.ts +2 -2
  112. package/src/theme-editor/index.ts +4 -13
  113. package/src/theme-editor/presets.ts +1 -1
  114. package/src/theme-editor/preview-utils.ts +22 -20
  115. package/src/theme-editor/preview.ts +4 -1
  116. package/src/theme-editor/role-mappings.ts +3 -3
  117. package/src/theme-editor/sections.ts +21 -20
  118. package/src/theme-editor/types.ts +1 -1
  119. package/src/theme-editor/webmcp/index.ts +1 -1
  120. package/src/theme-editor/webmcp/summary.ts +1 -2
  121. package/src/theme-editor/webmcp/tools.ts +30 -30
  122. package/src/theme-editor/webmcp/types.ts +2 -2
  123. package/src/theme-editor-preview.ts +10 -0
  124. package/src/theme-reference.ts +26 -26
  125. package/src/types.ts +368 -99
  126. package/src/ui.approval-plugin.test.ts +3 -3
  127. package/src/ui.ask-user-question-plugin.test.ts +9 -9
  128. package/src/ui.component-directive.test.ts +4 -4
  129. package/src/ui.composer-bar.test.ts +10 -11
  130. package/src/ui.composer-keyboard.test.ts +5 -5
  131. package/src/ui.header-icon-color.test.ts +59 -0
  132. package/src/ui.scroll.test.ts +5 -6
  133. package/src/ui.stop-button.test.ts +3 -4
  134. package/src/ui.suggest-replies.test.ts +2 -2
  135. package/src/ui.ts +292 -131
  136. package/src/utils/buttons.ts +1 -1
  137. package/src/utils/code-generators.ts +1 -1
  138. package/src/utils/composer-history.ts +3 -3
  139. package/src/utils/dom-context.test.ts +2 -2
  140. package/src/utils/dom-context.ts +7 -7
  141. package/src/utils/dom.test.ts +67 -0
  142. package/src/utils/dom.ts +75 -0
  143. package/src/utils/dropdown.ts +2 -2
  144. package/src/utils/event-stream-buffer.test.ts +2 -2
  145. package/src/utils/message-fingerprint.test.ts +9 -0
  146. package/src/utils/message-fingerprint.ts +5 -0
  147. package/src/utils/morph.ts +1 -1
  148. package/src/utils/sanitize.ts +32 -16
  149. package/src/utils/sequence-buffer.test.ts +8 -8
  150. package/src/utils/sequence-buffer.ts +4 -4
  151. package/src/utils/smart-dom-adapter.test.ts +1 -1
  152. package/src/utils/smart-dom-adapter.ts +4 -5
  153. package/src/utils/speech-text.test.ts +122 -0
  154. package/src/utils/speech-text.ts +101 -0
  155. package/src/utils/stream-animation.test.ts +3 -3
  156. package/src/utils/stream-animation.ts +6 -6
  157. package/src/utils/theme.test.ts +1 -1
  158. package/src/utils/throughput-tracker.test.ts +11 -11
  159. package/src/utils/throughput-tracker.ts +9 -9
  160. package/src/utils/tokens.ts +13 -13
  161. package/src/utils/virtual-scroller.ts +1 -1
  162. package/src/vendor/smart-dom-reader/README.md +4 -4
  163. package/src/vendor/smart-dom-reader/index.d.ts +1 -1
  164. package/src/vendor/smart-dom-reader/index.js +2 -2
  165. package/src/version.ts +1 -1
  166. package/src/voice/audio-playback-manager.test.ts +159 -0
  167. package/src/voice/audio-playback-manager.ts +145 -30
  168. package/src/voice/browser-speech-engine.ts +130 -0
  169. package/src/voice/fallback-speech-engine.ts +89 -0
  170. package/src/voice/index.ts +21 -0
  171. package/src/voice/read-aloud-controller.test.ts +143 -0
  172. package/src/voice/read-aloud-controller.ts +136 -0
  173. package/src/voice/runtype-speech-engine.test.ts +280 -0
  174. package/src/voice/runtype-speech-engine.ts +211 -0
  175. package/src/voice/runtype-tts-entry.ts +10 -0
  176. package/src/voice/runtype-tts-loader.test.ts +47 -0
  177. package/src/voice/runtype-tts-loader.ts +42 -0
  178. package/src/voice/runtype-voice-provider.ts +332 -537
  179. package/src/voice/voice-factory.ts +26 -4
  180. package/src/voice/voice.test.ts +321 -7
  181. package/src/voice/worklet-playback-engine.ts +272 -0
  182. package/src/voice-worklet-player.ts +39 -0
  183. package/src/webmcp-bridge.test.ts +6 -6
  184. package/src/webmcp-bridge.ts +24 -25
  185. package/src/webmcp-polyfill.ts +1 -1
  186. package/src/voice/voice-activity-detector.ts +0 -90
package/src/install.ts CHANGED
@@ -30,11 +30,11 @@ interface SiteAgentInstallConfig {
30
30
  */
31
31
  onScriptLoad?: (info: { version: string }) => void;
32
32
  /**
33
- * Fired when the floating launcher is painted on the page at page-load time.
33
+ * Fired when the floating launcher is painted on the page: at page-load time.
34
34
  * Deferred installs: the critical launcher mounts. Eager floating installs:
35
35
  * the full widget's launcher mounts. Use this for "widget appeared" analytics.
36
36
  * Does NOT fire for inline / docked / composer-bar installs (no floating
37
- * launcher) use `onChatReady` there.
37
+ * launcher): use `onChatReady` there.
38
38
  */
39
39
  onLauncherShown?: (info: { deferred: boolean; element?: HTMLElement }) => void;
40
40
  /**
@@ -88,7 +88,7 @@ declare global {
88
88
  if (configJson) {
89
89
  try {
90
90
  // HTML attributes preserve literal newlines/tabs which are invalid
91
- // control characters inside JSON string literals strip them.
91
+ // control characters inside JSON string literals: strip them.
92
92
  const normalizedJson = configJson.replace(/[\r\n]+\s*/g, '');
93
93
  const parsedConfig = JSON.parse(normalizedJson);
94
94
  // If it has nested 'config' property, use it; otherwise treat as widget config
@@ -152,7 +152,7 @@ declare global {
152
152
  try {
153
153
  window.dispatchEvent(new CustomEvent(name, { detail }));
154
154
  } catch {
155
- /* CustomEvent unsupported ignore */
155
+ /* CustomEvent unsupported: ignore */
156
156
  }
157
157
  };
158
158
  const fail = (phase: "css" | "bundle" | "init", error: unknown): void => {
@@ -168,14 +168,14 @@ declare global {
168
168
  if (!warnedOnReadyDeprecated) {
169
169
  warnedOnReadyDeprecated = true;
170
170
  console.warn(
171
- "[Persona] `onReady` is deprecated use `onChatReady`. `onReady` still works but is removed in the next major."
171
+ "[Persona] `onReady` is deprecated: use `onChatReady`. `onReady` still works but is removed in the next major."
172
172
  );
173
173
  }
174
174
  return config.onReady;
175
175
  }
176
176
  return undefined;
177
177
  };
178
- // True when the config renders a standard floating launcher button the only
178
+ // True when the config renders a standard floating launcher button: the only
179
179
  // case that paints a clickable launcher at load. Shared by the deferral gate
180
180
  // and the eager-path `onLauncherShown` so the event name stays honest.
181
181
  const hasFloatingLauncher = (widgetConfig: any): boolean => {
@@ -343,12 +343,23 @@ declare global {
343
343
  // first open is quick. Runs at idle so it never competes with the launcher.
344
344
  const prefetchFullBundle = (): void => {
345
345
  const addPrefetch = () => {
346
- if (isJsLoaded()) return;
347
- const link = document.createElement("link");
348
- link.rel = "prefetch";
349
- link.as = "script";
350
- link.href = jsUrl;
351
- document.head.appendChild(link);
346
+ if (!isJsLoaded()) {
347
+ const link = document.createElement("link");
348
+ link.rel = "prefetch";
349
+ link.as = "script";
350
+ link.href = jsUrl;
351
+ document.head.appendChild(link);
352
+ }
353
+
354
+ // Also prefetch the markdown parsers chunk
355
+ const markdownUrl = jsUrl.replace(/index\.global\.js($|\?)/, "markdown-parsers.js$1");
356
+ if (markdownUrl !== jsUrl) {
357
+ const link2 = document.createElement("link");
358
+ link2.rel = "prefetch";
359
+ link2.as = "script";
360
+ link2.href = markdownUrl;
361
+ document.head.appendChild(link2);
362
+ }
352
363
  };
353
364
  if (typeof requestIdleCallback !== "undefined") {
354
365
  requestIdleCallback(addPrefetch, { timeout: 4000 });
@@ -408,7 +419,7 @@ declare global {
408
419
  handle.open();
409
420
  }
410
421
 
411
- // Eager floating installs paint their launcher at load time too emit the
422
+ // Eager floating installs paint their launcher at load time too: emit the
412
423
  // same page-load "appeared" signal as the deferred path. The deferred
413
424
  // handoff (openAfter) already fired it at launcher mount, and non-floating
414
425
  // modes have no launcher, so guard on both.
@@ -420,7 +431,7 @@ declare global {
420
431
  // The full widget is initialized and its controller API is callable.
421
432
  safeCall(resolveChatReady(), handle);
422
433
  dispatchLifecycle("persona:chat-ready", handle);
423
- dispatchLifecycle("persona:ready", handle); // deprecated alias removed next major
434
+ dispatchLifecycle("persona:ready", handle); // deprecated alias: removed next major
424
435
  return handle;
425
436
  } catch (error) {
426
437
  fail("init", error);
@@ -449,11 +460,10 @@ declare global {
449
460
 
450
461
  // The deferred-launcher optimization only applies to the common floating case
451
462
  // that paints a collapsed launcher and waits for a click. Anything that starts
452
- // open or renders differently eager-loads the full bundle exactly as before
453
- // including the two open triggers config alone can't express: a host
463
+ // open or renders differently eager-loads the full bundle exactly as before: // including the two open triggers config alone can't express: a host
454
464
  // onStateLoaded hook that may request open, and a restored "was open" state.
455
465
  const shouldDeferPanel = (widgetConfig: any): boolean => {
456
- if (!launcherUrl) return false; // custom bundle URL override can't derive launcher URL
466
+ if (!launcherUrl) return false; // custom bundle URL override: can't derive launcher URL
457
467
  if (!hasFloatingLauncher(widgetConfig)) return false; // inline / docked / composer-bar
458
468
  const launcher = widgetConfig.launcher ?? {};
459
469
  if (launcher.autoExpand === true) return false; // starts open
@@ -488,7 +498,7 @@ declare global {
488
498
  const mounted = window.AgentWidgetLauncher.mount({ target, config: widgetConfig, onOpen });
489
499
  launcherHandle = mounted;
490
500
 
491
- // The real launcher is now painted at page-load time emit the page-load
501
+ // The real launcher is now painted at page-load time: emit the page-load
492
502
  // "appeared" signal (distinct from `onChatReady`, which waits for first open).
493
503
  safeCall(config.onLauncherShown, { deferred: true, element: mounted.element });
494
504
  dispatchLifecycle("persona:launcher-shown", { deferred: true, element: mounted.element });
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Critical-path launcher entry built to `launcher.global.js` (IIFE).
2
+ * Critical-path launcher entry: built to `launcher.global.js` (IIFE).
3
3
  *
4
- * Ships ONLY the real collapsed launcher (`createLauncherButton`) plus the full
5
- * theme system, so the launcher paints pixel-identically to the full widget from
6
- * a tiny bundle (~13 KB brotli vs ~134 KB for `index.global.js`). The heavy
7
- * conversation panel is deferred until first open by the installer (Phase 2).
4
+ * Ships ONLY the real collapsed launcher (`createLauncherButton`) plus the
5
+ * theme application path, so the launcher paints pixel-identically to the full
6
+ * widget from a tiny bundle. The heavy conversation panel is deferred until
7
+ * first open by the installer (Phase 2).
8
8
  *
9
9
  * The full Lucide icon registry is kept on purpose: any *supported* icon a site
10
10
  * configures (`launcher.agentIconName` / `callToActionIconName`) must render at
@@ -18,13 +18,13 @@
18
18
  */
19
19
  import { createLauncherButton } from "./components/launcher";
20
20
  import { applyThemeVariables } from "./utils/theme";
21
- import { mergeWithDefaults } from "./defaults";
21
+ import { DEFAULT_LAUNCHER_CONFIG } from "./defaults";
22
22
  import type { AgentWidgetConfig } from "./types";
23
23
 
24
24
  export interface AgentWidgetLauncherMountOptions {
25
25
  /** Where to mount. Defaults to `document.body` (the floating launcher is `position: fixed`). */
26
26
  target?: string | HTMLElement;
27
- /** The same widget config the full widget will receive drives theme, icons, position, copy. */
27
+ /** The same widget config the full widget will receive: drives theme, icons, position, copy. */
28
28
  config?: AgentWidgetConfig;
29
29
  /** Called when the launcher is clicked; the installer loads the full widget and opens the panel. */
30
30
  onOpen: () => void;
@@ -56,6 +56,20 @@ const resolveTarget = (target?: string | HTMLElement): HTMLElement => {
56
56
  return document.body;
57
57
  };
58
58
 
59
+ const mergeCriticalLauncherConfig = (
60
+ config?: AgentWidgetConfig
61
+ ): AgentWidgetConfig => ({
62
+ ...config,
63
+ launcher: {
64
+ ...DEFAULT_LAUNCHER_CONFIG,
65
+ ...config?.launcher,
66
+ dock: {
67
+ ...DEFAULT_LAUNCHER_CONFIG.dock,
68
+ ...config?.launcher?.dock,
69
+ },
70
+ },
71
+ });
72
+
59
73
  /**
60
74
  * Mount the real collapsed launcher from the critical bundle.
61
75
  *
@@ -77,12 +91,9 @@ export const mount = (
77
91
  const { onOpen } = options;
78
92
  const target = resolveTarget(options.target);
79
93
 
80
- // Render from the SAME effective config the full widget uses: `ui.ts` runs
81
- // `mergeWithDefaults(config)` before building the launcher (ui.ts:495). Without
82
- // this, the critical launcher misses launcher defaults like
83
- // `callToActionIconPadding` and `agentIconName`, so it looks subtly different
84
- // from the full widget that replaces it on open.
85
- const config = mergeWithDefaults(options.config) as AgentWidgetConfig;
94
+ // Render from the same launcher defaults the full widget uses without pulling
95
+ // the full widget default object into the critical bundle.
96
+ const config = mergeCriticalLauncherConfig(options.config);
86
97
 
87
98
  const root = document.createElement("div");
88
99
  root.setAttribute("data-persona-root", "true");
@@ -97,7 +108,7 @@ export const mount = (
97
108
  root,
98
109
  element: launcher.element,
99
110
  update: (next: AgentWidgetConfig) => {
100
- const merged = mergeWithDefaults(next) as AgentWidgetConfig;
111
+ const merged = mergeCriticalLauncherConfig(next);
101
112
  applyThemeVariables(root, merged);
102
113
  launcher.update(merged);
103
114
  },
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Eager markdown-parser registration for the bundled (ESM / CJS) builds.
3
+ *
4
+ * npm consumers bundle `marked` and `dompurify` directly anyway, so there is no
5
+ * benefit to deferring them — and the public `markdownPostprocessor()` /
6
+ * `createDefaultSanitizer()` API is synchronous. Importing this module for its
7
+ * side effect registers both parsers up front so `getMarkdownParsersSync()`
8
+ * resolves them on the first render.
9
+ *
10
+ * This module is intentionally NOT reachable from `index-global.ts` (the IIFE
11
+ * entry): it statically imports `marked` + `dompurify`, so pulling it into the
12
+ * CDN bundle would defeat the lazy `markdown-parsers.js` chunk. It is imported
13
+ * only from `index.ts` (the npm barrel) and from unit tests that exercise the
14
+ * synchronous parsers directly.
15
+ */
16
+ import { Marked } from "marked";
17
+ import DOMPurify from "dompurify";
18
+ import { provideMarkdownParsers } from "./markdown-parsers-loader";
19
+
20
+ provideMarkdownParsers({ Marked, DOMPurify });
@@ -0,0 +1,4 @@
1
+ import { Marked } from "marked";
2
+ import DOMPurify from "dompurify";
3
+
4
+ export { Marked, DOMPurify };
@@ -0,0 +1,49 @@
1
+ import type { Marked } from "marked";
2
+ import type DOMPurify from "dompurify";
3
+
4
+ export type MarkdownParsersModule = {
5
+ Marked: typeof Marked;
6
+ DOMPurify: typeof DOMPurify;
7
+ };
8
+
9
+ let loader: (() => Promise<MarkdownParsersModule>) | null = null;
10
+ let moduleCache: MarkdownParsersModule | null = null;
11
+ let loadPromise: Promise<MarkdownParsersModule> | null = null;
12
+
13
+ export const setMarkdownParsersLoader = (l: () => Promise<MarkdownParsersModule>) => {
14
+ loader = l;
15
+ };
16
+
17
+ /**
18
+ * Register the parsers synchronously. Used by the ESM/CJS build (where `marked`
19
+ * and `dompurify` are bundled directly via `markdown-parsers-eager.ts`), so
20
+ * `getMarkdownParsersSync()` returns them on the very first render and the
21
+ * synchronous `markdownPostprocessor` / `createDefaultSanitizer` API keeps
22
+ * working without an async round-trip. The IIFE/CDN build never calls this;
23
+ * it lazy-loads the `markdown-parsers.js` chunk instead.
24
+ */
25
+ export const provideMarkdownParsers = (mod: MarkdownParsersModule): void => {
26
+ moduleCache = mod;
27
+ };
28
+
29
+ export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
30
+ if (moduleCache) return Promise.resolve(moduleCache);
31
+ if (loadPromise) return loadPromise;
32
+ if (!loader) {
33
+ // Fallback for regular ESM/CJS consumers (they import directly)
34
+ loadPromise = import("./markdown-parsers-entry").then((mod) => {
35
+ moduleCache = mod;
36
+ return mod;
37
+ });
38
+ return loadPromise;
39
+ }
40
+ loadPromise = loader().then((mod) => {
41
+ moduleCache = mod;
42
+ return mod;
43
+ });
44
+ return loadPromise;
45
+ };
46
+
47
+ export const getMarkdownParsersSync = (): MarkdownParsersModule | null => {
48
+ return moduleCache;
49
+ };
@@ -31,7 +31,7 @@ describe("injectStyles / getStyleRoot", () => {
31
31
  expect(style?.textContent).toContain("color: red");
32
32
  });
33
33
 
34
- it("is idempotent repeated calls add only one <style>", () => {
34
+ it("is idempotent: repeated calls add only one <style>", () => {
35
35
  const el = document.createElement("div");
36
36
  document.body.appendChild(el);
37
37
 
package/src/plugin-kit.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Plugin Kit small, dependency-free utilities for authoring Persona plugins.
1
+ // Plugin Kit: small, dependency-free utilities for authoring Persona plugins.
2
2
  //
3
3
  // Plugin render hooks (`renderApproval`, `renderAskUserQuestion`, `renderMessage`,
4
4
  // …) return a detached `HTMLElement` that the widget morphs into the transcript.
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // 1. Injecting the plugin's own CSS so it survives the widget's Shadow-DOM mode
8
8
  // (a plain `document.head` <style> does NOT pierce a shadow root).
9
- // 2. Floating UI dropdowns, menus, tooltips that must overlay the rest of
9
+ // 2. Floating UI, dropdowns, menus, tooltips, that must overlay the rest of
10
10
  // the widget and not be clipped by the transcript's scroll container.
11
11
  //
12
12
  // Both are easy to get subtly wrong, so they live here as a supported, optional
@@ -42,7 +42,7 @@ function injectInto(root: Document | ShadowRoot, id: string, css: string): void
42
42
  root instanceof Document ? root.head : root;
43
43
  const escaped = id.replace(/["\\]/g, "\\$&");
44
44
  if (container.querySelector(`style[data-persona-plugin-style="${escaped}"]`)) {
45
- return; // Already present in this root idempotent.
45
+ return; // Already present in this root: idempotent.
46
46
  }
47
47
  const doc = root instanceof Document ? root : root.ownerDocument ?? document;
48
48
  const style = doc.createElement("style");
@@ -52,15 +52,15 @@ function injectInto(root: Document | ShadowRoot, id: string, css: string): void
52
52
  }
53
53
 
54
54
  /**
55
- * Inject a plugin's CSS once into the correct root the widget's shadow root
55
+ * Inject a plugin's CSS once into the correct root: the widget's shadow root
56
56
  * when shadowed, the document head otherwise. Idempotent: keyed by `id`, so it
57
57
  * is safe to call on every render.
58
58
  *
59
59
  * Pass the element you're about to return from a render hook. While building, an
60
60
  * element is detached and its eventual root is unknown, so this injects into the
61
61
  * owning document immediately (covering the default light-DOM case with no
62
- * flash) and then, on the next microtask after the widget has mounted the
63
- * element re-resolves and also injects into its shadow root if it landed in
62
+ * flash) and then, on the next microtask: after the widget has mounted the
63
+ * element: re-resolves and also injects into its shadow root if it landed in
64
64
  * one. You may also pass an explicit `Document` or `ShadowRoot`.
65
65
  *
66
66
  * @example
@@ -124,7 +124,7 @@ export interface PopoverOptions {
124
124
  zIndex?: number | null;
125
125
  /**
126
126
  * Where to mount `content`. Defaults to the anchor's shadow root (when
127
- * shadowed) or `document.body` keeping it inside the same style + stacking
127
+ * shadowed) or `document.body`: keeping it inside the same style + stacking
128
128
  * scope as the anchor while escaping the transcript's scroll clipping.
129
129
  */
130
130
  container?: HTMLElement | ShadowRoot;
@@ -64,7 +64,7 @@ export interface AgentWidgetPlugin {
64
64
  defaultRenderer: () => HTMLElement;
65
65
  onSubmit: (text: string) => void;
66
66
  /**
67
- * When true, the assistant stream is active same moment `session.isStreaming()` becomes true.
67
+ * When true, the assistant stream is active: same moment `session.isStreaming()` becomes true.
68
68
  * Prefer wiring controls to `data-persona-composer-disable-when-streaming` plus `setComposerDisabled`
69
69
  * in the host, or react to `footer.dataset.personaComposerStreaming === "true"`.
70
70
  */
@@ -113,14 +113,14 @@ export interface AgentWidgetPlugin {
113
113
  * Custom renderer for `ask_user_question` tool calls.
114
114
  *
115
115
  * When a plugin returns an `HTMLElement`, it is inserted into the transcript
116
- * in place of the default (which is no transcript bubble the built-in
116
+ * in place of the default (which is no transcript bubble: the built-in
117
117
  * renders a sheet over the composer). The built-in composer-overlay sheet
118
118
  * is suppressed so the plugin's UI fully owns the interaction.
119
119
  *
120
120
  * Return `null` to fall through to the built-in overlay sheet.
121
121
  *
122
122
  * The context gives you a pre-parsed `payload` (may be partial while the
123
- * tool call is still streaming check `complete`) and two callbacks:
123
+ * tool call is still streaming: check `complete`) and two callbacks:
124
124
  * `resolve(answer)` resumes the paused LOCAL tool with the user's answer,
125
125
  * and `dismiss()` cancels with the sentinel `"(dismissed)"`.
126
126
  *
@@ -170,8 +170,8 @@ export interface AgentWidgetPlugin {
170
170
  *
171
171
  * Return an `HTMLElement` to fully own the approval UI, `defaultRenderer()`
172
172
  * to render (or wrap) the built-in bubble, or `null` to fall through to the
173
- * default. Unlike the built-in bubble whose Approve/Deny buttons are wired
174
- * via delegation a fully custom element resolves the approval by calling
173
+ * default. Unlike the built-in bubble: whose Approve/Deny buttons are wired
174
+ * via delegation: a fully custom element resolves the approval by calling
175
175
  * the `approve`/`deny` callbacks. Both route through the same path the
176
176
  * built-in buttons use (optimistic update, `onDecision`, in-place anchoring).
177
177
  *
@@ -1,5 +1,6 @@
1
- import { Marked, type RendererObject } from "marked";
1
+ import type { RendererObject } from "marked";
2
2
  import type { AgentWidgetMarkdownConfig, AgentWidgetMarkdownRendererOverrides, AgentWidgetMarkdownOptions } from "./types";
3
+ import { getMarkdownParsersSync } from "./markdown-parsers-loader";
3
4
 
4
5
  /**
5
6
  * Options for creating a markdown processor
@@ -48,20 +49,32 @@ const convertRendererOverrides = (
48
49
  * ```
49
50
  */
50
51
  export const createMarkdownProcessor = (options?: MarkdownProcessorOptions) => {
51
- const opts = options?.markedOptions;
52
- const markedInstance = new Marked({
53
- gfm: opts?.gfm ?? true,
54
- breaks: opts?.breaks ?? true,
55
- pedantic: opts?.pedantic,
56
- silent: opts?.silent,
57
- });
58
-
59
- const rendererOverrides = convertRendererOverrides(options?.renderer);
60
- if (rendererOverrides) {
61
- markedInstance.use({ renderer: rendererOverrides });
62
- }
63
-
52
+ let markedInstance: any = null;
53
+
64
54
  return (text: string): string => {
55
+ const parsers = getMarkdownParsersSync();
56
+ if (!parsers) {
57
+ // If the markdown parser hasn't loaded yet, fall back to plain text with HTML escaped.
58
+ // The widget will re-render automatically once the parser finishes loading.
59
+ return escapeHtml(text);
60
+ }
61
+
62
+ if (!markedInstance) {
63
+ const { Marked } = parsers;
64
+ const opts = options?.markedOptions;
65
+ markedInstance = new Marked({
66
+ gfm: opts?.gfm ?? true,
67
+ breaks: opts?.breaks ?? true,
68
+ pedantic: opts?.pedantic,
69
+ silent: opts?.silent,
70
+ });
71
+
72
+ const rendererOverrides = convertRendererOverrides(options?.renderer);
73
+ if (rendererOverrides) {
74
+ markedInstance.use({ renderer: rendererOverrides });
75
+ }
76
+ }
77
+
65
78
  return markedInstance.parse(text) as string;
66
79
  };
67
80
  };
@@ -227,7 +227,7 @@ describe("createWidgetHostLayout docked", () => {
227
227
  });
228
228
 
229
229
  it("clamps push and overlay dock slots without sticky (in-flow/absolute contexts)", () => {
230
- // push: the slot is an in-flow `position: relative` column max-height cap
230
+ // push: the slot is an in-flow `position: relative` column: max-height cap
231
231
  // only, no sticky. overlay: keeps absolute positioning.
232
232
  const cases = [
233
233
  { reveal: "push" as const, position: "relative" },
@@ -33,7 +33,7 @@ const parseDockWidthToPx = (width: string, shellClientWidth: number): number =>
33
33
  * the dock column would grow with the conversation and scroll off the page.
34
34
  * Clamping the slot keeps the panel viewport-sized (messages scroll
35
35
  * internally) even when the page's height chain is missing. `100vh` is set
36
- * first as a fallback for engines without `dvh` support an invalid value
36
+ * first as a fallback for engines without `dvh` support: an invalid value
37
37
  * leaves the previous one in place.
38
38
  */
39
39
  const applyDockSlotMaxHeight = (
@@ -372,7 +372,7 @@ const applyDockStyles = (
372
372
  // the containing block for any `position: fixed` descendant, so host pages
373
373
  // that render viewport-fixed chrome inside the pushed content (e.g. the
374
374
  // dashboard editor's `fixed top-0 right-0` toolbar) resolve `right: 0`
375
- // against the track's right edge `panelPx` past the viewport, off-screen.
375
+ // against the track's right edge: `panelPx` past the viewport, off-screen.
376
376
  // `margin-left` produces the identical visual offset (the track is clipped
377
377
  // by the overflow:hidden shell) without establishing a containing block for
378
378
  // fixed OR absolutely-positioned descendants.
@@ -395,8 +395,7 @@ const applyDockStyles = (
395
395
  pushTrack.style.width = `${contentPx + panelPx}px`;
396
396
  pushTrack.style.transition = dockTransition;
397
397
  pushTrack.style.marginLeft = `${marginOffsetPx}px`;
398
- // Defensively clear any transform a previous reveal mode may have set
399
- // leaving one would re-establish the fixed-position containing block.
398
+ // Defensively clear any transform a previous reveal mode may have set: // leaving one would re-establish the fixed-position containing block.
400
399
  pushTrack.style.transform = "";
401
400
 
402
401
  contentSlot.style.flex = "0 0 auto";
@@ -536,7 +535,7 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
536
535
  syncPushResizeObserver();
537
536
  // Check the height chain once, the first time the panel is actually shown
538
537
  // in a layout that depends on it (mobile fullscreen is fixed-position and
539
- // doesn't keep checking until a dependent layout comes around).
538
+ // doesn't: keep checking until a dependent layout comes around).
540
539
  if (
541
540
  expanded &&
542
541
  !heightChainChecked &&
@@ -161,7 +161,7 @@ export const initAgentWidget = (
161
161
 
162
162
  mountController();
163
163
  // Fired when the controller is mounted and its API is callable. `onReady` is
164
- // the deprecated alias of `onChatReady` (removed next major) warn once.
164
+ // the deprecated alias of `onChatReady` (removed next major): warn once.
165
165
  if (options.onChatReady) {
166
166
  options.onChatReady();
167
167
  } else if (options.onReady) {
@@ -169,7 +169,7 @@ export const initAgentWidget = (
169
169
  warnedOnReadyDeprecated = true;
170
170
  // eslint-disable-next-line no-console
171
171
  console.warn(
172
- "[Persona] `onReady` is deprecated use `onChatReady`. `onReady` still works but is removed in the next major."
172
+ "[Persona] `onReady` is deprecated: use `onChatReady`. `onReady` still works but is removed in the next major."
173
173
  );
174
174
  }
175
175
  options.onReady();
@@ -379,8 +379,7 @@ describe('AgentWidgetSession - cancel()', () => {
379
379
 
380
380
  it('aborts the in-flight dispatch and flips streaming/status back to idle', async () => {
381
381
  let capturedSignal: AbortSignal | null = null;
382
- // Fetch returns a promise that only settles when the AbortSignal fires
383
- // modeling a dispatch that's still receiving SSE tokens.
382
+ // Fetch returns a promise that only settles when the AbortSignal fires: // modeling a dispatch that's still receiving SSE tokens.
384
383
  global.fetch = vi.fn().mockImplementation((_url: string, options: any) => {
385
384
  capturedSignal = options.signal as AbortSignal;
386
385
  return new Promise((_resolve, reject) => {
@@ -403,7 +402,7 @@ describe('AgentWidgetSession - cancel()', () => {
403
402
  }
404
403
  );
405
404
 
406
- // Kick off the dispatch but don't await we want it in-flight when we cancel.
405
+ // Kick off the dispatch but don't await: we want it in-flight when we cancel.
407
406
  const dispatchPromise = session.sendMessage('Hello');
408
407
  // Let the session set up the AbortController and call fetch.
409
408
  await Promise.resolve();
@@ -554,7 +553,7 @@ describe('AgentWidgetSession.resolveAskUserQuestion', () => {
554
553
 
555
554
  vi.spyOn(session, 'connectStream').mockResolvedValue(undefined);
556
555
 
557
- // Capture the flag state at the exact moment fetch is called this is
556
+ // Capture the flag state at the exact moment fetch is called: this is
558
557
  // the "before the POST fires" assertion. The flag must be flipped BEFORE
559
558
  // any network I/O so the subsequent stream-driven renders skip the sheet.
560
559
  let flagAtFetch: { awaiting?: boolean; answered?: boolean } | undefined;
@@ -666,7 +665,7 @@ describe('AgentWidgetSession.resolveAskUserQuestion', () => {
666
665
 
667
666
  vi.spyOn(session, 'connectStream').mockResolvedValue(undefined);
668
667
 
669
- // Kick off don't await.
668
+ // Kick off: don't await.
670
669
  const resolvePromise = session.resolveAskUserQuestion(awaiting, 'Hobbyists');
671
670
 
672
671
  // Yield a microtask so synchronous setup (markResolved → setStreaming(true)
@@ -920,7 +919,7 @@ describe('AgentWidgetSession - approval context across agent_approval_complete',
920
919
  }
921
920
  );
922
921
 
923
- // `agent_approval_complete` carries only the resolution none of the
922
+ // `agent_approval_complete` carries only the resolution: none of the
924
923
  // context fields from `agent_approval_start`. The session merge must keep
925
924
  // them so a full re-render of the resolved bubble (morph, virtual-scroll
926
925
  // re-mount, storage restore) still shows the tool, description, and the