Coordinated Live Regions Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [x]) syntax for tracking.
Goal: Provide independent live regions with one writer and reliable cleanup.
Architecture: A synchronous coordinator owns the writer, regions and geometry. An explicit handle coordinates ordinary writes; terminal controls use existing core encoders.
Tech Stack: Rust 2021, Rust 1.90, Cargo and the existing public Rich APIs.
Spec: Approved design.
Global Constraints¶
Read the release index and AGENTS.md before execution. Preserve default output; keep new library behaviour in rich-ext/rich-art. Do not add required Renderable methods or fields to ConsoleOptions/ImageOptions. No implicit ambient probes, source-file reads or global logger installation. Rust 1.90, Rust 2021 and unsafe_code = "deny" apply. Before each commit run cargo fmt --all --check, cargo clippy --all-targets -- -D warnings, and env -u NO_COLOR cargo test; commit only after all pass. Publication is a separate handoff.
Review Focus¶
- Content exceeds terminal width/height without implicit wrapping or scrolling (D1).
- Zero-width/one-row viewports disable dynamic updates (D1).
- Shrinking and expanding a viewport clears stale cells (D2).
- Partial writes and unwinding restore cursor state once (D2).
- Ordinary messages interleaved with region updates remain visible and ordered (D2).
D1: Single-writer region coordinator¶
Files: Create crates/rich-ext/src/live/mod.rs, crates/rich-ext/src/live/region.rs, crates/rich-ext/tests/live_regions.rs, crates/rich-ext/examples/live_regions.rs; modify ext lib.rs/README.md.
Interfaces: LiveCoordinator<W:std::io::Write>::new(writer:W,target:RenderTarget)->Self; add(&mut self,content:Vec<Segment>)->Result<RegionId,LiveError>, update(&mut self,id:RegionId,content:Vec<Segment>)->Result<(),LiveError>, remove(&mut self,id:RegionId)->Result<(),LiveError>, refresh(&mut self)->Result<(),LiveError>, resize(&mut self,width:usize,height:usize)->Result<(),LiveError>, print(&mut self,content:&[Segment])->Result<(),LiveError>, finish(&mut self)->Result<(),LiveError>, handle(&mut self)->LiveHandle<'_,W>. LiveHandle delegates print/update/refresh through its exclusive coordinator borrow. RegionId contains an opaque coordinator ownership token (Arc identity, held by the ID) and monotonically allocated u64; stale/foreign IDs return InvalidRegion. This avoids a process-global coordinator counter and ID reuse while old handles survive. LiveError includes InvalidRegion, ExhaustedIds, UnsupportedControl and Io(std::io::Error). Do not use independent Live instances on the writer. Consumes RenderTarget and B2 segment fitting; content is rendered by caller using the same target.
- [x] Add an in-memory writer test with target80x24 and three labelled regions. Update only middle region and check unchanged content stays visible; remove it and assert stale ID rejection. Test foreign coordinator ID rejection, colour segments and no raster/control injection. Width0/1 and height0/1 must emit no dynamic controls. Add a 200-line/200-column region and assert at most23 rows and79 drawable cells.
assert!(matches!(live.update(removed, Vec::new()), Err(LiveError::InvalidRegion)));
assert!(!bytes.windows(2).any(|pair| pair == b"\x1bP"));
- [x] Run
env -u NO_COLOR cargo test -p rs-rich-ext --test live_regions; expect missing live API. - [x] Keep regions in registration order, store dirty flags, and clip all segment rows before calculating movement. Reserve one insertion row and one guard column with saturating subtraction. First refresh hides cursor only when drawable area exists. Refresh erases/repaints changed rows; region height changes invalidate following rows. Never emit user-supplied control segments. Noninteractive targets print stable snapshots without cursor controls; unchanged refresh is a no-op. Use checked ID increments and existing rich::control encoders rather than separate hand-coded escape dialects.
let drawable_width = width.saturating_sub(1);
let drawable_height = height.saturating_sub(1);
let dynamic = interactive && drawable_width > 0 && drawable_height > 0;
- [x] Run region tests and existing core Live goldens, then global gates. Commit
feat: coordinate live regions with one terminal writer.
D2: Resize, ordinary writes and terminal-state evidence¶
Files: Create crates/rich-ext/tests/live_terminal.rs, scripts/test_live_regions_pty.py; modify ext live module/example, scripts/test_cli_terminal.py, docs/cli.md and docs/PORTING.md.
Interfaces: Keep D1 API. Tests use a test-only virtual terminal screen interpreter for the emitted Control subset and a Unix PTY harness; neither becomes a runtime dependency. Python test imports only stdlib unless the repository already supplies a terminal emulator. Interpreter maintains cursor position, wrapping, erasure and scrolling; unsupported escapes fail tests rather than disappearing.
- [x] Drive the example through sizes80x24→20x6→0x1→80x24 with ordinary messages between refreshes. Assert visible screen cells, not only escape substring presence. PTY case compares final screen for the same script. A writer that accepts N bytes then errors tests partial-write cleanup. catch_unwind around a scoped coordinator tests best-effort restoration:
assert_eq!(screen.visible_lines(), expected_lines);
assert!(screen.cursor_visible());
assert_eq!(cleanup_attempts_after_second_finish, cleanup_attempts_after_first_finish);
Expected lines are literal messages/regions from each test fixture, not output produced by the implementation. Add repeated finish, remove-all, terminal→noninteractive transition, and resize after partial write.
- [x] Run env -u NO_COLOR cargo test -p rs-rich-ext --test live_terminal; expect stale-screen/cleanup failures before implementation.
- [x] Track lifecycle state separately from geometry. On resize invalidate cached rows and reconcile owned area before repainting; ordinary print clears owned region, writes complete lines, then redraws below. On finish attempt erase and cursor restore once, retain first I/O error, and mark closed; Drop is best effort and never panics. Track each successfully emitted cursor-hide so a later failed write still attempts restoration. No terminal resets or unrelated screen clearing. If PTY resize reflow exposes an unsupported geometry assumption, resolve it before this task is complete.
if self.closed { return Ok(()); }
self.closed = true;
// Execute owned-region erasure and cursor restoration, retaining first error.
- [x] Run
python scripts/test_live_regions_pty.pyon Unix plus live_terminal and core Live goldens; document platform skip only when PTY unavailable, never call a skip a pass. Run global gates. Commitfix: restore coordinated live state across resize and failure.