diff --git a/src/main.rs b/src/main.rs index fbf3238..4708358 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; #[command(about = "A Rust-based AI agent for task execution", long_about = None)] struct Cli { #[command(subcommand)] - command: Commands, + command: Option, } #[derive(Subcommand)] @@ -33,6 +33,8 @@ enum Commands { #[arg(long)] max_iterations: Option, }, + /// Launch interactive TUI + Tui, } /// Find config file in standard locations @@ -70,7 +72,10 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - match &cli.command { + // Default to TUI if no command specified + let command = cli.command.unwrap_or(Commands::Tui); + + match command { Commands::Init { spec_dir } => { let dir = spec_dir.clone().unwrap_or_else(|| "specs".to_string()); @@ -121,9 +126,25 @@ async fn main() -> anyhow::Result<()> { let config = config::Config::load(&config_path)?; // Create and run Ralph loop - let ralph = ralph::RalphLoop::new(config, spec_file.clone(), *max_iterations)?; + let ralph = ralph::RalphLoop::new(config, spec_file.clone(), max_iterations)?; ralph.run().await?; } + Commands::Tui => { + let config_path = find_config_path()?; + let config = config::Config::load(&config_path)?; + let spec_dir = config.rustagent.spec_dir.clone(); + + use rustagent::tui; + + let mut terminal = tui::setup_terminal()?; + let mut app = tui::App::new(&spec_dir); + + let result = tui::run(&mut terminal, &mut app); + + tui::restore_terminal(&mut terminal)?; + + result?; + } } Ok(()) diff --git a/src/tui/app.rs b/src/tui/app.rs index 31d482d..8a87774 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -20,11 +20,14 @@ pub struct App { } impl App { - pub fn new() -> Self { + pub fn new(spec_dir: &str) -> Self { + let mut dashboard = DashboardState::new(); + dashboard.load_specs(spec_dir); + Self { running: true, active_tab: ActiveTab::Dashboard, - dashboard: DashboardState::new(), + dashboard, planning: PlanningState::new(), execution: ExecutionState::new(), side_panel: SidePanel::new(), @@ -88,6 +91,6 @@ impl App { impl Default for App { fn default() -> Self { - Self::new() + Self::new("") } } diff --git a/src/tui/views/dashboard.rs b/src/tui/views/dashboard.rs index 1eb4544..56bd8ca 100644 --- a/src/tui/views/dashboard.rs +++ b/src/tui/views/dashboard.rs @@ -1,3 +1,6 @@ +use std::fs; +use std::path::Path; + use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -5,6 +8,8 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, Paragraph}, }; +use crate::spec::Spec; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DashboardMode { Kanban, @@ -42,6 +47,59 @@ impl DashboardState { selected_row: 0, } } + + pub fn load_specs(&mut self, spec_dir: &str) { + self.specs.clear(); + + let spec_path = Path::new(spec_dir); + if !spec_path.exists() { + return; + } + + if let Ok(entries) = fs::read_dir(spec_path) { + for entry in entries.flatten() { + let path = entry.path(); + + // Look for spec.json files + let spec_file = if path.is_dir() { + path.join("spec.json") + } else if path.extension().is_some_and(|e| e == "json") { + path + } else { + continue; + }; + + if let Ok(spec) = Spec::load(&spec_file) { + let completed = spec + .tasks + .iter() + .filter(|t| t.status == crate::spec::TaskStatus::Complete) + .count(); + let total = spec.tasks.len(); + + let status = if completed == total && total > 0 { + SpecStatus::Completed + } else if spec + .tasks + .iter() + .any(|t| t.status == crate::spec::TaskStatus::InProgress) + { + SpecStatus::Running + } else if total > 0 { + SpecStatus::Ready + } else { + SpecStatus::Draft + }; + + self.specs.push(SpecSummary { + name: spec.name, + status, + task_progress: (completed, total), + }); + } + } + } + } } impl Default for DashboardState {