# `zc` Terminal Refactor — Handoff Spec
Self-contained brief for an agent taking over the interactive-terminal refactor.
Read this together with the full design in
[`docs/TERMINAL_REFACTOR.md`](./TERMINAL_REFACTOR.md). This file says what is
**done**, what is **in progress**, and exactly **what to do next**.
---
## 1. Goal & scope
Bring the `zc` interactive terminal up to a Claude Code–style interface. Scope is
the **interface only**, on three pillars:
1. **Rich REPL UX** — editable prompt, history, multiline, streaming scrollback.
2. **Slash commands & modes** — `/`-triggered command palette, autocomplete,
mode-aware event loop, status line.
3. **Theming & rendering** — `Theme` type + config, markdown/code rendering,
spinners, reusable panels.
**Out of scope (confirmed):** agentic tool-use / diff-apply loop.
## 2. Adopted decisions (from §7 of the design doc)
- **Launch:** ~~bare `zc` keeps printing help; the REPL launches via an explicit
`zc shell`.~~ **SUPERSEDED (2026-06, user decision):** bare `zc` now launches
the REPL by default (falling back to `help` when stdout is not a TTY).
`zc shell` / `zc repl` remain explicit entry points; `zc -h/--help/--full`
still print help.
- **Dependencies:** MSRV-safe baseline — prefer `pulldown-cmark` (markdown) +
`fuzzy-matcher` (palette). Treat `nucleo`, `syntect`, `tui-markdown` as
optional upgrades behind a Cargo feature, each validated individually.
- **Agentic loop:** excluded.
## 3. Hard constraints (do not violate)
- **MSRV is Rust 1.75.** Pinned by `ratatui = "0.26"` / `crossterm = "0.27"`.
Every new crate must build on 1.75 and be compatible with ratatui 0.26.
- **`cargo-deny` runs in CI** (`.github/workflows/security.yml`: `check
advisories bans licenses sources`). Any new dependency may require updating
the deny config. **Phase 0 added zero dependencies on purpose.**
- **No global lint gate.** There is no `#![deny(warnings)]` and CI does not run
`clippy -D warnings`. CI (`build.yml`) runs `cargo build --release` then
`cargo test --release --bin zc`. Warnings won't fail CI, but keep it clean.
- **Crate facts:** binary `zc`, package `zc2`, version `0.0.16`, edition 2021.
TUI stack is `ratatui` + `crossterm`. CLI uses **no** arg-parser crate — it's a
hand-rolled `match args.len()` ladder in `src/main.rs`.
- **`ratatui` has no `serde` feature enabled**, so `ratatui::style::Color` is
**not** `Serialize`/`Deserialize`. Persist colors as strings (already handled
in `tui/theme.rs`).
## 4. ⚠️ Verification environment
The agent that produced Phase 0 had **no Rust toolchain** (sandboxed Linux, no
root, rustup network-blocked) and **could not compile**. All Rust code so far is
written from careful static review only. **Before trusting any phase, compile on
a real machine:**
```sh
cargo build
cargo test --bin zc -- theme command # Phase 0 unit tests
cargo build --release # what CI runs
```
If you have a toolchain, compile-verify after every phase. If a phase doesn't
compile, the safest recovery is to delete the newly-added files for that phase
(the existing working code was left intact — see below).
---
## 5. Current status
### Phase 0 — DONE & COMPILE-VERIFIED (additive, no behavior change)
Compile-verified on a real toolchain (`cargo build`, `cargo test --bin zc --
theme command` → 10/10 pass). One fix was required: `builtin_specs()` returned a
reference to a temporary array; it now returns a `static SPECS: &[CommandSpec]`.
Files **added**:
- `src/tui/theme.rs` — `Theme` struct (ratatui `Color` palette). `Theme::dark()`
reproduces the legacy `broker::tui::colors` constants **exactly**; `light()`
variant; `named()` lookup; TOML load/save via a string-based `ThemeConfig`
(no ratatui serde dependency). Has unit tests.
- `src/command/spec.rs` — `CommandSpec`, `ArgSpec`, `Category` (declarative
command model; `Copy`-able; built from `&'static` data via const fns).
- `src/command/mod.rs` — `CommandRegistry` (alias-aware `resolve`, `by_category`,
substring `search`), `CommandResult`, `RenderBlock`, and `builtin_specs()` — the
full catalog mirroring today's `main.rs` commands. Has unit tests.
- `src/tui/mod.rs` — module root; declares `pub mod theme;`, documents target
layout.
Files **changed**:
- `src/main.rs` — added `mod command;` and `mod tui;` (only change). New modules
carry `#![allow(dead_code)]` so unused-warnings stay quiet until wired.
### Phase 1 — DONE & COMPILE-VERIFIED (cutover complete)
Goal achieved: a normalized `Snapshot` + `DataSource` trait now render the
dashboard from **one** code path. `broker/tui.rs` dropped from **1465 → 296
lines** (the entire duplicated `TuiApp`/`RemoteTuiApp` render + input bodies were
deleted).
Files **added**:
- `src/tui/views/snapshot.rs` — `Snapshot`, `SourceMode`, `Connection`,
`WorkerRow`, `WorkerKind` (normalized model; reuses `BrokerMetrics`,
`TransactionRecord`, `TaskOfferRecord` verbatim).
- `src/tui/views/monitor.rs` — the single `render(frame, &Snapshot, &Theme,
&MonitorUi)` renderer + `Panel`/`MonitorUi`. Local/remote-only differences
(task-pile columns, the stream panel, header, footer hint, help overlay keys)
are keyed off `Snapshot::mode` so visuals match the legacy UI exactly.
- `src/tui/views/mod.rs`, `src/tui/data/mod.rs` — module roots + re-exports.
- `src/tui/data/local.rs` — `LocalSource` (mirrors old `TuiApp::get_metrics`).
- `src/tui/data/remote.rs` — `RemoteSource` + `RemoteShared` (replaces the old
private `RemoteTuiShared`). `snapshot()` maps the latest `StatsResponse`;
stats-present takes priority over a stale error, matching legacy behavior.
Files **changed**:
- `src/tui/mod.rs` — declares `pub mod data;` / `pub mod views;`.
- `src/broker/tui.rs` — **cutover**. `run_tui` / `run_remote_tui` /
`cleanup_terminal` keep identical public signatures; they now build a
`DataSource`, run the event loop, and call `monitor::render`. The remote
background fetch/diff thread + `zk_<user>_…` key parsing were preserved
verbatim. `mod colors` and both `*TuiApp` structs are gone.
Verification: `cargo build`, `cargo build --release`, and `cargo test --release
--bin zc` (155 pass; the only failures are pre-existing/environmental — a
`bind 127.0.0.2:0` test that can't run on this host, and a broker-startup timeout
under heavy parallel test load). New TUI modules compile warning-free.
> ⚠️ Not yet done: interactive visual confirmation of `zc -t broker` and
> `zc attach …` on a live broker (needs a real broker + TTY). The port is a
> faithful 1:1 of the legacy render code, but a human should eyeball both
> dashboards before considering Phase 1 fully signed off.
---
## 6. Phase 1 — detailed spec
### 6.1 New files
```
src/tui/views/mod.rs
src/tui/views/snapshot.rs # normalized data model
src/tui/views/monitor.rs # single dashboard renderer: fn render(frame, &Snapshot, &Theme)
src/tui/data/mod.rs # DataSource trait
src/tui/data/local.rs # LocalSource: SharedBrokerState + StatsCollector -> Snapshot
src/tui/data/remote.rs # RemoteSource: wraps shared HTTP-fetch state -> Snapshot
```
Add to `src/tui/mod.rs`: `pub mod views;` and `pub mod data;`.
### 6.2 `Snapshot` model (`views/snapshot.rs`)
Reuse `broker::stats::{BrokerMetrics, TransactionRecord, TaskOfferRecord}`
directly (don't re-derive). Normalize only the worker shape, since local yields
`broker::worker::Worker` and remote yields `broker::stats::WorkerStats`.
```rust
pub struct Snapshot {
pub title: String, // "Zakuro Compute Broker"
pub mode: SourceMode, // Local | P2P | Remote
pub host_port: String, // "0.0.0.0:9000"
pub wireguard_ip: Option<String>,
pub ledger_connected: bool,
pub connection: Connection, // Connected | Connecting | Error(String)
pub metrics: BrokerMetrics, // reused
pub workers: Vec<WorkerRow>, // normalized
pub transactions: Vec<TransactionRecord>,
pub task_offers: Vec<TaskOfferRecord>,
pub rps_history: Vec<f64>,
pub events: Vec<String>, // stream log (remote only today)
}
pub enum SourceMode { Local, P2p, Remote }
pub enum Connection { Connected, Connecting, Error(String) }
pub struct WorkerRow {
pub status: WorkerKind, // normalized status
pub name: String,
pub cpus_available: f64,
pub memory_gib: f64,
pub avg_latency_ms: f64,
}
pub enum WorkerKind { Healthy, Busy, Unhealthy, Draining, Unknown }
```
### 6.3 `DataSource` trait (`data/mod.rs`)
```rust
pub trait DataSource: Send {
fn snapshot(&self) -> Snapshot; // non-blocking; for render
fn request_refresh(&self) {} // e.g. 'r' key (remote)
}
```
### 6.4 `monitor::render` (`views/monitor.rs`)
Port the existing render functions from `broker/tui.rs` **once**, taking
`&Snapshot` + `&Theme` instead of `&self` and `colors::*`:
- header (title, mode badge, ledger dot, host:port, wireguard, uptime)
- task pile, transactions (zebra rows via `theme.row_alt`), stream (remote)
- workers, metrics + RPS sparkline, footer, help overlay
Replace every `colors::X` with `theme.x`. Keep ratatui 0.26 APIs identical to the
current file (`Table::new(rows, widths)`, `Sparkline`, `symbols::bar::NINE_LEVELS`,
`frame.size()`, etc.).
### 6.5 Adapters
`LocalSource { state: SharedBrokerState, stats: Arc<StatsCollector> }` — mirror
`TuiApp::get_metrics`: `let workers = state.workers.list(); let active =
state.own_wireguard_ip.clone())`. Map each `Worker` → `WorkerRow` (memory:
`w.resources.memory_available as f64 / (1024.0^3)`).
`RemoteSource` — wrap the existing background-fetch shared state
(`Arc<Mutex<{stats, last_error, event_log}>>` + `refresh_requested: Arc<AtomicBool>`).
`snapshot()` reads the latest `StatsResponse` and maps `WorkerStats` → `WorkerRow`
(`memory_gib = w.memory_available_gib`, status from `w.status` string). Keep the
fetch thread + offer/transaction diffing from `run_remote_tui` as-is for the
cutover.
### 6.6 Cutover (final Phase 1 step, after the above compiles)
Re-implement `broker::tui::run_tui(state, stats, running)` and
`run_remote_tui(broker_url, api_key)` as thin wrappers: build the right
`DataSource`, then run a single shared event loop that calls
`tui::views::monitor::render`. Delete the duplicated `TuiApp` / `RemoteTuiApp`
render methods. Keep `cleanup_terminal()` and the public signatures unchanged
(callers: `broker::server` for `run_tui`; `src/main.rs` for `run_remote_tui` and
`cleanup_terminal`, re-exported in `broker/mod.rs`).
---
## 7. Domain-type cheat-sheet (verified against source)
`broker::stats::BrokerMetrics`: `total_requests u64, successful_requests u64,
failed_requests u64, total_credits_spent f64, requests_per_sec f64,
avg_latency_ms f64, p95_latency_ms f64, active_workers usize, total_workers usize,
uptime_secs u64, local_mode bool, ledger_connected bool, wireguard_ip
Option<String>, wireguard_connected bool`.
`broker::stats::StatsResponse`: `host String, port u16, transactions
Vec<TransactionRecord>, task_offers Vec<TaskOfferRecord>, workers
Vec<WorkerStats>, metrics BrokerMetrics, rps_history Vec<f64>, wireguard_ip
Option<String>, wireguard_connected bool`.
`broker::stats::WorkerStats`: `id, name, uri: String; status: String;
cpus_available f64, memory_available_gib f64, gpus_available u32, price_per_hour
f64, active_requests u32, avg_latency_ms f64`.
`StatsCollector` methods: `recent_task_offers(limit) -> Vec<TaskOfferRecord>`,
`recent_transactions(limit) -> Vec<TransactionRecord>`, `rps_history() ->
Vec<f64>`, `tick_rps()`, `metrics(active_workers usize, total_workers usize,
local_mode bool, ledger_connected bool, wireguard_ip Option<String>) ->
BrokerMetrics`.
`broker::worker`: `Worker { name: String, status: WorkerStatus, resources:
WorkerResources, avg_latency_ms: f64, .. }`; `WorkerResources { cpus_available
f64, memory_available u64 /* bytes */, .. }`; `enum WorkerStatus { Healthy, Busy,
Unhealthy, Draining }`; `WorkerRegistry::list() -> Vec<Worker>`.
`broker::SharedBrokerState = Arc<BrokerState>`; `BrokerState { workers:
WorkerRegistry, config: BrokerConfig, stats: Arc<StatsCollector>, own_wireguard_ip:
Option<String>, .. }`; `BrokerState::is_local_mode() -> bool`. `BrokerConfig` has
`host: String, port: u16`.
`broker::stats::{format_uptime(secs: u64) -> String, format_credits(amount: f64)
-> String}`.
Legacy palette in `broker/tui.rs` `mod colors` (now mirrored by `Theme::dark()`):
`BG=Reset, FG=White, ACCENT=Cyan, SUCCESS=Green, WARNING=Yellow, ERROR=Red,
MUTED=DarkGray, BORDER=DarkGray, HEADER_BG=Rgb(30,30,40), HIGHLIGHT=Rgb(60,60,80)`;
plus inline `Rgb(25,25,30)` (zebra rows → `theme.row_alt`) and `Rgb(20,20,30)`
(overlay bg → `theme.overlay_bg`).
---
## 8. Later phases (summary; full detail in `TERMINAL_REFACTOR.md` §3, §6)
- **Phase 2 — Command registry dispatch.** Give each `CommandSpec` an executable
handler (`Command` trait + `Ctx` output sink); replace the `match args.len()`
ladder in `main.rs` with `cli::run(argv)`; generate `help()` / `-h` from the
registry (delete the hand-written `println!` help wall). Builtins live in
`src/command/builtins/*.rs`, reused by CLI and REPL.
- **Phase 3 — REPL shell. DONE (compile-verified, headless TestBackend tests).**
`tui/app.rs` (Mode state machine + event loop), `input.rs` (prompt via
`tui-textarea`, history, multiline via Alt+Enter), `scrollback.rs` (blocks +
raw banner lines + scroll), `statusline.rs`. **Launch decision changed by user
(2026-06): bare `zc` now launches the shell by default** (falls back to `help`
when stdout isn't a TTY); `zc shell`/`zc repl` also work. Deps added:
`tui-textarea 0.4`, `fuzzy-matcher 0.3`, `pulldown-cmark 0.10` (see §4 note on
MSRV). **Not yet done:** long commands on a worker thread streaming
`RenderBlock`s — execution is currently shell-native commands only (`help`,
`theme`, `clear`, `quit`, `mode`).
- **Phase 2 — command execution. DONE (REPL path).** Non-shell-native commands
now run as a `zc <args>` subprocess on a worker thread, streaming stdout into
the scrollback live (`Scrollback::begin_stream`/`stream_line`), with a
braille spinner in the footer and **Ctrl-C to interrupt** (kills the child).
stderr is surfaced on completion; startup `DEBUG:` lines are filtered.
Interactive commands (`attach`) are blocked with guidance. **Not yet done**
(the originally-specified Phase 2): replacing the `main.rs` `match args.len()`
ladder with registry-driven dispatch + generated help, and a first-class
`Command` trait / `builtins/*.rs`. The REPL currently shells out to the same
binary rather than calling handlers in-process — a pragmatic, low-risk first
cut that the registry-dispatch refactor can later supersede.
- **Network mode + auth (user decision, 2026-06):** the shell starts in
**local** mode with **no forced login**. A `/mode` command shows/switches
`local`↔`p2p`; switching to **p2p** requires a `ZAKURO_API_KEY` — if absent,
a masked `Mode::Login` prompt opens and **keeps asking** until a
`zk_<user>_…`-shaped key is entered (invalid/empty keys are rejected, never
partially signed in, and the secret is never stored in command history).
On success the key is applied via `std::env::set_var` for the session and
the mode flips to p2p; Esc cancels back to local. The footer shows the
active mode (`● local` / `● p2p`).
- **Launch banner:** the Zakuro pomegranate logo (ASCII density map in
`LOGO`, downsampled 2:1 at runtime by `logo_lines`) + version/cwd + tip.
- **Phase 4 — Slash commands & palette. DONE (basics).** `tui/palette.rs`:
`/`-triggered fuzzy palette (`fuzzy-matcher`), Up/Down select, Tab complete,
Enter run, Esc cancel, arg hints from `CommandSpec::usage`. Mode badge in the
footer. Remaining: richer inline arg hints as you type past the command name.
- **Phase 5 — Rich rendering & theming. PARTIAL.** Done: `render/markdown.rs`
(pulldown-cmark 0.10 → ratatui `Text`; headings/bold/italic/code/lists/quote/
rule), `render/spinner.rs`, live `/theme` hot-swap. Remaining: `syntect`
highlight (feature-gated), `render/panels.rs`, loading a theme from
`~/.config/zc/theme.toml`.
- **Phase 6 — Verification & docs.** Snapshot tests via
`ratatui::backend::TestBackend` (+ `insta`); registry/palette/theme unit tests;
update `README.md` and `docs/USAGE.md`.
## 9. Immediate next actions for the takeover agent
1. ~~Compile-verify Phase 0 (§4).~~ **DONE** — 10/10 tests pass; fixed the
`builtin_specs()` temporary-array return (now a `static`).
2. ~~Implement Phase 1 §6.1–6.5 additively.~~ **DONE** — builds + tests green.
3. ~~Do the Phase 1 cutover (§6.6); verify duplicated lines are gone.~~ **DONE**
— `broker/tui.rs` 1465 → 296 lines. **Still TODO:** eyeball `zc -t broker`
and `zc attach …` on a live broker + TTY (couldn't be done headlessly).
4. **NEXT: Phase 2 — command registry dispatch.** Heads-up for the next agent:
`builtin_specs()` currently mirrors only the "documented" commands. The real
`main.rs` ladder (≈673 lines) also dispatches `dist`, `vars`, `push`,
`context`, `build`, and the `-t`/`-d`/`-h` flag prefixes — the registry +
`Command` trait must cover (or deliberately exclude) these before the
`match args.len()` ladder can be replaced. Treat the help-wall deletion as the
last step, after dispatch parity is proven.