@runtypelabs/persona
v4.6.0
Published
Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.
Readme
Streaming Agent Widget
Installable vanilla JavaScript widget for embedding a streaming AI assistant on any website.
Installation
npm install @runtypelabs/personaBuilding locally
pnpm builddist/index.js(ESM),dist/index.cjs(CJS), anddist/index.global.js(IIFE) provide different module formats.dist/widget.cssis the prefixed Tailwind bundle.dist/install.global.jsis the automatic installer script for easy script tag installation.dist/launcher.global.jsis the tiny critical launcher used by deferred script-tag installs before the full panel bundle loads.dist/webmcp-polyfill.jsis the lazy WebMCP polyfill chunk used by the IIFE bundle only whenconfig.webmcp.enabledis true and the page has nodocument.modelContext.
Using with modules
import '@runtypelabs/persona/widget.css';
import {
initAgentWidget,
createAgentExperience,
markdownPostprocessor,
DEFAULT_WIDGET_CONFIG
} from '@runtypelabs/persona';
const proxyUrl = '/api/chat/dispatch';
// Inline embed
const inlineHost = document.querySelector('#inline-widget')!;
createAgentExperience(inlineHost, {
...DEFAULT_WIDGET_CONFIG,
apiUrl: proxyUrl,
launcher: { enabled: false },
theme: {
semantic: { colors: { accent: '#2563eb' } }
},
suggestionChips: ['What can you do?', 'Show API docs'],
postprocessMessage: ({ text }) => markdownPostprocessor(text)
});
// Floating launcher with runtime updates
const controller = initAgentWidget({
target: '#launcher-root',
windowKey: 'chatController', // Optional: stores controller on window.chatController
config: {
...DEFAULT_WIDGET_CONFIG,
apiUrl: proxyUrl,
launcher: {
...DEFAULT_WIDGET_CONFIG.launcher,
title: 'AI Assistant',
subtitle: 'Here to help you get answers fast'
}
}
});
// Runtime theme update
document.querySelector('#dark-mode')?.addEventListener('click', () => {
controller.update({
theme: { semantic: { colors: { surface: '#0f172a', primary: '#f8fafc' } } }
});
});
// Docked panel that wraps a concrete workspace container
const docked = initAgentWidget({
target: '#workspace-main',
config: {
...DEFAULT_WIDGET_CONFIG,
apiUrl: proxyUrl,
launcher: {
...DEFAULT_WIDGET_CONFIG.launcher,
mountMode: 'docked',
dock: {
side: 'right',
width: '420px',
}
}
}
});Initialization options
initAgentWidget accepts the following options:
| Option | Type | Description |
| --- | --- | --- |
| target | string \| HTMLElement | CSS selector or element where widget mounts. |
| config | AgentWidgetConfig | Widget configuration object (see the Configuration Reference). |
| useShadowDom | boolean | Use Shadow DOM for style isolation (default: false). |
| onChatReady | () => void | Callback fired when the widget is initialized and its API is callable. |
| windowKey | string | If provided, stores the controller on window[windowKey] for global access. Automatically cleaned up on destroy(). |
When config.launcher.mountMode is 'docked', target is treated as the page container that Persona should wrap. Use a concrete element such as #workspace-main; body and html are rejected.
Height contract: the docked shell sizes itself with height: 100%, so give it a definite height: usually html, body { height: 100% } or a fixed-height app-shell container around the target. If no ancestor provides one, the panel is clamped to dock.maxHeight (default 100dvh; resize/emerge are also sticky-pinned : push/overlay get the cap only) so it stays viewport-sized and scrolls internally, and a console warning explains the fix. Override the cap with a CSS length or disable the guard with dock.maxHeight: false.
With dock.reveal: 'resize' (default), a closed dock uses a 0px column. 'emerge' uses the same column width animation (content reflows) but the chat panel stays dock.width wide and is clipped by the growing slot: like a normal-width widget emerging from the edge. 'overlay' overlays with transform. 'push' uses a sliding track (Shopify-style). The built-in launcher stays hidden in docked mode: open with controller.open() (or your own chrome).
Rounded / card layout: initAgentWidget inserts a flex shell as the direct child of your target’s parent, with your target in the content column and the dock beside it. Put border-radius, border, and overflow: hidden on that parent (or an ancestor that wraps only the shell) so the dock column sits inside the same visual card as your content.
Inner push/overlay: With reveal: 'push' or 'overlay', only the wrapped node moves. Use a narrow target (e.g. a main canvas div). For dock.side: 'left', place a persistent rail in flow next to the stage (e.g. flex [nav | stage]) so the dock doesn’t open under the sidebar. For a right dock, you can instead use a full-width stage with an absolute left rail if you want the canvas to translate behind that rail. position: fixed/sticky content inside the target stays viewport-anchored (it is not pushed), so offset it while the dock is open if needed, e.g. [data-persona-dock-open="true"] .my-fixed-bar { right: 420px; }.
Security note: Persona sanitizes rendered message HTML with DOMPurify by default (
sanitize: true), including output returned frompostprocessMessage,markdownPostprocessor, anddirectivePostprocessor. If your custom postprocessor intentionally returns tags or attributes outside the built-in allowlist, providesanitize: (html) => ...; only setsanitize: falsefor fully trusted content.
Documentation
The full reference lives in docs/ and the theming guide:
- Extending Persona: the map of every extension point: plugins, components, postprocessors, themes, stream parsers, animations, voice, sanitization, actions, context/WebMCP, layout slots, storage, and UI builders, each linked to its deep dive
- Authoring Plugins: the
AgentWidgetPlugincontract, all 14 render hooks, global vs per-instance registration, lifecycle, and the@runtypelabs/persona/plugin-kithelpers - Contributing: current guidance for contributing plugins, themes, adapters, examples, and other customizations back to this monorepo
- Programmatic Control & Events: controller API, message hooks and injection, enriched DOM context, WebMCP page tools, DOM and controller events, state loading
- UI Features & Components: message actions and feedback, loading/idle indicators, approvals, built-in
ask_user_questionandsuggest_repliestools, dropdown menus, button utilities, dynamic forms - Script Tag Installation & Framework Integration: automatic installer, deferred launcher lifecycle hooks, manual script tag setup, React, Next.js, Remix, Gatsby, and Astro guides
- Configuration Reference: every config option: core, client token mode, agent mode, UI & theme, launcher/docking, layout, voice, WebMCP, tool calls, features, suggestion chips, state & storage
- Stream Parser Configuration: JSON, XML, and plain-text stream parsers and custom parser factories
- Message Injection: full injection and component-directive reference
- Dynamic Forms: field schema, form styles, and recipes
- Code Generator:
@runtypelabs/persona/codegenoptions for CLI/server-side snippet generation - THEME-CONFIG.md: the complete theme and design-token reference
Optional Runtype proxy server
The @runtypelabs/persona-proxy package handles server-side API-key control and forwards requests to Runtype. You can configure it around a saved agent (recommended for most chat widgets) or a flow.
Option 1: Reference a Runtype agent ID (recommended)
// api/chat.ts
import { createChatProxyApp } from '@runtypelabs/persona-proxy';
export default createChatProxyApp({
path: '/api/chat/dispatch',
allowedOrigins: ['https://www.example.com'],
agentId: 'agent_abc123'
});Option 2: Use default flow
// api/chat.ts
import { createChatProxyApp } from '@runtypelabs/persona-proxy';
export default createChatProxyApp({
path: '/api/chat/dispatch',
allowedOrigins: ['https://www.example.com']
});Option 3: Reference a Runtype flow ID
import { createChatProxyApp } from '@runtypelabs/persona-proxy';
export default createChatProxyApp({
path: '/api/chat/dispatch',
allowedOrigins: ['https://www.example.com'],
flowId: 'flow_abc123' // Flow created in Runtype dashboard or API
});Option 4: Define a custom flow
import { createChatProxyApp } from '@runtypelabs/persona-proxy';
export default createChatProxyApp({
path: '/api/chat/dispatch',
allowedOrigins: ['https://www.example.com'],
flowConfig: {
name: "Custom Chat Flow",
description: "Specialized assistant flow",
steps: [
{
id: "custom_prompt",
name: "Custom Prompt",
type: "prompt",
enabled: true,
config: {
model: "meta/llama3.1-8b-instruct-free",
responseFormat: "markdown",
outputVariable: "prompt_result",
userPrompt: "{{user_message}}",
systemPrompt: "you are a helpful assistant, chatting with a user",
previousMessages: "{{messages}}"
}
}
]
}
});Hosting on Vercel:
import { createVercelHandler } from '@runtypelabs/persona-proxy';
export default createVercelHandler({
allowedOrigins: ['https://www.example.com'],
flowId: 'flow_abc123' // Optional
});Environment setup:
Add RUNTYPE_API_KEY to your environment. The proxy constructs the Runtype payload (including flow configuration) and streams the response back to the client.
Development notes
- The widget streams results using SSE and mirrors Persona's flow/agent events (which Runtype implements natively), including
awaitlocal-tool pauses and/resumecontinuations. - Tailwind classes are prefixed with
tvw-and scoped to[data-persona-root], so they won't collide with the host page. - Run
pnpm devfrom the repository root to boot the example Runtype proxy (examples/runtype-hono-proxy) and the vanilla demo (apps/web). - The proxy prefers port
43111but automatically selects the next free port if needed. features.askUserQuestion.exposeandfeatures.suggestReplies.exposeadvertise built-in LOCAL client tools throughclientTools[]; leaveexposeoff if the flow already declares those tools server-side.webmcp: { enabled: true }snapshots page-registered tools ondocument.modelContext, sends them asclientTools[], executes returnedwebmcp:*calls in the browser, and resumes the paused execution.
