Labs/Streaming specs and catalogs
03

Render before the brace closes

A model streams a UI spec token by token. Watch the partial-JSON repair, the progressive render, and a catalog guardrail reject a component that isn't allowed.

GoalUnderstand how declarative generative UI streams safely: repair the prefix, render what's known, validate against a catalog.

partial JSONprogressive renderingcomponent catalogschema validation
chunk 00 / 98
Model output
Render

Every component and prop is in the catalog.

1 · Raw stream0 / 977 chars

Press play. The model streams its UI spec ten characters at a time.

2 · What the parser seesnothing usable yet
3 · Rendered through the catalogprogressive

Nothing to draw yet.

NodeTypeStatusWhy
No nodes parsed yet.
The catalog the renderer enforcesclick to expand
// catalog.ts — the only parts the model may use
const actions = z.enum(["approve_expense", "reject_expense", "request_info"])

export const catalog = defineCatalog({
  Card:   { props: z.object({ title: z.string().optional() }), children: true },
  Text:   { props: z.object({ text: z.string(),
                              variant: z.enum(["heading", "body", "muted"]).optional() }) },
  Money:  { props: z.object({ amount: z.number(),
                              currency: z.enum(["USD", "EUR", "CAD"]) }),
            stream: "hold" },          // never show a half-arrived number
  Badge:  { props: z.object({ label: z.string(),
                              tone: z.enum(["ok", "warn", "fail"]).optional() }) },
  Row:    { props: z.object({}), children: true },
  Button: { props: z.object({ label: z.string(), action: actions,
                              variant: z.enum(["primary", "default"]).optional() }) },
})
// Illustrative API, in the spirit of json-render and A2UI catalogs.
How it works: repair, validate, renderclick to collapse

Repair the prefix. Close the open string, drop a dangling key or comma, then close every open bracket. Pane 2 shows exactly what was invented. It runs on every chunk, so it has to be cheap.

Pending is not invalid. Mid-stream, a missing prop or a half-typed enum is just late. Only refuse a value once it can no longer become valid, or once the stream is complete.

Hold what must be exact. A progressive amount reads $1, $12, $124 before $1,240. The Money component waits for its number to close. Decide per component what may flicker.

The catalog is the boundary. The model can ask for anything. The renderer draws only registered components with allowed props and actions, and refuses the rest visibly.

for await (const chunk of stream) {
  text += chunk
  const { value, complete } = parsePartialJson(text)  // repair the prefix
  render(value, { complete })
}

function render(node, ctx) {
  const entry = catalog[node.type]
  if (!entry) {
    // "Car" may still become "Card"; "CreditCardForm" never will
    return couldStillBecomeValid(node.type, ctx) ? <Pending/> : <Refused/>
  }
  const check = entry.props.safeParsePartial(node.props, ctx)
  if (check.refused) return <Refused reason={check.reason}/>
  return <entry.Component {...node.props}>
    {node.children?.map(c => render(c, ctx))}
  </entry.Component>
}