10 KiB
Sprint 0: TUI Extraction & Architecture
Duration: 4 days | Stories: 5 | Goal: Extract TUI from 20K-line main.rs, establish panic recovery, clean module boundaries
Why This Sprint Exists
main.rs is 20,020 lines. TUI code is embedded at line 7146. Every subsequent sprint adds slash commands and event handling to this file. Without extraction first:
- Merge conflicts between sprints are guaranteed
- Code review is impossible (who can review a 20K file?)
- Testing individual modules is blocked
This sprint creates the foundation all other sprints build on.
S0-1: Extract TUI integration from main.rs
Priority: P0 — Critical
Assignee: —
Estimate: 1.5 days
Description
Move all TUI-specific code from main.rs into dedicated modules. Create a clean interface boundary.
New Files
src/tui_repl.rs — TUI event loop and integration:
use crate::tui::{self, TuiApp, TuiReadOutcome, SharedDashboardState};
use crate::tui_commands::TuiCommand;
pub struct TuiRepl;
impl TuiRepl {
/// Entry point called from main.rs when `/tui` is typed.
pub fn run(cli: &mut LiveCli) -> Result<(), Box<dyn std::error::Error>> {
let dashboard_state = SharedDashboardState::default();
let mut app = TuiApp::init(dashboard_state.clone())?;
app.push_banner(&[
tui::BannerLine { text: "🦀 Claw Code".to_string(), color: ratatui::style::Color::Cyan },
]);
app.push_banner(&[
tui::BannerLine { text: format_connected_line(&cli.model), color: ratatui::style::Color::DarkGray },
]);
let mut pending_input: Option<String> = None;
loop {
// ... event loop (currently at main.rs:7159-7260)
// ...
}
app.restore_terminal()?;
Ok(())
}
}
src/tui_commands.rs — Slash command dispatch for TUI mode:
pub enum TuiCommand {
Theme(String), // /theme <name>
Keys(String), // /keys <preset>
Code, // /code
Ask, // /ask
Architect, // /architect
Diff { staged: bool },// /diff [--staged]
Undo { confirm: bool },// /undo [--confirm]
Ls { path: Option<String> }, // /ls [path]
Context, // /context
Help, // /help
}
impl TuiCommand {
pub fn parse(input: &str) -> Option<Self> { ... }
pub fn execute(&self, app: &mut TuiApp, cli: &mut LiveCli) -> Result<(), Error> { ... }
}
src/tui_update.rs — Dashboard state updates:
pub fn update_dashboard(state: &SharedDashboardState, cli: &LiveCli) { ... }
pub fn format_connected_line(model: &str) -> String { ... }
pub fn strip_ansi(text: &str) -> String { ... } // moved from main.rs
Implementation Steps
- Create
src/tui_repl.rs— moverun_tui_repl()body (main.rs:7146-7262) - Create
src/tui_commands.rs— extract slash command parsing and execution - Create
src/tui_update.rs— moveupdate_dashboard(),strip_ansi(),format_connected_line() - Update
main.rs— replacerun_tui_repl()with delegation:
Or remove entirely and callfn run_tui_repl(cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> { tui_repl::TuiRepl::run(cli) }TuiRepl::run()directly. - Add
moddeclarations tomain.rs:mod tui_repl; mod tui_commands; mod tui_update; - Verify
cargo buildsucceeds - Verify
cargo teststill passes
Acceptance Criteria
run_tui_repl()body moved tosrc/tui_repl.rs- Slash command handling moved to
src/tui_commands.rs update_dashboard()and helpers moved tosrc/tui_update.rsmain.rsreduced by ~200+ linescargo build --releasesucceedscargo test -p rusty-claude-clipasses/tuicommand still works in the REPL- No behavioral changes (pure extraction)
S0-2: Add panic hook for terminal cleanup
Priority: P0 — Critical
Assignee: —
Estimate: 0.5 day
Description
If any TUI code panics, the terminal is left in raw mode with hidden cursor and alternate screen. The user sees a broken terminal and must manually run reset. Add a panic hook that restores terminal state before printing the panic message.
Implementation
File: src/tui_repl.rs
use std::panic;
use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
use crossterm::execute;
/// Install a panic hook that restores terminal state before printing the panic.
/// Returns the previous hook so it can be restored on clean exit.
pub fn install_panic_hook() -> Box<dyn Fn(&panic::PanicInfo<'_>) + Send + Sync + 'static> {
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|info| {
// Best-effort terminal cleanup — ignore errors since we're panicking
let _ = disable_raw_mode();
let _ = execute!(std::io::stdout(), LeaveAlternateScreen);
let _ = execute!(std::io::stdout(), crossterm::cursor::Show);
// Print the panic message
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Box<dyn Any>".to_string()
};
let location = info.location()
.map(|l| format!(" at {}:{}", l.file(), l.line()))
.unwrap_or_default();
eprintln!("\n🦀 Claw TUI crashed{location}: {payload}");
eprintln!("Terminal has been restored to normal mode.\n");
}));
prev_hook
}
/// Restore the previous panic hook on clean exit.
pub fn restore_panic_hook(hook: Box<dyn Fn(&panic::PanicInfo<'_>) + Send + Sync + 'static>) {
panic::set_hook(hook);
}
Usage in TuiRepl::run():
pub fn run(cli: &mut LiveCli) -> Result<(), Box<dyn std::error::Error>> {
let prev_hook = install_panic_hook();
// ... TUI event loop ...
restore_panic_hook(prev_hook);
app.restore_terminal()?;
Ok(())
}
Acceptance Criteria
- Panic hook installed at TUI startup
- Previous hook restored on clean exit
- If TUI panics: terminal is restored, panic message printed, terminal usable
- No double-panic (hook itself doesn't panic)
disable_raw_mode()errors are silently ignored (best-effort)
Tests
#[test]
fn test_panic_hook_installs_and_restores() {
// Verify hook can be installed and restored without panic
let prev = install_panic_hook();
restore_panic_hook(prev);
}
Manual test: induce a panic in TUI code (e.g., panic!("test")), verify terminal is usable after.
S0-3: Add terminal resize handling
Priority: P1 — High
Assignee: —
Estimate: 0.5 day
Description
When the terminal is resized, the TUI must re-render with the new dimensions. Currently, resize events are ignored, causing layout corruption.
Implementation
File: src/tui_repl.rs — in the event loop:
Event::Resize(width, height) => {
// ratatui::Terminal automatically picks up the new size on next
// draw call via `f.area()`. We just need to force a redraw and
// re-wrap the conversation.
app.mark_resize(width, height);
}
File: src/tui.rs — add to TuiApp:
pub fn mark_resize(&mut self, _width: u16, _height: u16) {
// Clear cached wrapped lines so they get re-wrapped at new width
self.invalidate_render_cache();
self.needs_redraw = true;
}
Acceptance Criteria
- Resizing terminal triggers re-render
- Word-wrapping adjusts to new width
- Dashboard panel stays correctly sized
- Input area resizes appropriately
- No layout corruption after resize
Tests
Manual: launch TUI, resize terminal repeatedly, verify layout adapts.
S0-4: Add unicode-width dependency
Priority: P1 — High
Assignee: —
Estimate: 0.1 day
Description
Add the unicode-width crate to Cargo.toml for CJK-aware character width calculations. Needed by Sprint 7's word-wrapping fix, but adding now avoids a dependency change mid-sprint.
Implementation
File: rust/crates/rusty-claude-cli/Cargo.toml
[dependencies]
# ... existing deps ...
unicode-width = "0.2"
Acceptance Criteria
cargo buildsucceeds with new dependencyunicode_width::UnicodeWidthCharavailable in the crate
S0-5: Define shared TUI error type
Priority: P2 — Medium
Assignee: —
Estimate: 0.25 day
Description
Currently, TUI functions return Box<dyn std::error::Error>. Define a proper error type for better error handling across modules.
Implementation
New file: src/tui_error.rs
use std::fmt;
#[derive(Debug)]
pub enum TuiError {
Io(std::io::Error),
Terminal(String),
Runtime(String),
Config(String),
}
impl fmt::Display for TuiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TuiError::Io(e) => write!(f, "TUI I/O error: {e}"),
TuiError::Terminal(msg) => write!(f, "Terminal error: {msg}"),
TuiError::Runtime(msg) => write!(f, "Runtime error: {msg}"),
TuiError::Config(msg) => write!(f, "Config error: {msg}"),
}
}
}
impl std::error::Error for TuiError {}
impl From<std::io::Error> for TuiError {
fn from(e: std::io::Error) -> Self {
TuiError::Io(e)
}
}
Acceptance Criteria
TuiErrorenum compilesFrom<std::io::Error>conversion worksDisplayproduces readable messages- All new TUI modules use
TuiErrorinstead ofBox<dyn Error>
Sprint 0 Definition of Done
- All 5 stories completed
- TUI code extracted from
main.rsinto dedicated modules - Panic hook restores terminal on crash
- Terminal resize handled gracefully
unicode-widthin Cargo.tomlcargo build --releasesucceedscargo test -p rusty-claude-clipassescargo clippy -p rusty-claude-clihas no new warnings/tuicommand works identically to before extractionmain.rsline count reduced (target: -200 lines)