@runtypelabs/persona 2.2.0 → 2.3.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.
@@ -1,2 +1,2 @@
1
- "use strict";var SiteAgentInstaller=(()=>{var w=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var W=(s,o,d,t)=>{if(o&&typeof o=="object"||typeof o=="function")for(let a of P(o))!j.call(s,a)&&a!==d&&w(s,a,{get:()=>o[a],enumerable:!(t=v(o,a))||t.enumerable});return s};var T=s=>W(w({},"__esModule",{value:!0}),s);var F={};(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let o=(()=>{let n=document.currentScript;if(!n)return{};let e={},i=n.getAttribute("data-config");if(i)try{let c=JSON.parse(i);c.config?Object.assign(e,c):e.config=c}catch(c){console.error("Failed to parse data-config JSON:",c)}let r=n.getAttribute("data-runtype-token");r&&(e.clientToken=r);let l=n.getAttribute("data-flow-id");l&&(e.flowId=l);let f=n.getAttribute("data-api-url");f&&(e.apiUrl=f);let p=n.getAttribute("data-preview-param");return p&&(e.previewQueryParam=p),e})(),t={...window.siteAgentConfig||{},...o};if(!(()=>{if(!t.previewQueryParam)return!0;let e=new URLSearchParams(window.location.search).get(t.previewQueryParam);return e!==null&&e!==""&&e.toLowerCase()!=="false"&&e!=="0"})())return;let m=t.version||"latest",A=t.cdn||"jsdelivr",h=t.autoInit!==!1,C=()=>{if(t.cssUrl&&t.jsUrl)return{cssUrl:t.cssUrl,jsUrl:t.jsUrl};let e=`/npm/@runtypelabs/persona@${m}/dist`;return A==="unpkg"?{cssUrl:`https://unpkg.com${e}/widget.css`,jsUrl:`https://unpkg.com${e}/index.global.js`}:{cssUrl:`https://cdn.jsdelivr.net${e}/widget.css`,jsUrl:`https://cdn.jsdelivr.net${e}/index.global.js`}},{cssUrl:g,jsUrl:u}=C(),k=()=>!!document.head.querySelector("link[data-persona]")||!!document.head.querySelector('link[href*="widget.css"]'),y=()=>!!window.AgentWidget,S=n=>{let e=!1,i=()=>{e||(e=!0,n())},r=()=>{typeof requestIdleCallback!="undefined"?requestIdleCallback(()=>{requestAnimationFrame(()=>{requestAnimationFrame(i)})},{timeout:3e3}):setTimeout(i,300)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r()},U=()=>new Promise((n,e)=>{if(k()){n();return}let i=document.createElement("link");i.rel="stylesheet",i.href=g,i.setAttribute("data-persona","true"),i.onload=()=>n(),i.onerror=()=>e(new Error(`Failed to load CSS from ${g}`)),document.head.appendChild(i)}),b=()=>new Promise((n,e)=>{if(y()){n();return}let i=document.createElement("script");i.src=u,i.async=!0,i.onload=()=>n(),i.onerror=()=>e(new Error(`Failed to load JS from ${u}`)),document.head.appendChild(i)}),I=()=>{var r;if(!window.AgentWidget||!window.AgentWidget.initAgentWidget){console.warn("AgentWidget not available. Make sure the script loaded successfully.");return}let n=t.target||"body",e={...t.config};if(t.apiUrl&&!e.apiUrl&&(e.apiUrl=t.apiUrl),t.clientToken&&!e.clientToken&&(e.clientToken=t.clientToken),t.flowId&&!e.flowId&&(e.flowId=t.flowId),!(!(e.apiUrl||e.clientToken)&&Object.keys(e).length===0)){!e.postprocessMessage&&window.AgentWidget.markdownPostprocessor&&(e.postprocessMessage=({text:l})=>window.AgentWidget.markdownPostprocessor(l));try{window.AgentWidget.initAgentWidget({target:n,config:e,useShadowDom:(r=t.useShadowDom)!=null?r:!1})}catch(l){console.error("Failed to initialize AgentWidget:",l)}}};S(async()=>{try{await U(),await b(),h&&(t.config||t.apiUrl||t.clientToken)&&setTimeout(I,0)}catch(n){console.error("Failed to install AgentWidget:",n)}})})();return T(F);})();
1
+ "use strict";var SiteAgentInstaller=(()=>{var w=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var W=Object.prototype.hasOwnProperty;var T=(s,i,d,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let a of j(i))!W.call(s,a)&&a!==d&&w(s,a,{get:()=>i[a],enumerable:!(t=P(i,a))||t.enumerable});return s};var _=s=>T(w({},"__esModule",{value:!0}),s);var F={};(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let i=(()=>{let n=document.currentScript;if(!n)return{};let e={},o=n.getAttribute("data-config");if(o)try{let c=JSON.parse(o);if(c.config){let{__proto__:E,constructor:M,prototype:$,...v}=c;Object.assign(e,v)}else e.config=c}catch(c){console.error("Failed to parse data-config JSON:",c)}let r=n.getAttribute("data-runtype-token");r&&(e.clientToken=r);let l=n.getAttribute("data-flow-id");l&&(e.flowId=l);let f=n.getAttribute("data-api-url");f&&(e.apiUrl=f);let p=n.getAttribute("data-preview-param");return p&&(e.previewQueryParam=p),e})(),t={...window.siteAgentConfig||{},...i};if(!(()=>{if(!t.previewQueryParam)return!0;let e=new URLSearchParams(window.location.search).get(t.previewQueryParam);return e!==null&&e!==""&&e.toLowerCase()!=="false"&&e!=="0"})())return;let m=t.version||"latest",A=t.cdn||"jsdelivr",C=t.autoInit!==!1,h=()=>{if(t.cssUrl&&t.jsUrl)return{cssUrl:t.cssUrl,jsUrl:t.jsUrl};let e=`/npm/@runtypelabs/persona@${m}/dist`;return A==="unpkg"?{cssUrl:`https://unpkg.com${e}/widget.css`,jsUrl:`https://unpkg.com${e}/index.global.js`}:{cssUrl:`https://cdn.jsdelivr.net${e}/widget.css`,jsUrl:`https://cdn.jsdelivr.net${e}/index.global.js`}},{cssUrl:g,jsUrl:u}=h(),y=()=>!!document.head.querySelector("link[data-persona]")||!!document.head.querySelector('link[href*="widget.css"]'),k=()=>!!window.AgentWidget,S=n=>{let e=!1,o=()=>{e||(e=!0,n())},r=()=>{typeof requestIdleCallback!="undefined"?requestIdleCallback(()=>{requestAnimationFrame(()=>{requestAnimationFrame(o)})},{timeout:3e3}):setTimeout(o,300)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r()},U=()=>new Promise((n,e)=>{if(y()){n();return}let o=document.createElement("link");o.rel="stylesheet",o.href=g,o.setAttribute("data-persona","true"),o.onload=()=>n(),o.onerror=()=>e(new Error(`Failed to load CSS from ${g}`)),document.head.appendChild(o)}),b=()=>new Promise((n,e)=>{if(k()){n();return}let o=document.createElement("script");o.src=u,o.async=!0,o.onload=()=>n(),o.onerror=()=>e(new Error(`Failed to load JS from ${u}`)),document.head.appendChild(o)}),I=()=>{var r;if(!window.AgentWidget||!window.AgentWidget.initAgentWidget){console.warn("AgentWidget not available. Make sure the script loaded successfully.");return}let n=t.target||"body",e={...t.config};if(t.apiUrl&&!e.apiUrl&&(e.apiUrl=t.apiUrl),t.clientToken&&!e.clientToken&&(e.clientToken=t.clientToken),t.flowId&&!e.flowId&&(e.flowId=t.flowId),!(!(e.apiUrl||e.clientToken)&&Object.keys(e).length===0)){!e.postprocessMessage&&window.AgentWidget.markdownPostprocessor&&(e.postprocessMessage=({text:l})=>window.AgentWidget.markdownPostprocessor(l));try{window.AgentWidget.initAgentWidget({target:n,config:e,useShadowDom:(r=t.useShadowDom)!=null?r:!1})}catch(l){console.error("Failed to initialize AgentWidget:",l)}}};S(async()=>{try{await U(),await b(),C&&(t.config||t.apiUrl||t.clientToken)&&setTimeout(I,0)}catch(n){console.error("Failed to install AgentWidget:",n)}})})();return _(F);})();
2
2
  //# sourceMappingURL=install.global.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\nexport {};\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n // Client token mode options (can also be set via data attributes)\n clientToken?: string;\n flowId?: string;\n apiUrl?: string;\n // Optional query param key that gates widget installation in preview mode\n previewQueryParam?: string;\n // Shadow DOM option (defaults to false for better CSS compatibility)\n useShadowDom?: boolean;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n /**\n * Read configuration from data attributes on the current script tag.\n * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url\n */\n const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {\n // Try to get the current script element\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return {};\n\n const scriptConfig: Partial<SiteAgentInstallConfig> = {};\n\n // Full config from data-config attribute (JSON string)\n const configJson = script.getAttribute('data-config');\n if (configJson) {\n try {\n const parsedConfig = JSON.parse(configJson);\n // If it has nested 'config' property, use it; otherwise treat as widget config\n if (parsedConfig.config) {\n Object.assign(scriptConfig, parsedConfig);\n } else {\n // Treat the entire object as widget config\n scriptConfig.config = parsedConfig;\n }\n } catch (e) {\n console.error(\"Failed to parse data-config JSON:\", e);\n }\n }\n\n // Client token from data attribute (primary method for client token mode)\n const token = script.getAttribute('data-runtype-token');\n if (token) {\n scriptConfig.clientToken = token;\n }\n\n // Optional flow ID\n const flowId = script.getAttribute('data-flow-id');\n if (flowId) {\n scriptConfig.flowId = flowId;\n }\n\n // Optional API URL override\n const apiUrl = script.getAttribute('data-api-url');\n if (apiUrl) {\n scriptConfig.apiUrl = apiUrl;\n }\n\n // Optional preview query param gate\n const previewQueryParam = script.getAttribute('data-preview-param');\n if (previewQueryParam) {\n scriptConfig.previewQueryParam = previewQueryParam;\n }\n\n return scriptConfig;\n };\n\n // Get config from script attributes (must be called synchronously during script execution)\n const scriptConfig = getConfigFromScript();\n\n // Merge script attributes with window config (script attributes take precedence)\n const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };\n\n const isPreviewModeEnabled = (): boolean => {\n if (!config.previewQueryParam) {\n return true;\n }\n\n const params = new URLSearchParams(window.location.search);\n const value = params.get(config.previewQueryParam);\n return value !== null && value !== \"\" && value.toLowerCase() !== \"false\" && value !== \"0\";\n };\n\n if (!isPreviewModeEnabled()) {\n return;\n }\n \n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"@runtypelabs/persona\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-persona]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n /**\n * Wait for framework hydration to complete (Next.js, Nuxt, etc.)\n * This prevents the framework from removing dynamically added CSS during reconciliation.\n * Uses requestIdleCallback + double requestAnimationFrame for reliable detection.\n */\n const waitForHydration = (callback: () => void): void => {\n let executed = false;\n \n const execute = () => {\n if (executed) return;\n executed = true;\n callback();\n };\n\n const afterDom = () => {\n // Strategy 1: Use requestIdleCallback if available (best for detecting idle after hydration)\n if (typeof requestIdleCallback !== 'undefined') {\n requestIdleCallback(() => {\n // Double requestAnimationFrame ensures at least one full paint cycle completed\n requestAnimationFrame(() => {\n requestAnimationFrame(execute);\n });\n }, { timeout: 3000 }); // Max wait 3 seconds, then proceed anyway\n } else {\n // Strategy 2: Fallback for Safari (no requestIdleCallback)\n // 300ms is typically enough for hydration on most pages\n setTimeout(execute, 300);\n }\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', afterDom);\n } else {\n // DOM already ready, but still wait for potential hydration\n afterDom();\n }\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-persona\", \"true\");\n \n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge top-level config options into widget config\n const widgetConfig = { ...config.config };\n \n // Merge apiUrl from top-level config into widget config if present\n if (config.apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = config.apiUrl;\n }\n \n // Merge clientToken from top-level config into widget config if present\n if (config.clientToken && !widgetConfig.clientToken) {\n widgetConfig.clientToken = config.clientToken;\n }\n \n // Merge flowId from top-level config into widget config if present\n if (config.flowId && !widgetConfig.flowId) {\n widgetConfig.flowId = config.flowId;\n }\n\n // Only initialize if we have either apiUrl OR clientToken (or other config)\n const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;\n if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {\n return;\n }\n\n // Auto-apply markdown postprocessor if not explicitly set and available\n if (!widgetConfig.postprocessMessage && window.AgentWidget.markdownPostprocessor) {\n widgetConfig.postprocessMessage = ({ text }: { text: string }) => \n window.AgentWidget.markdownPostprocessor(text);\n }\n\n try {\n window.AgentWidget.initAgentWidget({\n target,\n config: widgetConfig,\n // Explicitly disable shadow DOM for better CSS compatibility with host page\n useShadowDom: config.useShadowDom ?? false\n });\n } catch (error) {\n console.error(\"Failed to initialize AgentWidget:\", error);\n }\n };\n\n // Main installation flow (called after hydration completes)\n const install = async () => {\n try {\n await loadCSS();\n await loadJS();\n \n // Auto-init if we have config OR apiUrl OR clientToken\n const shouldAutoInit = autoInit && (\n config.config || \n config.apiUrl || \n config.clientToken\n );\n \n if (shouldAutoInit) {\n // Wait a tick to ensure AgentWidget is fully initialized\n setTimeout(initWidget, 0);\n }\n } catch (error) {\n console.error(\"Failed to install AgentWidget:\", error);\n }\n };\n\n // Start installation after hydration completes\n // This prevents Next.js/Nuxt/etc. from removing dynamically added CSS\n waitForHydration(install);\n})();\n\n"],"mappings":"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","names":["install_exports","scriptConfig","script","configJson","parsedConfig","e","token","flowId","apiUrl","previewQueryParam","config","value","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","waitForHydration","callback","executed","execute","afterDom","loadCSS","resolve","reject","link","loadJS","initWidget","_a","target","widgetConfig","text","error"]}
