@runtypelabs/persona 3.34.1 → 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 +12 -12
  53. package/src/client.ts +30 -31
  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
@@ -1,18 +1,18 @@
1
1
  /**
2
- * IIFE entry point bundled for `<script>` tag consumers.
2
+ * IIFE entry point: bundled for `<script>` tag consumers.
3
3
  *
4
4
  * This file re-exports everything from the main entry AND side-imports all
5
5
  * built-in subpath animations so they register automatically. Script-tag
6
6
  * users who include the global build don't need extra script tags or
7
- * registration calls setting `features.streamAnimation.type` to any
7
+ * registration calls: setting `features.streamAnimation.type` to any
8
8
  * built-in name just works.
9
9
  *
10
10
  * npm consumers continue to import from the main entry (`import ... from
11
- * "@runtypelabs/persona"`) those animations stay in their subpath
11
+ * "@runtypelabs/persona"`): those animations stay in their subpath
12
12
  * modules so bundlers can tree-shake them.
13
13
  */
14
14
 
15
- // Re-export the full public API from `index-core` (NOT `index`) so the
15
+ // Re-export the full public API: from `index-core` (NOT `index`) so the
16
16
  // dev-only helpers (`generateCodeSnippet`, `createDemoCarousel`) stay out of the
17
17
  // CDN/IIFE bundle. npm consumers still get them via the `index.ts` barrel.
18
18
  export * from "./index-core";
@@ -35,7 +35,7 @@ export type { StreamAnimationPlugin, StreamAnimationContext } from "./types";
35
35
  // ---------------------------------------------------------------------------
36
36
  // Deferred WebMCP polyfill loading.
37
37
  //
38
- // This bundle is built with `@mcp-b/webmcp-polyfill` external the bridge's
38
+ // This bundle is built with `@mcp-b/webmcp-polyfill` external: the bridge's
39
39
  // default `import("@mcp-b/webmcp-polyfill")` is a bare specifier no browser
40
40
  // can resolve, so register a loader that imports the self-contained
41
41
  // `webmcp-polyfill.js` chunk from a sibling URL instead. Mirrors how
@@ -44,7 +44,7 @@ export type { StreamAnimationPlugin, StreamAnimationContext } from "./types";
44
44
 
45
45
  import { setWebMcpPolyfillLoader } from "./webmcp-bridge";
46
46
 
47
- // Capture at module-evaluation time `document.currentScript` is null once
47
+ // Capture at module-evaluation time: `document.currentScript` is null once
48
48
  // execution leaves the script's initial synchronous run.
49
49
  const widgetScriptSrc: string | null =
50
50
  typeof document !== "undefined"
