move UI commands from Mind to event_loop

/quit, /help, /save handled directly in the UI event loop.
/model and /model <name> moved to event_loop as cmd_switch_model().
Mind no longer needs tui::App for any command handling.

Mind's handle_command now only has commands that genuinely need
Mind state: /new, /retry, /score (turn_in_progress, DMN, scoring).

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 02:29:44 -04:00
parent 804d55a702
commit 178824fa01
3 changed files with 111 additions and 107 deletions

View file

@ -7,8 +7,13 @@
use anyhow::Result;
use crossterm::event::{Event, EventStream, KeyEventKind};
use futures::StreamExt;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use crate::agent::Agent;
use crate::agent::api::ApiClient;
use crate::config::SessionConfig;
use crate::user::{self as tui, HotkeyAction};
use crate::user::ui_channel::{self, UiMessage};
@ -18,9 +23,79 @@ pub enum MindMessage {
Hotkey(HotkeyAction),
}
fn send_help(ui_tx: &ui_channel::UiSender) {
let commands = &[
("/quit", "Exit consciousness"),
("/new", "Start fresh session (saves current)"),
("/save", "Save session to disk"),
("/retry", "Re-run last turn"),
("/model", "Show/switch model (/model <name>)"),
("/score", "Score memory importance"),
("/dmn", "Show DMN state"),
("/sleep", "Put DMN to sleep"),
("/wake", "Wake DMN to foraging"),
("/pause", "Full stop — no autonomous ticks (Ctrl+P)"),
("/help", "Show this help"),
];
for (name, desc) in commands {
let _ = ui_tx.send(UiMessage::Info(format!(" {:12} {}", name, desc)));
}
let _ = ui_tx.send(UiMessage::Info(String::new()));
let _ = ui_tx.send(UiMessage::Info(
"Keys: Tab=pane ^Up/Down=scroll PgUp/PgDn=scroll Mouse=click/scroll".into(),
));
let _ = ui_tx.send(UiMessage::Info(
" Alt+Enter=newline Esc=interrupt ^P=pause ^R=reasoning ^K=kill F10=context F2=agents".into(),
));
let _ = ui_tx.send(UiMessage::Info(
" Shift+click for native text selection (copy/paste)".into(),
));
}
pub async fn cmd_switch_model(
agent: &Arc<Mutex<Agent>>,
name: &str,
ui_tx: &ui_channel::UiSender,
) {
let resolved = {
let ag = agent.lock().await;
match ag.app_config.resolve_model(name) {
Ok(r) => r,
Err(e) => {
let _ = ui_tx.send(UiMessage::Info(format!("{}", e)));
return;
}
}
};
let new_client = ApiClient::new(&resolved.api_base, &resolved.api_key, &resolved.model_id);
let prompt_changed = {
let ag = agent.lock().await;
resolved.prompt_file != ag.prompt_file
};
let mut ag = agent.lock().await;
ag.swap_client(new_client);
if prompt_changed {
ag.prompt_file = resolved.prompt_file.clone();
ag.compact();
let _ = ui_tx.send(UiMessage::Info(format!(
"Switched to {} ({}) — prompt: {}, recompacted",
name, resolved.model_id, resolved.prompt_file,
)));
} else {
let _ = ui_tx.send(UiMessage::Info(format!(
"Switched to {} ({})", name, resolved.model_id,
)));
}
}
pub async fn run(
mut app: tui::App,
agent: Arc<Mutex<Agent>>,
mind_tx: tokio::sync::mpsc::UnboundedSender<MindMessage>,
ui_tx: ui_channel::UiSender,
mut ui_rx: ui_channel::UiReceiver,
mut observe_input_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
channel_tx: tokio::sync::mpsc::Sender<Vec<(String, bool, u32)>>,
@ -111,6 +186,35 @@ pub async fn run(
if input.is_empty() { continue; }
match input.as_str() {
"/quit" | "/exit" => app.should_quit = true,
"/save" => {
let _ = ui_tx.send(UiMessage::Info(
"Conversation is saved automatically (append-only log).".into()
));
}
"/help" => send_help(&ui_tx),
"/model" => {
if let Ok(ag) = agent.try_lock() {
let _ = ui_tx.send(UiMessage::Info(format!("Current model: {}", ag.model())));
let names = ag.app_config.model_names();
if !names.is_empty() {
let _ = ui_tx.send(UiMessage::Info(format!("Available: {}", names.join(", "))));
}
} else {
let _ = ui_tx.send(UiMessage::Info("(busy)".into()));
}
}
cmd if cmd.starts_with("/model ") => {
let name = cmd[7..].trim().to_string();
if name.is_empty() {
let _ = ui_tx.send(UiMessage::Info("Usage: /model <name>".into()));
} else {
let agent = agent.clone();
let ui_tx = ui_tx.clone();
tokio::spawn(async move {
cmd_switch_model(&agent, &name, &ui_tx).await;
});
}
}
_ => { let _ = mind_tx.send(MindMessage::UserInput(input)); }
}
}