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.
<GenUI
response={response}
streaming={streaming}
onAction={(action) => send(action.message)}
onStateChange={(state) => save(state)}
initialState={savedState}
onError={(errors) => send(formatErrors(errors))}
/>;| Prop | Type | Description |
|---|---|---|
response | unknown | The JSON text, or an already parsed object. null, undefined, and the empty string render nothing. |
streaming | boolean | The response is still arriving. Buttons and submits wait until it ends. |
catalog | readonly CatalogEntry[] | The components a response may use. Defaults to the built-in catalog. |
onAction | (action: GenUIAction) => void | A button was pressed, a form submitted, or a suggestion chosen. Send action.message as the next user turn. |
onStateChange | (state: GenUIState) => void | The person changed a control. Receives every stored value by state key. |
initialState | Readonly<Record<string, unknown>> | Values to restore, such as the state onStateChange last reported. Read once, on mount. |
onError | (errors: GenUIError[]) => void | The finished response had problems. Called once per distinct set of problems, never while streaming. |
store | GenUIStore | A store from createGenUIStore kept outside the component, which then ignores initialState. |
call | ToolCaller | Runs the host's tools for Query nodes and call steps: a server function that calls runTool, a fetch, or an MCP tools/call. |
slots | Record<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
typeis 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
streamingis 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
streamingbecomes false the full response is validated once, defaults are written into the state, andonErroris 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.
<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.
| Field | Type | Description |
|---|---|---|
type | "button" | "form" | "suggestion" | What the person did. |
name | string | The form's name, for a form and for a button inside one. |
message | string | The person's request in their own words. Send it as the next user turn. |
values | Record<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.
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.
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| Field | Type | Description |
|---|---|---|
path | string | JSON Pointer to the offending value, such as /children/2/options/1/value. The root is the empty string. |
code | GenUIErrorCode | What kind of problem it is; stable, so your app can branch on it. |
message | string | One sentence naming the problem and the expected form. |
component | string | The type of the component the problem belongs to, when there is one. |
| Code | Meaning |
|---|---|
syntax | The text is not valid JSON past this point, or the response is empty. |
truncated | The response ended before its JSON was closed. |
not-a-component | A value where a component belongs is not an object. |
missing-type | A component object has no type, or its type is not a string. |
unknown-type | The type is not a component in the catalog. |
missing-prop | A required prop is missing, so the component is not drawn. |
invalid-prop | A prop has the wrong type or value; it is dropped. |
unknown-prop | The component does not declare the prop; it is dropped. |
unsafe-url | A link or image URL uses a scheme that is not allowed; it is not used. |
expression | An $expr is not a string or does not parse. |
binding | A $bind name is not letters, digits, and underscores, or is longer than 64 characters. |
unknown-binding | An expression reads a name that no control binds. |
limit | The response is larger or deeper than the renderer draws, or more problems were found than are listed. |