@@ -70,3 +70,70 @@ setWebMcpPolyfillLoader(() => {
70
70
  // leaves it untouched (and must not try to bundle it).
71
71
  return import(/* @vite-ignore */ chunkUrl);
72
72
  });
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Deferred Markdown Parsers (marked + dompurify) loading.
76
+ //
77
+ // This bundle is built with `./markdown-parsers-entry` external: the Markdown
78
+ // and HTML sanitization libraries are kept out of the CDN payload. Register a
79
+ // loader that imports the self-contained `markdown-parsers.js` chunk from a sibling
80
+ // URL. The session prefetches it at init so it's warm before the first message.
81
+ // ---------------------------------------------------------------------------
82
+
83
+ import { setMarkdownParsersLoader, loadMarkdownParsers } from "./markdown-parsers-loader";
84
+
85
+ setMarkdownParsersLoader(() => {
86
+ const chunkUrl = widgetScriptSrc?.replace(
87
+ /index\.global\.js($|\?)/,
88
+ "markdown-parsers.js$1",
89
+ );
90
+ if (!chunkUrl || chunkUrl === widgetScriptSrc) {
91
+ return Promise.reject(
92
+ new Error(
93
+ "Could not derive the markdown-parsers.js URL from the widget script URL " +
94
+ `(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
95
+ "rename index.global.js should host markdown-parsers.js alongside it.",
96
+ ),
97
+ );
98
+ }
99
+ return import(/* @vite-ignore */ chunkUrl);
100
+ });
101
+
102
+ // Kick off the load immediately since it will likely be needed.
103
+ loadMarkdownParsers().catch(err => {
104
+ // It's okay if this fails (e.g. ad blocker), it'll just fall back to plain text.
105
+ if (typeof console !== "undefined") {
106
+ console.warn("[Persona] Failed to pre-load markdown parsers", err);
107
+ }
108
+ });
109
+ // ---------------------------------------------------------------------------
110
+ // Deferred Runtype TTS engine loading.
111
+ //
112
+ // This bundle is built with `./runtype-speech-engine` external (see
113
+ // `tsup.global.config.ts`): the hosted read-aloud engine + the
114
+ // AudioPlaybackManager it bundles are kept out of the CDN payload. Register a
115
+ // loader that imports the self-contained `runtype-tts.js` chunk from a sibling
116
+ // URL; the session prefetches it at init when `textToSpeech.provider:'runtype'`
117
+ // is set, so it's warm before the first click. Same pattern as the WebMCP
118
+ // polyfill above.
119
+ // ---------------------------------------------------------------------------
120
+
121
+ import { setRuntypeTtsLoader } from "./voice/runtype-tts-loader";
122
+
123
+ setRuntypeTtsLoader(() => {
124
+ const chunkUrl = widgetScriptSrc?.replace(
125
+ /index\.global\.js($|\?)/,
126
+ "runtype-tts.js$1",
127
+ );
128
+ if (!chunkUrl || chunkUrl === widgetScriptSrc) {
129
+ return Promise.reject(
130
+ new Error(
131
+ "Could not derive the runtype-tts.js URL from the widget script URL " +
132
+ `(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
133
+ "rename index.global.js should host runtype-tts.js alongside it, or set " +
134
+ "textToSpeech.createEngine to supply a speech engine directly.",
135
+ ),
136
+ );
137
+ }
138
+ return import(/* @vite-ignore */ chunkUrl);
139
+ });
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * npm package entry (`@runtypelabs/persona`).
3
3
  *
4
4
  * This is a thin barrel over `index-core.ts` (the shared public API) that adds
5
- * back the **dev/config-tool-only** helpers `generateCodeSnippet` and
5
+ * back the **dev/config-tool-only** helpers: `generateCodeSnippet` and
6
6
  * `createDemoCarousel`. Those are kept out of `index-core.ts` so the IIFE/CDN
7
7
  * build (`index-global.ts`, which re-exports from `index-core.ts`) doesn't ship
8
8
  * them: a running widget never needs them, only build-time/demo tooling does.
@@ -12,6 +12,12 @@
12
12
  * `createDemoCarousel`.
13
13
  */
14
14
 
15
+ // Register `marked` + `dompurify` synchronously for the bundled npm build so
16
+ // the synchronous markdown/sanitize API renders on first paint. The IIFE/CDN
17
+ // entry (`index-global.ts`) does NOT import this; it lazy-loads the parsers
18
+ // from the `markdown-parsers.js` chunk instead. Must run before any render.
19
+ import "./markdown-parsers-eager";
20
+
15
21
  // Full public API (everything except the two dev-only helpers below).
16
22
  export * from "./index-core";
17
23
  export { default } from "./index-core";
@@ -70,7 +70,7 @@ function provideLauncherBundle() {
70
70
  /**
71
71
  * Force the next dynamically-inserted <script> to error. jsdom never fires
72
72
  * load/error on injected scripts, so we trigger `onerror` on a fake timer that
73
- * `runAllTimersAsync()` flushes deterministic under fake timers.
73
+ * `runAllTimersAsync()` flushes: deterministic under fake timers.
74
74
  */
75
75
  function failNextScriptLoad() {
76
76
  const head = document.head;
@@ -137,15 +137,15 @@ afterEach(() => {
137
137
  vi.restoreAllMocks();
138
138
  });
139
139
 
140
- describe("install.ts onScriptLoad beacon", () => {
140
+ describe("install.ts: onScriptLoad beacon", () => {
141
141
  it("fires synchronously on script execution, before any loading or gating", async () => {
142
142
  const events = recordEvents();
143
143
  const onScriptLoad = vi.fn();
144
144
 
145
- // No config at all the beacon must still fire (diagnostics: "did my embed run").
145
+ // No config at all: the beacon must still fire (diagnostics: "did my embed run").
146
146
  await install({ onScriptLoad });
147
147
 
148
- // Fired during module evaluation no timer flush needed.
148
+ // Fired during module evaluation: no timer flush needed.
149
149
  expect(onScriptLoad).toHaveBeenCalledTimes(1);
150
150
  expect(onScriptLoad).toHaveBeenCalledWith({ version: "latest" });
151
151
  expect(events.types()[0]).toBe("persona:script-load");
@@ -160,7 +160,7 @@ describe("install.ts — onScriptLoad beacon", () => {
160
160
  });
161
161
  });
162
162
 
163
- describe("install.ts deferred launcher path", () => {
163
+ describe("install.ts: deferred launcher path", () => {
164
164
  it("mounts the critical launcher, fires onLauncherShown, and defers onChatReady until open", async () => {
165
165
  markCssLoaded();
166
166
  provideFullBundle(); // so loadJS resolves instantly when the user opens
@@ -186,7 +186,7 @@ describe("install.ts — deferred launcher path", () => {
186
186
  expect(launcherMount).toHaveBeenCalledTimes(1);
187
187
  expect(onLauncherShown).toHaveBeenCalledTimes(1);
188
188
  expect(onLauncherShown).toHaveBeenCalledWith({ deferred: true, element: launcherElement });
189
- // The full widget has NOT been initialized yet that waits for first open.
189
+ // The full widget has NOT been initialized yet: that waits for first open.
190
190
  expect(initAgentWidget).not.toHaveBeenCalled();
191
191
  expect(onChatReady).not.toHaveBeenCalled();
192
192
 
@@ -230,7 +230,7 @@ describe("install.ts — deferred launcher path", () => {
230
230
  });
231
231
  });
232
232
 
233
- describe("install.ts eager path", () => {
233
+ describe("install.ts: eager path", () => {
234
234
  it("non-floating (launcher disabled) eager-loads, fires onChatReady, and does NOT fire onLauncherShown", async () => {
235
235
  markCssLoaded();
236
236
  provideFullBundle();
@@ -272,7 +272,7 @@ describe("install.ts — eager path", () => {
272
272
  });
273
273
  });
274
274
 
275
- describe("install.ts deferral gate", () => {
275
+ describe("install.ts: deferral gate", () => {
276
276
  async function expectDecision(config: any, { defers }: { defers: boolean }) {
277
277
  markCssLoaded();
278
278
  provideFullBundle();
@@ -345,7 +345,7 @@ describe("install.ts — deferral gate", () => {
345
345
  });
346
346
  });
347
347
 
348
- describe("install.ts onReady deprecation alias", () => {
348
+ describe("install.ts: onReady deprecation alias", () => {
349
349
  it("still calls the deprecated onReady and warns once", async () => {
350
350
  markCssLoaded();
351
351
  provideFullBundle();
@@ -376,7 +376,7 @@ describe("install.ts — onReady deprecation alias", () => {
376
376
  });
377
377
  });
378
378
 
379
- describe("install.ts onError", () => {
379
+ describe("install.ts: onError", () => {
380
380
  it("fires onError with phase 'init' and dispatches persona:error when initialization throws", async () => {
381
381
  markCssLoaded();
382
382
  provideFullBundle();
@@ -420,7 +420,7 @@ describe("install.ts — onError", () => {
420
420
  });
421
421
  });
422
422
 
423
- describe("install.ts lifecycle callback safety", () => {
423
+ describe("install.ts: lifecycle callback safety", () => {
424
424
  it("a throwing onScriptLoad callback does not break the rest of installation", async () => {
425
425
  markCssLoaded();
426
426
  provideFullBundle();
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" },