@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.
- 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 +67 -12
- package/src/client.ts +40 -32
- 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/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" },
|
|
@@ -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
|
|
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
|
|
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
|
|
538
|
+
// doesn't: keep checking until a dependent layout comes around).
|
|
540
539
|
if (
|
|
541
540
|
expanded &&
|
|
542
541
|
!heightChainChecked &&
|
package/src/runtime/init.ts
CHANGED
|
@@ -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)
|
|
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
|
|
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();
|
package/src/session.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|