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.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/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,
  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 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, all 14 render 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, 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/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 features.suggestReplies.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.