diff --git a/crates/owlen-tui/Cargo.toml b/crates/owlen-tui/Cargo.toml index a3dd14e..2293bf6 100644 --- a/crates/owlen-tui/Cargo.toml +++ b/crates/owlen-tui/Cargo.toml @@ -42,6 +42,7 @@ uuid = { workspace = true } serde_json.workspace = true serde.workspace = true chrono = { workspace = true } +log = { workspace = true } [dev-dependencies] tokio-test = { workspace = true } diff --git a/crates/owlen-tui/src/app/handler.rs b/crates/owlen-tui/src/app/handler.rs new file mode 100644 index 0000000..6d80d0f --- /dev/null +++ b/crates/owlen-tui/src/app/handler.rs @@ -0,0 +1,135 @@ +use super::{App, messages::AppMessage}; +use log::warn; +use owlen_core::{ + provider::{GenerateChunk, GenerateRequest, ProviderStatus}, + state::AppState, +}; +use uuid::Uuid; + +/// Trait implemented by UI state containers to react to [`AppMessage`] events. +pub trait MessageState { + /// Called when a generation request is about to start. + #[allow(unused_variables)] + fn start_generation( + &mut self, + request_id: Uuid, + provider_id: &str, + request: &GenerateRequest, + ) -> AppState { + AppState::Running + } + + /// Called for every streamed generation chunk. + #[allow(unused_variables)] + fn append_chunk(&mut self, request_id: Uuid, chunk: &GenerateChunk) -> AppState { + AppState::Running + } + + /// Called when a generation finishes successfully. + #[allow(unused_variables)] + fn generation_complete(&mut self, request_id: Uuid) -> AppState { + AppState::Running + } + + /// Called when a generation fails. + #[allow(unused_variables)] + fn generation_failed(&mut self, request_id: Option, message: &str) -> AppState { + AppState::Running + } + + /// Called when refreshed model metadata is available. + fn update_model_list(&mut self) -> AppState { + AppState::Running + } + + /// Called when a models refresh has been requested. + fn refresh_model_list(&mut self) -> AppState { + AppState::Running + } + + /// Called when provider status updates arrive. + #[allow(unused_variables)] + fn update_provider_status(&mut self, provider_id: &str, status: ProviderStatus) -> AppState { + AppState::Running + } + + /// Called when a resize event occurs. + #[allow(unused_variables)] + fn handle_resize(&mut self, width: u16, height: u16) -> AppState { + AppState::Running + } + + /// Called on periodic ticks. + fn handle_tick(&mut self) -> AppState { + AppState::Running + } +} + +impl App { + /// Dispatch a message to the provided [`MessageState`]. Returns `true` when the + /// state indicates the UI should exit. + pub fn handle_message(&mut self, state: &mut State, message: AppMessage) -> bool + where + State: MessageState, + { + use AppMessage::*; + + let outcome = match message { + KeyPress(_) => AppState::Running, + Resize { width, height } => state.handle_resize(width, height), + Tick => state.handle_tick(), + GenerateStart { + request_id, + provider_id, + request, + } => state.start_generation(request_id, &provider_id, &request), + GenerateChunk { request_id, chunk } => state.append_chunk(request_id, &chunk), + GenerateComplete { request_id } => { + self.clear_active_generation(request_id); + state.generation_complete(request_id) + } + GenerateError { + request_id, + message, + } => { + self.clear_active_generation_optional(request_id); + state.generation_failed(request_id, &message) + } + ModelsRefresh => state.refresh_model_list(), + ModelsUpdated => state.update_model_list(), + ProviderStatus { + provider_id, + status, + } => state.update_provider_status(&provider_id, status), + }; + + matches!(outcome, AppState::Quit) + } + + fn clear_active_generation(&mut self, request_id: Uuid) { + if self + .active_generation + .as_ref() + .map(|active| active.request_id() == request_id) + .unwrap_or(false) + { + self.active_generation = None; + } else { + warn!( + "received completion for unknown request {}, ignoring", + request_id + ); + } + } + + fn clear_active_generation_optional(&mut self, request_id: Option) { + match request_id { + Some(id) => self.clear_active_generation(id), + None => { + if self.active_generation.is_some() { + self.active_generation = None; + } + } + } + } +} diff --git a/crates/owlen-tui/src/app/mod.rs b/crates/owlen-tui/src/app/mod.rs index f01066f..4bd0d50 100644 --- a/crates/owlen-tui/src/app/mod.rs +++ b/crates/owlen-tui/src/app/mod.rs @@ -1,4 +1,5 @@ mod generation; +mod handler; mod worker; pub mod messages; @@ -12,6 +13,7 @@ use tokio::{ }; use uuid::Uuid; +pub use handler::MessageState; pub use messages::AppMessage; /// High-level application state driving the non-blocking TUI. @@ -65,7 +67,6 @@ impl App { } struct ActiveGeneration { - #[allow(dead_code)] request_id: Uuid, #[allow(dead_code)] provider_id: String, @@ -88,4 +89,8 @@ impl ActiveGeneration { fn abort(self) { self.abort_handle.abort(); } + + fn request_id(&self) -> Uuid { + self.request_id + } } diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 36769a5..b8755e0 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -11200,3 +11200,5 @@ fn configure_textarea_defaults(textarea: &mut TextArea<'static>) { textarea.set_cursor_style(Style::default()); textarea.set_cursor_line_style(Style::default()); } + +impl crate::app::MessageState for ChatApp {}