Files
cli/sunbeam/src/code/mod.rs

253 lines
9.7 KiB
Rust
Raw Normal View History

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<String>,
/// Sol gRPC endpoint (default: from sunbeam config)
#[arg(long)]
endpoint: Option<String>,
},
/// Demo the TUI with sample data (no Sol connection needed)
#[command(hide = true)]
Demo,
}
pub async fn cmd_code(cmd: Option<CodeCommand>) -> 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<CodeCommand>) -> 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<String> {".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(())
}