Debugging & AI agents
A ztui UI is never a black box. The widget DOM and the rendered frame serialize to plain text, HTML, and JSON, the app runs headless with no TTY, and a REST inspector exposes the live screen over HTTP. That’s what makes ztui practical to test in CI and to operate with an LLM agent — the thing a native byte-stream backend can’t give you (see Why ztui).
Serialize any frame
Section titled “Serialize any frame”The composed cell grid renders to text or styled HTML — no terminal required:
import { App, MockDriver, renderBufferToText, renderBufferToHTML } from "@huyz0/ztui";import { render } from "@huyz0/ztui/react";
const app = new App(new MockDriver(40, 10)); // headless, fixed-size gridrender(<MyUI />, app.activeScreen);app.run();
renderBufferToText(app.buffer); // the screen as plain textrenderBufferToHTML(app.buffer); // the screen as styled HTML (for snapshots)MockDriver is a no-TTY backend built for this: it captures output and lets you
inject input, so the same widget tree you ship runs in a test or a script.
Assert on the UI in a test
Section titled “Assert on the UI in a test”Because the frame is text, assertions are ordinary string checks — no screenshots, no flaky pixel diffs:
import { App, MockDriver, renderBufferToText } from "@huyz0/ztui";import { render } from "@huyz0/ztui/react";
test("counter increments", async () => { const driver = new MockDriver(40, 5); const app = new App(driver); render(<Counter />, app.activeScreen); app.run(); await new Promise((r) => setTimeout(r, 15)); // let React commit + render
driver.simulateKey({ key: "enter", name: "enter", ctrl: false, meta: false, shift: false }); await new Promise((r) => setTimeout(r, 15));
expect(renderBufferToText(app.buffer)).toContain("Count: 1"); app.stop();});MockDriver exposes simulateKey / simulateMouse / simulateResize to drive
the app, and writtenData to assert on raw output.
The live REST inspector
Section titled “The live REST inspector”For a running app, startInspector(app) serves the live screen over HTTP — so a
human, a script, or an agent can watch and drive it from outside the process:
import { startInspector } from "@huyz0/ztui";const inspector = startInspector(app, 8000); // GET endpoints + POST /input| Endpoint | Returns |
|---|---|
GET /screenshot | the current screen as plain text |
GET /render | the current screen as styled HTML |
GET /dom | the widget tree as JSON (ids, classes, regions) |
GET /tree | the widget tree as an indented text outline |
GET /state | a high-level app snapshot (focus, size, capabilities) |
GET /log?lines=N | the last N log lines |
POST /input | inject a key or mouse event |
curl localhost:8000/screenshot # see the screencurl -XPOST localhost:8000/input -d '{"type":"key","key":"enter"}' # press a keycurl -XPOST localhost:8000/input -d '{"type":"mouse","x":4,"y":2,"action":"press"}'The agent loop
Section titled “The agent loop”These two facts — read the screen as text, inject input over HTTP — close the loop for an AI agent with no terminal at all:
GET /screenshot→ the agent reads the current UI as text.- The agent decides what to do.
POST /input→ it presses a key or clicks.- Repeat.
The same loop works offline against MockDriver (renderBufferToText +
simulateKey) for deterministic, fully-scripted runs in CI. An agent can build a
feature, drive its own UI, read back the rendered result, and assert it’s correct
— without a human relaying screenshots.
The web backend, headless
Section titled “The web backend, headless”The browser/canvas backend is inspectable too. bun run web:debug renders a UI
in headless Chromium, saves a screenshot, and prints a pixel-accurate report (row
gaps, overflow, font-loaded, cell width) — the way to verify the web backend in
CI without a person at a browser.
Performance: benchmarks & regression guards
Section titled “Performance: benchmarks & regression guards”ztui re-renders the whole widget tree to a cell buffer and diffs it to ANSI every frame, so a small algorithmic regression in the render/layout core can silently hurt interactivity. Two commands cover the hot paths (buffer/diff, ANSI serialization, text measurement & wrapping, layout, selection, markdown, CSS resolution, and the end-to-end frame):
bun run perf— ratio-guard tests (src/**/*.perf.ts). Each hot path is timed against a fixed calibration workload measured in the same process; the assertion is on the ratio, so it’s machine-independent and only trips on a real (order-of-magnitude) regression. Runs in CI. Some guards also assert deterministic invariants — e.g. an unchanged frame must diff to the empty string — which catch regressions with zero timing flake.bun run bench— vitestbench()tracking (src/**/*.bench.ts). Prints ops/sec for eyeballing gradual drift; not asserted.
Both use the shared harness in src/test/bench/perf-harness.ts and run on a
dedicated config (vitest.config.perf.ts), kept out of the default coverage gate
so commits stay fast. Budgets are committed constants set to ≈3× a healthy run;
if you make a deliberate change that moves a baseline, retune the budget in that
.perf.ts file (the failure message prints the observed ratio).
Why this is the selling point
Section titled “Why this is the selling point”Every other guarantee in ztui flows from this: a custom widget is correct if its serialized output is correct; a regression shows up as a text diff; an agent can operate an interface it can’t see. The UI is data you can query, not pixels you have to look at — which is exactly what testing and AI-assisted development need.