diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 5890d07..ca7180e 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -35,11 +35,6 @@ fn compaction_threshold(app: &AppConfig) -> u32 { (crate::agent::context::context_window() as u32) * app.compaction.hard_threshold_pct / 100 } -/// Result of slash command handling. -pub enum Command { - Handled, - None, -} // --- Mind: all mutable state for a running agent session --- @@ -345,104 +340,86 @@ impl Mind { self.spawn_turn(prompt, StreamTarget::Autonomous); } - /// Handle slash commands. Returns how the main loop should respond. - async fn handle_command(&mut self, input: &str) -> Command { - match input { - "/new" | "/clear" => { - if self.turn_in_progress { - let _ = self.ui_tx.send(UiMessage::Info("(turn in progress, please wait)".into())); - return Command::Handled; - } - { - let new_log = log::ConversationLog::new( - self.config.session_dir.join("conversation.jsonl"), - ).ok(); - let mut agent_guard = self.agent.lock().await; - let shared_ctx = agent_guard.shared_context.clone(); - let shared_tools = agent_guard.active_tools.clone(); - *agent_guard = Agent::new( - ApiClient::new(&self.config.api_base, &self.config.api_key, &self.config.model), - self.config.system_prompt.clone(), - self.config.context_parts.clone(), - self.config.app.clone(), - self.config.prompt_file.clone(), - new_log, - shared_ctx, - shared_tools, - ); - } - self.dmn = dmn::State::Resting { since: Instant::now() }; - let _ = self.ui_tx.send(UiMessage::Info("New session started.".into())); - Command::Handled - } - "/score" => { - if self.scoring_in_flight { - let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into())); - return Command::Handled; - } - let (context, client) = { - let agent = self.agent.lock().await; - (agent.context.clone(), agent.client_clone()) - }; - self.scoring_in_flight = true; - let agent = self.agent.clone(); - let ui_tx = self.ui_tx.clone(); - tokio::spawn(async move { - let result = learn::score_memories( - &context, &client, &ui_tx, - ).await; - let agent = agent.lock().await; - match result { - Ok(scores) => { - agent.publish_context_state_with_scores(Some(&scores)); - } - Err(e) => { - let _ = ui_tx.send(UiMessage::Info(format!("[scoring failed: {:#}]", e))); - } - } - }); - Command::Handled - } - "/dmn" => { - let _ = self.ui_tx.send(UiMessage::Info(format!("DMN state: {:?}", self.dmn))); - let _ = self.ui_tx.send(UiMessage::Info(format!("Next tick in: {:?}", self.dmn.interval()))); - let _ = self.ui_tx.send(UiMessage::Info(format!( - "Consecutive DMN turns: {}/{}", self.dmn_turns, self.max_dmn_turns, - ))); - Command::Handled - } - "/sleep" => { - self.dmn = dmn::State::Resting { since: Instant::now() }; - self.dmn_turns = 0; - let _ = self.ui_tx.send(UiMessage::Info( - "DMN sleeping (heartbeat every 5 min). Type anything to wake.".into(), - )); - Command::Handled - } - "/wake" => { - let was_paused = matches!(self.dmn, dmn::State::Paused | dmn::State::Off); - if matches!(self.dmn, dmn::State::Off) { - dmn::set_off(false); - } - self.dmn = dmn::State::Foraging; - self.dmn_turns = 0; - let msg = if was_paused { "DMN unpaused — entering foraging mode." } - else { "DMN waking — entering foraging mode." }; - let _ = self.ui_tx.send(UiMessage::Info(msg.into())); - self.update_status(); - Command::Handled - } - "/pause" => { - self.dmn = dmn::State::Paused; - self.dmn_turns = 0; - let _ = self.ui_tx.send(UiMessage::Info( - "DMN paused — no autonomous ticks. Ctrl+P or /wake to resume.".into(), - )); - self.update_status(); - Command::Handled - } - _ => Command::None, + async fn cmd_new(&mut self) { + let new_log = log::ConversationLog::new( + self.config.session_dir.join("conversation.jsonl"), + ).ok(); + let mut agent_guard = self.agent.lock().await; + let shared_ctx = agent_guard.shared_context.clone(); + let shared_tools = agent_guard.active_tools.clone(); + *agent_guard = Agent::new( + ApiClient::new(&self.config.api_base, &self.config.api_key, &self.config.model), + self.config.system_prompt.clone(), + self.config.context_parts.clone(), + self.config.app.clone(), + self.config.prompt_file.clone(), + new_log, + shared_ctx, + shared_tools, + ); + drop(agent_guard); + self.dmn = dmn::State::Resting { since: Instant::now() }; + let _ = self.ui_tx.send(UiMessage::Info("New session started.".into())); + } + + fn cmd_score(&mut self) { + if self.scoring_in_flight { + let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into())); + return; } + let agent = self.agent.clone(); + let ui_tx = self.ui_tx.clone(); + self.scoring_in_flight = true; + tokio::spawn(async move { + let (context, client) = { + let ag = agent.lock().await; + (ag.context.clone(), ag.client_clone()) + }; + let result = learn::score_memories(&context, &client, &ui_tx).await; + let ag = agent.lock().await; + match result { + Ok(scores) => ag.publish_context_state_with_scores(Some(&scores)), + Err(e) => { let _ = ui_tx.send(UiMessage::Info(format!("[scoring failed: {:#}]", e))); } + } + }); + } + + fn cmd_dmn_query(&self) { + let _ = self.ui_tx.send(UiMessage::Info(format!("DMN state: {:?}", self.dmn))); + let _ = self.ui_tx.send(UiMessage::Info(format!("Next tick in: {:?}", self.dmn.interval()))); + let _ = self.ui_tx.send(UiMessage::Info(format!( + "Consecutive DMN turns: {}/{}", self.dmn_turns, self.max_dmn_turns, + ))); + } + + fn cmd_dmn_sleep(&mut self) { + self.dmn = dmn::State::Resting { since: Instant::now() }; + self.dmn_turns = 0; + let _ = self.ui_tx.send(UiMessage::Info( + "DMN sleeping (heartbeat every 5 min). Type anything to wake.".into(), + )); + } + + fn cmd_dmn_wake(&mut self) { + let was_paused = matches!(self.dmn, dmn::State::Paused | dmn::State::Off); + if matches!(self.dmn, dmn::State::Off) { + dmn::set_off(false); + } + self.dmn = dmn::State::Foraging; + self.dmn_turns = 0; + let msg = if was_paused { "DMN unpaused — entering foraging mode." } + else { "DMN waking — entering foraging mode." }; + let _ = self.ui_tx.send(UiMessage::Info(msg.into())); + self.update_status(); + } + + fn cmd_dmn_pause(&mut self) { + self.dmn = dmn::State::Paused; + self.dmn_turns = 0; + let _ = self.ui_tx.send(UiMessage::Info( + "DMN paused — no autonomous ticks. Ctrl+P or /wake to resume.".into(), + )); + self.update_status(); } /// Interrupt: kill processes, abort current turn, clear pending queue. @@ -598,12 +575,7 @@ impl Mind { Some(msg) = input_rx.recv() => { match msg { - MindMessage::UserInput(input) => { - match self.handle_command(&input).await { - Command::Handled => {} - Command::None => self.submit_input(input), - } - } + MindMessage::UserInput(input) => self.submit_input(input), MindMessage::Hotkey(action) => { match action { HotkeyAction::CycleReasoning => self.cycle_reasoning(), @@ -622,6 +594,12 @@ impl Mind { } } } + MindMessage::NewSession => self.cmd_new().await, + MindMessage::Score => self.cmd_score(), + MindMessage::DmnQuery => self.cmd_dmn_query(), + MindMessage::DmnSleep => self.cmd_dmn_sleep(), + MindMessage::DmnWake => self.cmd_dmn_wake(), + MindMessage::DmnPause => self.cmd_dmn_pause(), } } diff --git a/src/user/event_loop.rs b/src/user/event_loop.rs index 50c9c6f..3112b37 100644 --- a/src/user/event_loop.rs +++ b/src/user/event_loop.rs @@ -21,6 +21,12 @@ use crate::user::ui_channel::{self, UiMessage}; pub enum MindMessage { UserInput(String), Hotkey(HotkeyAction), + DmnSleep, + DmnWake, + DmnPause, + DmnQuery, + NewSession, + Score, } fn send_help(ui_tx: &ui_channel::UiSender) { @@ -232,6 +238,12 @@ pub async fn run( let _ = ui_tx.send(UiMessage::Info("(busy)".into())); } } + "/new" | "/clear" => { let _ = mind_tx.send(MindMessage::NewSession); } + "/dmn" => { let _ = mind_tx.send(MindMessage::DmnQuery); } + "/sleep" => { let _ = mind_tx.send(MindMessage::DmnSleep); } + "/wake" => { let _ = mind_tx.send(MindMessage::DmnWake); } + "/pause" => { let _ = mind_tx.send(MindMessage::DmnPause); } + "/score" => { let _ = mind_tx.send(MindMessage::Score); } "/retry" => { let agent = agent.clone(); let ui_tx = ui_tx.clone();