consciousness/src/user/event_loop.rs

245 lines
8.4 KiB
Rust
Raw Normal View History

// event_loop.rs — TUI event loop
//
// Drives the terminal, renders the UI, dispatches user input and
// hotkey actions to the Mind via a channel. Reads shared state
// (agent, active tools) directly for rendering.
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};
/// Messages from the UI to the Mind.
pub enum MindMessage {
UserInput(String),
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)>>,
mut channel_rx: tokio::sync::mpsc::Receiver<Vec<(String, bool, u32)>>,
notify_rx: std::sync::mpsc::Receiver<crate::thalamus::channels::ChannelNotification>,
mut idle_state: crate::thalamus::idle::State,
) -> Result<()> {
let mut terminal = tui::init_terminal()?;
let mut reader = EventStream::new();
let mut render_interval = tokio::time::interval(Duration::from_millis(50));
render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut dirty = true;
terminal.hide_cursor()?;
// Initial render
app.drain_messages(&mut ui_rx);
terminal.draw(|f| app.draw(f))?;
loop {
tokio::select! {
biased;
maybe_event = reader.next() => {
match maybe_event {
Some(Ok(Event::Key(key))) => {
if key.kind != KeyEventKind::Press { continue; }
app.handle_key(key);
idle_state.user_activity();
if app.screen == tui::Screen::Thalamus {
let tx = channel_tx.clone();
tokio::spawn(async move {
let result = crate::thalamus::channels::fetch_all_channels().await;
let _ = tx.send(result).await;
});
}
dirty = true;
}
Some(Ok(Event::Mouse(mouse))) => {
app.handle_mouse(mouse);
dirty = true;
}
Some(Ok(Event::Resize(w, h))) => {
app.handle_resize(w, h);
terminal.clear()?;
dirty = true;
}
Some(Err(_)) => break,
None => break,
_ => continue,
}
}
Some(line) = observe_input_rx.recv() => {
app.submitted.push(line);
dirty = true;
}
_ = render_interval.tick() => {
idle_state.decay_ewma();
app.update_idle(&idle_state);
while let Ok(notif) = notify_rx.try_recv() {
let tx = channel_tx.clone();
tokio::spawn(async move {
let result = crate::thalamus::channels::fetch_all_channels().await;
let _ = tx.send(result).await;
});
}
}
Some(channels) = channel_rx.recv() => {
app.set_channel_status(channels);
dirty = true;
}
Some(msg) = ui_rx.recv() => {
app.handle_ui_message(msg);
dirty = true;
}
}
// Process submitted input
let submitted: Vec<String> = app.submitted.drain(..).collect();
for input in submitted {
let input = input.trim().to_string();
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)); }
}
}
// Send hotkey actions to Mind
let actions: Vec<HotkeyAction> = app.hotkey_actions.drain(..).collect();
for action in actions {
let _ = mind_tx.send(MindMessage::Hotkey(action));
}
if app.drain_messages(&mut ui_rx) {
dirty = true;
}
if dirty {
terminal.draw(|f| app.draw(f))?;
dirty = false;
}
if app.should_quit {
break;
}
}
tui::restore_terminal(&mut terminal)?;
Ok(())
}