# Persona
> Themeable, pluggable AI chat widget for websites. Zero framework dependencies.
> Source: https://github.com/runtypelabs/persona
> npm: https://www.npmjs.com/package/@runtypelabs/persona
> Docs & demos: https://persona-chat.dev
> Full reference (config + theming): https://persona-chat.dev/llms-full.txt
> Agent skills: https://github.com/runtypelabs/skills
Persona is a drop-in streaming chat UI for AI assistants that works on any website. It's built in TypeScript with vanilla JS: no React, Vue, or framework dependency. It can render inside an opt-in Shadow DOM (`useShadowDom: true`) for style isolation and ships as ESM, CJS, IIFE script-tag bundles, and focused subpath exports for codegen, plugin helpers, animations, theme tools, testing, and smart DOM page-context reading.
The fastest way to deploy an AI chat experience is with [Runtype](https://runtype.com): Persona is pre-integrated, so you get streaming chat with client tokens, WebMCP/page tools, built-in local client tools, voice, theming, analytics, approvals, and artifacts out of the box. Just set a `clientToken` and go. It also works with any SSE-capable backend if you want to bring your own.
## Agent Skills
If you're an AI coding agent, install the Runtype skills for guided help with Persona integration, Runtype product building, and more:
```bash
npx skills add runtypelabs/skills
```
This registers skills that activate contextually in Claude Code, Cursor, Copilot, Codex, Windsurf, and 30+ other agents. Key skills:
- **`runtype-persona`**: Embedding, configuring, theming, and debugging Persona widgets. Prefers MCP-generated embed code over hand-written snippets.
- **`runtype`**: Umbrella skill for all Runtype platform questions. Routes to focused skills and includes detailed reference material.
- **`runtype-build-product`**: Building and deploying Runtype products (agents, flows, tools, surfaces).
- **`runtype-admin`**: Operating and debugging live Runtype accounts (traces, logs, evals).
- **`runtype-templates`**: Packaging products as distributable FPO templates.
- **`runtype-sdk-marathon`**: Code-first workflows with the TypeScript/Python SDK and Marathon task harness.
Browse and discover skills at [skills.sh](https://skills.sh).
## Install
```bash
npm install @runtypelabs/persona
```
## Quick Start: ES Modules
```ts
import '@runtypelabs/persona/widget.css';
import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';
const chat = initAgentWidget({
target: 'body',
config: {
apiUrl: '/api/chat/dispatch',
launcher: { enabled: true, title: 'AI Assistant' },
theme: { semantic: { colors: { accent: '#2563eb' } } },
postprocessMessage: ({ text }) => markdownPostprocessor(text),
},
});
chat.open();
chat.submitMessage('Hello!');
chat.on('assistant:complete', (msg) => console.log(msg.content));
```
## Quick Start: Script Tag (CDN)
```html
```
## Quick Start: Client Token (No Proxy)
```ts
initAgentWidget({
target: 'body',
config: {
clientToken: 'ct_live_flow01k7_...',
launcher: { enabled: true },
},
});
```
## Recommended Modern Defaults
- Prefer `clientToken` for direct browser-to-Runtype installs when the surface is already configured in Runtype; use `apiUrl` + `@runtypelabs/persona-proxy` when you need server-side API-key control or custom flow definitions.
- Use `features.askUserQuestion.expose: true` to let an agent ask blocking clarifying questions through the built-in `ask_user_question` answer sheet. Leave it `false` when the flow already declares the tool server-side.
- Use `features.suggestReplies.expose: true` to let the agent push fire-and-forget quick-reply chips via `suggest_replies`; chips auto-resume the paused execution and clear after the next user message.
- Use `webmcp: { enabled: true }` to snapshot tools registered on `document.modelContext`, send them as `clientTools[]`, execute returned `webmcp:` calls on the page, and resume the agent with the result. Gate writes with native approval bubbles or `webmcp.onConfirm`; auto-approve safe reads with `webmcp.autoApprove`.
- Keep `sanitize` enabled (default) for DOMPurify sanitization of markdown/custom HTML. If you intentionally return custom HTML from `postprocessMessage`, either fit the built-in allowlist or provide a custom sanitizer; only set `sanitize: false` for fully trusted content.
- Script-tag installs automatically use the small `launcher.global.js` fast path for ordinary floating launchers and defer the full widget until first open. Use `onScriptLoad`, `onLauncherShown`, `onChatReady`, and `onError` (or matching `persona:*` DOM events) for lifecycle analytics.
## Initialization
Two entry points:
- `initAgentWidget({ target, config, useShadowDom?, onChatReady?, windowKey? })`: floating launcher or docked panel. Returns a controller.
- `createAgentExperience(element, config)`: inline embed with no launcher. Returns a controller.
### Controller API
| Method | Description |
|---|---|
| `open()` / `close()` / `toggle()` | Panel visibility |
| `setMessage(text)` | Set input text without submitting |
| `submitMessage(text?)` | Send a message |
| `clearChat()` | Clear conversation |
| `update(config)` | Merge new config at runtime |
| `destroy()` | Remove widget and clean up |
| `on(event, callback)` / `off(event, callback)` | Subscribe to events |
| `injectAssistantMessage({ content, llmContent? })` | Inject a message programmatically |
| `injectUserMessage(...)` / `injectSystemMessage(...)` | Inject user/system messages |
| `injectComponentDirective({ component, props })` | Render a registered component |
| `startVoiceRecognition()` / `stopVoiceRecognition()` | Voice control |
| `focusInput()` | Focus the composer |
| `showEventStream()` / `hideEventStream()` | Event inspector panel |
### Controller Events
| Event | Payload |
|---|---|
| `user:message` | `AgentWidgetMessage` (includes `viaVoice`) |
| `assistant:message` | `AgentWidgetMessage` (stream start) |
| `assistant:complete` | `AgentWidgetMessage` (stream end) |
| `voice:state` | `{ active, source, timestamp }` |
| `widget:opened` / `widget:closed` | `{ open, source, timestamp }` |
| `widget:state` | `{ open, launcherEnabled, voiceActive, streaming }` |
| `action:detected` | `{ action, message }` |
| `message:feedback` | `{ type: 'upvote'|'downvote', messageId, message }` |
| `message:copy` | `AgentWidgetMessage` |
## Core Config Sections
The full config is passed as `config` to `initAgentWidget()`. Key sections:
- **`apiUrl`**: Your proxy endpoint (or use `clientToken` for direct browser-to-API)
- **`flowId`**: Runtype flow ID for server-side flow selection
- **`agent`**, Agent loop config (model, systemPrompt, tools, loopConfig), mutually exclusive with `flowId`
- **`theme` / `darkTheme`**: Design tokens (palette, semantic, component-level). See `llms-full.txt` for the full token reference.
- **`colorScheme`**: `'light'`, `'dark'`, or `'auto'`
- **`launcher`**: Button/panel config (enabled, title, subtitle, position, mountMode, dock)
- **`layout`**: Header, messages, and slot configuration
- **`voiceRecognition`**: Speech-to-text (browser Web Speech API or Runtype WebSocket)
- **`textToSpeech`**: TTS for assistant responses
- **`features`**: Feature flags for reasoning/tool-call visibility, event stream, artifacts, scroll behavior, stream animations, composer history, `ask_user_question`, and `suggest_replies`
- **`webmcp`**: Page tool discovery/execution via WebMCP (`document.modelContext`) and `clientTools[]`
- **`contextProviders` / `requestMiddleware`**: Inject page/editor context into each request (for example, current slide selection)
- **`plugins`**: Array of plugin objects with render hooks
- **`parserType`**: `'plain'`, `'json'`, `'regex-json'`, or `'xml'` for structured streaming
- **`attachments`**: File upload config (types, size limits)
- **`messageActions`**: Copy/upvote/downvote buttons
- **`suggestionChips`**: Quick-reply buttons above the composer
- **`persistState`**: Save widget state across page navigations
- **`postprocessMessage` / `markdown` / `sanitize`**: Transform and render message HTML; DOMPurify sanitization is on by default
## Launcher Modes
- **Floating** (default): `launcher.mountMode: 'floating'`: corner-anchored button + popup panel. Script-tag installs defer the heavy panel bundle until first open when possible.
- **Docked**: `launcher.mountMode: 'docked'`: side panel that wraps a content container. `dock.reveal` controls the animation: `'resize'` (default), `'emerge'`, `'overlay'`, `'push'`. `dock.maxHeight` defaults to `100dvh` as a viewport guard when the page has no definite height chain.
## Message Injection
Inject messages from external code (tool callbacks, navigation events, etc.):
```ts
chat.injectAssistantMessage({
content: 'Displayed to user',
llmContent: 'Sent to the LLM instead',
});
```
Content priority: `contentParts > llmContent > rawContent > content`.
## Plugin System
14 render hooks for customizing any part of the UI:
```ts
const plugin = {
id: 'my-plugin',
renderLauncher: (ctx) => { /* return HTMLElement or null */ },
renderHeader: (ctx) => { /* ... */ },
renderMessage: (ctx) => { /* ... */ },
renderToolCall: (ctx) => { /* ... */ },
renderReasoning: (ctx) => { /* ... */ },
renderLoadingIndicator: (ctx) => { /* ... */ },
renderIdleIndicator: (ctx) => { /* ... */ },
renderAskUserQuestion: (ctx) => { /* ... */ },
// ... and more
};
initAgentWidget({ target: 'body', config: { plugins: [plugin] } });
```
Plugins are priority-ordered. Return `null` from a hook to fall through to the next plugin or the default renderer.
## Proxy Server
Optional `@runtypelabs/persona-proxy` package for server-side API key management:
```ts
import { createChatProxyApp } from '@runtypelabs/persona-proxy';
export default createChatProxyApp({
path: '/api/chat/dispatch',
allowedOrigins: ['https://example.com'],
flowId: 'flow_abc123', // or flowConfig: { ... }
});
```
Set `RUNTYPE_API_KEY` in the environment.
## Framework Integration
Works with any framework. In React/Next.js/Remix/etc., initialize in a `useEffect` and call `handle.destroy()` on cleanup. Next.js App Router needs `'use client'`. For SSR frameworks, use dynamic import or `typeof window` guard.
## Key Exports
| Export | Purpose |
|---|---|
| `initAgentWidget` | Mount floating/docked widget, returns controller |
| `createAgentExperience` | Mount inline widget, returns controller |
| `DEFAULT_WIDGET_CONFIG` | Sensible default config to spread over |
| `mergeWithDefaults` | Deep-merge your overrides with defaults |
| `markdownPostprocessor` / `createMarkdownProcessor` | Render markdown in messages |
| `createDefaultSanitizer` / `resolveSanitizer` | DOMPurify-backed sanitization helpers |
| `ASK_USER_QUESTION_CLIENT_TOOL` / `SUGGEST_REPLIES_CLIENT_TOOL` | Built-in local client tool definitions for server-side reuse |
| `parseAskUserQuestionPayload` / `parseSuggestRepliesPayload` | Parse built-in client-tool payloads |
| `WebMcpBridge` / `WEBMCP_TOOL_PREFIX` | WebMCP bridge utilities |
| `generateCodeSnippet` (subpath `@runtypelabs/persona/codegen`) | Server/CLI-safe embed snippet generation |
| `createJsonStreamParser` | JSON stream parser factory |
| `createXmlParser` | XML stream parser factory |
| `createPlainTextParser` | Plain text parser (default) |
| `componentRegistry` | Register custom components for directive rendering |
| `createTheme` | Build a theme from token overrides |
| `brandPlugin` / `accessibilityPlugin` | Built-in theme plugins |
| `createDropdownMenu` | Dropdown menu utility |
| `createIconButton` / `createLabelButton` / `createToggleGroup` | Button utilities |
| `collectEnrichedPageContext` / `formatEnrichedContext` | DOM context collection for tool use |
| `@runtypelabs/persona/smart-dom-reader` | Optional Shadow-DOM/iframe-aware page context provider |
| `@runtypelabs/persona/plugin-kit` | Shadow-DOM-safe plugin utilities (`injectStyles`, `createPopover`, `isEditableEventTarget`) |
| `@runtypelabs/persona/animations/*` | Optional stream animation plugins such as `wipe` and `glyph-cycle` |
## License
MIT