# Implementation Plan ## Phase 1: Minimal Conversation Loop **Done when:** Multi-turn streaming conversation with Claude works in terminal ### 1.1 Project Scaffolding - `Cargo.toml` with initial dependencies: - `ratatui`, `crossterm` — TUI - `tokio` (full features) — async runtime - `serde`, `serde_json` — serialization - `thiserror` — error types - `tracing`, `tracing-subscriber` — structured logging - `reqwest` (with `stream` feature) — HTTP client for SSE - `futures` — stream combinators - Establish `src/{app,tui,core,provider}/mod.rs` stubs - `cargo build` passes; `cargo clippy -- -D warnings` passes on empty stubs ### 1.2 Shared Types (`src/core/types.rs`) - `StreamEvent` enum: `TextDelta(String)`, `InputTokens(u32)`, `OutputTokens(u32)`, `Done`, `Error(String)` - `UserAction` enum (TUI → core channel): `SendMessage(String)`, `Quit` - `UIEvent` enum (core → TUI channel): `StreamDelta(String)`, `TurnComplete`, `Error(String)` - `ConversationMessage` struct: `role: Role`, `content: String` - All types derive `Debug`; all public types have doc comments ### 1.3 Provider: `ModelProvider` Trait + Claude SSE (`src/provider/`) - `ModelProvider` trait: `async fn stream(&self, messages: &[ConversationMessage]) -> impl Stream` - `ClaudeProvider` struct: API key from env, `reqwest` HTTP client - Serialize messages to Anthropic Messages API JSON format - Parse SSE byte stream → `StreamEvent` (handle `content_block_delta`, `message_delta` for tokens, `message_stop`) - Unit tests: SSE parsing from hardcoded byte fixtures in `#[cfg(test)]` ### 1.4 Core: Conversation State + Orchestrator Loop (`src/core/`) - `ConversationHistory`: `Vec` with `push` and `messages()` (flat list, no tree yet) - `Orchestrator` struct holding history, provider, channel senders/receivers - Orchestrator loop: 1. Await `UserAction` from TUI channel 2. On `SendMessage`: append user message, call `provider.stream()` 3. Forward each `StreamEvent` as `UIEvent` to TUI 4. Accumulate deltas into assistant message; append to history on `Done` 5. On `Quit`: break loop ### 1.5 TUI: Layout + Input + Streaming Display (`src/tui/`) - `AppState` struct: `messages: Vec<(Role, String)>`, `input: String`, `scroll: u16` - Ratatui layout: full-height `Paragraph` output area (scrollable) + single-line `Paragraph` input - Insert mode only — printable chars append to `input`, Enter sends `UserAction::SendMessage`, Backspace deletes - On `UIEvent::StreamDelta`: append to last assistant message in `messages`, re-render - On `UIEvent::TurnComplete`: finalize assistant message - Crossterm raw mode enter/exit; restore terminal on panic or clean exit ### 1.6 App Wiring + Entry Point (`src/app/`, `src/main.rs`) - `main.rs`: parse `--project-dir ` CLI arg - Initialize `tracing_subscriber` (log to file, not stdout — avoids TUI interference) - Create `tokio::sync::mpsc` channel pair for `UserAction` and `UIEvent` - Spawn `Orchestrator::run()` as a tokio task - Run TUI event loop on main thread (Ratatui requires main thread for crossterm) - On `UserAction::Quit` or Ctrl-C: signal orchestrator shutdown, restore terminal, exit cleanly ### 1.7 Phase 1 Unit Tests - Provider: SSE byte fixture → correct `StreamEvent` sequence - Provider: `ConversationMessage` vec → correct Anthropic API JSON shape - Core: `ConversationHistory` push/read roundtrip - Core: Orchestrator state transitions against mock `StreamEvent` sequence (no real API) ## Phase 2: Vim Modes and Navigation - Normal, Insert, Command modes with visual indicator - `j`/`k` scroll in Normal mode - `:quit`, `:clear` commands - **Done when:** Fluid mode switching and scrolling feels vim-native ## Phase 3: Tool Execution - `Tool` trait, `ToolRegistry`, core tools (`read_file`, `write_file`, `shell_exec`) - Tool definitions in API requests, parse tool-use responses - Approval gate: core → TUI pending event → user approve/deny → result back - Working directory confinement + path validation (no Landlock yet) - **Done when:** Claude can read, modify files, and run commands with user approval ## Phase 4: Sandboxing - Landlock: read-only system, read-write project dir, network blocked - Tools execute through `Sandbox`, never directly - `:net on/off` toggle, state in status bar - Graceful degradation on older kernels - **Done when:** Writes outside project dir fail; network toggle works