Models make small mistakes: a prop that does not exist, an enum value that is close but wrong, a reference nobody defined. GistUI repairs them in code, with no model call, and shows the repaired UI in place.
What gets repaired
| Mistake | Repair |
|---|---|
| Unknown prop, extra argument | Removed |
| Invalid value on an optional prop | Removed, so the default applies |
| Invalid value on a required prop | The closest allowed value |
| Missing required id or name | Filled from the call’s own text or its id |
Reference that misses a statement by case or one letter (MetricsRow, incidentsTable) |
Points at that statement |
| Reference to something never defined | The reference is removed |
| A section defined but never used | Added to the root’s children |
| A data table defined but never used | Shown in a Table on the root |
A table written inside a call (Table(|A|B|, |1|2|)) |
Moved to its own statement, every row kept |
Curly quotes used as quotes, a component named without () |
Read as meant |
One unreadable argument ("Today is " + new Date(), a stray ?) |
Its text is kept, or the argument is dropped; the rest of the statement stays |
No root statement, several sections |
A root is added that holds them all |
| Unknown component | Renamed when it is clearly a typo (Txt to Text); otherwise a plain container that keeps its children |
| A component whose required data is missing, or a cycle | That statement is removed |
Syntax slips: single quotes, key=value, a missing closing bracket, a trailing comma |
Read as meant, while streaming |
The last row happens during parsing: the format defines a numbered recovery rule for each slip, so every implementation reads imperfect text the same way. The others happen once, when the stream ends.
In the renderer
autofix is on by default in every renderer. onAutofix reports what changed.
<GistUI
library={ui}
stream={stream}
onAutofix={({ changes, errors }) => console.log(changes)} // changes: string[]
/>The repair code loads on demand, only when a program actually needs it, and only what changed re-renders. autofix={false} turns it off.
How much it helps
On the public benchmark (46 screens × 4 runs), autofix takes validity to 100% with gpt-oss-120b and to 99.5% with gpt-5-nano. Checking and repairing a screen takes 1–2 ms at the median and 7 ms at the slowest. OpenUI repairs with a second model call in OpenUI Cloud; GistUI’s repair is code in the library. No repaired screen lost a component.
| Model | As written | After autofix |
|---|---|---|
| gpt-oss-120b | 87.9% | 100% |
| gpt-5-nano | 22.8% | 99.5% |
Details in Benchmarks.
On the server
Use @gistui/server when the stored or logged answer should be the repaired one, or when you want to validate before sending.
import { library } from "@gistui/catalog";
import { repairProgram, repairStream, toReadable, validateProgram } from "@gistui/server";
validateProgram(text, library); // { valid, errors, fixed }
// A finished program:
const { valid, source, patch, errors, fixed } = await repairProgram(text, library);
// A stream: passes the text through, appends repairs as edits at the end.
return new Response(toReadable(repairStream(textStream, library)));Asking the model to fix the rest
Code cannot repair everything: a required prop whose value only the model knows, for example. Give repairProgram or repairStream a complete function and it asks your model to fix only the statements that still fail, with a small prompt.
const result = await repairProgram(text, library, {
complete: async ({ system, user }) => {
const r = await generateText({ model, system, prompt: user });
return r.text;
},
});- Deterministic repair always runs first. The model is asked only if errors remain.
- A model round is kept only when it leaves fewer errors than before.
- The program and its errors are sent inside
<program>and<errors>tags and described as data, so text inside the program cannot pass for instructions.
With @gistui/core directly
import { library } from "@gistui/catalog";
import { autofix, parse } from "@gistui/core";
const r = parse(source, library);
r.valid.strict; // no diagnostics at all
r.valid.lenient; // every problem was repaired by a rule
r.errors; // [{ code, stmtId, line, message, hint }]
const fixed = autofix(source, library); // the repaired program and the list of changesEvery diagnostic code is listed in Diagnostics.