1
+ {"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\nexport {};\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n // Client token mode options (can also be set via data attributes)\n clientToken?: string;\n flowId?: string;\n apiUrl?: string;\n // Optional query param key that gates widget installation in preview mode\n previewQueryParam?: string;\n // Shadow DOM option (defaults to false for better CSS compatibility)\n useShadowDom?: boolean;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n /**\n * Read configuration from data attributes on the current script tag.\n * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url\n */\n const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {\n // Try to get the current script element\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return {};\n\n const scriptConfig: Partial<SiteAgentInstallConfig> = {};\n\n // Full config from data-config attribute (JSON string)\n const configJson = script.getAttribute('data-config');\n if (configJson) {\n try {\n const parsedConfig = JSON.parse(configJson);\n // If it has nested 'config' property, use it; otherwise treat as widget config\n if (parsedConfig.config) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsedConfig;\n Object.assign(scriptConfig, safeConfig);\n } else {\n // Treat the entire object as widget config\n scriptConfig.config = parsedConfig;\n }\n } catch (e) {\n console.error(\"Failed to parse data-config JSON:\", e);\n }\n }\n\n // Client token from data attribute (primary method for client token mode)\n const token = script.getAttribute('data-runtype-token');\n if (token) {\n scriptConfig.clientToken = token;\n }\n\n // Optional flow ID\n const flowId = script.getAttribute('data-flow-id');\n if (flowId) {\n scriptConfig.flowId = flowId;\n }\n\n // Optional API URL override\n const apiUrl = script.getAttribute('data-api-url');\n if (apiUrl) {\n scriptConfig.apiUrl = apiUrl;\n }\n\n // Optional preview query param gate\n const previewQueryParam = script.getAttribute('data-preview-param');\n if (previewQueryParam) {\n scriptConfig.previewQueryParam = previewQueryParam;\n }\n\n return scriptConfig;\n };\n\n // Get config from script attributes (must be called synchronously during script execution)\n const scriptConfig = getConfigFromScript();\n\n // Merge script attributes with window config (script attributes take precedence)\n const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };\n\n const isPreviewModeEnabled = (): boolean => {\n if (!config.previewQueryParam) {\n return true;\n }\n\n const params = new URLSearchParams(window.location.search);\n const value = params.get(config.previewQueryParam);\n return value !== null && value !== \"\" && value.toLowerCase() !== \"false\" && value !== \"0\";\n };\n\n if (!isPreviewModeEnabled()) {\n return;\n }\n \n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"@runtypelabs/persona\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-persona]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n /**\n * Wait for framework hydration to complete (Next.js, Nuxt, etc.)\n * This prevents the framework from removing dynamically added CSS during reconciliation.\n * Uses requestIdleCallback + double requestAnimationFrame for reliable detection.\n */\n const waitForHydration = (callback: () => void): void => {\n let executed = false;\n \n const execute = () => {\n if (executed) return;\n executed = true;\n callback();\n };\n\n const afterDom = () => {\n // Strategy 1: Use requestIdleCallback if available (best for detecting idle after hydration)\n if (typeof requestIdleCallback !== 'undefined') {\n requestIdleCallback(() => {\n // Double requestAnimationFrame ensures at least one full paint cycle completed\n requestAnimationFrame(() => {\n requestAnimationFrame(execute);\n });\n }, { timeout: 3000 }); // Max wait 3 seconds, then proceed anyway\n } else {\n // Strategy 2: Fallback for Safari (no requestIdleCallback)\n // 300ms is typically enough for hydration on most pages\n setTimeout(execute, 300);\n }\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', afterDom);\n } else {\n // DOM already ready, but still wait for potential hydration\n afterDom();\n }\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-persona\", \"true\");\n \n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge top-level config options into widget config\n const widgetConfig = { ...config.config };\n \n // Merge apiUrl from top-level config into widget config if present\n if (config.apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = config.apiUrl;\n }\n \n // Merge clientToken from top-level config into widget config if present\n if (config.clientToken && !widgetConfig.clientToken) {\n widgetConfig.clientToken = config.clientToken;\n }\n \n // Merge flowId from top-level config into widget config if present\n if (config.flowId && !widgetConfig.flowId) {\n widgetConfig.flowId = config.flowId;\n }\n\n // Only initialize if we have either apiUrl OR clientToken (or other config)\n const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;\n if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {\n return;\n }\n\n // Auto-apply markdown postprocessor if not explicitly set and available\n if (!widgetConfig.postprocessMessage && window.AgentWidget.markdownPostprocessor) {\n widgetConfig.postprocessMessage = ({ text }: { text: string }) => \n window.AgentWidget.markdownPostprocessor(text);\n }\n\n try {\n window.AgentWidget.initAgentWidget({\n target,\n config: widgetConfig,\n // Explicitly disable shadow DOM for better CSS compatibility with host page\n useShadowDom: config.useShadowDom ?? false\n });\n } catch (error) {\n console.error(\"Failed to initialize AgentWidget:\", error);\n }\n };\n\n // Main installation flow (called after hydration completes)\n const install = async () => {\n try {\n await loadCSS();\n await loadJS();\n \n // Auto-init if we have config OR apiUrl OR clientToken\n const shouldAutoInit = autoInit && (\n config.config || \n config.apiUrl || \n config.clientToken\n );\n \n if (shouldAutoInit) {\n // Wait a tick to ensure AgentWidget is fully initialized\n setTimeout(initWidget, 0);\n }\n } catch (error) {\n console.error(\"Failed to install AgentWidget:\", error);\n }\n };\n\n // Start installation after hydration completes\n // This prevents Next.js/Nuxt/etc. from removing dynamically added CSS\n waitForHydration(install);\n})();\n\n"],"mappings":"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","names":["install_exports","scriptConfig","script","configJson","parsedConfig","_a","_b","_c","safeConfig","e","token","flowId","apiUrl","previewQueryParam","config","value","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","waitForHydration","callback","executed","execute","afterDom","loadCSS","resolve","reject","link","loadJS","initWidget","target","widgetConfig","text","error"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -23,6 +23,7 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
+ "dompurify": "^3.3.3",
26
27
  "idiomorph": "^0.7.4",
27
28
  "lucide": "^0.552.0",
28
29
  "marked": "^12.0.2",
@@ -1,6 +1,7 @@
1
1
  import { createElement } from "../utils/dom";
2
2
  import type { AgentWidgetConfig, AgentWidgetMessage, PersonaArtifactRecord } from "../types";
3
3
  import { escapeHtml, createMarkdownProcessorFromConfig } from "../postprocessors";
4
+ import { resolveSanitizer } from "../utils/sanitize";
4
5
  import { componentRegistry, type ComponentContext } from "./registry";
5
6
  import { renderLucideIcon } from "../utils/icons";
6
7
 
@@ -102,7 +103,11 @@ export function createArtifactPane(
102
103
  const panePadding = layout?.panePadding?.trim();
103
104
 
104
105
  const md = config.markdown ? createMarkdownProcessorFromConfig(config.markdown) : null;
105
- const toHtml = (text: string) => (md ? md(text) : escapeHtml(text));
106
+ const sanitize = resolveSanitizer(config.sanitize);
107
+ const toHtml = (text: string) => {
108
+ const raw = md ? md(text) : escapeHtml(text);
109
+ return sanitize ? sanitize(raw) : raw;
110
+ };
106
111
 
107
112
  const backdrop =
108
113
  typeof document !== "undefined"
@@ -0,0 +1,97 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect } from "vitest";
3
+ import { createStandardBubble, isSafeImageSrc } from "./message-bubble";
4
+ import type { AgentWidgetMessage } from "../types";
5
+
6
+ const makeMessage = (overrides: Partial<AgentWidgetMessage> = {}): AgentWidgetMessage => ({
7
+ id: "msg-1",
8
+ role: "assistant",
9
+ content: "",
10
+ createdAt: new Date().toISOString(),
11
+ ...overrides,
12
+ });
13
+
14
+ describe("isSafeImageSrc", () => {
15
+ it("allows https URLs", () => {
16
+ expect(isSafeImageSrc("https://example.com/img.png")).toBe(true);
17
+ });
18
+
19
+ it("allows http URLs", () => {
20
+ expect(isSafeImageSrc("http://example.com/img.png")).toBe(true);
21
+ });
22
+
23
+ it("allows blob URLs", () => {
24
+ expect(isSafeImageSrc("blob:http://example.com/abc-123")).toBe(true);
25
+ });
26
+
27
+ it("allows data:image/png URIs", () => {
28
+ expect(isSafeImageSrc("data:image/png;base64,abc123")).toBe(true);
29
+ });
30
+
31
+ it("allows data:image/jpeg URIs", () => {
32
+ expect(isSafeImageSrc("data:image/jpeg;base64,abc123")).toBe(true);
33
+ });
34
+
35
+ it("allows data:image/gif URIs", () => {
36
+ expect(isSafeImageSrc("data:image/gif;base64,abc123")).toBe(true);
37
+ });
38
+
39
+ it("allows data:image/webp URIs", () => {
40
+ expect(isSafeImageSrc("data:image/webp;base64,abc123")).toBe(true);
41
+ });
42
+
43
+ it("blocks data:image/svg+xml URIs", () => {
44
+ expect(isSafeImageSrc("data:image/svg+xml,<svg onload=alert(1)>")).toBe(false);
45
+ });
46
+
47
+ it("blocks data:image/svg+xml with base64", () => {
48
+ expect(isSafeImageSrc("data:image/svg+xml;base64,PHN2Zz4=")).toBe(false);
49
+ });
50
+
51
+ it("blocks mixed-case SVG data URIs", () => {
52
+ expect(isSafeImageSrc("data:image/Svg+xml,<svg onload=alert(1)>")).toBe(false);
53
+ expect(isSafeImageSrc("data:image/SVG+XML,<svg>")).toBe(false);
54
+ expect(isSafeImageSrc("data:Image/SVG+XML;base64,abc")).toBe(false);
55
+ });
56
+
57
+ it("blocks javascript: URIs", () => {
58
+ expect(isSafeImageSrc("javascript:alert(1)")).toBe(false);
59
+ });
60
+
61
+ it("blocks data:text/html URIs", () => {
62
+ expect(isSafeImageSrc("data:text/html,<script>alert(1)</script>")).toBe(false);
63
+ });
64
+
65
+ it("allows relative paths (no colon)", () => {
66
+ expect(isSafeImageSrc("relative/path.png")).toBe(true);
67
+ });
68
+
69
+ it("allows dot-relative paths", () => {
70
+ expect(isSafeImageSrc("./image.png")).toBe(true);
71
+ });
72
+
73
+ it("allows empty string", () => {
74
+ expect(isSafeImageSrc("")).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe("createStandardBubble", () => {
79
+ it("skips rendering blocked image previews while keeping safe ones", () => {
80
+ const bubble = createStandardBubble(
81
+ makeMessage({
82
+ content: "Image attachments",
83
+ contentParts: [
84
+ { type: "image", image: "https://example.com/safe.png", alt: "Safe image" },
85
+ { type: "image", image: "data:image/svg+xml,<svg onload=alert(1)>", alt: "Blocked image" },
86
+ ],
87
+ }),
88
+ ({ text }) => text
89
+ );
90
+
91
+ const previewImages = bubble.querySelectorAll('[data-message-attachments="images"] img');
92
+
93
+ expect(previewImages).toHaveLength(1);
94
+ expect(previewImages[0]?.getAttribute("src")).toBe("https://example.com/safe.png");
95
+ expect(previewImages[0]?.getAttribute("alt")).toBe("Safe image");
96
+ });
97
+ });
@@ -12,6 +12,17 @@ import {
12
12
  import { renderLucideIcon } from "../utils/icons";
13
13
  import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
14
14
 
15
+ /** Validate that an image src URL uses a safe scheme (blocks javascript: and SVG data URIs). */
16
+ export const isSafeImageSrc = (src: string): boolean => {
17
+ const lower = src.toLowerCase();
18
+ if (lower.startsWith("data:image/svg+xml")) return false;
19
+ if (/^(?:https?|blob):/i.test(src)) return true;
20
+ if (lower.startsWith("data:image/")) return true;
21
+ // Relative URLs are safe
22
+ if (!src.includes(":")) return true;
23
+ return false;
24
+ };
25
+
15
26
  export type LoadingIndicatorRenderer = (context: LoadingIndicatorRenderContext) => HTMLElement | null;
16
27
 
17
28
  export type MessageTransform = (context: {
@@ -100,8 +111,15 @@ const createMessageImagePreviews = (
100
111
  settled = true;
101
112
  });
102
113
 
103
- imageElement.src = imagePart.image;
104
- container.appendChild(imageElement);
114
+ if (isSafeImageSrc(imagePart.image)) {
115
+ imageElement.src = imagePart.image;
116
+ container.appendChild(imageElement);
117
+ } else {
118
+ // Treat blocked images like load errors so fallback triggers correctly
119
+ settled = true;
120
+ visiblePreviewCount = Math.max(0, visiblePreviewCount - 1);
121
+ imageElement.remove();
122
+ }
105
123
  });
106
124
 
107
125
  if (visiblePreviewCount === 0) {
package/src/index.ts CHANGED
@@ -121,6 +121,11 @@ export {
121
121
  createDirectivePostprocessor
122
122
  } from "./postprocessors";
123
123
  export type { MarkdownProcessorOptions } from "./postprocessors";
124
+ export {
125
+ createDefaultSanitizer,
126
+ resolveSanitizer
127
+ } from "./utils/sanitize";
128
+ export type { SanitizeFunction } from "./utils/sanitize";
124
129
  export {
125
130
  createPlainTextParser,
126
131
  createJsonStreamParser,
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("prototype pollution guard", () => {
4
+ it("strips __proto__, constructor, and prototype from parsed config", () => {
5
+ // Simulates the destructuring pattern used in install.ts
6
+ const malicious = JSON.parse(
7
+ '{"config":{"apiUrl":"http://localhost"},"__proto__":{"polluted":true},"constructor":{"polluted":true},"prototype":{"polluted":true}}'
8
+ );
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = malicious;
12
+
13
+ const target: Record<string, unknown> = {};
14
+ Object.assign(target, safeConfig);
15
+
16
+ // The dangerous keys should not be on the target
17
+ expect(Object.keys(target)).toEqual(["config"]);
18
+ expect((target as any).__proto__).toBe(Object.prototype); // normal prototype, not polluted
19
+ expect((target as any).constructor).toBe(Object); // normal constructor
20
+
21
+ // Global prototype should not be polluted
22
+ expect(({} as any).polluted).toBeUndefined();
23
+ });
24
+
25
+ it("preserves safe config properties", () => {
26
+ const parsed = JSON.parse(
27
+ '{"config":{"apiUrl":"http://localhost"},"clientToken":"tok_123","flowId":"flow-1"}'
28
+ );
29
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
30
+ const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsed;
31
+
32
+ expect(safeConfig).toEqual({
33
+ config: { apiUrl: "http://localhost" },
34
+ clientToken: "tok_123",
35
+ flowId: "flow-1",
36
+ });
37
+ });
38
+ });
package/src/install.ts CHANGED
@@ -58,7 +58,9 @@ declare global {
58
58
  const parsedConfig = JSON.parse(configJson);
59
59
  // If it has nested 'config' property, use it; otherwise treat as widget config
60
60
  if (parsedConfig.config) {
61
- Object.assign(scriptConfig, parsedConfig);
61
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
62
+ const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsedConfig;
63
+ Object.assign(scriptConfig, safeConfig);
62
64
  } else {
63
65
  // Treat the entire object as widget config
64
66
  scriptConfig.config = parsedConfig;
@@ -0,0 +1,84 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect } from "vitest";
3
+ import {
4
+ createMarkdownProcessor,
5
+ createDirectivePostprocessor,
6
+ escapeHtml,
7
+ } from "./postprocessors";
8
+ import { createDefaultSanitizer } from "./utils/sanitize";
9
+
10
+ describe("markdown + sanitization integration", () => {
11
+ const md = createMarkdownProcessor();
12
+ const sanitize = createDefaultSanitizer();
13
+
14
+ it("strips script tags from markdown output", () => {
15
+ const html = sanitize(md("# Title\n<script>alert(1)</script>"));
16
+ expect(html).toContain("<h1>Title</h1>");
17
+ expect(html).not.toContain("<script>");
18
+ expect(html).not.toContain("alert(1)");
19
+ });
20
+
21
+ it("strips onerror handlers from img tags in markdown", () => {
22
+ const html = sanitize(md('<img src="x" onerror="alert(1)">'));
23
+ expect(html).not.toContain("onerror");
24
+ });
25
+
26
+ it("strips javascript: URIs from markdown links", () => {
27
+ const html = sanitize(md('[click](javascript:alert(1))'));
28
+ expect(html).not.toContain("javascript:");
29
+ });
30
+
31
+ it("preserves safe markdown headings", () => {
32
+ const html = sanitize(md("## Hello\n\nParagraph text."));
33
+ expect(html).toContain("<h2>Hello</h2>");
34
+ expect(html).toContain("<p>Paragraph text.</p>");
35
+ });
36
+
37
+ it("preserves safe markdown code blocks", () => {
38
+ const html = sanitize(md("```js\nconst x = 1;\n```"));
39
+ expect(html).toContain("<code");
40
+ expect(html).toContain("const x = 1;");
41
+ });
42
+
43
+ it("preserves safe links", () => {
44
+ const html = sanitize(md("[example](https://example.com)"));
45
+ expect(html).toContain('href="https://example.com"');
46
+ });
47
+ });
48
+
49
+ describe("directive postprocessor + sanitization", () => {
50
+ const directive = createDirectivePostprocessor();
51
+ const sanitize = createDefaultSanitizer();
52
+
53
+ it("preserves form directive placeholders", () => {
54
+ const html = sanitize(directive('<Form type="init" />'));
55
+ expect(html).toContain('data-tv-form="init"');
56
+ expect(html).toContain("persona-form-directive");
57
+ });
58
+
59
+ it("sanitizes content surrounding directives", () => {
60
+ const html = sanitize(directive('<Form type="init" />\n<script>bad</script>'));
61
+ expect(html).toContain('data-tv-form="init"');
62
+ expect(html).not.toContain("<script>");
63
+ expect(html).not.toContain("bad");
64
+ });
65
+
66
+ it("handles JSON-style directives", () => {
67
+ const html = sanitize(
68
+ directive('<Directive>{"component":"form","type":"contact"}</Directive>')
69
+ );
70
+ expect(html).toContain('data-tv-form="contact"');
71
+ });
72
+ });
73
+
74
+ describe("escapeHtml", () => {
75
+ it("escapes all HTML special characters", () => {
76
+ expect(escapeHtml('<script>alert("xss")&</script>')).toBe(
77
+ "&lt;script&gt;alert(&quot;xss&quot;)&amp;&lt;/script&gt;"
78
+ );
79
+ });
80
+
81
+ it("escapes single quotes", () => {
82
+ expect(escapeHtml("it's")).toBe("it&#39;s");
83
+ });
84
+ });
package/src/types.ts CHANGED
@@ -2571,7 +2571,21 @@ export type AgentWidgetConfig = {
2571
2571
  * ```
2572
2572
  */
2573
2573
  markdown?: AgentWidgetMarkdownConfig;
2574
-
2574
+
2575
+ /**
2576
+ * HTML sanitization for rendered message content.
2577
+ *
2578
+ * The widget renders AI-generated markdown as HTML. By default, all HTML
2579
+ * output is sanitized using DOMPurify to prevent XSS attacks.
2580
+ *
2581
+ * - `true` (default): sanitize using built-in DOMPurify
2582
+ * - `false`: disable sanitization (only use with fully trusted content sources)
2583
+ * - `(html: string) => string`: custom sanitizer function
2584
+ *
2585
+ * @default true
2586
+ */
2587
+ sanitize?: boolean | ((html: string) => string);
2588
+
2575
2589
  /**
2576
2590
  * Configuration for message action buttons (copy, upvote, downvote).
2577
2591
  * Shows action buttons on assistant messages for user feedback.
package/src/ui.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
2
+ import { resolveSanitizer } from "./utils/sanitize";
2
3
  import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
3
4
  import {
4
5
  AgentWidgetConfig,
@@ -321,6 +322,9 @@ const buildPostprocessor = (
321
322
  ? createMarkdownProcessorFromConfig(cfg.markdown)
322
323
  : null;
323
324
 
325
+ // Resolve sanitizer: enabled by default, can be disabled or replaced
326
+ const sanitize = resolveSanitizer(cfg?.sanitize);
327
+
324
328
  return (context) => {
325
329
  let nextText = context.text ?? "";
326
330
  const rawPayload = context.message.rawContent ?? null;
@@ -347,20 +351,20 @@ const buildPostprocessor = (
347
351
  }
348
352
 
349
353
  // Priority: postprocessMessage > markdown config > escapeHtml
354
+ let html: string;
350
355
  if (cfg?.postprocessMessage) {
351
- return cfg.postprocessMessage({
356
+ html = cfg.postprocessMessage({
352
357
  ...context,
353
358
  text: nextText,
354
359
  raw: rawPayload ?? context.text ?? ""
355
360
  });
361
+ } else if (markdownProcessor) {
362
+ html = markdownProcessor(nextText);
363
+ } else {
364
+ html = escapeHtml(nextText);
356
365
  }
357
366
 
358
- // Use markdown processor if markdown config is provided
359
- if (markdownProcessor) {
360
- return markdownProcessor(nextText);
361
- }
362
-
363
- return escapeHtml(nextText);
367
+ return sanitize ? sanitize(html) : html;
364
368
  };
365
369
  };
366
370
 
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ defaultJsonActionParser,
4
+ defaultActionHandlers,
5
+ createActionManager,
6
+ } from "./actions";
7
+ import type { AgentWidgetMessage } from "../types";
8
+
9
+ const makeMessage = (overrides: Partial<AgentWidgetMessage> = {}): AgentWidgetMessage => ({
10
+ id: "msg-1",
11
+ role: "assistant",
12
+ content: "",
13
+ createdAt: new Date().toISOString(),
14
+ ...overrides,
15
+ });
16
+
17
+ describe("defaultJsonActionParser", () => {
18
+ it("parses valid action JSON", () => {
19
+ const result = defaultJsonActionParser({
20
+ text: '{"action":"message","text":"hi"}',
21
+ message: makeMessage(),
22
+ });
23
+ expect(result).toEqual({
24
+ type: "message",
25
+ payload: { text: "hi" },
26
+ raw: { action: "message", text: "hi" },
27
+ });
28
+ });
29
+
30
+ it("returns null for non-action JSON", () => {
31
+ const result = defaultJsonActionParser({
32
+ text: '{"foo":"bar"}',
33
+ message: makeMessage(),
34
+ });
35
+ expect(result).toBeNull();
36
+ });
37
+
38
+ it("returns null for non-JSON text", () => {
39
+ const result = defaultJsonActionParser({
40
+ text: "hello world",
41
+ message: makeMessage(),
42
+ });
43
+ expect(result).toBeNull();
44
+ });
45
+
46
+ it("returns null for empty text", () => {
47
+ expect(defaultJsonActionParser({ text: "", message: makeMessage() })).toBeNull();
48
+ });
49
+
50
+ it("strips code fences before parsing", () => {
51
+ const text = '```json\n{"action":"message","text":"fenced"}\n```';
52
+ const result = defaultJsonActionParser({ text, message: makeMessage() });
53
+ expect(result).toEqual({
54
+ type: "message",
55
+ payload: { text: "fenced" },
56
+ raw: { action: "message", text: "fenced" },
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("createActionManager.process", () => {
62
+ const makeManager = (overrides?: Record<string, unknown>) => {
63
+ let metadata: Record<string, unknown> = {};
64
+ return createActionManager({
65
+ parsers: [defaultJsonActionParser],
66
+ handlers: [defaultActionHandlers.message],
67
+ getSessionMetadata: () => metadata,
68
+ updateSessionMetadata: (updater) => { metadata = updater(metadata); },
69
+ emit: vi.fn(),
70
+ documentRef: null,
71
+ ...overrides,
72
+ });
73
+ };
74
+
75
+ it("skips streaming messages", () => {
76
+ const manager = makeManager();
77
+ const result = manager.process({
78
+ text: '{"action":"message","text":"hi"}',
79
+ message: makeMessage(),
80
+ streaming: true,
81
+ });
82
+ expect(result).toBeNull();
83
+ });
84
+
85
+ it("skips non-assistant messages", () => {
86
+ const manager = makeManager();
87
+ const result = manager.process({
88
+ text: '{"action":"message","text":"hi"}',
89
+ message: makeMessage({ role: "user" }),
90
+ streaming: false,
91
+ });
92
+ expect(result).toBeNull();
93
+ });
94
+
95
+ it("deduplicates by message ID", () => {
96
+ const manager = makeManager();
97
+ const msg = makeMessage({ content: '{"action":"message","text":"hi"}' });
98
+ const first = manager.process({ text: msg.content, message: msg, streaming: false });
99
+ expect(first).not.toBeNull();
100
+
101
+ const second = manager.process({ text: msg.content, message: msg, streaming: false });
102
+ expect(second).toBeNull();
103
+ });
104
+
105
+ it("processes valid action and returns display text", () => {
106
+ const manager = makeManager();
107
+ const result = manager.process({
108
+ text: '{"action":"message","text":"hello"}',
109
+ message: makeMessage(),
110
+ streaming: false,
111
+ });
112
+ expect(result).toEqual({ text: "hello", persist: true, resubmit: undefined });
113
+ });
114
+ });