feat: headless JSON runner, Web Viewer, widget registry, Octant/Framebuffer#129
Merged
mathiasbourgoin merged 16 commits intomainfrom Mar 10, 2026
Merged
feat: headless JSON runner, Web Viewer, widget registry, Octant/Framebuffer#129mathiasbourgoin merged 16 commits intomainfrom
mathiasbourgoin merged 16 commits intomainfrom
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…less
Any Miaou TUI app can now be driven headlessly by an AI agent or CI
script without a separate binary or app changes. Set
MIAOU_DRIVER=headless and the app reads newline-delimited JSON commands
from stdin and writes JSON frames to stdout.
Protocol (subset):
{"cmd":"render"} → {"type":"frame","text":"...","rows":R,"cols":C}
{"cmd":"key","key":"Tab"} → frame (after 3 idle ticks)
{"cmd":"tick","n":N} → frame
{"cmd":"resize","rows":R,"cols":C} → frame
{"cmd":"quit"} → {"type":"nav","action":"quit"}
Implementation:
- New src/miaou_runner/headless_json_runner.ml — JSON loop using
Lib_miaou_internal.Headless_driver.Stateful; inline ANSI strip.
- runner_tui.ml — env-var check at top of run dispatches to headless
runner before any terminal backend initialisation.
- dune — adds headless_json_runner module, miaou-core.core,
miaou-core.lib_miaou_internal, yojson, unix deps.
- New standalone `miaou-registry` opam package (zero deps beyond stdlib):
`Miaou_registry.{register,list,find,search}` with Mutex-safe global table.
- 23 widgets self-register at library load time via `[%blob "...mli"]`:
8 display (pager, list, description_list, table, tree, sparkline,
line_chart, bar_chart), 8 input (textbox, textarea, validated_textbox,
select, checkbox, radio, button, switch), 7 layout (box, progress,
spinner, file_browser, card, toast, canvas).
- Non-widget utility modules marked `[@@@enforce_exempt]`.
- `ppx_enforce` (new companion to ppx_forbid, same repo) enforces the
presence of `Miaou_registry.register` in every widget `.ml` — new
widgets without a registration call fail to compile.
Config: `.ppx_enforce` at project root.
- Explicit `(preprocessor_deps (file ...mli))` per widget to make blobs
work without glob-induced dependency cycles.
…le_key Pages that implement the new on_key API (like the miaou-composer designer) had their key events silently dropped because the headless driver was calling the deprecated handle_key stub (which returns pstate unchanged). Switch both the batch run() loop and Stateful.install_page to call P.on_key with a proper Keys.t value converted from the string key name.
The row_gap code emitted \n BEFORE each gap blank line but not AFTER the last one, so the next row's first content line was concatenated onto the gap blank. When Flex truncates to width, the actual content was lost (only whitespace survived). Fix: emit \n AFTER each gap blank line instead of before. This ensures each gap blank and each row's first line start on their own buffer line. Add row_gap and row_gap_multi_line test cases to verify.
The headless driver's idle_wait loop was purely synchronous, never yielding to the eio scheduler. Fibers spawned via Fiber.fork (e.g. async LLM calls in crucible) were never scheduled, causing the app to spin indefinitely with a spinner but no actual work happening. Add Eio.Fiber.yield() on each iteration so forked fibers can make progress between ticks.
The headless JSON runner's main loop used `input_line stdin` which is a blocking POSIX syscall. This froze the entire eio event loop between commands, preventing background fibers (e.g. LLM subprocess I/O) from making progress. Replace with Eio_unix.run_in_systhread to run the blocking read in a system thread, keeping the eio event loop active so background fibers can process I/O events (subprocess stdout, network calls, etc.).
Add optional ?on_frame:(string -> unit) parameter to Headless_json_runner.run and Runner_tui.run. When provided, the callback is invoked with the raw ANSI frame content (before stripping) on every frame emit. Add Web_viewer module to miaou-driver-web: a standalone viewer-only HTTP+WebSocket server that serves the xterm.js viewer page and broadcasts raw ANSI frames to all connected viewers. Designed to run alongside the headless driver so a human can observe an AI agent's TUI session in a browser.
xterm.js with convertEol:false treats bare LF as move-down without carriage return. The headless driver produces newline-separated text with bare LF. Convert \n to \r\n in broadcast and prepend clear-screen so each frame redraws cleanly in the viewer terminal.
- on_frame callback now passes ~rows ~cols so viewers know the terminal size
- Web_viewer.broadcast accepts ~rows ~cols, sends {"type":"dimensions",...}
JSON to viewers when dimensions change
- New viewers receive current dimensions + last frame on connect (no blank screen)
- client.js: viewers handle dimensions message to resize xterm.js, FitAddon
auto-fit disabled for viewers (size controlled by server)
- client.js: auto-reconnect with 2s retry when viewer WebSocket disconnects
Document the new Web_viewer module, on_frame callback, viewer auto-reconnect, and dimension sync in the unreleased section.
When a web viewer is attached (on_frame callback set), fork an Eio daemon fiber that re-renders the screen every 200ms and broadcasts changes. This keeps the viewer up-to-date even when the agent is idle, catching background activity (timers, async I/O, spinners).
The viewer-refresh daemon fiber was calling idle_wait ~iterations:1 every 200ms. idle_wait yields to the eio scheduler on every iteration, allowing the daemon to interleave with the command handler's own idle_wait call. Both fibers then concurrently mutate the shared page-state ref and double-tick clocks/timers. Fix: the daemon now reads the cached screen content directly (HD.Screen.get) without advancing any state. The cache is always up-to-date because every tick/key/render command handler updates it before returning a response.
- Implement Octant_canvas for high-resolution (2x4) colored charts using Unicode 16 octants. - Add Octant mode support to Sparkline, Line_chart, and Bar_chart widgets. - Introduce Framebuffer_widget for direct pixel/cell-based manipulation. - Add Terminal_caps for detecting Unicode 16 (octant) support. - Add "Framebuffer & Octant Charts" demo to the gallery. - Update System Monitor and Line Chart demos to include Octant toggles.
… in CI - README: step-by-step build-from-source guide covering system SDL libs, ppx_forbid/ppx_enforce pins, and opam install; update the opam snippet in the Dependencies section to match - Makefile: deps target now pins ppx_forbid and ppx_enforce before running opam install so `make deps` works out of the box - CI: also pin ppx_enforce (lives in the same repo as ppx_forbid) so `opam install --deps-only` can resolve it Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MIAOU_DRIVER=headless): drive TUI pages over stdin/stdout as newline-delimited JSON for programmatic control and testingon_framecallback; supports auto-reconnect and dimension syncmiaou-registry: new package for runtime widget discoverability with mli-embedded self-registration