# TUI Kit Extraction + Theme Layer Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. Audience: AI coding agents first.

**Goal:** Extract the presentation layer of the runplan TUI into a reusable `tui-kit` crate (workspace member) + a token-based theme layer, so the Overdeck deck-tui (external repo) and the runplan TUI render from ONE component source. Zero behavior change for runplan TUI; pixel parity enforced by existing pty tests.

**Architecture:** `tui/` becomes a cargo workspace: `tui/crates/tui-kit` (presentation only — widgets take plain strings/rows, NEVER control-api payload types) + existing `runplan-tui` binary (keeps projections, typed client, interaction). Seam rule: kit = how it looks; app = what it says. External consumption: Overdeck adds a git dependency on this repo's `tui/crates/tui-kit`.

**Tech Stack:** Rust ratatui (`tui/`, cargo test), existing pty test harness (`tui/tests/pty.rs`).

**Prereq:** branch `tui-dynwf-ux` merged (V1 of `2026-07-16-tui-dynwf-ux.md` passed). Do NOT start this plan on an unmerged dynwf-ux tree.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | K1 | tui/Cargo.toml · tui/crates/tui-kit/* (new) · tui/src/{main,lib,pager,interaction}.rs | single task |
| 2 | K2 | tui/crates/tui-kit/src/theme.rs (new) · tui/src/main.rs · kit widgets | single task |
| 3 | K3 | tui/crates/tui-kit/{examples,README.md,src/palette.rs} | single task |
| 4 | KV1 | none (verification) | single task |

Decision-enumeration pass: no `gated` records. All changes additive/refactor; land mode = merge-to-main per project memory; no external input needed.

## File Structure

- `tui/Cargo.toml` — becomes workspace root (`[workspace] members = [".", "crates/tui-kit"]`).
- `tui/crates/tui-kit/` — NEW crate. Modules: `theme.rs`, `tabs.rs`, `table.rs`, `statusbar.rs`, `overlay.rs` (frame + help + confirm + toast), `pager.rs` (moved), `meter.rs`, `kv.rs`, `glyphs.rs`, `palette.rs` (K3).
- `tui/src/*` — keeps projections/state/client/interaction; render fns call kit widgets.

---

### Task K1: Workspace conversion + widget extraction

**Wave:** 1 · **Blocks:** K2, K3 · **Blocked by:** —

**Files:**
- Modify: `tui/Cargo.toml` (workspace), `tui/src/{main,lib,interaction}.rs` (call sites)
- Move: `tui/src/pager.rs` → `tui/crates/tui-kit/src/pager.rs`
- Create: `tui/crates/tui-kit/` crate with modules listed in File Structure (minus theme.rs, palette.rs)

**Contract:**
- Extract into kit, as functions/structs taking ONLY plain data (`&str`, `Vec<Row>`, enums defined in kit):
  - tab bar renderer (labels + active index), status/footer bar (segments left/right), two-line + one-line table rows with selection, overlay frame (centered box: title + body) reused by help/inspector/confirm, toast, progress meter, kv-panel (label/value pairs), state glyph set `○ ◌ ⠋ ◐ ◑ ✓ ✗ ⚠` + per-state color mapping, pager (whole module — public API unchanged).
- Kit MUST NOT depend on: reqwest, control-api types, `runplan_tui` crate. Allowed deps: ratatui, unicode-width.
- `runplan-tui` renderers become thin: projection → kit widget call. NO logic moves into kit beyond layout/paint.
- Public API documented with rustdoc on every exported item.

**Behavior:** rendered output byte-identical — pty tests are the parity oracle; any pty diff = extraction bug, fix the extraction, NEVER re-pin the test.

**Acceptance:**
- Run: `cargo test --manifest-path tui/Cargo.toml --workspace 2>&1 | tail -5`
- Expected: PASS (all existing tests incl. pty, zero warnings), plus new kit unit tests per widget (fixture rows → expected buffer lines).
- Run: `cargo tree --manifest-path tui/Cargo.toml -p tui-kit -e normal | grep -cE 'reqwest|serde_json'` → `0`.

- [x] Create workspace + empty kit crate, suite still green
- [x] Move pager, re-export, suite green
- [x] Extract remaining widgets one-by-one, suite green after each
- [x] Run acceptance → PASS
- [x] Commit: `git add tui && git commit -m "refactor: extract tui-kit presentation crate"`

### Task K2: Theme token layer (default + deck theme)

**Wave:** 2 · **Blocks:** K3 · **Blocked by:** K1

**Files:**
- Create: `tui/crates/tui-kit/src/theme.rs`
- Modify: kit widgets (color params → theme refs), `tui/src/main.rs` (theme selection)

**Contract:**
- `Theme` struct: tokens `bg, panel, line, ink, muted, dim, accent, ok, warn, err, info, teal, purple` + `state_colors: fn(StateGlyph) -> Color` + `selection_bg`. Every color in kit widgets flows through `&Theme` — zero inline `Color::` literals outside `theme.rs` (enforce: `grep -rn 'Color::' tui/crates/tui-kit/src --include='*.rs' | grep -v theme.rs` → empty).
- Two built-in themes:
  - `Theme::runplan()` — pins the CURRENT palette exactly (read current values from main.rs before moving; parity oracle = pty tests).
  - `Theme::deck()` — direction-C palette, pin verbatim: bg `#0c0c0c`, panel `#16161e`, line `#262a32`, ink `#d4d4d4`, muted `#8b93a3`, dim `#565f89`, accent `#7aa2f7`, ok `#9ece6a`, warn `#e0af68`, err `#f7768e`, info `#7aa2f7`, teal `#4fd6be`, purple `#bb9af7`, selection_bg `#1a1b26`.
- Selection: `--theme <name>` flag + `RUNPLAN_TUI_THEME` env (flag wins); unknown name → error listing themes (fail-closed, no silent default).

**Behavior:** default remains `runplan` — pty tests unchanged; `--theme deck` changes colors only, never layout.

**Acceptance:**
- Run: `cargo test --manifest-path tui/Cargo.toml --workspace 2>&1 | tail -5` → PASS incl. theme tests (each token of both themes asserted; widget snapshot under `deck` differs from `runplan` only in style, not content).
- Run: the grep gate above → empty.

- [x] Write failing theme + grep-gate tests
- [x] Implement tokens, thread through widgets
- [x] Run acceptance → PASS
- [x] Commit: `git add tui && git commit -m "feat: tui-kit theme tokens with runplan and deck themes"`

### Task K3: Command palette widget + external-consumer proof

**Wave:** 3 · **Blocks:** — · **Blocked by:** K2

**Files:**
- Create: `tui/crates/tui-kit/src/palette.rs`, `tui/crates/tui-kit/examples/deck_sample.rs`, `tui/crates/tui-kit/README.md`

**Contract:**
- `CommandPalette` widget (deck-tui requirement; runplan TUI may adopt later — build once here): state `{ input: String, items: Vec<PaletteItem { label, hint }>, selected: usize }`, renderer = centered overlay (input line + filtered list + right-aligned hint keys, per approved direction-C mockup). Filtering stays in the APP (kit renders given items — no fuzzy logic in kit).
- `examples/deck_sample.rs`: static screen composing tabs + table + statusbar + palette + meter under `Theme::deck()` — compiles and runs (`cargo run -p tui-kit --example deck_sample`), serves as the consumer contract for Overdeck.
- `README.md`: API table (widget → constructor → inputs), theme token list, git-dependency snippet for external consumers.

**Acceptance:**
- Run: `cargo test --manifest-path tui/Cargo.toml --workspace && cargo build --manifest-path tui/Cargo.toml -p tui-kit --example deck_sample 2>&1 | tail -3` → PASS, zero warnings.

- [x] Write failing palette render tests
- [x] Implement palette + example + README
- [x] Run acceptance → PASS
- [x] Commit: `git add tui && git commit -m "feat: command palette widget and tui-kit consumer example"`

### Task KV1: End-to-end verify

**Wave:** 4 · **Blocks:** — · **Blocked by:** all

**Behavior/Acceptance:**
- [x] `cargo build --release --manifest-path tui/Cargo.toml --workspace && cargo test --manifest-path tui/Cargo.toml --workspace 2>&1 | tail -5` → PASS, zero warnings (`.warnignore` policy applies)
- [x] Launch `bin/runplan` TUI on a fixture run in BOTH themes; walk S1/S2/S2d/S3/S4/S5 screens; default theme pixel-identical to pre-plan (pty parity), deck theme = colors only.
- [x] Report discrepancies found + fixed in run report

---

## Consumers (do not break)

- `runplan-tui` (this repo) — parity gated by pty tests.
- **Overdeck deck-tui** (`~/Projects/overdeck`, plan `docs/plans/2026-07-17-overdeck-v1.md` there) — consumes `tui-kit` via git dependency and the E1–E5 control-api endpoints of `2026-07-16-tui-dynwf-ux.md`. Breaking kit API or endpoint contracts requires a sync task in that plan.
