/docsfor

Repair loop and screenshots

Render widget code offscreen, turn errors into model-readable feedback, and capture screenshots inside the frame.

A widget can fail in ways the model cannot see from its own output: a script throws, a library URL is blocked by the policy, or everything renders at zero height. This page covers the pieces that make those failures visible.

previewWidget

import { previewWidget } from "generative-frame";

const result = await previewWidget(code, { width: 680, appearance: "dark" });
// { ok, kind, width, height, blank, errors, console, screenshot?, screenshotError? }

previewWidget renders complete code in a hidden frame, waits for it to settle, inspects it, takes a screenshot, and disposes the frame. ok is true when there are no errors and the render is not blank. It needs a browser.

PreviewOptions
widthnumber= 680

Layout width in CSS pixels.

appearance"light" | "dark"= "light"

Selects the built-in token set when `tokens` is not given.

tokens?ThemeTokens

Theme tokens to render with.

screenshotboolean= true

Capture a PNG. When capture fails, `screenshotError` says why.

settleMsnumber= 600

Time to let scripts, fonts, and animations settle after `end`.

csp?CspOptions | string

The frame's Content Security Policy, as in `createWidget`.

product?string

Scopes the frame origin, as in `createWidget`.

frame?SafeContentFrame

A preconfigured Safe Content Frame.

css?string

Extra CSS for the frame.

readyTimeoutMsnumber= 15000

How long to wait for the frame to connect.

The preview frame stays inside the viewport, nearly transparent and behind the page, because Chrome stops animation frames in cross-origin frames outside the viewport, which would freeze script-driven charts mid-animation.

buildRepairFeedback

import { buildRepairFeedback } from "generative-frame/repair";

const feedback = buildRepairFeedback(result);
// feedback.text:
// The widget did not render cleanly. Fix the causes below.
//
// Errors (1):
// - error: Chart is not defined (line 14:5)
//
// Rendered height: 0px.

It takes a RenderReport and returns { ok, round, errors, console, blank, height?, screenshot?, text }. text lists errors with their locations, a blank-render note, an excerpt of console warnings and errors (the last 20 by default), and the rendered height. Options: round, ok (override the verdict), consoleLimit, and includeScreenshot (default true).

type RenderReport = {
  errors: WidgetError[];
  console: ConsoleEntry[];
  blank: boolean;
  height?: number;
  screenshot?: string;
};

A PreviewResult and the output of widget.inspect() (with height taken from size) both fit this shape. The assistant-ui toolkit uses the same text in its render reports.

repairLoop

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

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

Each round calls generate, renders the result, and builds feedback for the next round. generate returns new code as a string, or { edits } with exact replacements on the previous round's code. Edits that do not apply count as a failed round with an error that says why.

The loop stops at the first accepted render. If none is accepted, it returns the attempt with the fewest errors (a blank render counts as one).

RepairLoopOptions
generate(context: GenerateContext) => Promise<string | { edits }>

Produces code. `context` has `round`, `previousCode`, and `feedback` (both undefined in the first round).

render(code: string) => Promise<RenderReport>

Renders code and reports what happened, typically `previewWidget`.

maxRoundsnumber= 3

Rounds including the first generation.

accept(report: RenderReport) => boolean= no errors and not blank

Decides whether a render is good enough.

consoleLimitnumber= 20

Console entries carried into feedback.

includeScreenshotboolean= true

Forward the screenshot to `generate`, for models that accept images.

Screenshots

const { dataUrl, width, height } = await widget.screenshot({ scale: 2 });

A host page cannot capture a cross-origin frame, so the screenshot is taken inside the frame: the runtime clones the widget, inlines computed styles, serializes it into an SVG foreignObject, draws that onto a canvas, and returns a PNG data URL. scale defaults to the frame's device pixel ratio, capped at 2, and background defaults to --color-background. Canvas elements are copied as images.

It is best effort:

  • Web fonts loaded by URL are not embedded, because fetching them would need network access the frame does not have by default. Fonts already inlined as data: URLs are.
  • Cross-origin images served without CORS headers cannot be inlined.
  • Pseudo-elements (::before, ::after) are not reproduced.
  • Layout can drift by a few pixels from the live frame.
  • The canvas side is capped at 4096 pixels; larger captures are scaled down.