Add clipboard demo with modal workflow#128
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>
|
Reviewed against current main after #146-#151 landed. The branch itself is stale and does not rebase cleanly: it still carries old runner/headless/registry commits before the clipboard demo work, and rebasing hits conflicts in src/miaou_runner/dune, src/miaou_runner/headless_json_runner.ml, src/miaou_runner/runner_tui.ml, then the gallery launcher.\n\nI replayed only the clipboard-specific commits onto current origin/main in an isolated local review worktree. That replay builds after resolving the launcher placement and formatting, but there is still a behavioral issue to fix before this should merge: the modal copy path calls clip.copy directly and then reports success without checking clip.copy_available, while the quick-copy path does check it. With a disabled clipboard capability, quick copy warns correctly but modal copy silently no-ops and later shows "Sent to clipboard". The modal path should reuse the same copy_text logic/result handling instead of duplicating a weaker copy path via the pending_modal_copy ref.\n\nSuggested path: recreate/rebase this PR from current main with only the clipboard demo commits, place Clipboard at the end of the current launcher demo list (Core group index 52 in my local replay), run dune fmt, and fix the modal copy path to honor copy_available. |
|
Closed in favor of the clean current-main implementation merged in #152. |
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)