//! Boot screen displayed after all initialization completes. //! //! Shows a polished ANSI-styled status panel summarizing the agent's runtime //! state: model, database, tool count, enabled features, active channels, //! and the gateway URL. /// All displayable fields for the boot screen. pub struct BootInfo { pub version: String, pub agent_name: String, pub llm_backend: String, pub llm_model: String, pub cheap_model: Option, pub db_backend: String, pub db_connected: bool, pub tool_count: usize, pub gateway_url: Option, pub embeddings_enabled: bool, pub embeddings_provider: Option, pub heartbeat_enabled: bool, pub heartbeat_interval_secs: u64, pub sandbox_enabled: bool, pub docker_status: crate::sandbox::detect::DockerStatus, pub claude_code_enabled: bool, pub routines_enabled: bool, pub skills_enabled: bool, pub channels: Vec, /// Public URL from a managed tunnel (e.g., "https://abc.ngrok.io"). pub tunnel_url: Option, /// Provider name for the managed tunnel (e.g., "ngrok"). pub tunnel_provider: Option, } /// Print the boot screen to stdout. pub fn print_boot_screen(info: &BootInfo) { // ANSI codes matching existing REPL palette let bold = "\x1b[1m"; let cyan = "\x1b[36m"; let dim = "\x1b[90m"; let yellow = "\x1b[33m"; let yellow_underline = "\x1b[33;4m"; let reset = "\x1b[0m"; let border = format!(" {dim}{}{reset}", "\u{2576}".repeat(58)); println!(); println!("{border}"); println!(); println!(" {bold}{}{reset} v{}", info.agent_name, info.version); println!(); // Model line let model_display = if let Some(ref cheap) = info.cheap_model { format!( "{cyan}{}{reset} {dim}cheap{reset} {cyan}{}{reset}", info.llm_model, cheap ) } else { format!("{cyan}{}{reset}", info.llm_model) }; println!( " {dim}model{reset} {model_display} {dim}via {}{reset}", info.llm_backend ); // Database line let db_status = if info.db_connected { "connected" } else { "none" }; println!( " {dim}database{reset} {cyan}{}{reset} {dim}({db_status}){reset}", info.db_backend ); // Tools line println!( " {dim}tools{reset} {cyan}{}{reset} {dim}registered{reset}", info.tool_count ); // Features line let mut features = Vec::new(); if info.embeddings_enabled { if let Some(ref provider) = info.embeddings_provider { features.push(format!("embeddings ({provider})")); } else { features.push("embeddings".to_string()); } } if info.heartbeat_enabled { let mins = info.heartbeat_interval_secs / 60; features.push(format!("heartbeat ({mins}m)")); } match info.docker_status { crate::sandbox::detect::DockerStatus::Available => { features.push("sandbox".to_string()); } crate::sandbox::detect::DockerStatus::NotInstalled => { features.push(format!("{yellow}sandbox (docker not installed){reset}")); } crate::sandbox::detect::DockerStatus::NotRunning => { features.push(format!("{yellow}sandbox (docker not running){reset}")); } crate::sandbox::detect::DockerStatus::Disabled => { // Don't show sandbox when disabled } } if info.claude_code_enabled { features.push("claude-code".to_string()); } if info.routines_enabled { features.push("routines".to_string()); } if info.skills_enabled { features.push("skills".to_string()); } if !features.is_empty() { println!( " {dim}features{reset} {cyan}{}{reset}", features.join(" ") ); } // Channels line if !info.channels.is_empty() { println!( " {dim}channels{reset} {cyan}{}{reset}", info.channels.join(" ") ); } // Gateway URL (highlighted) if let Some(ref url) = info.gateway_url { println!(); println!(" {dim}gateway{reset} {yellow_underline}{url}{reset}"); } // Tunnel URL if let Some(ref url) = info.tunnel_url { let provider_tag = info .tunnel_provider .as_deref() .map(|p| format!(" {dim}({p}){reset}")) .unwrap_or_default(); println!(" {dim}tunnel{reset} {yellow_underline}{url}{reset}{provider_tag}"); } println!(); println!("{border}"); println!(); println!(" /help for commands, /quit to exit"); println!(); } #[cfg(test)] mod tests { use super::*; use crate::sandbox::detect::DockerStatus; #[test] fn test_print_boot_screen_full() { let info = BootInfo { version: "0.2.0".to_string(), agent_name: "ironclaw".to_string(), llm_backend: "nearai".to_string(), llm_model: "claude-3-5-sonnet-20241022".to_string(), cheap_model: Some("gpt-4o-mini".to_string()), db_backend: "libsql".to_string(), db_connected: true, tool_count: 24, gateway_url: Some("http://127.0.0.1:3001/?token=abc123".to_string()), embeddings_enabled: true, embeddings_provider: Some("openai".to_string()), heartbeat_enabled: true, heartbeat_interval_secs: 1800, sandbox_enabled: true, docker_status: DockerStatus::Available, claude_code_enabled: false, routines_enabled: true, skills_enabled: true, channels: vec![ "repl".to_string(), "gateway".to_string(), "telegram".to_string(), ], tunnel_url: Some("https://abc123.ngrok.io".to_string()), tunnel_provider: Some("ngrok".to_string()), }; // Should not panic print_boot_screen(&info); } #[test] fn test_print_boot_screen_minimal() { let info = BootInfo { version: "0.2.0".to_string(), agent_name: "ironclaw".to_string(), llm_backend: "nearai".to_string(), llm_model: "gpt-4o".to_string(), cheap_model: None, db_backend: "none".to_string(), db_connected: false, tool_count: 5, gateway_url: None, embeddings_enabled: false, embeddings_provider: None, heartbeat_enabled: false, heartbeat_interval_secs: 0, sandbox_enabled: false, docker_status: DockerStatus::Disabled, claude_code_enabled: false, routines_enabled: false, skills_enabled: false, channels: vec![], tunnel_url: None, tunnel_provider: None, }; // Should not panic print_boot_screen(&info); } #[test] fn test_print_boot_screen_no_features() { let info = BootInfo { version: "0.1.0".to_string(), agent_name: "test".to_string(), llm_backend: "openai".to_string(), llm_model: "gpt-4o".to_string(), cheap_model: None, db_backend: "postgres".to_string(), db_connected: true, tool_count: 10, gateway_url: None, embeddings_enabled: false, embeddings_provider: None, heartbeat_enabled: false, heartbeat_interval_secs: 0, sandbox_enabled: false, docker_status: DockerStatus::Disabled, claude_code_enabled: false, routines_enabled: false, skills_enabled: false, channels: vec!["repl".to_string()], tunnel_url: None, tunnel_provider: None, }; // Should not panic print_boot_screen(&info); } }