//! OWLEN CLI - Chat TUI client use anyhow::Result; use clap::{Arg, Command}; use owlen_core::{session::SessionController, storage::StorageManager}; use owlen_ollama::OllamaProvider; use owlen_tui::{config, ui, AppState, ChatApp, Event, EventHandler, SessionEvent}; use std::io; use std::sync::Arc; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use crossterm::{ event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{backend::CrosstermBackend, Terminal}; #[tokio::main] async fn main() -> Result<()> { let matches = Command::new("owlen") .about("OWLEN - A chat-focused TUI client for Ollama") .version(env!("CARGO_PKG_VERSION")) .arg( Arg::new("model") .short('m') .long("model") .value_name("MODEL") .help("Preferred model to use for this session"), ) .get_matches(); let mut config = config::try_load_config().unwrap_or_default(); if let Some(model) = matches.get_one::("model") { config.general.default_model = Some(model.clone()); } // Prepare provider from configuration let provider_name = config.general.default_provider.clone(); let provider_cfg = config::ensure_provider_config(&mut config, &provider_name).clone(); let provider_type = provider_cfg.provider_type.to_ascii_lowercase(); if provider_type != "ollama" && provider_type != "ollama-cloud" { anyhow::bail!( "Unsupported provider type '{}' configured for provider '{}'", provider_cfg.provider_type, provider_name ); } let provider = Arc::new(OllamaProvider::from_config( &provider_cfg, Some(&config.general), )?); let storage = Arc::new(StorageManager::new().await?); // Chat client - code execution tools disabled (only available in code client) let controller = SessionController::new(provider, config.clone(), storage.clone(), false)?; let (mut app, mut session_rx) = ChatApp::new(controller).await?; app.initialize_models().await?; // Event infrastructure let cancellation_token = CancellationToken::new(); let (event_tx, event_rx) = mpsc::unbounded_channel(); let event_handler = EventHandler::new(event_tx, cancellation_token.clone()); let event_handle = tokio::spawn(async move { event_handler.run().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 app, event_rx, &mut session_rx).await; // Shutdown cancellation_token.cancel(); event_handle.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>, app: &mut ChatApp, mut event_rx: mpsc::UnboundedReceiver, session_rx: &mut mpsc::UnboundedReceiver, ) -> Result<()> { loop { // Advance loading animation frame app.advance_loading_animation(); terminal.draw(|f| ui::render_chat(f, app))?; // Process any pending LLM requests AFTER UI has been drawn if let Err(e) = app.process_pending_llm_request().await { eprintln!("Error processing LLM request: {}", e); } // Process any pending tool executions AFTER UI has been drawn if let Err(e) = app.process_pending_tool_execution().await { eprintln!("Error processing tool execution: {}", e); } tokio::select! { Some(event) = event_rx.recv() => { if let AppState::Quit = app.handle_event(event).await? { return Ok(()); } } Some(session_event) = session_rx.recv() => { app.handle_session_event(session_event)?; } // Add a timeout to keep the animation going even when there are no events _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { // This will cause the loop to continue and advance the animation } } } }