@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.
- package/README.md +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
package/src/index-global.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* IIFE entry point
|
|
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
|
|
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"`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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";
|
package/src/install.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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())
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 });
|
package/src/launcher-global.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Critical-path launcher entry
|
|
2
|
+
* Critical-path launcher entry: built to `launcher.global.js` (IIFE).
|
|
3
3
|
*
|
|
4
|
-
* Ships ONLY the real collapsed launcher (`createLauncherButton`) plus the
|
|
5
|
-
* theme
|
|
6
|
-
* a tiny bundle
|
|
7
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
81
|
-
//
|
|
82
|
-
|
|
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 =
|
|
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,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
|
+
};
|
package/src/plugin-kit.test.ts
CHANGED
|
@@ -31,7 +31,7 @@ describe("injectStyles / getStyleRoot", () => {
|
|
|
31
31
|
expect(style?.textContent).toContain("color: red");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it("is idempotent
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
-
* element
|
|
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
|
|
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;
|
package/src/plugins/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
174
|
-
* via delegation
|
|
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
|
*
|
package/src/postprocessors.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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" },
|