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:
parent
804d55a702
commit
178824fa01
3 changed files with 111 additions and 107 deletions
|
|
@ -90,8 +90,8 @@ pub struct Agent {
|
|||
pub context: ContextState,
|
||||
/// Shared live context summary — TUI reads this directly for debug screen.
|
||||
pub shared_context: SharedContextState,
|
||||
/// App config — used to reload identity on compaction.
|
||||
app_config: crate::config::AppConfig,
|
||||
/// App config — used to reload identity on compaction and model switching.
|
||||
pub app_config: crate::config::AppConfig,
|
||||
pub prompt_file: String,
|
||||
/// Stable session ID for memory-search dedup across turns.
|
||||
pub session_id: String,
|
||||
|
|
|
|||
110
src/mind/mod.rs
110
src/mind/mod.rs
|
|
@ -183,7 +183,9 @@ impl Mind {
|
|||
));
|
||||
}
|
||||
if let Some(model_name) = turn_result.model_switch {
|
||||
self.switch_model(&model_name).await;
|
||||
crate::user::event_loop::cmd_switch_model(
|
||||
&self.agent, &model_name, &self.ui_tx,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -329,28 +331,7 @@ impl Mind {
|
|||
|
||||
/// Handle slash commands. Returns how the main loop should respond.
|
||||
async fn handle_command(&mut self, input: &str) -> Command {
|
||||
const COMMANDS: &[(&str, &str)] = &[
|
||||
("/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)"),
|
||||
("/test", "Run tool smoke tests"),
|
||||
("/help", "Show this help"),
|
||||
];
|
||||
|
||||
match input {
|
||||
"/save" => {
|
||||
let _ = self.ui_tx.send(UiMessage::Info(
|
||||
"Conversation is saved automatically (append-only log).".into()
|
||||
));
|
||||
Command::Handled
|
||||
}
|
||||
"/new" | "/clear" => {
|
||||
if self.turn_in_progress {
|
||||
let _ = self.ui_tx.send(UiMessage::Info("(turn in progress, please wait)".into()));
|
||||
|
|
@ -378,18 +359,6 @@ impl Mind {
|
|||
let _ = self.ui_tx.send(UiMessage::Info("New session started.".into()));
|
||||
Command::Handled
|
||||
}
|
||||
"/model" => {
|
||||
if let Ok(agent) = self.agent.try_lock() {
|
||||
let _ = self.ui_tx.send(UiMessage::Info(format!("Current model: {}", agent.model())));
|
||||
let names = self.config.app.model_names();
|
||||
if !names.is_empty() {
|
||||
let _ = self.ui_tx.send(UiMessage::Info(format!("Available: {}", names.join(", "))));
|
||||
}
|
||||
} else {
|
||||
let _ = self.ui_tx.send(UiMessage::Info("(busy)".into()));
|
||||
}
|
||||
Command::Handled
|
||||
}
|
||||
"/score" => {
|
||||
if self.scoring_in_flight {
|
||||
let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into()));
|
||||
|
|
@ -486,31 +455,6 @@ impl Mind {
|
|||
}
|
||||
Command::Handled
|
||||
}
|
||||
"/help" => {
|
||||
for (name, desc) in COMMANDS {
|
||||
let _ = self.ui_tx.send(UiMessage::Info(format!(" {:12} {}", name, desc)));
|
||||
}
|
||||
let _ = self.ui_tx.send(UiMessage::Info(String::new()));
|
||||
let _ = self.ui_tx.send(UiMessage::Info(
|
||||
"Keys: Tab=pane ^Up/Down=scroll PgUp/PgDn=scroll Mouse=click/scroll".into(),
|
||||
));
|
||||
let _ = self.ui_tx.send(UiMessage::Info(
|
||||
" Alt+Enter=newline Esc=interrupt ^P=pause ^R=reasoning ^K=kill F10=context F2=agents".into(),
|
||||
));
|
||||
let _ = self.ui_tx.send(UiMessage::Info(
|
||||
" Shift+click for native text selection (copy/paste)".into(),
|
||||
));
|
||||
Command::Handled
|
||||
}
|
||||
cmd if cmd.starts_with("/model ") => {
|
||||
let name = cmd[7..].trim();
|
||||
if name.is_empty() {
|
||||
let _ = self.ui_tx.send(UiMessage::Info("Usage: /model <name>".into()));
|
||||
return Command::Handled;
|
||||
}
|
||||
self.switch_model(name).await;
|
||||
Command::Handled
|
||||
}
|
||||
_ => Command::None,
|
||||
}
|
||||
}
|
||||
|
|
@ -609,49 +553,6 @@ impl Mind {
|
|||
}
|
||||
|
||||
/// Switch to a named model from the config registry.
|
||||
async fn switch_model(&mut self, name: &str) {
|
||||
if self.turn_in_progress {
|
||||
let _ = self.ui_tx.send(UiMessage::Info("(turn in progress, please wait)".into()));
|
||||
return;
|
||||
}
|
||||
|
||||
let resolved = match self.config.app.resolve_model(name) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let _ = self.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 = resolved.prompt_file != self.config.prompt_file;
|
||||
let mut agent_guard = self.agent.lock().await;
|
||||
agent_guard.swap_client(new_client);
|
||||
|
||||
self.config.model = resolved.model_id.clone();
|
||||
self.config.api_base = resolved.api_base;
|
||||
self.config.api_key = resolved.api_key;
|
||||
|
||||
if prompt_changed {
|
||||
self.config.prompt_file = resolved.prompt_file.clone();
|
||||
agent_guard.prompt_file = resolved.prompt_file.clone();
|
||||
agent_guard.compact();
|
||||
let _ = self.ui_tx.send(UiMessage::Info(format!(
|
||||
"Switched to {} ({}) — prompt: {}, recompacted",
|
||||
name, resolved.model_id, resolved.prompt_file,
|
||||
)));
|
||||
} else {
|
||||
let _ = self.ui_tx.send(UiMessage::Info(format!(
|
||||
"Switched to {} ({})", name, resolved.model_id,
|
||||
)));
|
||||
}
|
||||
|
||||
drop(agent_guard);
|
||||
self.update_status();
|
||||
self.send_context_info();
|
||||
}
|
||||
|
||||
fn load_context_groups(&self) -> Vec<config::ContextGroup> {
|
||||
config::get().context_groups.clone()
|
||||
}
|
||||
|
|
@ -859,15 +760,14 @@ pub async fn run(cli: crate::user::CliArgs) -> Result<()> {
|
|||
|
||||
// App for TUI
|
||||
let app = tui::App::new(mind.config.model.clone(), shared_context, shared_active_tools);
|
||||
let ui_agent = mind.agent.clone();
|
||||
|
||||
// Spawn Mind event loop
|
||||
tokio::spawn(async move {
|
||||
mind.run(mind_rx, turn_rx).await;
|
||||
});
|
||||
|
||||
// Run UI event loop on main thread (needs terminal access)
|
||||
crate::user::event_loop::run(
|
||||
app, mind_tx, ui_rx, observe_input_rx,
|
||||
app, ui_agent, mind_tx, ui_tx, ui_rx, observe_input_rx,
|
||||
channel_tx, channel_rx, notify_rx, idle_state,
|
||||
).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue