diff --git a/crates/owlen-cli/Cargo.toml b/crates/owlen-cli/Cargo.toml index b18c813..adf59ae 100644 --- a/crates/owlen-cli/Cargo.toml +++ b/crates/owlen-cli/Cargo.toml @@ -17,6 +17,11 @@ name = "owlen" path = "src/main.rs" required-features = ["chat-client"] +[[bin]] +name = "owlen-code" +path = "src/code_main.rs" +required-features = ["chat-client"] + [[bin]] name = "owlen-agent" path = "src/agent_main.rs" diff --git a/crates/owlen-cli/src/bootstrap.rs b/crates/owlen-cli/src/bootstrap.rs new file mode 100644 index 0000000..6cef493 --- /dev/null +++ b/crates/owlen-cli/src/bootstrap.rs @@ -0,0 +1,318 @@ +use std::borrow::Cow; +use std::io; +use std::sync::Arc; + +use anyhow::{Result, anyhow}; +use async_trait::async_trait; +use crossterm::{ + event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use futures::stream; +use owlen_core::{ + ChatStream, Error, Provider, + config::{Config, McpMode}, + mcp::remote_client::RemoteMcpClient, + mode::Mode, + provider::ProviderManager, + providers::OllamaProvider, + session::SessionController, + storage::StorageManager, + types::{ChatRequest, ChatResponse, Message, ModelInfo}, +}; +use owlen_tui::{ + ChatApp, SessionEvent, + app::App as RuntimeApp, + config, + tui_controller::{TuiController, TuiRequest}, + ui, +}; +use ratatui::{Terminal, prelude::CrosstermBackend}; +use tokio::sync::mpsc; + +use crate::commands::cloud::{load_runtime_credentials, set_env_var}; + +pub async fn launch(initial_mode: Mode) -> Result<()> { + set_env_var("OWLEN_AUTO_CONSENT", "1"); + + let color_support = detect_terminal_color_support(); + let mut cfg = config::try_load_config().unwrap_or_default(); + let _ = cfg.refresh_mcp_servers(None); + + if let Some(previous_theme) = apply_terminal_theme(&mut cfg, &color_support) { + let term_label = match &color_support { + TerminalColorSupport::Limited { term } => Cow::from(term.as_str()), + TerminalColorSupport::Full => Cow::from("current terminal"), + }; + eprintln!( + "Terminal '{}' lacks full 256-color support. Using '{}' theme instead of '{}'.", + term_label, BASIC_THEME_NAME, previous_theme + ); + } else if let TerminalColorSupport::Limited { term } = &color_support { + eprintln!( + "Warning: terminal '{}' may not fully support 256-color themes.", + term + ); + } + + cfg.validate()?; + let storage = Arc::new(StorageManager::new().await?); + load_runtime_credentials(&mut cfg, storage.clone()).await?; + + let (tui_tx, _tui_rx) = mpsc::unbounded_channel::(); + let tui_controller = Arc::new(TuiController::new(tui_tx)); + + let provider = build_provider(&cfg)?; + let mut offline_notice: Option = None; + let provider = match provider.health_check().await { + Ok(_) => provider, + Err(err) => { + let hint = if matches!(cfg.mcp.mode, McpMode::RemotePreferred | McpMode::RemoteOnly) + && !cfg.effective_mcp_servers().is_empty() + { + "Ensure the configured MCP server is running and reachable." + } else { + "Ensure Ollama is running (`ollama serve`) and reachable at the configured base_url." + }; + let notice = + format!("Provider health check failed: {err}. {hint} Continuing in offline mode."); + eprintln!("{notice}"); + offline_notice = Some(notice.clone()); + let fallback_model = cfg + .general + .default_model + .clone() + .unwrap_or_else(|| "offline".to_string()); + Arc::new(OfflineProvider::new(notice, fallback_model)) as Arc + } + }; + + let controller = + SessionController::new(provider, cfg, storage.clone(), tui_controller, false).await?; + let provider_manager = Arc::new(ProviderManager::default()); + let mut runtime = RuntimeApp::new(provider_manager); + let (mut app, mut session_rx) = ChatApp::new(controller).await?; + app.initialize_models().await?; + if let Some(notice) = offline_notice.clone() { + app.set_status_message(¬ice); + app.set_system_status(notice); + } + + app.set_mode(initial_mode).await; + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + EnableBracketedPaste + )?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let result = run_app(&mut terminal, &mut runtime, &mut app, &mut session_rx).await; + + config::save_config(&app.config())?; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture, + DisableBracketedPaste + )?; + terminal.show_cursor()?; + + if let Err(err) = result { + println!("{err:?}"); + } + + Ok(()) +} + +fn build_provider(cfg: &Config) -> Result> { + match cfg.mcp.mode { + McpMode::RemotePreferred => { + let remote_result = if let Some(mcp_server) = cfg.effective_mcp_servers().first() { + RemoteMcpClient::new_with_config(mcp_server) + } else { + RemoteMcpClient::new() + }; + + match remote_result { + Ok(client) => Ok(Arc::new(client) as Arc), + Err(err) if cfg.mcp.allow_fallback => { + log::warn!( + "Remote MCP client unavailable ({}); falling back to local provider.", + err + ); + build_local_provider(cfg) + } + Err(err) => Err(anyhow!(err)), + } + } + McpMode::RemoteOnly => { + let mcp_server = cfg.effective_mcp_servers().first().ok_or_else(|| { + anyhow!("[[mcp_servers]] must be configured when [mcp].mode = \"remote_only\"") + })?; + let client = RemoteMcpClient::new_with_config(mcp_server)?; + Ok(Arc::new(client) as Arc) + } + McpMode::LocalOnly | McpMode::Legacy => build_local_provider(cfg), + McpMode::Disabled => Err(anyhow!( + "MCP mode 'disabled' is not supported by the owlen TUI" + )), + } +} + +fn build_local_provider(cfg: &Config) -> Result> { + let provider_name = cfg.general.default_provider.clone(); + let provider_cfg = cfg.provider(&provider_name).ok_or_else(|| { + anyhow!(format!( + "No provider configuration found for '{provider_name}' in [providers]" + )) + })?; + + match provider_cfg.provider_type.as_str() { + "ollama" | "ollama_cloud" => { + let provider = OllamaProvider::from_config(provider_cfg, Some(&cfg.general))?; + Ok(Arc::new(provider) as Arc) + } + other => Err(anyhow!(format!( + "Provider type '{other}' is not supported in legacy/local MCP mode" + ))), + } +} + +const BASIC_THEME_NAME: &str = "ansi_basic"; + +#[derive(Debug, Clone)] +enum TerminalColorSupport { + Full, + Limited { term: String }, +} + +fn detect_terminal_color_support() -> TerminalColorSupport { + let term = std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()); + let colorterm = std::env::var("COLORTERM").unwrap_or_default(); + let term_lower = term.to_lowercase(); + let color_lower = colorterm.to_lowercase(); + + let supports_extended = term_lower.contains("256color") + || color_lower.contains("truecolor") + || color_lower.contains("24bit") + || color_lower.contains("fullcolor"); + + if supports_extended { + TerminalColorSupport::Full + } else { + TerminalColorSupport::Limited { term } + } +} + +fn apply_terminal_theme(cfg: &mut Config, support: &TerminalColorSupport) -> Option { + match support { + TerminalColorSupport::Full => None, + TerminalColorSupport::Limited { .. } => { + if cfg.ui.theme != BASIC_THEME_NAME { + let previous = std::mem::replace(&mut cfg.ui.theme, BASIC_THEME_NAME.to_string()); + Some(previous) + } else { + None + } + } + } +} + +struct OfflineProvider { + reason: String, + placeholder_model: String, +} + +impl OfflineProvider { + fn new(reason: String, placeholder_model: String) -> Self { + Self { + reason, + placeholder_model, + } + } + + fn friendly_response(&self, requested_model: &str) -> ChatResponse { + let mut message = String::new(); + message.push_str("⚠️ Owlen is running in offline mode.\n\n"); + message.push_str(&self.reason); + if !requested_model.is_empty() && requested_model != self.placeholder_model { + message.push_str(&format!( + "\n\nYou requested model '{}', but no providers are reachable.", + requested_model + )); + } + message.push_str( + "\n\nStart your preferred provider (e.g. `ollama serve`) or switch providers with `:provider` once connectivity is restored.", + ); + + ChatResponse { + message: Message::assistant(message), + usage: None, + is_streaming: false, + is_final: true, + } + } +} + +#[async_trait] +impl Provider for OfflineProvider { + fn name(&self) -> &str { + "offline" + } + + async fn list_models(&self) -> Result, Error> { + Ok(vec![ModelInfo { + id: self.placeholder_model.clone(), + provider: "offline".to_string(), + name: format!("Offline (fallback: {})", self.placeholder_model), + description: Some("Placeholder model used while no providers are reachable".into()), + context_window: None, + capabilities: vec![], + supports_tools: false, + }]) + } + + async fn send_prompt(&self, request: ChatRequest) -> Result { + Ok(self.friendly_response(&request.model)) + } + + async fn stream_prompt(&self, request: ChatRequest) -> Result { + let response = self.friendly_response(&request.model); + Ok(Box::pin(stream::iter(vec![Ok(response)]))) + } + + async fn health_check(&self) -> Result<(), Error> { + Err(Error::Provider(anyhow!( + "offline provider cannot reach any backing models" + ))) + } + + fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { + self + } +} + +async fn run_app( + terminal: &mut Terminal>, + runtime: &mut RuntimeApp, + app: &mut ChatApp, + session_rx: &mut mpsc::UnboundedReceiver, +) -> Result<()> { + let mut render = |terminal: &mut Terminal>, + state: &mut ChatApp| + -> Result<()> { + terminal.draw(|f| ui::render_chat(f, state))?; + Ok(()) + }; + + runtime.run(terminal, app, session_rx, &mut render).await?; + Ok(()) +} diff --git a/crates/owlen-cli/src/code_main.rs b/crates/owlen-cli/src/code_main.rs new file mode 100644 index 0000000..184e4e2 --- /dev/null +++ b/crates/owlen-cli/src/code_main.rs @@ -0,0 +1,16 @@ +//! Owlen CLI entrypoint optimised for code-first workflows. +#![allow(dead_code, unused_imports)] + +mod bootstrap; +mod commands; +mod mcp; + +use anyhow::Result; +use owlen_core::config as core_config; +use owlen_core::mode::Mode; +use owlen_tui::config; + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<()> { + bootstrap::launch(Mode::Code).await +} diff --git a/crates/owlen-cli/src/commands/providers.rs b/crates/owlen-cli/src/commands/providers.rs index 5fda892..a1746b0 100644 --- a/crates/owlen-cli/src/commands/providers.rs +++ b/crates/owlen-cli/src/commands/providers.rs @@ -195,13 +195,13 @@ async fn list_models(filter: Option<&str>) -> Result<()> { } fn verify_provider_filter(config: &Config, filter: Option<&str>) -> Result<()> { - if let Some(filter) = filter { - if !config.providers.contains_key(filter) { - return Err(anyhow!( - "Provider '{}' is not defined in configuration.", - filter - )); - } + if let Some(filter) = filter + && !config.providers.contains_key(filter) + { + return Err(anyhow!( + "Provider '{}' is not defined in configuration.", + filter + )); } Ok(()) } @@ -254,10 +254,10 @@ fn toggle_provider(provider: &str, enable: bool) -> Result<()> { entry.enabled = previous_enabled; } config.general.default_provider = previous_default; - if let Some(enabled) = previous_fallback_enabled { - if let Some(entry) = config.providers.get_mut("ollama_local") { - entry.enabled = enabled; - } + if let Some(enabled) = previous_fallback_enabled + && let Some(entry) = config.providers.get_mut("ollama_local") + { + entry.enabled = enabled; } return Err(anyhow!(err)); } @@ -273,12 +273,11 @@ fn toggle_provider(provider: &str, enable: bool) -> Result<()> { } fn choose_fallback_provider(config: &Config, exclude: &str) -> Option { - if exclude != "ollama_local" { - if let Some(cfg) = config.providers.get("ollama_local") { - if cfg.enabled { - return Some("ollama_local".to_string()); - } - } + if exclude != "ollama_local" + && let Some(cfg) = config.providers.get("ollama_local") + && cfg.enabled + { + return Some("ollama_local".to_string()); } let mut candidates: Vec = config @@ -300,10 +299,10 @@ async fn register_enabled_providers( let mut records = Vec::new(); for (id, cfg) in &config.providers { - if let Some(filter) = filter { - if id != filter { - continue; - } + if let Some(filter) = filter + && id != filter + { + continue; } let mut record = ProviderRecord::from_config(id, cfg, id == &default_provider); @@ -537,10 +536,10 @@ fn print_models( } else { for entry in entries { let mut line = format!(" - {}", entry.model.name); - if let Some(description) = &entry.model.description { - if !description.trim().is_empty() { - line.push_str(&format!(" — {}", description.trim())); - } + if let Some(description) = &entry.model.description + && !description.trim().is_empty() + { + line.push_str(&format!(" — {}", description.trim())); } println!("{}", line); } @@ -549,10 +548,10 @@ fn print_models( println!(" (no models reported)"); } - if let Some(ProviderStatus::RequiresSetup) = status_value { - if record.requires_auth { - println!(" configure provider credentials or API key"); - } + if let Some(ProviderStatus::RequiresSetup) = status_value + && record.requires_auth + { + println!(" configure provider credentials or API key"); } println!(); } diff --git a/crates/owlen-cli/src/main.rs b/crates/owlen-cli/src/main.rs index 0391985..0d9f4e6 100644 --- a/crates/owlen-cli/src/main.rs +++ b/crates/owlen-cli/src/main.rs @@ -2,44 +2,21 @@ //! OWLEN CLI - Chat TUI client +mod bootstrap; mod commands; mod mcp; -use anyhow::{Result, anyhow}; -use async_trait::async_trait; +use anyhow::Result; use clap::{Parser, Subcommand}; use commands::{ - cloud::{CloudCommand, load_runtime_credentials, run_cloud_command, set_env_var}, + cloud::{CloudCommand, run_cloud_command}, providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command}, }; use mcp::{McpCommand, run_mcp_command}; use owlen_core::config as core_config; -use owlen_core::{ - ChatStream, Error, Provider, - config::{Config, McpMode}, - mcp::remote_client::RemoteMcpClient, - mode::Mode, - provider::ProviderManager, - providers::OllamaProvider, - session::SessionController, - storage::StorageManager, - types::{ChatRequest, ChatResponse, Message, ModelInfo}, -}; -use owlen_tui::tui_controller::{TuiController, TuiRequest}; -use owlen_tui::{ChatApp, SessionEvent, app::App as RuntimeApp, config, ui}; -use std::any::Any; -use std::borrow::Cow; -use std::io; -use std::sync::Arc; -use tokio::sync::mpsc; - -use crossterm::{ - event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture}, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, -}; -use futures::stream; -use ratatui::{Terminal, prelude::CrosstermBackend}; +use owlen_core::config::McpMode; +use owlen_core::mode::Mode; +use owlen_tui::config; /// Owlen - Terminal UI for LLM chat #[derive(Parser, Debug)] @@ -81,66 +58,6 @@ enum ConfigCommand { Path, } -fn build_provider(cfg: &Config) -> anyhow::Result> { - match cfg.mcp.mode { - McpMode::RemotePreferred => { - let remote_result = if let Some(mcp_server) = cfg.effective_mcp_servers().first() { - RemoteMcpClient::new_with_config(mcp_server) - } else { - RemoteMcpClient::new() - }; - - match remote_result { - Ok(client) => { - let provider: Arc = Arc::new(client); - Ok(provider) - } - Err(err) if cfg.mcp.allow_fallback => { - log::warn!( - "Remote MCP client unavailable ({}); falling back to local provider.", - err - ); - build_local_provider(cfg) - } - Err(err) => Err(anyhow::Error::from(err)), - } - } - McpMode::RemoteOnly => { - let mcp_server = cfg.effective_mcp_servers().first().ok_or_else(|| { - anyhow::anyhow!( - "[[mcp_servers]] must be configured when [mcp].mode = \"remote_only\"" - ) - })?; - let client = RemoteMcpClient::new_with_config(mcp_server)?; - let provider: Arc = Arc::new(client); - Ok(provider) - } - McpMode::LocalOnly | McpMode::Legacy => build_local_provider(cfg), - McpMode::Disabled => Err(anyhow::anyhow!( - "MCP mode 'disabled' is not supported by the owlen TUI" - )), - } -} - -fn build_local_provider(cfg: &Config) -> anyhow::Result> { - let provider_name = cfg.general.default_provider.clone(); - let provider_cfg = cfg.provider(&provider_name).ok_or_else(|| { - anyhow::anyhow!(format!( - "No provider configuration found for '{provider_name}' in [providers]" - )) - })?; - - match provider_cfg.provider_type.as_str() { - "ollama" | "ollama_cloud" => { - let provider = OllamaProvider::from_config(provider_cfg, Some(&cfg.general))?; - Ok(Arc::new(provider) as Arc) - } - other => Err(anyhow::anyhow!(format!( - "Provider type '{other}' is not supported in legacy/local MCP mode" - ))), - } -} - async fn run_command(command: OwlenCommand) -> Result<()> { match command { OwlenCommand::Config(config_cmd) => run_config_command(config_cmd), @@ -299,120 +216,6 @@ fn run_config_doctor() -> Result<()> { Ok(()) } -const BASIC_THEME_NAME: &str = "ansi_basic"; - -#[derive(Debug, Clone)] -enum TerminalColorSupport { - Full, - Limited { term: String }, -} - -fn detect_terminal_color_support() -> TerminalColorSupport { - let term = std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()); - let colorterm = std::env::var("COLORTERM").unwrap_or_default(); - let term_lower = term.to_lowercase(); - let color_lower = colorterm.to_lowercase(); - - let supports_extended = term_lower.contains("256color") - || color_lower.contains("truecolor") - || color_lower.contains("24bit") - || color_lower.contains("fullcolor"); - - if supports_extended { - TerminalColorSupport::Full - } else { - TerminalColorSupport::Limited { term } - } -} - -fn apply_terminal_theme(cfg: &mut Config, support: &TerminalColorSupport) -> Option { - match support { - TerminalColorSupport::Full => None, - TerminalColorSupport::Limited { .. } => { - if cfg.ui.theme != BASIC_THEME_NAME { - let previous = std::mem::replace(&mut cfg.ui.theme, BASIC_THEME_NAME.to_string()); - Some(previous) - } else { - None - } - } - } -} - -struct OfflineProvider { - reason: String, - placeholder_model: String, -} - -impl OfflineProvider { - fn new(reason: String, placeholder_model: String) -> Self { - Self { - reason, - placeholder_model, - } - } - - fn friendly_response(&self, requested_model: &str) -> ChatResponse { - let mut message = String::new(); - message.push_str("⚠️ Owlen is running in offline mode.\n\n"); - message.push_str(&self.reason); - if !requested_model.is_empty() && requested_model != self.placeholder_model { - message.push_str(&format!( - "\n\nYou requested model '{}', but no providers are reachable.", - requested_model - )); - } - message.push_str( - "\n\nStart your preferred provider (e.g. `ollama serve`) or switch providers with `:provider` once connectivity is restored.", - ); - - ChatResponse { - message: Message::assistant(message), - usage: None, - is_streaming: false, - is_final: true, - } - } -} - -#[async_trait] -impl Provider for OfflineProvider { - fn name(&self) -> &str { - "offline" - } - - async fn list_models(&self) -> Result, Error> { - Ok(vec![ModelInfo { - id: self.placeholder_model.clone(), - provider: "offline".to_string(), - name: format!("Offline (fallback: {})", self.placeholder_model), - description: Some("Placeholder model used while no providers are reachable".into()), - context_window: None, - capabilities: vec![], - supports_tools: false, - }]) - } - - async fn send_prompt(&self, request: ChatRequest) -> Result { - Ok(self.friendly_response(&request.model)) - } - - async fn stream_prompt(&self, request: ChatRequest) -> Result { - let response = self.friendly_response(&request.model); - Ok(Box::pin(stream::iter(vec![Ok(response)]))) - } - - async fn health_check(&self) -> Result<(), Error> { - Err(Error::Provider(anyhow!( - "offline provider cannot reach any backing models" - ))) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} - #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { // Parse command-line arguments @@ -421,122 +224,5 @@ async fn main() -> Result<()> { return run_command(command).await; } let initial_mode = if code { Mode::Code } else { Mode::Chat }; - - // Set auto-consent for TUI mode to prevent blocking stdin reads - set_env_var("OWLEN_AUTO_CONSENT", "1"); - - let color_support = detect_terminal_color_support(); - // Load configuration (or fall back to defaults) for the session controller. - let mut cfg = config::try_load_config().unwrap_or_default(); - let _ = cfg.refresh_mcp_servers(None); - if let Some(previous_theme) = apply_terminal_theme(&mut cfg, &color_support) { - let term_label = match &color_support { - TerminalColorSupport::Limited { term } => Cow::from(term.as_str()), - TerminalColorSupport::Full => Cow::from("current terminal"), - }; - eprintln!( - "Terminal '{}' lacks full 256-color support. Using '{}' theme instead of '{}'.", - term_label, BASIC_THEME_NAME, previous_theme - ); - } else if let TerminalColorSupport::Limited { term } = &color_support { - eprintln!( - "Warning: terminal '{}' may not fully support 256-color themes.", - term - ); - } - cfg.validate()?; - let storage = Arc::new(StorageManager::new().await?); - load_runtime_credentials(&mut cfg, storage.clone()).await?; - - let (tui_tx, _tui_rx) = mpsc::unbounded_channel::(); - let tui_controller = Arc::new(TuiController::new(tui_tx)); - - // Create provider according to MCP configuration (supports legacy/local fallback) - let provider = build_provider(&cfg)?; - let mut offline_notice: Option = None; - let provider = match provider.health_check().await { - Ok(_) => provider, - Err(err) => { - let hint = if matches!(cfg.mcp.mode, McpMode::RemotePreferred | McpMode::RemoteOnly) - && !cfg.effective_mcp_servers().is_empty() - { - "Ensure the configured MCP server is running and reachable." - } else { - "Ensure Ollama is running (`ollama serve`) and reachable at the configured base_url." - }; - let notice = - format!("Provider health check failed: {err}. {hint} Continuing in offline mode."); - eprintln!("{notice}"); - offline_notice = Some(notice.clone()); - let fallback_model = cfg - .general - .default_model - .clone() - .unwrap_or_else(|| "offline".to_string()); - Arc::new(OfflineProvider::new(notice, fallback_model)) as Arc - } - }; - - let controller = - SessionController::new(provider, cfg, storage.clone(), tui_controller, false).await?; - let provider_manager = Arc::new(ProviderManager::default()); - let mut runtime = RuntimeApp::new(provider_manager); - let (mut app, mut session_rx) = ChatApp::new(controller).await?; - app.initialize_models().await?; - if let Some(notice) = offline_notice { - app.set_status_message(¬ice); - app.set_system_status(notice); - } - - // Set the initial mode - app.set_mode(initial_mode).await; - - // Terminal setup - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!( - stdout, - EnterAlternateScreen, - EnableMouseCapture, - EnableBracketedPaste - )?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let result = run_app(&mut terminal, &mut runtime, &mut app, &mut session_rx).await; - - // Persist configuration updates (e.g., selected model) - config::save_config(&app.config())?; - - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture, - DisableBracketedPaste - )?; - terminal.show_cursor()?; - - if let Err(err) = result { - println!("{err:?}"); - } - - Ok(()) -} - -async fn run_app( - terminal: &mut Terminal>, - runtime: &mut RuntimeApp, - app: &mut ChatApp, - session_rx: &mut mpsc::UnboundedReceiver, -) -> Result<()> { - let mut render = |terminal: &mut Terminal>, - state: &mut ChatApp| - -> Result<()> { - terminal.draw(|f| ui::render_chat(f, state))?; - Ok(()) - }; - - runtime.run(terminal, app, session_rx, &mut render).await?; - Ok(()) + bootstrap::launch(initial_mode).await }