gigi-cli 1.0.0

Gigi — A Claude Code-like AI coding assistant CLI in Rust
pub mod commit;
pub mod memory;
pub mod review;

use anyhow::Result;
use colored::*;

use crate::agent::Agent;

// =============================================================================
// Slash Command Dispatcher
//
// Intercepts user input starting with '/' and routes to the appropriate handler.
// Returns Ok(true) if a command was handled, Ok(false) if not a command.
// =============================================================================

pub async fn dispatch(agent: &mut Agent, input: &str) -> Result<bool> {
    let input = input.trim();

    if !input.starts_with('/') {
        return Ok(false);
    }

    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    let command = parts[0];
    let args = parts.get(1).unwrap_or(&"").trim();

    match command {
        "/help" => {
            print_help();
            Ok(true)
        }

        "/clear" => {
            agent.session.messages.clear();
            println!("{}", "✓ Conversation history cleared.".green());
            Ok(true)
        }

        "/model" => {
            if args.is_empty() {
                println!("Current model: {}", agent.model_info().cyan());
                println!(
                    "\nUsage: {} <provider> [model_name]",
                    "/model".bold()
                );
                println!("Available providers: anthropic, groq, google, ollama, lm_studio, llama_cpp, custom");
            } else {
                let parts: Vec<&str> = args.splitn(2, ' ').collect();
                let provider_name = parts[0].trim();
                let model_name = parts.get(1).map(|s| s.trim().to_string());

                let mut config = agent.config.clone();
                match config.prompt_for_key_if_missing(provider_name) {
                    Ok(_) => {
                        agent.config = config;
                        match crate::query::create_provider(&agent.config, provider_name, model_name) {
                            Ok(provider) => {
                                agent.engine.switch_provider(provider);
                                // Also update the session info
                                agent.session.provider_name = agent.engine.provider_name().to_string();
                                agent.session.model_name = agent.engine.model_id().to_string();
                                println!(
                                    "{}",
                                    format!("✓ Switched model provider to: {}", agent.model_info()).green()
                                );
                            }
                            Err(e) => {
                                println!("{}", format!("✗ Failed to switch model provider: {}", e).red());
                            }
                        }
                    }
                    Err(e) => {
                        println!("{}", format!("✗ Failed to configure key: {}", e).red());
                    }
                }
            }
            Ok(true)
        }

        "/commit" => {
            commit::handle(agent, args).await?;
            Ok(true)
        }

        "/review" => {
            review::handle(agent, args).await?;
            Ok(true)
        }

        "/memory" => {
            memory::handle(agent, args).await?;
            Ok(true)
        }

        "/sessions" => {
            list_sessions(agent).await?;
            Ok(true)
        }

        "/config" => {
            println!("{}", agent.config.display_summary());
            Ok(true)
        }

        "/jobs" => {
            if args.is_empty() || args == "list" {
                let tasks = crate::tools::task_manager::TaskManager::global().list_tasks();
                if tasks.is_empty() {
                    println!("{}", "No background tasks registered.".dimmed());
                } else {
                    println!("{}", "\n━━━ Background Tasks ━━━".bold());
                    for task in &tasks {
                        let status_str = match &task.status {
                            crate::tools::task_manager::TaskStatus::Running => "Running".cyan(),
                            crate::tools::task_manager::TaskStatus::Finished { exit_code } => format!("Finished ({})", exit_code).green(),
                            crate::tools::task_manager::TaskStatus::Failed { error } => format!("Failed: {}", error).red(),
                            crate::tools::task_manager::TaskStatus::Killed => "Killed".yellow(),
                        };
                        println!(
                            "  {} [{}] — {}\n     cmd: {}\n     logs: {}",
                            task.id.bold().cyan(),
                            status_str,
                            task.started_at.format("%Y-%m-%d %H:%M:%S"),
                            task.command.dimmed(),
                            task.log_path
                        );
                    }
                }
            } else if let Some(task_id) = args.strip_prefix("kill ") {
                let task_id = task_id.trim();
                match crate::tools::task_manager::TaskManager::global().kill_task(task_id) {
                    Ok(true) => println!("{}", format!("✓ Successfully killed task '{}'", task_id).green()),
                    Ok(false) => println!("{}", format!("✗ Task '{}' is not running or not found.", task_id).red()),
                    Err(e) => println!("{}", format!("✗ Error killing task '{}': {}", task_id, e).red()),
                }
            } else if let Some(task_id) = args.strip_prefix("logs ") {
                let task_id = task_id.trim();
                let tasks = crate::tools::task_manager::TaskManager::global().list_tasks();
                if let Some(task) = tasks.iter().find(|t| t.id == task_id) {
                    let log_path = std::path::Path::new(&task.log_path);
                    if log_path.exists() {
                        match std::fs::read_to_string(log_path) {
                            Ok(content) => {
                                println!("{}", format!("\n━━━ Logs for {} ━━━", task_id).bold());
                                let lines: Vec<&str> = content.lines().collect();
                                let start = lines.len().saturating_sub(20);
                                if start > 0 {
                                    println!("{}", "... (truncated)".dimmed());
                                }
                                for line in &lines[start..] {
                                    println!("{}", line);
                                }
                            }
                            Err(e) => println!("{}", format!("✗ Failed to read log file: {}", e).red()),
                        }
                    } else {
                        println!("{}", "✗ Log file does not exist.".red());
                    }
                } else {
                    println!("{}", format!("✗ Task '{}' not found.", task_id).red());
                }
            } else {
                println!("{}", "Usage: /jobs [list|kill <task_id>|logs <task_id>]".yellow());
            }
            Ok(true)
        }

        _ => {
            println!(
                "{}",
                format!("Unknown command: {}. Type /help for available commands.", command).red()
            );
            Ok(true)
        }
    }
}

fn print_help() {
    println!("{}", "\n━━━ Available Commands ━━━".bold());
    println!("  {}     Ask the AI to generate a commit message and commit", "/commit".cyan());
    println!("  {}     Ask the AI to review recent git changes", "/review".cyan());
    println!("  {}  Show or set persistent memory notes", "/memory".cyan());
    println!("  {}   Switch the active model provider", "/model".cyan());
    println!("  {}      Clear conversation history", "/clear".cyan());
    println!("  {} List saved sessions", "/sessions".cyan());
    println!("  {}    Show current configuration", "/config".cyan());
    println!("  {}     Manage background jobs", "/jobs".cyan());
    println!("  {}       Show this help message", "/help".cyan());
    println!();
}

async fn list_sessions(agent: &Agent) -> Result<()> {
    let summaries = crate::session::Session::list_all(&agent.config.session_dir).await?;

    if summaries.is_empty() {
        println!("{}", "No saved sessions found.".dimmed());
    } else {
        println!("{}", "\n━━━ Saved Sessions ━━━".bold());
        for summary in &summaries {
            println!("  {}", summary);
        }
        println!(
            "\n{}",
            "Use: gigi resume <session-id> to resume a session.".dimmed()
        );
    }

    Ok(())
}