pub mod client; pub mod config; pub mod project; pub mod tools; pub mod tui; use clap::Subcommand; use tracing::info; #[derive(Subcommand, Debug)] pub enum CodeCommand { /// Start a coding session (default — can omit subcommand) Start { /// Model override (e.g., devstral-2) #[arg(long)] model: Option, /// Sol gRPC endpoint (default: from sunbeam config) #[arg(long)] endpoint: Option, }, /// Demo the TUI with sample data (no Sol connection needed) #[command(hide = true)] Demo, } pub async fn cmd_code(cmd: Option) -> sunbeam_sdk::error::Result<()> { cmd_code_inner(cmd).await.map_err(|e| sunbeam_sdk::error::SunbeamError::Other(e.to_string())) } async fn cmd_code_inner(cmd: Option) -> anyhow::Result<()> { let cmd = cmd.unwrap_or(CodeCommand::Start { model: None, endpoint: None, }); match cmd { CodeCommand::Demo => { return run_demo().await; } CodeCommand::Start { model, endpoint } => { let endpoint = endpoint.unwrap_or_else(|| "http://127.0.0.1:50051".into()); // Discover project context let project = project::discover_project(".")?; info!( project = project.name.as_str(), path = project.path.as_str(), branch = project.git_branch.as_deref().unwrap_or("?"), "Discovered project" ); // Load project config let cfg = config::load_project_config(&project.path); let model = model .or(cfg.model_name.clone()) .unwrap_or_else(|| "devstral-small-2506".into()); // Connect to Sol let mut session = client::connect(&endpoint, &project, &cfg, &model).await?; info!( session_id = session.session_id.as_str(), room_id = session.room_id.as_str(), model = session.model.as_str(), "Connected to Sol" ); // TUI event loop use crossterm::event::{self, Event, KeyCode, KeyModifiers}; let mut terminal = tui::setup_terminal()?; let branch = project.git_branch.as_deref().unwrap_or("?"); let mut app = tui::App::new(&project.name, branch, &model); let result = loop { terminal.draw(|frame| tui::draw(frame, &app))?; if event::poll(std::time::Duration::from_millis(50))? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break Ok(()), KeyCode::Char(c) => { app.input.insert(app.cursor_pos, c); app.cursor_pos += 1; } KeyCode::Backspace => { if app.cursor_pos > 0 { app.cursor_pos -= 1; app.input.remove(app.cursor_pos); } } KeyCode::Left => app.cursor_pos = app.cursor_pos.saturating_sub(1), KeyCode::Right => app.cursor_pos = (app.cursor_pos + 1).min(app.input.len()), KeyCode::Up => app.scroll_offset = app.scroll_offset.saturating_sub(1), KeyCode::Down => app.scroll_offset = app.scroll_offset.saturating_add(1), KeyCode::Enter => { if !app.input.is_empty() { let text = app.input.clone(); app.input.clear(); app.cursor_pos = 0; if text == "/exit" { let _ = session.end().await; break Ok(()); } app.push_log(tui::LogEntry::UserInput(text.clone())); app.is_thinking = true; // Force a redraw to show "thinking..." terminal.draw(|frame| tui::draw(frame, &app))?; match session.chat(&text).await { Ok(response) => { app.is_thinking = false; app.push_log(tui::LogEntry::AssistantText(response)); } Err(e) => { app.is_thinking = false; app.push_log(tui::LogEntry::Error(e.to_string())); } } } } _ => {} } } } }; tui::restore_terminal(&mut terminal)?; result } } } async fn run_demo() -> anyhow::Result<()> { use crossterm::event::{self, Event, KeyCode, KeyModifiers}; let mut terminal = tui::setup_terminal()?; let mut app = tui::App::new("sol", "mainline ±", "devstral-2"); // Populate with sample conversation app.push_log(tui::LogEntry::UserInput("fix the token validation bug in auth.rs".into())); app.push_log(tui::LogEntry::AssistantText( "Looking at the auth module, I can see the issue on line 42 where the token \ is not properly validated before use. The expiry check is missing entirely." .into(), )); app.push_log(tui::LogEntry::ToolSuccess { name: "file_read".into(), detail: "src/auth.rs (127 lines)".into(), }); app.push_log(tui::LogEntry::ToolOutput { lines: vec![ "38│ fn validate_token(token: &str) -> bool {".into(), "39│ let decoded = decode(token);".into(), "40│ // BUG: missing expiry check".into(), "41│ decoded.is_ok()".into(), "42│ }".into(), "43│".into(), "44│ fn refresh_token(token: &str) -> Result {".into(), "45│ let client = reqwest::Client::new();".into(), "46│ // ...".into(), ], collapsed: true, }); app.push_log(tui::LogEntry::ToolSuccess { name: "search_replace".into(), detail: "src/auth.rs — applied 1 replacement (line 41)".into(), }); app.push_log(tui::LogEntry::ToolExecuting { name: "bash".into(), detail: "cargo test --lib".into(), }); app.push_log(tui::LogEntry::ToolOutput { lines: vec![ "running 23 tests".into(), "test auth::tests::test_validate_token ... ok".into(), "test auth::tests::test_expired_token ... ok".into(), "test auth::tests::test_refresh_flow ... ok".into(), "test result: ok. 23 passed; 0 failed".into(), ], collapsed: false, }); app.push_log(tui::LogEntry::AssistantText( "Fixed. The token validation now checks expiry before use. All 23 tests pass." .into(), )); app.push_log(tui::LogEntry::UserInput("now add rate limiting to the auth endpoint".into())); app.push_log(tui::LogEntry::ToolExecuting { name: "file_read".into(), detail: "src/routes/auth.rs".into(), }); app.is_thinking = true; app.input_tokens = 2400; app.output_tokens = 890; loop { terminal.draw(|frame| tui::draw(frame, &app))?; if event::poll(std::time::Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break, KeyCode::Char('q') => break, KeyCode::Char(c) => { app.input.insert(app.cursor_pos, c); app.cursor_pos += 1; } KeyCode::Backspace => { if app.cursor_pos > 0 { app.cursor_pos -= 1; app.input.remove(app.cursor_pos); } } KeyCode::Left => { app.cursor_pos = app.cursor_pos.saturating_sub(1); } KeyCode::Right => { app.cursor_pos = (app.cursor_pos + 1).min(app.input.len()); } KeyCode::Enter => { if !app.input.is_empty() { let text = app.input.clone(); app.input.clear(); app.cursor_pos = 0; if text == "/exit" { break; } app.push_log(tui::LogEntry::UserInput(text)); app.is_thinking = true; } } KeyCode::Up => { app.scroll_offset = app.scroll_offset.saturating_sub(1); } KeyCode::Down => { app.scroll_offset = app.scroll_offset.saturating_add(1); } _ => {} } } } } tui::restore_terminal(&mut terminal)?; Ok(()) }