summaryrefslogblamecommitdiff
path: root/makima/src/bin/makima.rs
blob: c4183f33c6ec3d7dc15bc62eecf256e05666dafd (plain) (tree)
1
2
3
4
5
6
7
8
9





                                                                     
                                                            
                          
                                                             
                                                  
  
                                                                                                                                                               


                                                      
                          














                                                                         
                                                             
                                                     
                                                       

















                                                             
                                   
























                                                           








                                                           

                                                         






                                                      


                                                        

     






                                                                             








                                                       
                                    
                                                
































                                                                                                 
                                                                                          

















































                                                                                        




                                                                  





                                                                               

                                                                  
                                                                      






                                                                
                                      
                
                                               
                                                                                      
                                                                      

      







                                                                                              













                                                                              















                                                                                               





























                                                                       





                                                                  






































                                                                                   

                                                                            
              




















































                                                                                                      























                                                                                   
                                                                   

                                                          
                                                               











                                                                                   
                             







                                                                     
                                                                                 







                                                                           







                                                                                   
                                                                                                                                                               


                                                              

                                                                                   




                                                                                                         
                               
                                                                                               


                                                              









                                                                                       
































































                                                                                            























                                                                                                 














                                                                                   




























































































                                                                                             
































































































                                                                                         








                                                                                   


                                                                                   
                                                                                                     


                                                              









                                                                                                                                                               



















































                                                                                                                 

                                                              




          
                             
                                                                                           




















                                                                                           
                        
                                                                   
 
                                                       
                                                         
 









                                                

                         





                                                                       










































                                                                                                 











                                                                                                                                     



                                                                                                     

                                                                        






































                                                                                 






































                                                                                                  
                           
                                                    


                                   
                          









                                          

























































                                                                                                          




















                                                                                                                                      

                                                                   









































































































                                                                                                                                   








                                                                                                                   
                                                     
                                                           





                                                                                






























                                                                                                                           



                                                                              


                                                                                                                      







                                                                                                   


































































                                                                                                                      














                                   




























                                                               





























                                                                        











































                                                                                               



















































































































































                                                                                         
//! Makima CLI - unified CLI for server, daemon, and task management.

use std::io::{self, Read};
use std::path::Path;
use std::sync::Arc;

use makima::daemon::api::{ApiClient, CreateContractRequest};
use makima::daemon::cli::{
    Cli, CliConfig, Commands, ConfigCommand, ContractCommand,
    DirectiveCommand, SupervisorCommand, ViewArgs,
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion};
use makima::daemon::config::{DaemonConfig, RepoEntry};
use makima::daemon::db::LocalDb;
use makima::daemon::error::DaemonError;
use makima::daemon::setup;
use makima::daemon::task::{TaskConfig, TaskManager};
use makima::daemon::ws::{DaemonCommand, WsClient};
use tokio::process::Command;
use tokio::sync::mpsc;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let cli = Cli::parse_args();

    match cli.command {
        Commands::Server(args) => run_server(args).await,
        Commands::Daemon(args) => run_daemon(args).await,
        Commands::Supervisor(cmd) => run_supervisor(cmd).await,
        Commands::Contract(cmd) => run_contract(cmd).await,
        Commands::Directive(cmd) => run_directive(cmd).await,
        Commands::View(args) => run_view(args).await,
        Commands::Config(cmd) => run_config(cmd).await,
    }
}

/// Run the makima server.
async fn run_server(
    args: makima::daemon::cli::ServerArgs,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // Initialize logging
    init_logging(&args.log_level, "text");

    eprintln!("=== Makima Server Starting ===");
    eprintln!("Port: {}", args.port);

    // Create app state
    let mut app_state = makima::server::state::AppState::new(
        &args.parakeet_model_dir,
        &args.parakeet_eou_dir,
        &args.sortformer_model_path,
        &args.chatterbox_model_dir,
    );

    // Connect to database if URL provided
    if let Some(ref db_url) = args.database_url {
        eprintln!("Connecting to database...");
        let pool = makima::db::create_pool(db_url).await?;
        app_state = app_state.with_db_pool(pool);
        eprintln!("Database connected");
    }

    let state = Arc::new(app_state);
    let addr = format!("0.0.0.0:{}", args.port);

    eprintln!("Starting server on {}", addr);
    makima::server::run_server(state, &addr).await?;

    Ok(())
}

