Open
Conversation
…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).
Add a new clipboard demo that demonstrates copying text to the system clipboard using a modal-based workflow: - Open modal with Space/Enter to type custom text - Press Enter in modal to copy text to clipboard - Quick copy samples (1-5 keys) for instant copying - Toast notifications for user feedback - Copy counter and last copied text display The demo works with both native clipboard tools (wl-copy, xclip, xsel, pbcopy) and falls back to OSC 52 escape sequences when native tools are unavailable. Co-Authored-By: Claude <noreply@anthropic.com>
When copying from the modal, the demo now shows toast notifications indicating success or failure, just like the quick copy samples (1-5). The fix uses a global ref to store pending copies from the modal's on_close callback, which is then processed in the refresh cycle to update the state and show appropriate toast messages. This ensures users get immediate feedback when they press Enter to copy text from the modal.
- Change toast from Success to Info level - Change 'Copied' to 'Sent to clipboard' to reflect uncertainty - Add instructions about native clipboard tools requirement - Explain OSC 52 fallback limitations The clipboard API is fire-and-forget and cannot verify success, so we should be honest with users about reliability. Co-Authored-By: Claude <noreply@anthropic.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.
Summary
Adds a new clipboard demo that demonstrates copying text to the system clipboard using a modal-based workflow.
Features
Demo Location
dune exec miaou.clipboard-demoRequirements
Clipboard works with:
wl-clipboard(Wayland),xclip(X11)