skate/PLAN.md

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.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<Item = StreamEvent>
  • 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<ConversationMessage> 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 <path> 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