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
|
|
@ -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