/// Run the makima daemon.
async fn run_daemon(
    args: makima::daemon::cli::DaemonArgs,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    eprintln!("=== Makima Daemon Starting ===");

    // Check dependencies unless skipped
    if !args.skip_setup_check {
        eprintln!("[0/5] Checking dependencies...");
        let dep_result = setup::check_dependencies().await;
        setup::print_dependency_summary(&dep_result);

        // Check for missing critical dependencies
        if !dep_result.claude.installed {
            let os = setup::OperatingSystem::detect();
            setup::print_claude_install_instructions(os);
            std::process::exit(1);
        }

        if !dep_result.git.installed {
            let os = setup::OperatingSystem::detect();
            setup::print_git_install_instructions(os);
            std::process::exit(1);
        }

        // Print git authentication warnings (non-fatal)
        setup::print_git_auth_warnings(&dep_result);
    }

    // Install Claude Code skills for makima commands
    eprintln!("[0.5/5] Installing Claude Code skills...");
    if let Err(e) = makima::daemon::skill_installer::install_skills().await {
        eprintln!("      WARNING: Failed to install skills: {}", e);
        // Non-fatal: continue even if skill installation fails
    }

    // Build a temporary CLI struct for config loading
    let cli = makima::daemon::cli::daemon::DaemonArgs {
        config: args.config,
        repos_dir: args.repos_dir,
        worktrees_dir: args.worktrees_dir,
        server_url: args.server_url,
        api_key: args.api_key,
        max_tasks: args.max_tasks,
        log_level: args.log_level,
        bubblewrap: args.bubblewrap,
        skip_setup_check: args.skip_setup_check,
    };

    // Load configuration with CLI overrides
    eprintln!("[1/5] Loading configuration...");
    let config = match DaemonConfig::load_with_daemon_args(&cli) {
        Ok(cfg) => {
            eprintln!("      Config loaded: server={}", cfg.server.url);
            cfg
        }
        Err(e) => {
            eprintln!("Failed to load configuration: {}", e);
            eprintln!();
            eprintln!("Use CLI flags:");
            eprintln!("  makima daemon --server-url ws://localhost:8080 --api-key your-api-key");
            eprintln!();
            eprintln!("Or set environment variables:");
            eprintln!("  MAKIMA_DAEMON_SERVER_URL=ws://localhost:8080");
            eprintln!("  MAKIMA_API_KEY=your-api-key");
            eprintln!();
            eprintln!("Or create a config file: makima-daemon.toml");
            std::process::exit(1);
        }
    };

    // Initialize logging
    init_logging(&config.logging.level, &config.logging.format);
    eprintln!("[2/5] Logging initialized");

    // Initialize local database
    eprintln!(
        "[3/5] Opening local database: {}",
        config.local_db.path.display()
    );
    let local_db = Arc::new(std::sync::Mutex::new(LocalDb::open(&config.local_db.path)?));
    eprintln!("      Database opened");

    // Initialize worktree directories
    eprintln!("[4/5] Setting up directories...");
    tokio::fs::create_dir_all(&config.worktree.base_dir).await?;
    tokio::fs::create_dir_all(&config.worktree.repos_dir).await?;
    tokio::fs::create_dir_all(&config.repos.home_dir).await?;
    eprintln!(
        "      Worktree base: {}",
        config.worktree.base_dir.display()
    );
    eprintln!("      Repos cache: {}", config.worktree.repos_dir.display());
    eprintln!("      Home dir: {}", config.repos.home_dir.display());

    // Auto-clone repositories if configured
    if !config.repos.auto_clone.is_empty() {
        eprintln!(
            "      Auto-cloning {} repositories...",
            config.repos.auto_clone.len()
        );
        for repo_entry in &config.repos.auto_clone {
            if let Err(e) = auto_clone_repo(repo_entry, &config.repos.home_dir).await {
                eprintln!("      WARNING: Failed to clone {}: {}", repo_entry.url(), e);
            }
        }
    }

    // Create channels for communication
    let (command_tx, mut command_rx) = mpsc::channel::<DaemonCommand>(64);

    // Get machine info
    let machine_id = get_machine_id();
    let hostname = get_hostname();
    eprintln!("      Machine ID: {}", machine_id);
    eprintln!("      Hostname: {}", hostname);

    // Create WebSocket client
    eprintln!("[5/5] Connecting to server: {}", config.server.url);
    let mut ws_client = WsClient::new(
        config.server.clone(),
        machine_id,
        hostname,
        config.process.max_concurrent_tasks as i32,
        command_tx,
    );

    // Get sender for task manager
    let ws_tx = ws_client.sender();

    // Create task configuration
    let bubblewrap_config = if config.process.bubblewrap.enabled {
        Some(config.process.bubblewrap.clone())
    } else {
        None
    };
    // Derive HTTP API URL from WebSocket server URL (wss://... -> https://...)
    let api_url = config
        .server
        .url
        .replace("wss://", "https://")
        .replace("ws://", "http://");
    let task_config = TaskConfig {
        max_concurrent_tasks: config.process.max_concurrent_tasks,
        max_tasks_per_contract: config.process.max_tasks_per_contract,
        worktree_base_dir: config.worktree.base_dir.clone(),
        env_vars: config.process.env_vars.clone(),
        claude_command: config.process.claude_command.clone(),
        claude_args: config.process.claude_args.clone(),
        claude_pre_args: config.process.claude_pre_args.clone(),
        enable_permissions: config.process.enable_permissions,
        disable_verbose: config.process.disable_verbose,
        bubblewrap: bubblewrap_config,
        api_url,
        api_key: config.server.api_key.clone(),
        heartbeat_commit_interval_secs: config.process.heartbeat_commit_interval_secs,
        checkpoint_patches: config.process.checkpoint_patches.clone(),
    };

    // Create task manager with local database for crash recovery
    let task_manager = Arc::new(TaskManager::new(task_config, ws_tx.clone(), local_db));

    // Recover any orphaned tasks from previous daemon run
    let recovered = task_manager.recover_orphaned_tasks().await;
    if !recovered.is_empty() {
        eprintln!("      Recovered {} orphaned tasks with intact worktrees", recovered.len());
    }

    // Spawn command handler
    let task_manager_clone = task_manager.clone();
    tokio::spawn(async move {
        tracing::info!("Command handler started, waiting for commands...");
        while let Some(command) = command_rx.recv().await {
            tracing::info!("Received command from channel: {:?}", command);
            if let Err(e) = task_manager_clone.handle_command(command).await {
                tracing::error!("Failed to handle command: {}", e);
            }
        }
        tracing::info!("Command handler stopped");
    });

    // Spawn periodic worktree health check (every 60 seconds)
    let health_check_manager = task_manager.clone();
    tokio::spawn(async move {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
        loop {
            interval.tick().await;
            let affected = health_check_manager.check_worktree_health().await;
            if !affected.is_empty() {
                tracing::info!(
                    count = affected.len(),
                    "Worktree health check detected missing worktrees - tasks marked for retry"
                );
            }
        }
    });

    // Handle shutdown signals
    let shutdown_signal = async {
        tokio::signal::ctrl_c()
            .await
            .expect("Failed to install Ctrl+C handler");
        eprintln!("\nReceived shutdown signal");
    };

    eprintln!("=== Daemon running (Ctrl+C to stop) ===");

    // Run WebSocket client with shutdown handling
    tokio::select! {
        result = ws_client.run() => {
            match result {
                Ok(()) => eprintln!("WebSocket client exited cleanly"),
                Err(DaemonError::AuthFailed(msg)) => {
                    eprintln!("ERROR: Authentication failed: {}", msg);
                    std::process::exit(1);
                }
                Err(e) => {
                    eprintln!("ERROR: WebSocket client error: {}", e);
                    std::process::exit(1);
                }
            }
        }
        _ = shutdown_signal => {
            eprintln!("Shutting down...");
        }
    }

    // Gracefully shutdown all running Claude processes
    eprintln!("Terminating Claude processes...");
    task_manager
        .shutdown_all_processes(std::time::Duration::from_secs(5))
        .await;

    // Cleanup
    tracing::info!("Daemon stopped");

    Ok(())
}

