TUI dev harness¶
The ralph CLI's terminal output — the iteration headers, peek panel,
result lines, run summary, and error logs — is rendered by
ConsoleEmitter in src/ralphify/_console_emitter.py. Iterating on
that visual design needs visual feedback, but coding agents running in
a non-interactive environment can't watch a live terminal. The harness
at scripts/tui_dev/ solves this — it generates PNG snapshots of any
ConsoleEmitter state that any agent (or human) can view.
Usage¶
./scripts/tui_dev/run.sh # regenerate everything (~15s)
./scripts/tui_dev/run.sh snapshot # fixture-driven mode only
./scripts/tui_dev/run.sh live # real ralph run via pty only
Outputs land in scripts/tui_dev/output/*.png (gitignored). The
iteration cycle is: edit _console_emitter.py → rerun the harness →
view the PNGs → repeat.
Modes¶
Snapshot mode (snapshot.py)¶
Drives the real ConsoleEmitter in-process through canned fixtures
defined in fixtures.py. Two flavours of fixture are supported:
Peek-panel scenarios (ALL_SCENARIOS)¶
Each scenario is a list of parsed Claude stream-json dicts that get
fed through a real _IterationPanel. Used for iterating on the live
activity feed (the bordered panel shown when peek is on).
| Scenario | What it shows |
|---|---|
01_empty |
Peek toggled on, zero scroll lines yet |
02_single_tool |
First tool call after peek turns on |
03_mixed_activity |
Thinking, tool calls, text preview — typical mid-iteration |
04_scroll_buffer_full |
17 tool calls — exceeds the visible scroll cap |
05_heavy_tokens |
1M+ input tokens, exercising the M unit in the token formatter |
06_rate_limit |
Rate-limit event mixed with normal activity |
07_tool_error |
Red tool_result error branch |
08_raw_spinner |
_IterationSpinner path for non-Claude agents |
09_peek_off |
Peek toggled off mid-iteration — verifies the buffer persists |
Event-sequence scenarios (EVENT_SCENARIOS)¶
Each scenario is a list of (EventType, dict) tuples that get fed
through the real emitter. Whatever the emitter prints to the recording
console is what ends up in the snapshot. Used for iterating on
anything that isn't the peek panel — iteration result lines, run
summaries, error logs, markdown result rendering.
| Scenario | What it shows |
|---|---|
10_iteration_success |
Happy path: green checkmark + markdown result |
11_iteration_failed |
Red failure with exit code and log file path |
12_iteration_timeout |
Yellow timeout branch |
13_run_summary_mixed |
Multi-iteration run with success / failure / timeout |
14_log_error |
Error log message with traceback |
Snapshot mode subclasses ConsoleEmitter to disable Live.start, so
each scenario produces exactly one frozen render. Rendering goes
through Rich's own save_svg → headless Chrome → PNG — no lossy
intermediaries, and colors/fonts match what Rich emits to the terminal.
Fast (~15s total), deterministic, no subprocess — this is the mode to use for most design work.
Live mode (live.py)¶
Maximum fidelity. Spawns the real ralph binary in a pseudo-terminal
via pty.openpty(), with fake_bin/claude (a Python script named
claude so _is_claude_command treats it as a structured agent)
emitting realistic stream-json on a 1.8s cadence. The capture
deliberately freezes at t=9s so the Live panel is still
mid-iteration, then feeds the raw ANSI byte stream through
pyte — a pure-Python terminal
emulator — to reduce cursor moves and clears to a stable screen grid.
That grid is then rendered through Rich for the final SVG → PNG.
pyte is installed on-demand via uv run --with pyte (not a project
dep).
Adding a new scenario¶
Peek-panel scenario¶
Append a builder function to scripts/tui_dev/fixtures.py and
register it in ALL_SCENARIOS:
def scenario_my_new_case() -> list[dict[str, Any]]:
return [
system_init(),
assistant_tool_use("Bash", {"command": "pytest -x"}, input_tokens=1200),
# ...
]
ALL_SCENARIOS = {
# ...
"15_my_new_case": scenario_my_new_case(),
}
Event-sequence scenario¶
Append an events builder and register it in EVENT_SCENARIOS:
def events_my_new_case() -> list[tuple[EventType, dict[str, Any]]]:
return [
_run_started("demo / 16_my_new_case"),
(EventType.ITERATION_STARTED, {"iteration": 1}),
(EventType.ITERATION_COMPLETED, {
"iteration": 1,
"detail": "completed (5s)",
"log_file": None,
"result_text": "all good",
}),
]
EVENT_SCENARIOS = {
# ...
"16_my_new_case": events_my_new_case(),
}
The next ./scripts/tui_dev/run.sh snapshot will produce
scripts/tui_dev/output/16_my_new_case.png.
Files¶
scripts/tui_dev/
├── run.sh # one-command launcher
├── snapshot.py # mode A: in-process ConsoleEmitter + fixtures
├── live.py # mode B: real ralph run via pty + pyte
├── fixtures.py # canned event sequences (peek + general)
├── render.py # shared SVG → PNG via headless Chrome
├── fake_bin/claude # stub Claude agent used by live mode
├── demo_ralph/RALPH.md # minimal ralph dir used by live mode
└── output/ # PNG + SVG snapshots (gitignored)