|
| 1 | +# TUI dev harness |
| 2 | + |
| 3 | +The `ralph` CLI's terminal output — the iteration headers, peek panel, |
| 4 | +result lines, run summary, and error logs — is rendered by |
| 5 | +`ConsoleEmitter` in `src/ralphify/_console_emitter.py`. Iterating on |
| 6 | +that visual design needs visual feedback, but coding agents running in |
| 7 | +a non-interactive environment can't watch a live terminal. The harness |
| 8 | +at `scripts/tui_dev/` solves this — it generates PNG snapshots of any |
| 9 | +`ConsoleEmitter` state that any agent (or human) can view. |
| 10 | + |
| 11 | +## Usage |
| 12 | + |
| 13 | +```bash |
| 14 | +./scripts/tui_dev/run.sh # regenerate everything (~15s) |
| 15 | +./scripts/tui_dev/run.sh snapshot # fixture-driven mode only |
| 16 | +./scripts/tui_dev/run.sh live # real ralph run via pty only |
| 17 | +``` |
| 18 | + |
| 19 | +Outputs land in `scripts/tui_dev/output/*.png` (gitignored). The |
| 20 | +iteration cycle is: edit `_console_emitter.py` → rerun the harness → |
| 21 | +view the PNGs → repeat. |
| 22 | + |
| 23 | +## Modes |
| 24 | + |
| 25 | +### Snapshot mode (`snapshot.py`) |
| 26 | + |
| 27 | +Drives the real `ConsoleEmitter` in-process through canned fixtures |
| 28 | +defined in `fixtures.py`. Two flavours of fixture are supported: |
| 29 | + |
| 30 | +#### Peek-panel scenarios (`ALL_SCENARIOS`) |
| 31 | + |
| 32 | +Each scenario is a list of parsed Claude stream-json dicts that get |
| 33 | +fed through a real `_IterationPanel`. Used for iterating on the live |
| 34 | +activity feed (the bordered panel shown when peek is on). |
| 35 | + |
| 36 | +| Scenario | What it shows | |
| 37 | +|---|---| |
| 38 | +| `01_empty` | Peek toggled on, zero scroll lines yet | |
| 39 | +| `02_single_tool` | First tool call after peek turns on | |
| 40 | +| `03_mixed_activity` | Thinking, tool calls, text preview — typical mid-iteration | |
| 41 | +| `04_scroll_buffer_full` | 17 tool calls — exceeds the visible scroll cap | |
| 42 | +| `05_heavy_tokens` | 1M+ input tokens, exercising the `M` unit in the token formatter | |
| 43 | +| `06_rate_limit` | Rate-limit event mixed with normal activity | |
| 44 | +| `07_tool_error` | Red `tool_result` error branch | |
| 45 | +| `08_raw_spinner` | `_IterationSpinner` path for non-Claude agents | |
| 46 | +| `09_peek_off` | Peek toggled off mid-iteration — verifies the buffer persists | |
| 47 | + |
| 48 | +#### Event-sequence scenarios (`EVENT_SCENARIOS`) |
| 49 | + |
| 50 | +Each scenario is a list of `(EventType, dict)` tuples that get fed |
| 51 | +through the real emitter. Whatever the emitter prints to the recording |
| 52 | +console is what ends up in the snapshot. Used for iterating on |
| 53 | +**anything that isn't the peek panel** — iteration result lines, run |
| 54 | +summaries, error logs, markdown result rendering. |
| 55 | + |
| 56 | +| Scenario | What it shows | |
| 57 | +|---|---| |
| 58 | +| `10_iteration_success` | Happy path: green checkmark + markdown result | |
| 59 | +| `11_iteration_failed` | Red failure with exit code and log file path | |
| 60 | +| `12_iteration_timeout` | Yellow timeout branch | |
| 61 | +| `13_run_summary_mixed` | Multi-iteration run with success / failure / timeout | |
| 62 | +| `14_log_error` | Error log message with traceback | |
| 63 | + |
| 64 | +Snapshot mode subclasses `ConsoleEmitter` to disable `Live.start`, so |
| 65 | +each scenario produces exactly one frozen render. Rendering goes |
| 66 | +through Rich's own `save_svg` → headless Chrome → PNG — no lossy |
| 67 | +intermediaries, and colors/fonts match what Rich emits to the terminal. |
| 68 | + |
| 69 | +Fast (~15s total), deterministic, no subprocess — this is the mode to |
| 70 | +use for most design work. |
| 71 | + |
| 72 | +### Live mode (`live.py`) |
| 73 | + |
| 74 | +Maximum fidelity. Spawns the real `ralph` binary in a pseudo-terminal |
| 75 | +via `pty.openpty()`, with `fake_bin/claude` (a Python script named |
| 76 | +`claude` so `_is_claude_command` treats it as a structured agent) |
| 77 | +emitting realistic stream-json on a 1.8s cadence. The capture |
| 78 | +deliberately freezes at t=9s so the `Live` panel is still |
| 79 | +mid-iteration, then feeds the raw ANSI byte stream through |
| 80 | +[`pyte`](https://github.com/selectel/pyte) — a pure-Python terminal |
| 81 | +emulator — to reduce cursor moves and clears to a stable screen grid. |
| 82 | +That grid is then rendered through Rich for the final SVG → PNG. |
| 83 | + |
| 84 | +pyte is installed on-demand via `uv run --with pyte` (not a project |
| 85 | +dep). |
| 86 | + |
| 87 | +## Adding a new scenario |
| 88 | + |
| 89 | +### Peek-panel scenario |
| 90 | + |
| 91 | +Append a builder function to `scripts/tui_dev/fixtures.py` and |
| 92 | +register it in `ALL_SCENARIOS`: |
| 93 | + |
| 94 | +```python |
| 95 | +def scenario_my_new_case() -> list[dict[str, Any]]: |
| 96 | + return [ |
| 97 | + system_init(), |
| 98 | + assistant_tool_use("Bash", {"command": "pytest -x"}, input_tokens=1200), |
| 99 | + # ... |
| 100 | + ] |
| 101 | + |
| 102 | + |
| 103 | +ALL_SCENARIOS = { |
| 104 | + # ... |
| 105 | + "15_my_new_case": scenario_my_new_case(), |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### Event-sequence scenario |
| 110 | + |
| 111 | +Append an events builder and register it in `EVENT_SCENARIOS`: |
| 112 | + |
| 113 | +```python |
| 114 | +def events_my_new_case() -> list[tuple[EventType, dict[str, Any]]]: |
| 115 | + return [ |
| 116 | + _run_started("demo / 16_my_new_case"), |
| 117 | + (EventType.ITERATION_STARTED, {"iteration": 1}), |
| 118 | + (EventType.ITERATION_COMPLETED, { |
| 119 | + "iteration": 1, |
| 120 | + "detail": "completed (5s)", |
| 121 | + "log_file": None, |
| 122 | + "result_text": "all good", |
| 123 | + }), |
| 124 | + ] |
| 125 | + |
| 126 | + |
| 127 | +EVENT_SCENARIOS = { |
| 128 | + # ... |
| 129 | + "16_my_new_case": events_my_new_case(), |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +The next `./scripts/tui_dev/run.sh snapshot` will produce |
| 134 | +`scripts/tui_dev/output/16_my_new_case.png`. |
| 135 | + |
| 136 | +## Files |
| 137 | + |
| 138 | +``` |
| 139 | +scripts/tui_dev/ |
| 140 | +├── run.sh # one-command launcher |
| 141 | +├── snapshot.py # mode A: in-process ConsoleEmitter + fixtures |
| 142 | +├── live.py # mode B: real ralph run via pty + pyte |
| 143 | +├── fixtures.py # canned event sequences (peek + general) |
| 144 | +├── render.py # shared SVG → PNG via headless Chrome |
| 145 | +├── fake_bin/claude # stub Claude agent used by live mode |
| 146 | +├── demo_ralph/RALPH.md # minimal ralph dir used by live mode |
| 147 | +└── output/ # PNG + SVG snapshots (gitignored) |
| 148 | +``` |
0 commit comments