/// Run supervisor commands.
async fn run_supervisor(
    cmd: SupervisorCommand,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    use makima::daemon::api::supervisor::*;

    match cmd {
        SupervisorCommand::Tasks(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.supervisor_tasks(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Tree(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.supervisor_tree(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Spawn(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Creating task: {}...", args.name);
            let req = SpawnTaskRequest {
                name: args.name,
                plan: args.plan,
                contract_id: args.common.contract_id,
                parent_task_id: args.parent,
                checkpoint_sha: args.checkpoint,
            };
            let result = client.supervisor_spawn(req).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Wait(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!(
                "Waiting for task {} (timeout: {}s, poll interval: {}s)...",
                args.task_id, args.timeout, args.poll_interval
            );

            let start_time = std::time::Instant::now();
            let timeout_duration = std::time::Duration::from_secs(args.timeout as u64);
            let poll_interval = std::time::Duration::from_secs(args.poll_interval);
            let server_wait_timeout = 30i32; // Short timeout for server-side wait

            loop {
                // Check if we've exceeded the total timeout
                let remaining = timeout_duration.saturating_sub(start_time.elapsed());
                if remaining.is_zero() {
                    eprintln!("Timeout reached after {}s", args.timeout);
                    let result = client.supervisor_get_task(args.task_id).await?;
                    println!("{}", serde_json::to_string(&result.0)?);
                    break;
                }

                // Try server-side wait with short timeout
                let wait_timeout = std::cmp::min(server_wait_timeout, remaining.as_secs() as i32);

                match client.supervisor_wait(args.task_id, wait_timeout).await {
                    Ok(result) => {
                        if let Some(completed) = result.0.get("completed").and_then(|c| c.as_bool()) {
                            if completed {
                                println!("{}", serde_json::to_string(&result.0)?);
                                break;
                            }
                        }
                        // Not completed yet, continue loop
                        eprintln!("Task still running (elapsed: {:?})", start_time.elapsed());
                    }
                    Err(e) => {
                        eprintln!("Warning: Server wait failed: {}. Falling back to polling...", e);
                        // Fall back to simple status poll
                        if let Ok(result) = client.supervisor_get_task(args.task_id).await {
                            if let Some(status) = result.0.get("status").and_then(|s| s.as_str()) {
                                if status == "done" || status == "failed" || status == "merged" {
                                    let wait_response = serde_json::json!({
                                        "taskId": args.task_id,
                                        "status": status,
                                        "completed": true,
                                        "outputSummary": result.0.get("progressSummary")
                                    });
                                    println!("{}", serde_json::to_string(&wait_response)?);
                                    break;
                                }
                            }
                        }
                    }
                }

                // Small delay before retrying
                tokio::time::sleep(poll_interval).await;
            }
        }
        SupervisorCommand::ReadFile(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .supervisor_read_file(args.task_id, &args.file_path)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Branch(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Creating branch: {}...", args.name);
            let result = client.supervisor_branch(&args.name, args.from).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Merge(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Merging task {}...", args.task_id);
            let result = client
                .supervisor_merge(args.task_id, args.to, args.squash)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Pr(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Creating PR for branch {}...", args.branch);
            let body = args.body.as_deref().unwrap_or("");
            let result = client
                .supervisor_pr(&args.branch, &args.title, body)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Diff(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client.supervisor_diff(args.task_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Checkpoint(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let task_id = args
                .common
                .self_task_id
                .ok_or("MAKIMA_TASK_ID is required for checkpoint")?;
            let result = client
                .supervisor_checkpoint(task_id, &args.message)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Checkpoints(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let task_id = args.self_task_id.ok_or("MAKIMA_TASK_ID is required")?;
            let result = client.supervisor_checkpoints(task_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Status(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.supervisor_status(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Ask(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Asking user: {}...", args.question);
            let choices = args
                .choices
                .map(|c| c.split(',').map(|s| s.trim().to_string()).collect())
                .unwrap_or_default();
            let result = client
                .supervisor_ask(&args.question, choices, args.context, args.timeout, args.phaseguard, args.multi_select, args.non_blocking, args.question_type)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::AdvancePhase(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            if args.confirmed {
                eprintln!("Advancing contract to phase: {} (confirmed)...", args.phase);
            } else {
                eprintln!("Requesting phase advance to: {} (use --confirmed to proceed)...", args.phase);
            }
            let result = client
                .supervisor_advance_phase(args.common.contract_id, &args.phase, args.confirmed)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Task(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client.supervisor_get_task(args.target_task_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::Output(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client.supervisor_get_task_output(args.target_task_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        SupervisorCommand::TaskHistory(args) => {
            eprintln!(
                "Task history for {} (limit: {:?}, format: {})",
                args.task_id, args.limit, args.format
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!("  GET /api/v1/mesh/tasks/{}/conversation", args.task_id);
        }
        SupervisorCommand::TaskCheckpoints(args) => {
            eprintln!(
                "Task checkpoints for {} (with_diff: {})",
                args.task_id, args.with_diff
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!("  GET /api/v1/mesh/tasks/{}/checkpoints", args.task_id);
        }
        SupervisorCommand::Resume(args) => {
            eprintln!(
                "Resume supervisor for contract {} (mode: {}, checkpoint: {:?})",
                args.common.contract_id, args.mode, args.checkpoint
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!(
                "  POST /api/v1/contracts/{}/supervisor/resume",
                args.common.contract_id
            );
        }
        SupervisorCommand::TaskResumeFrom(args) => {
            eprintln!(
                "Resume task {} from checkpoint {} with plan: {}",
                args.task_id, args.checkpoint, args.plan
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!(
                "  POST /api/v1/mesh/tasks/{}/checkpoints/{}/resume",
                args.task_id, args.checkpoint
            );
        }
        SupervisorCommand::TaskRewind(args) => {
            eprintln!(
                "Rewind task {} to checkpoint {} (preserve: {}, branch: {:?})",
                args.task_id, args.checkpoint, args.preserve, args.branch_name
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!("  POST /api/v1/mesh/tasks/{}/rewind", args.task_id);
        }
        SupervisorCommand::TaskFork(args) => {
            eprintln!(
                "Fork task {} from checkpoint {} as '{}' with plan: {}",
                args.task_id, args.checkpoint, args.name, args.plan
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!("  POST /api/v1/mesh/tasks/{}/fork", args.task_id);
        }
        SupervisorCommand::RewindConversation(args) => {
            eprintln!(
                "Rewind conversation for contract {} (by: {:?}, to: {:?}, rewind_code: {})",
                args.common.contract_id, args.by_messages, args.to_message, args.rewind_code
            );
            eprintln!("CLI integration not yet implemented. Use the API directly:");
            eprintln!(
                "  POST /api/v1/contracts/{}/supervisor/conversation/rewind",
                args.common.contract_id
            );
        }
        SupervisorCommand::Complete(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!("Marking contract {} as complete...", args.common.contract_id);
            match client.supervisor_complete(args.common.contract_id).await {
                Ok(_) => {
                    println!(r#"{{"success": true, "message": "Contract marked as complete"}}"#);
                }
                Err(e) => {
                    eprintln!("Error: {}", e);
                    println!(r#"{{"success": false, "error": "{}"}}"#, e);
                    std::process::exit(1);
                }
            }
        }
        SupervisorCommand::ResumeContract(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            eprintln!("Resuming contract {}...", args.contract_id);
            let result = client.supervisor_resume_contract(args.contract_id).await?;
            println!("{}", serde_json::to_string(&serde_json::json!({
                "success": true,
                "message": "Contract resumed",
                "contract": result.0
            }))?);
        }
        SupervisorCommand::MarkDeliverable(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            eprintln!(
                "Marking deliverable '{}' as complete for contract {}...",
                args.deliverable_id, args.common.contract_id
            );
            let result = client
                .supervisor_mark_deliverable(
                    args.common.contract_id,
                    &args.deliverable_id,
                    args.phase.as_deref(),
                )
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
    }

    Ok(())
}

/// Run contract commands.
async fn run_contract(
    cmd: ContractCommand,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    match cmd {
        ContractCommand::Status(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.contract_status(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::Checklist(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.contract_checklist(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::Goals(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.contract_goals(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::Files(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.contract_files(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::File(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .contract_file(args.common.contract_id, args.file_id)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::Report(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .contract_report(args.common.contract_id, &args.message, args.common.task_id)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::SuggestAction(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.contract_suggest_action(args.contract_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::CompletionAction(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let files = args.files.map(|f| {
                f.split(',')
                    .map(|s| s.trim().to_string())
                    .collect::<Vec<_>>()
            });
            let result = client
                .contract_completion_action(
                    args.common.contract_id,
                    args.common.task_id,
                    files,
                    args.lines_added,
                    args.lines_removed,
                    args.code,
                )
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::UpdateFile(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            // Read content from stdin
            let mut content = String::new();
            io::stdin().read_to_string(&mut content)?;
            let result = client
                .contract_update_file(args.common.contract_id, args.file_id, &content)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        ContractCommand::CreateFile(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            // Read content from stdin
            let mut content = String::new();
            io::stdin().read_to_string(&mut content)?;
            let result = client
                .contract_create_file(args.common.contract_id, &args.name, &content)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
    }

    Ok(())
}

/// Run directive commands.
async fn run_directive(
    cmd: DirectiveCommand,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    use makima::daemon::api::directive::*;

    match cmd {
        DirectiveCommand::List(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.list_directives().await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Get(args) | DirectiveCommand::Status(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.get_directive(args.directive_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::AddStep(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let depends_on: Vec<uuid::Uuid> = args
                .depends_on
                .map(|d| {
                    d.split(',')
                        .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
                        .collect()
                })
                .unwrap_or_default();
            let req = CreateStepRequest {
                name: args.name,
                description: args.description,
                task_plan: args.task_plan,
                depends_on,
                order_index: args.order_index,
            };
            let result = client.directive_add_step(args.common.directive_id, req).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::RemoveStep(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            client.directive_remove_step(args.common.directive_id, args.step_id).await?;
            println!(r#"{{"success": true}}"#);
        }
        DirectiveCommand::SetDeps(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let depends_on: Vec<uuid::Uuid> = args
                .depends_on
                .split(',')
                .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
                .collect();
            let result = client
                .directive_set_deps(args.common.directive_id, args.step_id, depends_on)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Start(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.directive_start(args.directive_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Pause(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.directive_pause(args.directive_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Advance(args) => {
            let client = ApiClient::new(args.api_url, args.api_key)?;
            let result = client.directive_advance(args.directive_id).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::CompleteStep(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .directive_complete_step(args.common.directive_id, args.step_id)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::FailStep(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .directive_fail_step(args.common.directive_id, args.step_id)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::SkipStep(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .directive_skip_step(args.common.directive_id, args.step_id)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::UpdateGoal(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .directive_update_goal(args.common.directive_id, &args.goal)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::BatchAddSteps(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let steps: serde_json::Value = serde_json::from_str(&args.json)
                .map_err(|e| format!("Invalid JSON: {}", e))?;
            let result = client
                .directive_batch_add_steps(args.common.directive_id, steps)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Update(args) => {
            let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
            let result = client
                .directive_update(args.common.directive_id, args.pr_url, args.pr_branch, args.status)
                .await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
        DirectiveCommand::Ask(args) => {
            let client = ApiClient::new(args.common.api_url.clone(), args.common.api_key.clone())?;
            eprintln!("Asking user: {}...", args.question);
            let choices = args
                .choices
                .map(|c| c.split(',').map(|s| s.trim().to_string()).collect())
                .unwrap_or_default();
            let result = client
                .supervisor_ask(&args.question, choices, args.context, args.timeout, args.phaseguard, args.multi_select, args.non_blocking, args.question_type)
                .await?;
            let mut response_value = result.0;

            // If the server returned still_pending, poll until we get a real response
            while response_value.get("stillPending").and_then(|v| v.as_bool()).unwrap_or(false) {
                // Extract question_id for polling
                let question_id_str = response_value.get("questionId")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| {
                        Box::<dyn std::error::Error + Send + Sync>::from(
                            "Missing questionId in still_pending response"
                        )
                    })?;
                let question_id: uuid::Uuid = question_id_str.parse().map_err(|e| {
                    Box::<dyn std::error::Error + Send + Sync>::from(
                        format!("Invalid questionId: {}", e)
                    )
                })?;

                eprintln!("Waiting for user response (polling)...");
                let poll_result = client.supervisor_poll_question(question_id).await?;
                response_value = poll_result.0;
            }

            println!("{}", serde_json::to_string(&response_value)?);
        }
        DirectiveCommand::CreateOrder(args) => {
            // Validate order_type is spike or chore
            if args.order_type != "spike" && args.order_type != "chore" {
                eprintln!("Error: Only 'spike' and 'chore' order types are allowed. Got: '{}'", args.order_type);
                std::process::exit(1);
            }
            let client = ApiClient::new(args.common.api_url.clone(), args.common.api_key.clone())?;
            eprintln!("Creating order: {}...", args.title);
            let labels = args
                .labels
                .map(|l| {
                    serde_json::Value::Array(
                        l.split(',')
                            .map(|s| serde_json::Value::String(s.trim().to_string()))
                            .collect(),
                    )
                })
                .unwrap_or_else(|| serde_json::json!([]));
            let req = makima::daemon::api::supervisor::CreateOrderRequest {
                title: args.title,
                description: args.description,
                priority: args.priority,
                order_type: args.order_type,
                labels,
                repository_url: None,
            };
            let result = client.create_order(&req).await?;
            println!("{}", serde_json::to_string(&result.0)?);
        }
    }

    Ok(())
}

/// Run the TUI view command.
async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // Load CLI config for defaults
    let config = CliConfig::load();

    // Get API URL and key, preferring CLI args > env vars > config file
    // Filter out empty strings
    let api_url = args.api_url
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| config.get_api_url());
    let api_key = match args.api_key.filter(|s| !s.is_empty()) {
        Some(key) => key,
        None => config.get_api_key().ok_or_else(|| {
            eprintln!("Error: No API key provided.");
            eprintln!();
            eprintln!("Set your API key using one of these methods:");
            eprintln!("  1. Run: makima config set-key YOUR_API_KEY");
            eprintln!("  2. Set environment variable: export MAKIMA_API_KEY=YOUR_API_KEY");
            eprintln!("  3. Pass via CLI: makima view --api-key YOUR_API_KEY");
            "No API key configured"
        })?,
    };

    // Create API client
    let client = ApiClient::new(api_url.clone(), api_key.clone())?;

    // Start WebSocket client for task output streaming
    let ws_client = TuiWsClient::start(api_url, api_key);

    // Start at contracts view
    let mut app = App::new(ViewType::Contracts);

    // Set initial search query if provided
    if let Some(ref query) = args.query {
        app.search_query = query.clone();
    }

    // Load initial contracts
    let items = load_contracts(&client).await?;
    app.set_items(items);

    // Run TUI with navigation support
    let result = run_tui_with_navigation(app, client, ws_client).await;

    result
}

/// Run config commands.
async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    match cmd {
        ConfigCommand::SetKey(args) => {
            let mut config = CliConfig::load();
            config.api_key = Some(args.api_key);
            config.save()?;
            println!("API key saved to {:?}", CliConfig::config_path().unwrap_or_default());
            Ok(())
        }
        ConfigCommand::SetUrl(args) => {
            let mut config = CliConfig::load();
            config.api_url = args.api_url;
            config.save()?;
            println!("API URL saved to {:?}", CliConfig::config_path().unwrap_or_default());
            Ok(())
        }
        ConfigCommand::Show => {
            let config = CliConfig::load();
            println!("Configuration:");
            println!("  API URL: {}", config.api_url);
            println!("  API Key: {}", config.api_key.as_ref().map(|k| {
                if k.len() > 10 {
                    format!("{}...{}", &k[..6], &k[k.len()-4..])
                } else {
                    "***".to_string()
                }
            }).unwrap_or_else(|| "(not set)".to_string()));
            println!();
            println!("Config file: {:?}", CliConfig::config_path().unwrap_or_default());
            Ok(())
        }
        ConfigCommand::Path => {
            if let Some(path) = CliConfig::config_path() {
                println!("{}", path.display());
            } else {
                eprintln!("Could not determine config path");
            }
            Ok(())
        }
    }
}

/// Load contracts from API
async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
    let result = client.list_contracts().await?;
    let items = result.0.get("contracts")
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(ListItem::from_contract).collect())
        .unwrap_or_default();
    Ok(items)
}

/// Load tasks for a contract from API
async fn load_tasks(client: &ApiClient, contract_id: uuid::Uuid) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
    // Use get_contract which returns tasks as part of the response (works with regular API key auth)
    let result = client.get_contract(contract_id).await?;
    let mut items: Vec<ListItem> = result.0.get("tasks")
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(ListItem::from_task).collect())
        .unwrap_or_default();

    // Sort tasks: supervisor first, then by status (running first), then by name
    items.sort_by(|a, b| {
        // Check if task is supervisor (role field in extra data)
        let a_is_supervisor = a.extra.get("role")
            .and_then(|v| v.as_str())
            .map(|s| s == "supervisor")
            .unwrap_or(false);
        let b_is_supervisor = b.extra.get("role")
            .and_then(|v| v.as_str())
            .map(|s| s == "supervisor")
            .unwrap_or(false);

        // Supervisor first
        match (a_is_supervisor, b_is_supervisor) {
            (true, false) => std::cmp::Ordering::Less,
            (false, true) => std::cmp::Ordering::Greater,
            _ => {
                // Then by status: running/working tasks first
                let status_order = |s: Option<&String>| -> i32 {
                    match s.map(|x| x.as_str()) {
                        Some("running") | Some("working") => 0,
                        Some("pending") | Some("queued") => 1,
                        Some("completed") | Some("done") => 2,
                        Some("failed") | Some("error") => 3,
                        _ => 4,
                    }
                };
                let a_order = status_order(a.status.as_ref());
                let b_order = status_order(b.status.as_ref());

                match a_order.cmp(&b_order) {
                    std::cmp::Ordering::Equal => a.name.cmp(&b.name),
                    other => other,
                }
            }
        }
    });

    Ok(items)
}

/// Run the TUI with navigation support for drill-down views
async fn run_tui_with_navigation(
    mut app: App,
    client: ApiClient,
    ws_client: TuiWsClient,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    use crossterm::{
        event::{DisableMouseCapture, EnableMouseCapture},
        execute,
        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    };
    use ratatui::backend::CrosstermBackend;
    use std::io;

    // Setup terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = ratatui::Terminal::new(backend)?;

    let result = run_tui_loop(&mut terminal, &mut app, &client, &ws_client).await;

    // Cleanup WebSocket
    ws_client.shutdown();

    // Cleanup terminal
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    match result {
        Ok(Some(path)) => {
            // Output the path for shell integration
            tui::print_path(&path);
        }
        Ok(None) => {
            // Normal exit
        }
        Err(e) => {
            eprintln!("TUI error: {}", e);
            std::process::exit(1);
        }
    }

    Ok(())
}

/// Main TUI event loop with async data loading
async fn run_tui_loop(
    terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>,
    app: &mut App,
    client: &ApiClient,
    ws_client: &TuiWsClient,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
    use crossterm::event::{self, Event};
    use std::time::Duration;

    // Track currently subscribed task for cleanup
    let mut subscribed_task_id: Option<uuid::Uuid> = None;

    loop {
        terminal.draw(|f| tui::ui::render(f, app))?;

        // Process WebSocket events (non-blocking)
        while let Some(ws_event) = ws_client.try_recv() {
            handle_ws_event(app, ws_event);
        }

        // Poll for keyboard events with short timeout (50ms for responsive WS handling)
        if event::poll(Duration::from_millis(50))? {
            if let Event::Key(key) = event::read()? {
                let action = tui::event::handle_key_event(app, key);
                match action {
                    Action::Quit => break,
                    Action::OutputPath(path) => return Ok(Some(path)),
                    Action::None => {}
                    _ => {
                        let result = app.handle_action(action);
                        match result {
                            Action::OutputPath(path) => return Ok(Some(path)),
                            Action::LoadTasks { contract_id, contract_name: _ } => {
                                // Unsubscribe from any previous task
                                if let Some(task_id) = subscribed_task_id.take() {
                                    ws_client.unsubscribe(task_id);
                                }
                                // Load tasks for the selected contract
                                match load_tasks(client, contract_id).await {
                                    Ok(items) => {
                                        app.set_items(items);
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Failed to load tasks: {}", e));
                                    }
                                }
                            }
                            Action::LoadTaskOutput { task_id, task_name: _ } => {
                                // Clear previous output
                                app.output_buffer.clear();
                                app.ws_state = WsConnectionState::Connecting;

                                // Unsubscribe from previous task if any
                                if let Some(old_task_id) = subscribed_task_id.take() {
                                    ws_client.unsubscribe(old_task_id);
                                }

                                // Load task output history first
                                app.status_message = Some("Loading output history...".to_string());
                                match client.get_task_output(task_id).await {
                                    Ok(result) => {
                                        // Parse the entries array from response
                                        if let Some(entries) = result.0.get("entries").and_then(|v| v.as_array()) {
                                            for entry in entries {
                                                if let Some(line) = parse_output_entry(entry) {
                                                    app.output_buffer.add_line(line);
                                                }
                                            }
                                            let count = entries.len();
                                            app.status_message = Some(format!("Loaded {} history entries, streaming live...", count));
                                        }
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Failed to load history: {}", e));
                                    }
                                }

                                // Subscribe to new task output for live updates
                                ws_client.subscribe(task_id);
                                subscribed_task_id = Some(task_id);
                            }
                            Action::PerformDelete { id, item_type } => {
                                // Perform the delete API call
                                let delete_result = match item_type {
                                    ViewType::Contracts => {
                                        client.delete_contract(id).await
                                    }
                                    ViewType::Tasks => {
                                        client.delete_task(id).await
                                    }
                                    ViewType::TaskOutput => {
                                        // Can't delete from output view
                                        Ok(())
                                    }
                                };

                                match delete_result {
                                    Ok(()) => {
                                        app.status_message = Some("Deleted successfully".to_string());
                                        // Remove item from list
                                        app.items.retain(|item| item.id != id);
                                        app.update_filtered_items();
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Delete failed: {}", e));
                                    }
                                }
                            }
                            Action::PerformUpdate { id, item_type, name, description } => {
                                // Perform the update API call
                                let update_result = match item_type {
                                    ViewType::Contracts => {
                                        client.update_contract(id, Some(name.clone()), Some(description.clone())).await.map(|_| ())
                                    }
                                    ViewType::Tasks => {
                                        // For tasks, description is the plan
                                        client.update_task(id, Some(name.clone()), Some(description.clone())).await.map(|_| ())
                                    }
                                    ViewType::TaskOutput => {
                                        // Can't edit from output view
                                        Ok(())
                                    }
                                };

                                match update_result {
                                    Ok(()) => {
                                        app.status_message = Some("Updated successfully".to_string());
                                        // Update item in list
                                        for item in &mut app.items {
                                            if item.id == id {
                                                item.name = name.clone();
                                                item.description = Some(description.clone());
                                                break;
                                            }
                                        }
                                        app.update_filtered_items();
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Update failed: {}", e));
                                    }
                                }
                            }
                            Action::Refresh => {
                                // Reload data for current view
                                match app.view_type {
                                    ViewType::Contracts => {
                                        // Unsubscribe from task when going back to contracts
                                        if let Some(task_id) = subscribed_task_id.take() {
                                            ws_client.unsubscribe(task_id);
                                        }
                                        match load_contracts(client).await {
                                            Ok(items) => app.set_items(items),
                                            Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)),
                                        }
                                    }
                                    ViewType::Tasks => {
                                        // Unsubscribe from task when going back to tasks
                                        if let Some(task_id) = subscribed_task_id.take() {
                                            ws_client.unsubscribe(task_id);
                                        }
                                        if let Some(contract_id) = app.contract_id {
                                            match load_tasks(client, contract_id).await {
                                                Ok(items) => app.set_items(items),
                                                Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)),
                                            }
                                        }
                                    }
                                    ViewType::TaskOutput => {
                                        // Re-subscribe to the task output
                                        if let Some(task_id) = app.task_id {
                                            app.output_buffer.clear();
                                            app.ws_state = WsConnectionState::Connecting;
                                            ws_client.subscribe(task_id);
                                            subscribed_task_id = Some(task_id);
                                            app.status_message = Some("Reconnecting...".to_string());
                                        }
                                    }
                                }
                            }
                            Action::GoBack => {
                                // Unsubscribe when going back from output view
                                if let Some(task_id) = subscribed_task_id.take() {
                                    ws_client.unsubscribe(task_id);
                                    app.ws_state = WsConnectionState::Disconnected;
                                }
                            }
                            Action::PerformCreateContract { name, description, contract_type, repository_url } => {
                                // Create the contract via API
                                let req = CreateContractRequest {
                                    name: name.clone(),
                                    description: if description.is_empty() { None } else { Some(description) },
                                    contract_type: Some(contract_type),
                                    initial_phase: None,
                                    autonomous_loop: None,
                                    phase_guard: None,
                                    local_only: None,
                                    auto_merge_local: None,
                                };

                                match client.create_contract(req).await {
                                    Ok(result) => {
                                        let contract_name = result.0.get("name")
                                            .and_then(|v| v.as_str())
                                            .unwrap_or(&name)
                                            .to_string();
                                        let contract_id = result.0.get("id")
                                            .and_then(|v| v.as_str())
                                            .and_then(|s| uuid::Uuid::parse_str(s).ok());

                                        // Add repository if provided
                                        if let (Some(repo_url), Some(cid)) = (repository_url.as_ref(), contract_id) {
                                            if !repo_url.is_empty() {
                                                // Extract repo name from URL (e.g., "owner/repo" from GitHub URL)
                                                let repo_name = extract_repo_name(repo_url);
                                                match client.add_remote_repository(cid, &repo_name, repo_url, true).await {
                                                    Ok(_) => {
                                                        app.status_message = Some(format!(
                                                            "Created contract '{}' with repository",
                                                            contract_name
                                                        ));
                                                    }
                                                    Err(e) => {
                                                        app.status_message = Some(format!(
                                                            "Created contract but failed to add repository: {}",
                                                            e
                                                        ));
                                                    }
                                                }
                                            } else {
                                                app.status_message = Some(format!("Created contract: {}", contract_name));
                                            }
                                        } else {
                                            app.status_message = Some(format!("Created contract: {}", contract_name));
                                        }

                                        // Refresh the contracts list
                                        match load_contracts(client).await {
                                            Ok(items) => app.set_items(items),
                                            Err(e) => {
                                                let msg = app.status_message.take().unwrap_or_default();
                                                app.status_message = Some(format!("{} (refresh failed: {})", msg, e));
                                            }
                                        }
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Create failed: {}", e));
                                    }
                                }
                            }
                            Action::LoadRepoSuggestions => {
                                // Load repository suggestions for the create form
                                app.status_message = Some("Loading recent repositories...".to_string());
                                // Force a redraw to show the status
                                terminal.draw(|f| tui::ui::render(f, app)).ok();

                                // Fetch all repository types (remote and local)
                                match client.get_repository_suggestions(None, Some(10)).await {
                                    Ok(result) => {
                                        // Parse suggestions from API response
                                        let suggestions: Vec<RepositorySuggestion> = result.0
                                            .get("entries")
                                            .and_then(|v| v.as_array())
                                            .map(|arr| {
                                                arr.iter().filter_map(|entry| {
                                                    let name = entry.get("name")
                                                        .and_then(|v| v.as_str())
                                                        .unwrap_or("")
                                                        .to_string();
                                                    let repository_url = entry.get("repositoryUrl")
                                                        .or_else(|| entry.get("repository_url"))
                                                        .and_then(|v| v.as_str())
                                                        .map(|s| s.to_string());
                                                    let local_path = entry.get("localPath")
                                                        .or_else(|| entry.get("local_path"))
                                                        .and_then(|v| v.as_str())
                                                        .map(|s| s.to_string());
                                                    let source_type = entry.get("sourceType")
                                                        .or_else(|| entry.get("source_type"))
                                                        .and_then(|v| v.as_str())
                                                        .unwrap_or("remote")
                                                        .to_string();
                                                    let use_count = entry.get("useCount")
                                                        .or_else(|| entry.get("use_count"))
                                                        .and_then(|v| v.as_i64())
                                                        .unwrap_or(0) as i32;

                                                    // Only include if we have a URL or path
                                                    if repository_url.is_some() || local_path.is_some() {
                                                        Some(RepositorySuggestion {
                                                            name,
                                                            repository_url,
                                                            local_path,
                                                            source_type,
                                                            use_count,
                                                        })
                                                    } else {
                                                        None
                                                    }
                                                }).collect()
                                            })
                                            .unwrap_or_default();

                                        let count = suggestions.len();
                                        app.create_state.set_suggestions(suggestions);
                                        if count > 0 {
                                            app.status_message = Some(format!("Found {} recent repositories", count));
                                        } else {
                                            app.status_message = Some("No recent repositories found".to_string());
                                        }
                                    }
                                    Err(e) => {
                                        app.status_message = Some(format!("Could not load suggestions: {}", e));
                                        app.create_state.suggestions_loaded = true;
                                    }
                                }
                            }
                            _ => {}
                        }
                    }
                }
            }
        }

        if app.should_quit {
            break;
        }
    }

    Ok(None)
}

/// Extract a repository name from a URL.
/// E.g., "https://github.com/owner/repo.git" -> "owner/repo"
fn extract_repo_name(url: &str) -> String {
    // Remove .git suffix if present
    let url = url.trim_end_matches(".git");

    // Try to extract owner/repo from common Git hosting URLs
    if let Some(path) = url.strip_prefix("https://github.com/")
        .or_else(|| url.strip_prefix("https://gitlab.com/"))
        .or_else(|| url.strip_prefix("https://bitbucket.org/"))
        .or_else(|| url.strip_prefix("git@github.com:"))
        .or_else(|| url.strip_prefix("git@gitlab.com:"))
        .or_else(|| url.strip_prefix("git@bitbucket.org:"))
    {
        // Return owner/repo
        return path.to_string();
    }

    // Fallback: try to get the last path segment
    if let Some(last_segment) = url.rsplit('/').next() {
        if !last_segment.is_empty() {
            return last_segment.to_string();
        }
    }

    // Last resort: use the full URL as the name
    url.to_string()
}

/// Parse an output entry from the API response into an OutputLine
fn parse_output_entry(entry: &serde_json::Value) -> Option<OutputLine> {
    let message_type = entry.get("messageType")
        .and_then(|v| v.as_str())
        .unwrap_or("raw");
    let content = entry.get("content")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let tool_name = entry.get("toolName")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());
    let is_error = entry.get("isError")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let cost_usd = entry.get("costUsd")
        .and_then(|v| v.as_f64());
    let duration_ms = entry.get("durationMs")
        .and_then(|v| v.as_u64());

    Some(OutputLine {
        message_type: OutputMessageType::from_str(message_type),
        content,
        tool_name,
        is_error,
        cost_usd,
        duration_ms,
    })
}

/// Handle a WebSocket event and update app state
fn handle_ws_event(app: &mut App, event: WsEvent) {
    match event {
        WsEvent::Connected => {
            app.ws_state = WsConnectionState::Connected;
            app.status_message = Some("Connected".to_string());
        }
        WsEvent::Disconnected => {
            app.ws_state = WsConnectionState::Disconnected;
            app.status_message = Some("Disconnected".to_string());
        }
        WsEvent::Reconnecting { attempt } => {
            app.ws_state = WsConnectionState::Reconnecting;
            app.status_message = Some(format!("Reconnecting (attempt {})...", attempt));
        }
        WsEvent::Subscribed { task_id: _ } => {
            app.ws_state = WsConnectionState::Connected;
            app.status_message = Some("Subscribed to task output".to_string());
        }
        WsEvent::Unsubscribed { task_id: _ } => {
            // No status update needed
        }
        WsEvent::TaskOutput(output) => {
            // Convert WebSocket event to OutputLine
            let line = OutputLine {
                message_type: OutputMessageType::from_str(&output.message_type),
                content: output.content,
                tool_name: output.tool_name,
                is_error: output.is_error.unwrap_or(false),
                cost_usd: output.cost_usd,
                duration_ms: output.duration_ms,
            };
            app.output_buffer.add_line(line);
            // Clear status message once we're receiving output
            if app.status_message.as_ref().map(|s| s.contains("Subscribed")).unwrap_or(false) {
                app.status_message = None;
            }
        }
        WsEvent::Error { message } => {
            app.status_message = Some(format!("WS Error: {}", message));
        }
    }
}

fn init_logging(level: &str, format: &str) {
    let filter = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new(level))
        .unwrap_or_else(|_| EnvFilter::new("info"));

    let subscriber = tracing_subscriber::registry().with(filter);

    if format == "json" {
        subscriber.with(fmt::layer().json()).init();
    } else {
        subscriber.with(fmt::layer()).init();
    }
}

fn get_machine_id() -> String {
    // Try to read machine-id from standard locations
    #[cfg(target_os = "linux")]
    {
        if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
            return id.trim().to_string();
        }
        if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
            return id.trim().to_string();
        }
    }

    #[cfg(target_os = "macos")]
    {
        // Use IOPlatformSerialNumber
        if let Ok(output) = std::process::Command::new("ioreg")
            .args(["-rd1", "-c", "IOPlatformExpertDevice"])
            .output()
        {
            let stdout = String::from_utf8_lossy(&output.stdout);
            for line in stdout.lines() {
                if line.contains("IOPlatformUUID") {
                    if let Some(uuid) = line.split('"').nth(3) {
                        return uuid.to_string();
                    }
                }
            }
        }
    }

    // Fallback: generate a random ID and persist it
    let state_dir = dirs_next::data_local_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("."))
        .join("makima");
    let machine_id_file = state_dir.join("machine-id");

    if let Ok(id) = std::fs::read_to_string(&machine_id_file) {
        return id.trim().to_string();
    }

    // Generate new ID
    let new_id = uuid::Uuid::new_v4().to_string();
    std::fs::create_dir_all(&state_dir).ok();
    std::fs::write(&machine_id_file, &new_id).ok();
    new_id
}

fn get_hostname() -> String {
    hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string())
}

/// Auto-clone a repository to the home directory if it doesn't exist.
async fn auto_clone_repo(
    repo_entry: &RepoEntry,
    home_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let dir_name = repo_entry
        .dir_name()
        .ok_or("Could not determine directory name from URL")?;
    let target_dir = home_dir.join(&dir_name);

    // Check if already cloned
    if target_dir.exists() {
        eprintln!("      [skip] {} (already exists)", dir_name);
        return Ok(());
    }

    let url = repo_entry.expanded_url();
    eprintln!("      [clone] {} -> {}", url, target_dir.display());

    // Build git clone command
    let mut args = vec!["clone".to_string()];

    // Add shallow clone if requested
    if repo_entry.shallow() {
        args.push("--depth".to_string());
        args.push("1".to_string());
    }

    // Add branch if specified
    if let Some(branch) = repo_entry.branch() {
        args.push("--branch".to_string());
        args.push(branch.to_string());
    }

    args.push(url.clone());
    args.push(target_dir.to_string_lossy().to_string());

    // Run git clone
    let output = Command::new("git").args(&args).output().await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("git clone failed: {}", stderr).into());
    }

    eprintln!("      [done] {}", dir_name);
    Ok(())
}

/// dirs_next minimal replacement
mod dirs_next {
    use std::path::PathBuf;

    pub fn data_local_dir() -> Option<PathBuf> {
        #[cfg(target_os = "macos")]
        {
            std::env::var("HOME")
                .ok()
                .map(|h| PathBuf::from(h).join("Library").join("Application Support"))
        }
        #[cfg(target_os = "linux")]
        {
            std::env::var("XDG_DATA_HOME")
                .ok()
                .map(PathBuf::from)
                .or_else(|| {
                    std::env::var("HOME")
                        .ok()
                        .map(|h| PathBuf::from(h).join(".local").join("share"))
                })
        }
        #[cfg(target_os = "windows")]
        {
            std::env::var("LOCALAPPDATA").ok().map(PathBuf::from)
        }
        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
        {
            None
        }
    }
}