npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@runtypelabs/persona

v4.34.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/persona

Building locally

pnpm build
  • dist/index.js (ESM), dist/index.cjs (CJS), and dist/index.global.js (IIFE) provide different module formats.
  • dist/widget.css is the prefixed Tailwind bundle.
  • dist/install.global.js is the automatic installer script for easy script tag installation.
  • dist/launcher.global.js is the tiny critical launcher used by deferred script-tag installs before the full panel bundle loads.
  • dist/webmcp-polyfill.js is the lazy WebMCP polyfill chunk used by the IIFE bundle only when config.webmcp.enabled is true and the page has no document.modelContext.

Using with modules

import '@runtypelabs/persona/widget.css';
import {
  initAgentWidget,
  createAgentExperience,
  markdownPostprocessor,
  preprocessStreamingMarkdown,
  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' } }
  },
  suggestions: { starters: { items: ['What can you do?', 'Show API docs'] } },
  postprocessMessage: ({ text, streaming }) =>
    markdownPostprocessor(preprocessStreamingMarkdown(text, streaming))
});

// 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',
      }
    }
  }
});

Update merge policy

update() applies a recursive patch, not a shallow replace. A key merges into the live config only when both the previous value and the patch value are plain objects, so a partial patch (for example launcher.title) preserves defaulted and earlier-set sibling values instead of dropping them. Arrays, functions, class instances, and scalar values replace wholesale, and so do a small set of replace-leaf fields whose plain-object values must not be spliced together: headers, agent, storageAdapter, components, targetProviders, voiceRecognition.provider.custom, and features.streamAnimation.plugins. Omitting a key preserves its current value: to reset a field, pass it explicitly with undefined, which falls back to the field's default, or leaves it unset when no default exists.

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 once the configured renderers and styles are ready for display. | | windowKey | string | If provided, stores the controller on window[windowKey] for global access. Automatically cleaned up on destroy(). |

The returned controller is callable immediately. Await controller.ready when you need the complete UI: Persona prepares its renderers and styles before revealing the widget. Script-tag installs keep the launcher visible until this finishes. An update that needs another renderer keeps the current UI until that renderer is ready; ordinary updates remain synchronous. Read controller.ready after the update to await its completion. Failed preparation rejects the promise; calling update({}) retries missing modules. The installer reports failures through onError and lets the launcher retry on the next click. This covers Persona's renderer modules and styles, not external assets fetched by custom renderers.

Script-tag installs accept a managed renderLoading hook for custom loading UI, with the same 300ms delay and automatic cleanup as the default indicator. See Custom loading indicator.

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 from postprocessMessage, markdownPostprocessor, and directivePostprocessor. If your custom postprocessor intentionally returns tags or attributes outside the built-in allowlist, provide sanitize: (html) => ...; only set sanitize: false for 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 AgentWidgetPlugin contract, render/data/interaction hooks, global vs per-instance registration, lifecycle, and the @runtypelabs/persona/plugin-kit helpers
  • 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_question and suggest_replies tools, 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, suggestions, state & storage
  • Context Mentions: @-mention sources, resolve lifecycle, chip vs inline display, menu positioning, slash commands, and render overrides
  • 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/codegen options 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 await local-tool pauses and /resume continuations.
  • Tailwind classes are prefixed with tvw- and scoped to [data-persona-root], so they won't collide with the host page.
  • Run pnpm dev from the repository root to boot the example Runtype proxy (examples/runtype-hono-proxy) and the vanilla demo (apps/web).
  • The proxy prefers port 43111 but automatically selects the next free port if needed.
  • features.askUserQuestion.expose and suggestions.followUps.expose advertise built-in LOCAL client tools through clientTools[]; leave expose off if the flow already declares those tools server-side.
  • webmcp: { enabled: true } snapshots page-registered tools on document.modelContext, sends them as clientTools[], executes returned webmcp:* calls in the browser, and resumes the paused execution.

WebMCP compatibility

Persona supports host-installed @mcp-b/webmcp-polyfill v5 and v6 without replacing their registry. Producer pages using v5 call initializeWebMCPPolyfill(); pages using v6 call installWebMCP() before registering tools. Both initializer exports also work with custom Persona polyfill loaders. Tool execution supports both JSON-string and object-input contracts with one invocation, not a failed-call retry.

When no registry exists, Persona lazily installs v5. This preserves the previous browser requirements and callback-result behavior. The demos use v6 explicitly; that runtime requires HTTPS/localhost and Chrome/Firefox 126+ or Safari 18+.

Only the current document's tools are exposed, even when the registry also discovers tools in related frames. The dispatch protocol addresses tools by name and does not support frame-qualified calls.

Callback semantics belong to the installed runtime. In v6, callbacks receive (input, { signal }); validate input, forward the signal to cancellable work, and return a JSON-serializable value. Return an MCP result with isError: true for an actionable error message: v6 replaces thrown callback errors with a generic error and rejects undefined and other non-serializable results. v5's existing result conversion remains unchanged. Registration cleanup uses an AbortSignal in both versions.

Custom Markdown rendering during streaming

postprocessMessage receives the original display text, including incomplete Markdown. After applying your own text transforms, call preprocessStreamingMarkdown(text, streaming) before your Markdown parser to use the same display repairs as the built-in renderer:

postprocessMessage: ({ text, streaming }) => {
  const transformed = text.replace(/<answer>|<\/answer>/g, '');
  return markdownPostprocessor(preprocessStreamingMarkdown(transformed, streaming));
}

The helper returns completed text unchanged and does not change stored message content or the callback's raw payload. It does not parse HTML or sanitize output; Persona's configured sanitizer still runs after the callback. On the CDN build it is available as AgentWidget.preprocessStreamingMarkdown. It uses the existing lazy Markdown parser chunk when ready, with lightweight table repairs available before that chunk loads.