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,
|
pub context: ContextState,
|
||||||
/// Shared live context summary — TUI reads this directly for debug screen.
|
/// Shared live context summary — TUI reads this directly for debug screen.
|
||||||
pub shared_context: SharedContextState,
|
pub shared_context: SharedContextState,
|
||||||
/// App config — used to reload identity on compaction.
|
/// App config — used to reload identity on compaction and model switching.
|
||||||
app_config: crate::config::AppConfig,
|
pub app_config: crate::config::AppConfig,
|
||||||
pub prompt_file: String,
|
pub prompt_file: String,
|
||||||
/// Stable session ID for memory-search dedup across turns.
|
/// Stable session ID for memory-search dedup across turns.
|
||||||
pub session_id: String,
|
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 {
|
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) => {
|
Err(e) => {
|
||||||
|
|
@ -329,28 +331,7 @@ impl Mind {
|
||||||
|
|
||||||
/// Handle slash commands. Returns how the main loop should respond.
|
/// Handle slash commands. Returns how the main loop should respond.
|
||||||
async fn handle_command(&mut self, input: &str) -> Command {
|
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 {
|
match input {
|
||||||
"/save" => {
|
|
||||||
let _ = self.ui_tx.send(UiMessage::Info(
|
|
||||||
"Conversation is saved automatically (append-only log).".into()
|
|
||||||
));
|
|
||||||
Command::Handled
|
|
||||||
}
|
|
||||||
"/new" | "/clear" => {
|
"/new" | "/clear" => {
|
||||||
if self.turn_in_progress {
|
if self.turn_in_progress {
|
||||||
let _ = self.ui_tx.send(UiMessage::Info("(turn in progress, please wait)".into()));
|
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()));
|
let _ = self.ui_tx.send(UiMessage::Info("New session started.".into()));
|
||||||
Command::Handled
|
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" => {
|
"/score" => {
|
||||||
if self.scoring_in_flight {
|
if self.scoring_in_flight {
|
||||||
let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into()));
|
let _ = self.ui_tx.send(UiMessage::Info("(scoring already in progress)".into()));
|
||||||
|
|
@ -486,31 +455,6 @@ impl Mind {
|
||||||
}
|
}
|
||||||
Command::Handled
|
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,
|
_ => Command::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,49 +553,6 @@ impl Mind {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to a named model from the config registry.
|
/// 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> {
|
fn load_context_groups(&self) -> Vec<config::ContextGroup> {
|
||||||
config::get().context_groups.clone()
|
config::get().context_groups.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -859,15 +760,14 @@ pub async fn run(cli: crate::user::CliArgs) -> Result<()> {
|
||||||
|
|
||||||
// App for TUI
|
// App for TUI
|
||||||
let app = tui::App::new(mind.config.model.clone(), shared_context, shared_active_tools);
|
let app = tui::App::new(mind.config.model.clone(), shared_context, shared_active_tools);
|
||||||
|
let ui_agent = mind.agent.clone();
|
||||||
|
|
||||||
// Spawn Mind event loop
|
// Spawn Mind event loop
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
mind.run(mind_rx, turn_rx).await;
|
mind.run(mind_rx, turn_rx).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run UI event loop on main thread (needs terminal access)
|
|
||||||
crate::user::event_loop::run(
|
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,
|
channel_tx, channel_rx, notify_rx, idle_state,
|
||||||
).await
|
).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,13 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{Event, EventStream, KeyEventKind};
|
use crossterm::event::{Event, EventStream, KeyEventKind};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
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::{self as tui, HotkeyAction};
|
||||||
use crate::user::ui_channel::{self, UiMessage};
|
use crate::user::ui_channel::{self, UiMessage};
|
||||||
|
|
||||||
|
|
@ -18,9 +23,79 @@ pub enum MindMessage {
|
||||||
Hotkey(HotkeyAction),
|
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(
|
pub async fn run(
|
||||||
mut app: tui::App,
|
mut app: tui::App,
|
||||||
|
agent: Arc<Mutex<Agent>>,
|
||||||
mind_tx: tokio::sync::mpsc::UnboundedSender<MindMessage>,
|
mind_tx: tokio::sync::mpsc::UnboundedSender<MindMessage>,
|
||||||
|
ui_tx: ui_channel::UiSender,
|
||||||
mut ui_rx: ui_channel::UiReceiver,
|
mut ui_rx: ui_channel::UiReceiver,
|
||||||
mut observe_input_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
mut observe_input_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||||
channel_tx: tokio::sync::mpsc::Sender<Vec<(String, bool, u32)>>,
|
channel_tx: tokio::sync::mpsc::Sender<Vec<(String, bool, u32)>>,
|
||||||
|
|
@ -111,6 +186,35 @@ pub async fn run(
|
||||||
if input.is_empty() { continue; }
|
if input.is_empty() { continue; }
|
||||||
match input.as_str() {
|
match input.as_str() {
|
||||||
"/quit" | "/exit" => app.should_quit = true,
|
"/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)); }
|
_ => { let _ = mind_tx.send(MindMessage::UserInput(input)); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue