feat(code): wire TUI into real code path, /exit, color swap
- user input: white text, dim > prompt - sol responses: warm yellow - /exit slash command quits cleanly - TUI replaces stdin loop in sunbeam code start - hidden demo mode for testing (sunbeam code demo)
This commit is contained in:
@@ -2,6 +2,7 @@ pub mod client;
|
||||
pub mod config;
|
||||
pub mod project;
|
||||
pub mod tools;
|
||||
pub mod tui;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tracing::info;
|
||||
@@ -17,6 +18,9 @@ pub enum CodeCommand {
|
||||
#[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<()> {
|
||||
@@ -30,6 +34,9 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
@@ -59,39 +66,187 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
"Connected to Sol"
|
||||
);
|
||||
|
||||
// For now, simple stdin loop (ratatui TUI in Phase 4)
|
||||
println!("sunbeam code · {} · {}", project.name, model);
|
||||
println!("connected to Sol (session: {})", &session.session_id[..8]);
|
||||
println!("type a message, /quit to exit\n");
|
||||
// TUI event loop
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let reader = tokio::io::BufReader::new(stdin);
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut lines = reader.lines();
|
||||
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);
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let line = line.trim().to_string();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line == "/quit" {
|
||||
session.end().await?;
|
||||
println!("session ended.");
|
||||
break;
|
||||
}
|
||||
let result = loop {
|
||||
terminal.draw(|frame| tui::draw(frame, &app))?;
|
||||
|
||||
print!("> ");
|
||||
match session.chat(&line).await {
|
||||
Ok(response) => {
|
||||
println!("\n{}\n", response);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user