feat(app): introduce MessageState trait and handler for AppMessage dispatch
- Add `MessageState` trait defining UI reaction callbacks for generation lifecycle, model updates, provider status, resize, and tick events. - Implement `App::handle_message` to route `AppMessage` variants to the provided `MessageState` and determine exit condition. - Add `handler.rs` module with the trait and dispatch logic; re-export `MessageState` in `app/mod.rs`. - Extend `ActiveGeneration` with a public `request_id` getter and clean up dead code annotations. - Implement empty `MessageState` for `ChatApp` to integrate UI handling. - Add `log` crate dependency for warning messages.
This commit is contained in:
@@ -42,6 +42,7 @@ uuid = { workspace = true }
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = { workspace = true }
|
tokio-test = { workspace = true }
|
||||||
|
|||||||
135
crates/owlen-tui/src/app/handler.rs
Normal file
135
crates/owlen-tui/src/app/handler.rs
Normal file
@@ -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<Uuid>, 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<State>(&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<Uuid>) {
|
||||||
|
match request_id {
|
||||||
|
Some(id) => self.clear_active_generation(id),
|
||||||
|
None => {
|
||||||
|
if self.active_generation.is_some() {
|
||||||
|
self.active_generation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod generation;
|
mod generation;
|
||||||
|
mod handler;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
@@ -12,6 +13,7 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use handler::MessageState;
|
||||||
pub use messages::AppMessage;
|
pub use messages::AppMessage;
|
||||||
|
|
||||||
/// High-level application state driving the non-blocking TUI.
|
/// High-level application state driving the non-blocking TUI.
|
||||||
@@ -65,7 +67,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveGeneration {
|
struct ActiveGeneration {
|
||||||
#[allow(dead_code)]
|
|
||||||
request_id: Uuid,
|
request_id: Uuid,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
provider_id: String,
|
provider_id: String,
|
||||||
@@ -88,4 +89,8 @@ impl ActiveGeneration {
|
|||||||
fn abort(self) {
|
fn abort(self) {
|
||||||
self.abort_handle.abort();
|
self.abort_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_id(&self) -> Uuid {
|
||||||
|
self.request_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11200,3 +11200,5 @@ fn configure_textarea_defaults(textarea: &mut TextArea<'static>) {
|
|||||||
textarea.set_cursor_style(Style::default());
|
textarea.set_cursor_style(Style::default());
|
||||||
textarea.set_cursor_line_style(Style::default());
|
textarea.set_cursor_line_style(Style::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::app::MessageState for ChatApp {}
|
||||||
|
|||||||
Reference in New Issue
Block a user