//! Boot screen displayed after all initialization completes. //! //! Shows a compact ANSI-styled status panel with three tiers: //! - **Tier 1 (always):** Name + version, model + backend. //! - **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels. //! - **Tier 3 (removed):** Database, tool count, features → use `ironclaw status`. use crate::cli::fmt; /// 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, /// Time elapsed during startup. Shown at the bottom when present. pub startup_elapsed: Option, } const KW: usize = 10; /// Print the boot screen to stdout. /// /// **Tier 1 (always):** Name + version, model + backend. /// **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels. /// **Tier 3 (removed):** Database, tool count, features — use `ironclaw status`. pub fn print_boot_screen(info: &BootInfo) { let border = format!(" {}", fmt::separator(58)); println!(); println!("{border}"); println!(); // ── Tier 1: always shown ────────────────────────────────────────── println!( " {}{}{} v{}", fmt::bold(), info.agent_name, fmt::reset(), info.version ); println!(); // Model line let model_display = if let Some(ref cheap) = info.cheap_model { format!( "{}{}{} {}cheap{} {}{}{}", fmt::accent(), info.llm_model, fmt::reset(), fmt::dim(), fmt::reset(), fmt::accent(), cheap, fmt::reset(), ) } else { format!("{}{}{}", fmt::accent(), info.llm_model, fmt::reset()) }; println!( " {}{: = info .channels .iter() .filter(|c| !matches!(c.as_str(), "repl" | "gateway")) .map(|c| c.as_str()) .collect(); if !non_default.is_empty() { println!( " {}{: = Vec::new(); // Database if info.db_connected { tags.push(format!("db:{}", info.db_backend)); } // Tool count if info.tool_count > 0 { tags.push(format!("tools:{}", info.tool_count)); } // Routines if info.routines_enabled { tags.push("routines".to_string()); } // Heartbeat with interval if info.heartbeat_enabled { let interval = if info.heartbeat_interval_secs >= 3600 && info.heartbeat_interval_secs.is_multiple_of(3600) { format!("{}h", info.heartbeat_interval_secs / 3600) } else if info.heartbeat_interval_secs >= 60 && info.heartbeat_interval_secs.is_multiple_of(60) { format!("{}m", info.heartbeat_interval_secs / 60) } else { format!("{}s", info.heartbeat_interval_secs) }; tags.push(format!("heartbeat:{interval}")); } // Skills if info.skills_enabled { tags.push("skills".to_string()); } // Sandbox / Docker if info.sandbox_enabled { let suffix = match info.docker_status { crate::sandbox::detect::DockerStatus::Available => "", crate::sandbox::detect::DockerStatus::NotRunning => ":stopped", _ => ":unavail", }; tags.push(format!("sandbox{suffix}")); } // Embeddings if info.embeddings_enabled { if let Some(ref provider) = info.embeddings_provider { tags.push(format!("embeddings:{provider}")); } else { tags.push("embeddings".to_string()); } } // Claude Code bridge if info.claude_code_enabled { tags.push("claude-code".to_string()); } if !tags.is_empty() { println!( " {}{: