4.4 KiB
4.4 KiB
Implementation Plan
Phase 1: Minimal Conversation Loop
Done when: Multi-turn streaming conversation with Claude works in terminal
1.1 Project Scaffolding
Cargo.tomlwith initial dependencies:ratatui,crossterm— TUItokio(full features) — async runtimeserde,serde_json— serializationthiserror— error typestracing,tracing-subscriber— structured loggingreqwest(withstreamfeature) — HTTP client for SSEfutures— stream combinators
- Establish
src/{app,tui,core,provider}/mod.rsstubs cargo buildpasses;cargo clippy -- -D warningspasses on empty stubs
1.2 Shared Types (src/core/types.rs)
StreamEventenum:TextDelta(String),InputTokens(u32),OutputTokens(u32),Done,Error(String)UserActionenum (TUI → core channel):SendMessage(String),QuitUIEventenum (core → TUI channel):StreamDelta(String),TurnComplete,Error(String)ConversationMessagestruct:role: Role,content: String- All types derive
Debug; all public types have doc comments
1.3 Provider: ModelProvider Trait + Claude SSE (src/provider/)
ModelProvidertrait:async fn stream(&self, messages: &[ConversationMessage]) -> impl Stream<Item = StreamEvent>ClaudeProviderstruct: API key from env,reqwestHTTP client- Serialize messages to Anthropic Messages API JSON format
- Parse SSE byte stream →
StreamEvent(handlecontent_block_delta,message_deltafor 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<ConversationMessage>withpushandmessages()(flat list, no tree yet)Orchestratorstruct holding history, provider, channel senders/receivers- Orchestrator loop:
- Await
UserActionfrom TUI channel - On
SendMessage: append user message, callprovider.stream() - Forward each
StreamEventasUIEventto TUI - Accumulate deltas into assistant message; append to history on
Done - On
Quit: break loop
- Await
1.5 TUI: Layout + Input + Streaming Display (src/tui/)
AppStatestruct:messages: Vec<(Role, String)>,input: String,scroll: u16- Ratatui layout: full-height
Paragraphoutput area (scrollable) + single-lineParagraphinput - Insert mode only — printable chars append to
input, Enter sendsUserAction::SendMessage, Backspace deletes - On
UIEvent::StreamDelta: append to last assistant message inmessages, 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 <path>CLI arg- Initialize
tracing_subscriber(log to file, not stdout — avoids TUI interference) - Create
tokio::sync::mpscchannel pair forUserActionandUIEvent - Spawn
Orchestrator::run()as a tokio task - Run TUI event loop on main thread (Ratatui requires main thread for crossterm)
- On
UserAction::Quitor Ctrl-C: signal orchestrator shutdown, restore terminal, exit cleanly
1.7 Phase 1 Unit Tests
- Provider: SSE byte fixture → correct
StreamEventsequence - Provider:
ConversationMessagevec → correct Anthropic API JSON shape - Core:
ConversationHistorypush/read roundtrip - Core: Orchestrator state transitions against mock
StreamEventsequence (no real API)
Phase 2: Vim Modes and Navigation
- Normal, Insert, Command modes with visual indicator
j/kscroll in Normal mode:quit,:clearcommands- Done when: Fluid mode switching and scrolling feels vim-native
Phase 3: Tool Execution
Tooltrait,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/offtoggle, state in status bar- Graceful degradation on older kernels
- Done when: Writes outside project dir fail; network toggle works