From 05d6bbc9129c77d3af4a3529a168158404faf80a Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 02:44:58 -0400 Subject: [PATCH] move hotkey handlers from Mind to event_loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cycle_reasoning, kill_processes, and AdjustSampling only need the Agent lock — they're pure Agent operations. Handle them directly in the UI event loop instead of routing through Mind. Mind now only receives Interrupt and CycleAutonomy as hotkeys, which genuinely need Mind state (turn handles, DMN state). mind/mod.rs: 957 → 688 lines across the session. Co-Authored-By: Kent Overstreet --- src/mind/mod.rs | 52 +------------------------------------ src/user/event_loop.rs | 59 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index ca7180e..565f509 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -454,45 +454,6 @@ impl Mind { } /// Cycle reasoning effort: none → low → high → none. - fn cycle_reasoning(&mut self) { - if let Ok(mut agent_guard) = self.agent.try_lock() { - let next = match agent_guard.reasoning_effort.as_str() { - "none" => "low", - "low" => "high", - _ => "none", - }; - agent_guard.reasoning_effort = next.to_string(); - let label = match next { - "none" => "off (monologue hidden)", - "low" => "low (brief monologue)", - "high" => "high (full monologue)", - _ => next, - }; - let _ = self.ui_tx.send(UiMessage::Info(format!("Reasoning: {} — ^R to cycle", label))); - } else { - let _ = self.ui_tx.send(UiMessage::Info( - "(agent busy — reasoning change takes effect next turn)".into(), - )); - } - } - - /// Show and kill running tool calls (Ctrl+K). - async fn kill_processes(&mut self) { - let active_tools = self.agent.lock().await.active_tools.clone(); - let mut tools = active_tools.lock().unwrap(); - if tools.is_empty() { - let _ = self.ui_tx.send(UiMessage::Info("(no running tool calls)".into())); - } else { - for entry in tools.drain(..) { - let elapsed = entry.started.elapsed(); - let _ = self.ui_tx.send(UiMessage::Info(format!( - " killing {} ({:.0}s): {}", entry.name, elapsed.as_secs_f64(), entry.detail, - ))); - entry.handle.abort(); - } - } - } - /// Cycle DMN autonomy: foraging → resting → paused → off → foraging. fn cycle_autonomy(&mut self) { let (new_state, label) = match &self.dmn { @@ -578,20 +539,9 @@ impl Mind { MindMessage::UserInput(input) => self.submit_input(input), MindMessage::Hotkey(action) => { match action { - HotkeyAction::CycleReasoning => self.cycle_reasoning(), - HotkeyAction::KillProcess => self.kill_processes().await, HotkeyAction::Interrupt => self.interrupt().await, HotkeyAction::CycleAutonomy => self.cycle_autonomy(), - HotkeyAction::AdjustSampling(param, delta) => { - if let Ok(mut agent) = self.agent.try_lock() { - match param { - 0 => agent.temperature = (agent.temperature + delta).clamp(0.0, 2.0), - 1 => agent.top_p = (agent.top_p + delta).clamp(0.0, 1.0), - 2 => agent.top_k = (agent.top_k as f32 + delta).max(0.0) as u32, - _ => {} - } - } - } + _ => {} // Other hotkeys handled directly by UI } } MindMessage::NewSession => self.cmd_new().await, diff --git a/src/user/event_loop.rs b/src/user/event_loop.rs index 3112b37..a42399a 100644 --- a/src/user/event_loop.rs +++ b/src/user/event_loop.rs @@ -86,6 +86,55 @@ async fn cmd_retry( } } +fn cmd_cycle_reasoning(agent: &Arc>, ui_tx: &ui_channel::UiSender) { + if let Ok(mut ag) = agent.try_lock() { + let next = match ag.reasoning_effort.as_str() { + "none" => "low", + "low" => "high", + _ => "none", + }; + ag.reasoning_effort = next.to_string(); + let label = match next { + "none" => "off (monologue hidden)", + "low" => "low (brief monologue)", + "high" => "high (full monologue)", + _ => next, + }; + let _ = ui_tx.send(UiMessage::Info(format!("Reasoning: {} — ^R to cycle", label))); + } else { + let _ = ui_tx.send(UiMessage::Info( + "(agent busy — reasoning change takes effect next turn)".into(), + )); + } +} + +async fn cmd_kill_processes(agent: &Arc>, ui_tx: &ui_channel::UiSender) { + let active_tools = agent.lock().await.active_tools.clone(); + let mut tools = active_tools.lock().unwrap(); + if tools.is_empty() { + let _ = ui_tx.send(UiMessage::Info("(no running tool calls)".into())); + } else { + for entry in tools.drain(..) { + let elapsed = entry.started.elapsed(); + let _ = ui_tx.send(UiMessage::Info(format!( + " killing {} ({:.0}s): {}", entry.name, elapsed.as_secs_f64(), entry.detail, + ))); + entry.handle.abort(); + } + } +} + +fn cmd_adjust_sampling(agent: &Arc>, param: usize, delta: f32) { + if let Ok(mut ag) = agent.try_lock() { + match param { + 0 => ag.temperature = (ag.temperature + delta).clamp(0.0, 2.0), + 1 => ag.top_p = (ag.top_p + delta).clamp(0.0, 1.0), + 2 => ag.top_k = (ag.top_k as f32 + delta).max(0.0) as u32, + _ => {} + } + } +} + pub async fn cmd_switch_model( agent: &Arc>, name: &str, @@ -271,10 +320,16 @@ pub async fn run( } } - // Send hotkey actions to Mind + // Handle hotkey actions let actions: Vec = app.hotkey_actions.drain(..).collect(); for action in actions { - let _ = mind_tx.send(MindMessage::Hotkey(action)); + match action { + HotkeyAction::CycleReasoning => cmd_cycle_reasoning(&agent, &ui_tx), + HotkeyAction::KillProcess => cmd_kill_processes(&agent, &ui_tx).await, + HotkeyAction::Interrupt => { let _ = mind_tx.send(MindMessage::Hotkey(action)); } + HotkeyAction::CycleAutonomy => { let _ = mind_tx.send(MindMessage::Hotkey(action)); } + HotkeyAction::AdjustSampling(param, delta) => cmd_adjust_sampling(&agent, param, delta), + } } if app.drain_messages(&mut ui_rx) {