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

generative-frame

v0.0.6

Published

Stream model-generated HTML and SVG widgets into Safe Content Frames, with a host bridge, theming, diagnostics, screenshots, tools, and prompts

Readme

generative-frame

Render model-generated HTML and SVG widgets safely while they stream. By default each widget runs in a Safe Content Frame on its own site (or, with opaqueOrigin, in a null-origin sandbox), under a strict Content Security Policy, and code streams into it chunk by chunk without reloading the frame.

  • Streaming: partial markup renders as it arrives and is morphed in place, so finished elements stay put while new ones fade in. Scripts are held and run once, in order, when the code is complete.
  • Host bridge: widgets call sendPrompt(text), openLink(url), and genframe.callTool(name, args). The frame speaks the MCP Apps ui/* JSON-RPC protocol, so MCP Apps widgets work too.
  • Theming: your page's theme (shadcn/ui variables or the canonical tokens) becomes CSS variables in the frame, also under the MCP Apps standard names, and updates live.
  • Diagnostics: errors, unhandled rejections, console output, failed resources, CSP violations, and blank renders, plus a PNG screenshot taken inside the frame.
  • Model side: provider-agnostic tool definitions (read_me, show_widget, edit_widget, preview_widget, and render_spec from the spec entries), a deterministic guidance generator, and a repair loop.
  • Spec mode: the model streams JSONL patches against a catalog of your own components; no frame, your design system.
  • assistant-ui: toolkit entries that render the tool calls in a thread as their arguments stream.

Framework-agnostic; the React, Vue, and assistant-ui bindings are optional, and each framework is an optional peer of the entries that use it. On React Native, generative-frame/spec/react and generative-frame/spec/assistant-ui resolve to React Native builds through the react-native export condition.

| Entry | Contents | | --- | --- | | generative-frame | createWidget, previewWidget, theme, CSP, runtime | | generative-frame/react | <Widget>, useWidget, useThemeTokens | | generative-frame/tools | widget tools, toAISDKTools, getToolDeclarations, buildWidgetInstructions | | generative-frame/prompts | buildWidgetGuidance | | generative-frame/repair | repairLoop | | generative-frame/assistant-ui | createWidgetToolkit, useWidgetInstructions, useAssistantUiThemeTokens | | generative-frame/spec | emptySpec, defineCatalog, createSpecStream, parseSpecStream, applyPatch, validateSpec, formatSpecIssues, createStateStore, and types | | generative-frame/spec/react | <SpecRenderer>, useSpecStream | | generative-frame/spec/vue | SpecRenderer, useSpecStream, useSpecElement for Vue | | generative-frame/spec/tools | createSpecTools (render_spec), specGuidanceModule | | generative-frame/spec/assistant-ui | createSpecToolkit | | generative-frame/spec/assistant-ui/vue | createSpecToolkit for @assistant-ui/vue |

The frame entries and the spec entries never import each other, so an app that only renders HTML widgets ships no spec code, and a spec-only app ships no frame, runtime, or Safe Content Frame. Combine them by passing values in, as shown under Spec mode. src/bundle-boundary.test.ts enforces this.

Installation

npm install generative-frame

Usage

import { createWidget, readThemeTokens } from "generative-frame";

const widget = createWidget({
  container: document.getElementById("widget")!,
  tokens: readThemeTokens(),
  onPrompt: (text) => sendChatMessage(text),
});

for await (const chunk of modelStream) widget.write(chunk);
await widget.end();

createWidget returns immediately; writes made before the frame connects are queued. The iframe resizes to its content (maxHeight caps it).

| Method | Purpose | | --- | --- | | write(chunk) | Append streamed code. Rendering is coalesced to animation frames. | | end() | Mark the code complete, run held scripts, and resolve with { size, blank, errorCount }. | | replace(code) | Render new complete code. Morphs in place, or remounts the frame if scripts already ran. | | setTheme(tokens) / setContext(context) | Update the theme or MCP Apps host context. | | notifyToolInput(args, { partial }) / notifyToolResult(result) | Forward tool data to MCP Apps widgets. | | screenshot() | Capture the widget as a PNG data URL (best effort, see below). | | inspect() | Errors, console, size, blank flag, kind, and the code so far. | | on(event, fn) | ready, resize, error, log, end. | | dispose() | Remove the frame. |

Pass id to give a widget a stable origin, so its localStorage and IndexedDB persist across reloads for that id on your site; choose it on the host, never from model output. clearWidgetStorage(id) wipes it. Without id, every frame gets a fresh origin.

Handlers: onPrompt, onMessage, onOpenLink, onCallTool, onInitialized, onRequestDisplayMode, onUpdateModelContext, onWidgetState, onResize, onError, onLog.

onInitialized runs when an MCP Apps widget sends ui/notifications/initialized (for a frame that replace() remounts, once that frame is shown) and receives the params of its ui/initialize request (appInfo, appCapabilities, and protocolVersion). When a widget declares appCapabilities.availableDisplayModes, the MCP Apps spec forbids switching it into a display mode missing from that list, so check the list in onRequestDisplayMode.

Code that starts with <svg renders as a standalone SVG; anything else is an HTML fragment.

Preview

previewWidget(code, { width, appearance }) renders complete code in a hidden frame and resolves with { ok, kind, width, height, blank, errors, console, screenshot }, plus screenshotError when the capture failed.

Content Security Policy

By default a frame blocks fetch, XHR, and WebSocket (connect-src 'none'), loads scripts, styles, images, and fonts only from cdnjs, jsDelivr, unpkg, and esm.sh, and blocks form submissions and eval. Configure with csp: { cdnOrigins, fontStyleOrigins, fontOrigins, connectOrigins, imageOrigins, frameOrigins, allowEval }, or pass a policy string. fontStyleOrigins and fontOrigins default to empty and add hosts only to style-src and font-src, respectively. To opt into Google Fonts, set fontStyleOrigins: ["https://fonts.googleapis.com"] and fontOrigins: ["https://fonts.gstatic.com"]. With csp: { cdnOrigins: [] }, external scripts, styles, fonts, images, and media are blocked unless explicitly allowed. Opaque-origin frames also enforce the parent page's CSP; these options cannot relax it. If the parent policy blocks the inline bootstrap runtime, the frame never becomes ready and fails with a ready timeout. A hash-based parent policy that allows the bootstrap runtime may still block model-written widget scripts.

React

import { Widget, useThemeTokens } from "generative-frame/react";

function ToolCall({ args, status }) {
  const tokens = useThemeTokens();
  return (
    <Widget
      code={args.widget_code ?? ""}
      streaming={status.type === "running"}
      tokens={tokens}
      onPrompt={(text) => composer.send(text)}
    />
  );
}

<Widget> accepts code that is complete or still growing: it writes only the new suffix, ends when streaming turns false, and replaces when the code is not an extension of what it has. useWidget(options) gives you the handle directly.

Tools and prompts

import { jsonSchema } from "ai";
import { createWidgetTools, toAISDKTools } from "generative-frame/tools";

const tools = createWidgetTools();
streamText({ model, tools: toAISDKTools(tools, { jsonSchema }) });

Each tool is { name, description, inputSchema, execute } with a JSON Schema input, so it adapts to any provider. edit_widget applies exact old_string → new_string replacements to the latest code of a widget, tracked by title in a registry, and fails without changes when a match is missing or ambiguous. preview_widget renders through the preview option, typically previewWidget in the browser; without it the tool returns an error result, so leave it out of tool sets that run on a server.

generative-frame/prompts exports buildWidgetGuidance({ modules, platform, tokens, cdnOrigins, fontStyleOrigins, fontOrigins, connectOrigins, allowEval, width, hostApi }), which read_me returns: base rules, a token reference generated from your tokens, platform notes, rules that match the frame's CSP (cdnOrigins, fontStyleOrigins, fontOrigins, connectOrigins, allowEval) and host API (hostApi), and modules (diagram, chart, data_viz, interactive, mockup, elicitation, art). Guidance only describes the policy to the model; it does not grant access. Configure matching origins in createWidget({ csp }).

Repair loop

import { previewWidget } from "generative-frame";
import { repairLoop } from "generative-frame/repair";

const { code, ok } = await repairLoop({
  generate: ({ feedback, previousCode }) => askModel(feedback?.text, previousCode),
  render: (code) => previewWidget(code),
  maxRounds: 3,
});

generate returns new code or { edits }. Feedback lists errors with locations, a console excerpt, a blank-render flag, and optionally the screenshot.

Spec mode

For UI built from your own components, the model writes a flat spec ({ root, elements: { id: { type, props, children } }, state }) as RFC 6902 JSON Patch operations, one per line, against a catalog you define. Nothing runs in a frame: your components render it.

import { defineCatalog } from "generative-frame/spec";

export const catalog = defineCatalog({
  components: {
    Card: {
      description: "A titled panel.",
      props: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
      slots: ["default", "footer"],
    },
    Button: {
      description: "A button.",
      props: { type: "object", properties: { label: { type: "string" } }, required: ["label"] },
      events: ["press"],
    },
  },
  actions: {
    refresh: { description: "Reloads the data.", params: { type: "object", properties: { range: { type: "string" } } } },
  },
});

const guidance = catalog.prompt({ mode: "inline" }); // or "jsonl" for render_spec

Props are JSON Schema; a Standard Schema that exposes JSON Schema (Zod 4) works too. catalog.prompt() documents the components, actions, the patch protocol, expressions, and rules.

import { SpecRenderer, useSpecStream } from "generative-frame/spec/react";

const { spec, text } = useSpecStream({ source: modelOutput, mode: "inline", complete: !streaming });

<SpecRenderer
  spec={spec}
  catalog={catalog}
  components={{ Card: ({ props, children }) => <section><h2>{props.title}</h2>{children}</section>, Button: ({ props, emit }) => <button onClick={() => emit("press")}>{props.label}</button> }}
  handlers={{ refresh: ({ range }, { state }) => state.set("/rows", load(range)) }}
  streaming={streaming}
/>;
  • Streaming: createSpecStream() (and useSpecStream) apply each complete line as it arrives, skip and report bad lines, and in inline mode separate prose from patches (in a ```spec fence or bare {"op" lines). While the spec streams, elements that have not arrived take no space, an element with invalid props keeps its last valid render, and one pending placeholder follows the spec; once it stops, unknown types, invalid props, and components that throw render a small placeholder, as do cycles at any time.
  • Expressions: { $state: "/path" }, { $bindState: "/path" } (two-way, via setProp), { $template: "Hi ${/name}" }, { $cond, $then, $else }, { $calc: "${/price} * ${/qty}" } (bounded formulas with arithmetic, comparisons, list totals, and formatting), and $item/$bindItem/$index inside repeat. visible takes conditions with eq, neq, gt, gte, lt, lte, not, $and, $or, and arrays (all).
  • Actions: on: { press: { action, params } } runs the built-in setState or a catalog action through your handlers; watch: { "/path": action } runs when a state value changes. User changes to state survive the model streaming more state.
  • Validation: validateSpec(spec, catalog) returns issues with codes and JSON Pointers (unknown-type, invalid-props, missing-child, cycle, unknown-action, …); formatSpecIssues turns them into repair feedback.
  • Tool: createSpecTools(catalog) from generative-frame/spec/tools returns render_spec({ title, patches | spec }), which validates and returns the issues. Patches on a title already rendered apply to that spec, so repairs are small. A spec-only app puts catalog.prompt() in its system prompt; next to the widget tools, compose them with createWidgetTools({ extraTools: createSpecTools(catalog), modules: [specGuidanceModule(catalog)] }), which also adds a spec module to read_me.
  • Vue: generative-frame/spec/vue exports SpecRenderer, useSpecStream, and useSpecElement for Vue and needs vue (an optional peer, imported only by that entry). useSpecStream returns refs, and a Vue component gets the props it declares, its children and named slots as slots, and emits the spec events it declares in emits.

The format follows the flat-spec and JSONL-patch shape of Vercel's json-render, without depending on it.

Delegated generation

To keep widget code out of the main agent's context, give it one tool that asks a second model for the widget and checks it with repairLoop and previewWidget. The docs have a short recipe; the package ships no agent of its own.

assistant-ui

generative-frame/assistant-ui and generative-frame/spec/assistant-ui need @assistant-ui/react (an optional peer, imported only by these entries); on React Native, generative-frame/spec/assistant-ui resolves to a build that needs @assistant-ui/react-native instead. generative-frame/spec/assistant-ui/vue builds the spec toolkit for @assistant-ui/vue instead and needs it and @assistant-ui/core.

import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { createWidgetToolkit, useWidgetInstructions } from "generative-frame/assistant-ui";
import { createSpecToolkit } from "generative-frame/spec/assistant-ui";

const widgets = createWidgetToolkit({
  widget: { maxHeight: 700 },
  spec: createSpecToolkit(catalog, { components }), // optional, from generative-frame/spec/assistant-ui
});

function Provider({ children }) {
  const runtime = useChatRuntime();
  const config = AuiConfig({ tools: Tools({ toolkit: widgets.toolkit }) });
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      <Instructions />
      {children}
    </AssistantRuntimeProvider>
  );
}

function Instructions() {
  useWidgetInstructions(widgets.tools); // adds when-to-use rules to the system prompt
  return null;
}
  • show_widget streams widget_code into a <Widget> from the partial tool arguments; edit_widget replays the edits from the thread, so history renders after a reload; render_spec streams patches into a <SpecRenderer>.
  • Within one message, a later show_widget with the same title takes over a widget as it streams, and a later edit_widget once its edits are complete and apply, so the widget shows once; a streaming or failed edit leaves it in place, and earlier messages keep their own widgets.
  • In frontend execution, calls that render a frame wait for it to finish and return a render report (errors, console warnings, blank flag, height, and feedback text), so the model can fix a broken widget with edit_widget. A superseded call returns immediately with a timeout report explaining that it was not rendered. Tune with renderReport: { timeoutMs, settleMs } (10000 and 300 ms by default) or turn it off with renderReport: false.
  • preview_widget runs previewWidget in the browser. Its result leaves out the PNG unless previewScreenshot: true, because a base64 image in a JSON tool result is large.
  • A widget's sendPrompt(text) appends a user message to the thread.
  • Frames follow the app's shadcn/ui theme (useAssistantUiThemeTokens).
  • The tools execute in the browser by default; the server forwards their schemas with frontendTools(tools) from @assistant-ui/ai-sdk. With execution: "backend", run toAISDKTools(createWidgetTools(), { jsonSchema }) on the server and the toolkit only renders. getToolDeclarations and buildWidgetInstructions from generative-frame/tools give a server the schemas and the instructions text.

Limits

  • Screenshots serialize the DOM into an SVG foreignObject. Web fonts loaded by URL, cross-origin images without CORS, and pseudo-elements are not reproduced, and layout can drift by a few pixels.
  • A widget that changes after its scripts ran is remounted in a new frame, because classic scripts cannot be re-run in the same document.

Development

src/runtime is the code that runs inside the frame. scripts/build-runtime.mjs bundles it into src/runtime/generated.ts (gitignored) before build, test, and typecheck. pnpm dev serves demos at http://localhost:5199 (/, /spec.html, /thread.html, /storage.html); append ?auto (and &dark) to run a scripted scenario, which saves screenshots and reports to .screenshots/. The thread demo uses recorded model streams, so they make no network model calls.