diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index c233784..1e47fb5 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -22,7 +22,7 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; /// DMN state machine. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum State { /// Responding to user input. Short interval — stay engaged. Engaged, diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 225897e..07deb39 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -37,6 +37,7 @@ fn compaction_threshold(app: &AppConfig) -> u32 { /// Shared state between Mind and UI. +#[derive(Clone)] pub struct MindState { /// Pending user input — UI pushes, Mind consumes after turn completes. pub input: Vec, @@ -319,10 +320,7 @@ impl Mind { Some(msg) = input_rx.recv() => { match msg { MindMessage::Hotkey(HotkeyAction::CycleAutonomy) => { - let label = self.shared.lock().unwrap().cycle_autonomy(); - let _ = self.ui_tx.send(UiMessage::Info( - format!("DMN → {} (Ctrl+P to cycle)", label), - )); + self.shared.lock().unwrap().cycle_autonomy(); } MindMessage::Hotkey(HotkeyAction::Interrupt) => { self.shared.lock().unwrap().interrupt(); @@ -333,7 +331,6 @@ impl Mind { if let Some(h) = self.turn_handle.take() { h.abort(); } self.shared.lock().unwrap().turn_active = false; let _ = self.turn_watch.send(false); - let _ = self.ui_tx.send(UiMessage::Info("(interrupted)".into())); } MindMessage::NewSession => { self.shared.lock().unwrap().dmn_sleep(); @@ -349,7 +346,6 @@ impl Mind { self.config.app.clone(), self.config.prompt_file.clone(), new_log, shared_ctx, shared_tools, ); - let _ = self.ui_tx.send(UiMessage::Info("New session started.".into())); } MindMessage::Score => { let mut s = self.shared.lock().unwrap(); @@ -357,16 +353,12 @@ impl Mind { s.scoring_in_flight = true; drop(s); self.start_memory_scoring(); - } else { - let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into())); } } _ => {} } - // Check for pending input let action = self.shared.lock().unwrap().take_pending_input(); self.execute(action); - crate::user::event_loop::update_status(&self.shared, &self.ui_tx); } Some((result, target)) = turn_rx.recv() => { @@ -374,20 +366,12 @@ impl Mind { let model_switch = self.shared.lock().unwrap().complete_turn(&result, target); let _ = self.turn_watch.send(false); - if let Err(ref e) = result { - let msg = match target { - StreamTarget::Autonomous => UiMessage::DmnAnnotation(format!("[error: {:#}]", e)), - StreamTarget::Conversation => UiMessage::Info(format!("Error: {:#}", e)), - }; - let _ = self.ui_tx.send(msg); - } if let Some(name) = model_switch { crate::user::event_loop::cmd_switch_model(&self.agent, &name, &self.ui_tx).await; } self.check_compaction(); if !self.config.no_agents { self.start_memory_scoring(); } - crate::user::event_loop::update_status(&self.shared, &self.ui_tx); let action = self.shared.lock().unwrap().take_pending_input(); self.execute(action); @@ -400,7 +384,6 @@ impl Mind { let action = self.shared.lock().unwrap().dmn_tick(); self.execute(action); } - crate::user::event_loop::update_status(&self.shared, &self.ui_tx); } } } @@ -500,7 +483,6 @@ pub async fn run(cli: crate::user::CliArgs) -> Result<()> { let shared_mind = shared_mind_state(config.app.dmn.max_turns); crate::user::event_loop::send_context_info(&config, &ui_tx); let mut mind = Mind::new(agent, shared_mind.clone(), config, ui_tx.clone(), turn_tx); - crate::user::event_loop::update_status(&shared_mind, &ui_tx); if !no_agents { mind.start_memory_scoring(); } diff --git a/src/user/event_loop.rs b/src/user/event_loop.rs index 2881657..1615601 100644 --- a/src/user/event_loop.rs +++ b/src/user/event_loop.rs @@ -130,18 +130,6 @@ fn cmd_adjust_sampling(agent: &Arc>, param: usize, delta: f32) { } } -pub fn update_status(shared: &crate::mind::SharedMindState, ui_tx: &ui_channel::UiSender) { - let s = shared.lock().unwrap(); - let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo { - dmn_state: s.dmn.label().to_string(), - dmn_turns: s.dmn_turns, - dmn_max_turns: s.max_dmn_turns, - prompt_tokens: 0, completion_tokens: 0, - model: String::new(), turn_tools: 0, - context_budget: String::new(), - })); -} - pub fn send_context_info(config: &crate::config::SessionConfig, ui_tx: &ui_channel::UiSender) { let context_groups = crate::config::get().context_groups.clone(); let (instruction_files, memory_files) = crate::mind::identity::context_file_info( @@ -216,6 +204,7 @@ pub async fn run( let mut render_interval = tokio::time::interval(Duration::from_millis(50)); render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); let mut dirty = true; + let mut prev_mind = shared_mind.lock().unwrap().clone(); terminal.hide_cursor()?; @@ -266,6 +255,26 @@ pub async fn run( idle_state.decay_ewma(); app.update_idle(&idle_state); + // Diff MindState — generate UI messages from changes + { + let cur = shared_mind.lock().unwrap().clone(); + if cur.dmn.label() != prev_mind.dmn.label() || cur.dmn_turns != prev_mind.dmn_turns { + let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo { + dmn_state: cur.dmn.label().to_string(), + dmn_turns: cur.dmn_turns, + dmn_max_turns: cur.max_dmn_turns, + prompt_tokens: 0, completion_tokens: 0, + model: String::new(), turn_tools: 0, + context_budget: String::new(), + })); + dirty = true; + } + if cur.turn_active != prev_mind.turn_active { + dirty = true; + } + prev_mind = cur; + } + while let Ok(notif) = notify_rx.try_recv() { let tx = channel_tx.clone(); tokio::spawn(async move {