Skip to content

GenUI

Browser renderer

GenUI renders a response in the browser while it streams, reports what the person does, and says what the model should fix. Tools, actions, and errors work the same with server components.

Render a response

Hand GenUI the whole text so far on every chunk, or an already parsed object. Output sits in a comp0 BusyRegion, so it is aria-busy while streaming is true.

Exampletsx
<GenUI
  response={response}
  streaming={streaming}
  onAction={(action) => send(action.message)}
  onStateChange={(state) => save(state)}
  initialState={savedState}
  onError={(errors) => send(formatErrors(errors))}
/>;
GenUI props
PropTypeDescription
responseunknownThe JSON text, or an already parsed object. null, undefined, and the empty string render nothing.
streamingbooleanThe response is still arriving. Buttons and submits wait until it ends.
catalogreadonly CatalogEntry[]The components a response may use. Defaults to the built-in catalog.
onAction(action: GenUIAction) => voidA button was pressed, a form submitted, or a suggestion chosen. Send action.message as the next user turn.
onStateChange(state: GenUIState) => voidThe person changed a control. Receives every stored value by state key.
initialStateReadonly<Record<string, unknown>>Values to restore, such as the state onStateChange last reported. Read once, on mount.
onError(errors: GenUIError[]) => voidThe finished response had problems. Called once per distinct set of problems, never while streaming.
storeGenUIStoreA store from createGenUIStore kept outside the component, which then ignores initialState.
callToolCallerRuns the host's tools for Query nodes and call steps: a server function that calls runTool, a fetch, or an MCP tools/call.
slotsRecord<string, ReactNode>Host views the model can place by name with a Slot.

The streaming contract

Any provider that streams text works, because the renderer only sees a growing string.

  • Every prefix of a response renders without errors, warnings, or thrown exceptions.
  • A component appears once its type is complete and every prop it requires has arrived. A string that is still being written shows as written, except URLs, binding names, and expressions, which are used only once complete.
  • Nodes are keyed by their JSON Pointer, so a component already drawn is never replaced as the response grows; typed text, focus, and open sections survive.
  • While streaming is true, buttons, suggestions, copy buttons, and submits are disabled, nothing is reported as an error, and comp0 parts hold back data warnings and focus moves.
  • When streaming becomes false the full response is validated once, defaults are written into the state, and onError is called if there are problems.

Connect the interface to your app

The model composes layout; your app supplies what it can reach. A tool is a function with a schema for its input, declared with defineTool. The interface calls tools through one call(name, args) function you pass to the renderer, so the same tools work from an HTTP endpoint (handleToolRequest on the server, httpToolCaller or just the endpoint's URL in the browser), a function that calls runTool (which validates the arguments and keeps error details on the server), or an MCP tools/call.

A Query node calls a tool and stores the result under a name, again whenever a bound argument changes, without another turn; a chart's data, a table's rows, or a list's items bound to that name follow it. A button's action runs steps in order: call a tool, set a value, or send a message. A Slot places one of your own views, which read the same values with hooks, so a slider the model wrote can drive your canvas live.

With React Server Components, @comp0/genui/rsc streams layout as server HTML: GenUIRoot holds the values, slots, and tool caller, and any number of GenUIView elements stream responses into it, sharing them. Server components covers it in full.

Inside an MCP Apps host, @comp0/genui/mcp renders a response the model writes as a tool call's arguments: genuiAppTool describes that tool on your MCP server, and McpAppGenUI renders it in the app's ui:// resource while the arguments stream, with tools going through the host's tools/call and actions becoming the person's next message.

Exampletsx
<GenUI
  response={response}
  streaming={streaming}
  call={callTool}
  slots={{ shader: <ShaderCanvas /> }}
/>;

// With React Server Components
<GenUIRoot call={callTool} slots={{ shader: <ShaderCanvas /> }}>
  <GenUIView stream={modelStream} />
</GenUIRoot>;

Turn actions into the next turn

A form's message uses the visible labels and, for choices, the visible option labels: Plan: Pro; Seats: 5. A form submitted untouched falls back to its submit label. A Button sends its message or its label, and inside a form it also carries the values so far. A suggestion sends its text.

GenUIAction
FieldTypeDescription
type"button" | "form" | "suggestion"What the person did.
namestringThe form's name, for a form and for a button inside one.
messagestringThe person's request in their own words. Send it as the next user turn.
valuesRecord<string, JsonValue>The form's controls by control name, when the action belongs to a form.

Save and restore what the person entered

onStateChange receives every stored value by state key whenever the person edits a control. Save it with the message, and pass it back as initialState to restore the answer later. To keep state across remounts, create a store with createGenUIStore() and pass it as store.

Exampletsx
const store = createGenUIStore(savedState);

<GenUI response={response} store={store} />;

Errors the model can fix

Once the response is complete, onError receives the problems found in it, at most 50, each written to be sent back to the model. formatErrors turns them into one repair message. The renderer still shows everything that is valid: unknown components and props are left out, a prop of the wrong type is dropped, an unsafe URL is not used, and a component missing a required prop is not drawn.

Exampletsx
formatErrors(errors);
// The response has 1 problem. Fix it and answer again with the complete corrected JSON:
// - /children/1 [Select]: required property label is missing
GenUIError
FieldTypeDescription
pathstringJSON Pointer to the offending value, such as /children/2/options/1/value. The root is the empty string.
codeGenUIErrorCodeWhat kind of problem it is; stable, so your app can branch on it.
messagestringOne sentence naming the problem and the expected form.
componentstringThe type of the component the problem belongs to, when there is one.
GenUIErrorCode
CodeMeaning
syntaxThe text is not valid JSON past this point, or the response is empty.
truncatedThe response ended before its JSON was closed.
not-a-componentA value where a component belongs is not an object.
missing-typeA component object has no type, or its type is not a string.
unknown-typeThe type is not a component in the catalog.
missing-propA required prop is missing, so the component is not drawn.
invalid-propA prop has the wrong type or value; it is dropped.
unknown-propThe component does not declare the prop; it is dropped.
unsafe-urlA link or image URL uses a scheme that is not allowed; it is not used.
expressionAn $expr is not a string or does not parse.
bindingA $bind name is not letters, digits, and underscores, or is longer than 64 characters.
unknown-bindingAn expression reads a name that no control binds.
limitThe response is larger or deeper than the renderer draws, or more problems were found than are listed.