From 306788e0f196b6aef80eae089dc57cc5b1f27868 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 21:16:49 -0400 Subject: [PATCH] kill event_loop.rs --- src/user/event_loop.rs | 391 ----------------------------------------- src/user/mod.rs | 383 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 379 insertions(+), 395 deletions(-) delete mode 100644 src/user/event_loop.rs diff --git a/src/user/event_loop.rs b/src/user/event_loop.rs deleted file mode 100644 index 21ffe0c..0000000 --- a/src/user/event_loop.rs +++ /dev/null @@ -1,391 +0,0 @@ -// 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 ratatui::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::mind::MindCommand; -use crate::user::{self as tui, HotkeyAction, ScreenView}; -use crate::user::ui_channel::{self, UiMessage}; - -// ── Slash commands ───────────────────────────────────────────── - -/// Top-level entry point — creates Mind and UI, wires them together. -pub async fn start(cli: crate::user::CliArgs) -> Result<()> { - let (config, _figment) = crate::config::load_session(&cli)?; - - if config.app.debug { - unsafe { std::env::set_var("POC_DEBUG", "1") }; - } - - let (ui_tx, ui_rx) = ui_channel::channel(); - let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1); - let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel(); - - let mind = crate::mind::Mind::new(config, ui_tx.clone(), turn_tx); - - let shared_context = mind.agent.lock().await.shared_context.clone(); - let shared_active_tools = mind.agent.lock().await.active_tools.clone(); - - let mut result = Ok(()); - tokio_scoped::scope(|s| { - // Mind event loop — init + run - s.spawn(async { - mind.init().await; - mind.run(mind_rx, turn_rx).await; - }); - - // UI event loop - s.spawn(async { - result = run( - tui::App::new(String::new(), shared_context, shared_active_tools), - &mind, mind_tx, ui_tx, ui_rx, - ).await; - }); - }); - result -} - - - -fn hotkey_cycle_reasoning(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { - if let Ok(mut ag) = mind.agent.try_lock() { - let next = match ag.reasoning_effort.as_str() { - "none" => "low", - "low" => "high", - _ => "none", - }; - ag.reasoning_effort = next.to_string(); - let label = match next { - "none" => "off (monologue hidden)", - "low" => "low (brief monologue)", - "high" => "high (full monologue)", - _ => next, - }; - let _ = ui_tx.send(UiMessage::Info(format!("Reasoning: {} — ^R to cycle", label))); - } else { - let _ = ui_tx.send(UiMessage::Info( - "(agent busy — reasoning change takes effect next turn)".into(), - )); - } -} - -async fn hotkey_kill_processes(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { - let active_tools = mind.agent.lock().await.active_tools.clone(); - let mut tools = active_tools.lock().unwrap(); - if tools.is_empty() { - let _ = ui_tx.send(UiMessage::Info("(no running tool calls)".into())); - } else { - for entry in tools.drain(..) { - let elapsed = entry.started.elapsed(); - let _ = ui_tx.send(UiMessage::Info(format!( - " killing {} ({:.0}s): {}", entry.name, elapsed.as_secs_f64(), entry.detail, - ))); - entry.handle.abort(); - } - } -} - -fn hotkey_cycle_autonomy(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { - let mut s = mind.shared.lock().unwrap(); - let label = match &s.dmn { - crate::mind::dmn::State::Engaged | crate::mind::dmn::State::Working | crate::mind::dmn::State::Foraging => { - s.dmn = crate::mind::dmn::State::Resting { since: std::time::Instant::now() }; - "resting" - } - crate::mind::dmn::State::Resting { .. } => { - s.dmn = crate::mind::dmn::State::Paused; - "PAUSED" - } - crate::mind::dmn::State::Paused => { - crate::mind::dmn::set_off(true); - s.dmn = crate::mind::dmn::State::Off; - "OFF (persists across restarts)" - } - crate::mind::dmn::State::Off => { - crate::mind::dmn::set_off(false); - s.dmn = crate::mind::dmn::State::Foraging; - "foraging" - } - }; - s.dmn_turns = 0; - drop(s); - let _ = ui_tx.send(UiMessage::Info(format!("DMN → {} (Ctrl+P to cycle)", label))); -} - -fn hotkey_adjust_sampling(mind: &crate::mind::Mind, param: usize, delta: f32) { - if let Ok(mut ag) = mind.agent.try_lock() { - match param { - 0 => ag.temperature = (ag.temperature + delta).clamp(0.0, 2.0), - 1 => ag.top_p = (ag.top_p + delta).clamp(0.0, 1.0), - 2 => ag.top_k = (ag.top_k as f32 + delta).max(0.0) as u32, - _ => {} - } - } -} - -pub fn send_context_info(config: &crate::config::SessionConfig, ui_tx: &ui_channel::UiSender) { - let context_groups = crate::config::get().context_groups.clone(); - let (instruction_files, memory_files) = crate::mind::identity::context_file_info( - &config.prompt_file, - config.app.memory_project.as_deref(), - &context_groups, - ); - let _ = ui_tx.send(UiMessage::Info(format!( - " context: {}K chars ({} config, {} memory files)", - config.context_parts.iter().map(|(_, c)| c.len()).sum::() / 1024, - instruction_files.len(), memory_files.len(), - ))); -} - - -fn diff_mind_state( - cur: &crate::mind::MindState, - prev: &crate::mind::MindState, - ui_tx: &ui_channel::UiSender, - dirty: &mut bool, -) { - if cur.dmn.label() != prev.dmn.label() || cur.dmn_turns != prev.dmn_turns { - let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo { - dmn_state: cur.dmn.label().to_string(), - dmn_turns: cur.dmn_turns, - dmn_max_turns: cur.max_dmn_turns, - prompt_tokens: 0, completion_tokens: 0, - model: String::new(), turn_tools: 0, - context_budget: String::new(), - })); - *dirty = true; - } - // Input consumed — Mind started a turn with it - if !prev.input.is_empty() && cur.input.is_empty() { - *dirty = true; - } - if cur.turn_active != prev.turn_active { - *dirty = true; - } - if cur.scoring_in_flight != prev.scoring_in_flight { - if !cur.scoring_in_flight && prev.scoring_in_flight { - let _ = ui_tx.send(UiMessage::Info("[scoring complete]".into())); - } - *dirty = true; - } - if cur.compaction_in_flight != prev.compaction_in_flight { - if !cur.compaction_in_flight && prev.compaction_in_flight { - let _ = ui_tx.send(UiMessage::Info("[compacted]".into())); - } - *dirty = true; - } -} - -pub async fn run( - mut app: tui::App, - mind: &crate::mind::Mind, - mind_tx: tokio::sync::mpsc::UnboundedSender, - ui_tx: ui_channel::UiSender, - mut ui_rx: ui_channel::UiReceiver, -) -> Result<()> { - let agent = &mind.agent; - let shared_mind = &mind.shared; - let turn_watch = mind.turn_watch(); - // UI-owned state - let mut idle_state = crate::thalamus::idle::State::new(); - idle_state.load(); - - let (channel_tx, mut channel_rx) = tokio::sync::mpsc::channel::>(4); - { - let tx = channel_tx.clone(); - tokio::spawn(async move { - let result = crate::thalamus::channels::fetch_all_channels().await; - let _ = tx.send(result).await; - }); - } - let notify_rx = crate::thalamus::channels::subscribe_all(); - - // InteractScreen held separately for UiMessage routing - let mut interact = crate::user::chat::InteractScreen::new( - mind.agent.clone(), mind.shared.clone(), mind_tx.clone(), ui_tx.clone(), - ); - // Overlay screens: F2=conscious, F3=subconscious, F4=unconscious, F5=thalamus - let mut screens: Vec> = vec![ - Box::new(crate::user::context::ConsciousScreen::new()), - Box::new(crate::user::subconscious::SubconsciousScreen::new()), - Box::new(crate::user::unconscious::UnconsciousScreen::new()), - Box::new(crate::user::thalamus::ThalamusScreen::new()), - ]; - let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay - tui::set_screen_legend(tui::screen_legend_from(&interact, &screens)); - - 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; - let mut prev_mind = shared_mind.lock().unwrap().clone(); - let mut pending_key: Option = None; - - terminal.hide_cursor()?; - - let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into())); - - // Initial render - terminal.draw(|f| { - let area = f.area(); - interact.tick(f, area, None, &mut app); - })?; - - // Replay conversation after Mind init completes (non-blocking check) - let mut startup_done = false; - - loop { - tokio::select! { - biased; - - maybe_event = reader.next() => { - match maybe_event { - Some(Ok(Event::Key(key))) => { - if key.kind != KeyEventKind::Press { continue; } - idle_state.user_activity(); - - // F-keys switch screens - if let ratatui::crossterm::event::KeyCode::F(n) = key.code { - active_screen = match n { - 1 => 0, // interact - n @ 2..=5 if (n as usize - 2) < screens.len() => n as usize - 1, - _ => active_screen, - }; - if active_screen == 4 { // thalamus — refresh channels - 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; - continue; - } - - // Global keys (Ctrl combos) — only pass to screen if not consumed - if !app.handle_global_key(key) { - pending_key = Some(key); - } - dirty = true; - } - Some(Ok(Event::Mouse(_mouse))) => { - // TODO: route to active screen - dirty = true; - } - Some(Ok(Event::Resize(_w, _h))) => { - terminal.clear()?; - dirty = true; - } - Some(Err(_)) => break, - None => break, - _ => continue, - } - } - - - _ = render_interval.tick() => { - idle_state.decay_ewma(); - app.update_idle(&idle_state); - - // One-time: replay conversation after Mind init - if !startup_done { - if let Ok(ag) = agent.try_lock() { - if !ag.entries().is_empty() { - ui_channel::replay_session_to_ui(ag.entries(), &ui_tx); - let _ = ui_tx.send(UiMessage::Info("--- restored from conversation log ---".into())); - } - let _ = ui_tx.send(UiMessage::Info(format!(" model: {}", ag.model()))); - startup_done = true; - dirty = true; - } - } - - // Diff MindState — generate UI messages from changes - { - let cur = shared_mind.lock().unwrap(); - diff_mind_state(&cur, &prev_mind, &ui_tx, &mut dirty); - prev_mind = cur.clone(); - } - - 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() => { - interact.handle_ui_message(&msg, &mut app); - dirty = true; - } - } - - // Handle hotkey actions - let actions: Vec = app.hotkey_actions.drain(..).collect(); - for action in actions { - match action { - HotkeyAction::CycleReasoning => hotkey_cycle_reasoning(mind, &ui_tx), - HotkeyAction::KillProcess => hotkey_kill_processes(mind, &ui_tx).await, - HotkeyAction::Interrupt => { let _ = mind_tx.send(MindCommand::Interrupt); } - HotkeyAction::CycleAutonomy => hotkey_cycle_autonomy(mind, &ui_tx), - HotkeyAction::AdjustSampling(param, delta) => hotkey_adjust_sampling(mind, param, delta), - } - } - - // Drain UiMessages to interact screen - while let Ok(msg) = ui_rx.try_recv() { - interact.handle_ui_message(&msg, &mut app); - dirty = true; - } - - if dirty { - let key = pending_key.take(); - let mut screen_action = None; - if active_screen == 0 { - terminal.draw(|f| { - let area = f.area(); - screen_action = interact.tick(f, area, key, &mut app); - })?; - } else { - let screen = &mut screens[active_screen - 1]; - terminal.draw(|f| { - let area = f.area(); - screen_action = screen.tick(f, area, key, &mut app); - })?; - } - if let Some(action) = screen_action { - match action { - tui::ScreenAction::Switch(i) => { active_screen = i; dirty = true; continue; } - tui::ScreenAction::Hotkey(h) => app.hotkey_actions.push(h), - } - } - dirty = false; - } - - if app.should_quit { - break; - } - } - - tui::restore_terminal(&mut terminal)?; - Ok(()) -} diff --git a/src/user/mod.rs b/src/user/mod.rs index c0234b9..5108986 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -3,14 +3,21 @@ // TUI, UI channel, parsing. The cognitive layer (session state // machine, DMN, identity) lives in mind/. -pub mod ui_channel; -pub mod event_loop; - pub mod chat; pub mod context; pub mod subconscious; pub mod unconscious; pub mod thalamus; +pub mod ui_channel; + +use anyhow::Result; +use ratatui::crossterm::event::{Event, EventStream, KeyEventKind}; +use futures::StreamExt; +use std::time::Duration; + +use crate::mind::MindCommand; +use crate::user::{self as tui}; +use crate::user::ui_channel::UiMessage; // --- TUI infrastructure (moved from tui/mod.rs) --- @@ -175,6 +182,374 @@ pub fn restore_terminal(terminal: &mut ratatui::Terminal Result<()> { + let (config, _figment) = crate::config::load_session(&cli)?; + + if config.app.debug { + unsafe { std::env::set_var("POC_DEBUG", "1") }; + } + + let (ui_tx, ui_rx) = ui_channel::channel(); + let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1); + let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel(); + + let mind = crate::mind::Mind::new(config, ui_tx.clone(), turn_tx); + + let shared_context = mind.agent.lock().await.shared_context.clone(); + let shared_active_tools = mind.agent.lock().await.active_tools.clone(); + + let mut result = Ok(()); + tokio_scoped::scope(|s| { + // Mind event loop — init + run + s.spawn(async { + mind.init().await; + mind.run(mind_rx, turn_rx).await; + }); + + // UI event loop + s.spawn(async { + result = run( + tui::App::new(String::new(), shared_context, shared_active_tools), + &mind, mind_tx, ui_tx, ui_rx, + ).await; + }); + }); + result +} + +fn hotkey_cycle_reasoning(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { + if let Ok(mut ag) = mind.agent.try_lock() { + let next = match ag.reasoning_effort.as_str() { + "none" => "low", + "low" => "high", + _ => "none", + }; + ag.reasoning_effort = next.to_string(); + let label = match next { + "none" => "off (monologue hidden)", + "low" => "low (brief monologue)", + "high" => "high (full monologue)", + _ => next, + }; + let _ = ui_tx.send(UiMessage::Info(format!("Reasoning: {} — ^R to cycle", label))); + } else { + let _ = ui_tx.send(UiMessage::Info( + "(agent busy — reasoning change takes effect next turn)".into(), + )); + } +} + +async fn hotkey_kill_processes(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { + let active_tools = mind.agent.lock().await.active_tools.clone(); + let mut tools = active_tools.lock().unwrap(); + if tools.is_empty() { + let _ = ui_tx.send(UiMessage::Info("(no running tool calls)".into())); + } else { + for entry in tools.drain(..) { + let elapsed = entry.started.elapsed(); + let _ = ui_tx.send(UiMessage::Info(format!( + " killing {} ({:.0}s): {}", entry.name, elapsed.as_secs_f64(), entry.detail, + ))); + entry.handle.abort(); + } + } +} + +fn hotkey_cycle_autonomy(mind: &crate::mind::Mind, ui_tx: &ui_channel::UiSender) { + let mut s = mind.shared.lock().unwrap(); + let label = match &s.dmn { + crate::mind::dmn::State::Engaged | crate::mind::dmn::State::Working | crate::mind::dmn::State::Foraging => { + s.dmn = crate::mind::dmn::State::Resting { since: std::time::Instant::now() }; + "resting" + } + crate::mind::dmn::State::Resting { .. } => { + s.dmn = crate::mind::dmn::State::Paused; + "PAUSED" + } + crate::mind::dmn::State::Paused => { + crate::mind::dmn::set_off(true); + s.dmn = crate::mind::dmn::State::Off; + "OFF (persists across restarts)" + } + crate::mind::dmn::State::Off => { + crate::mind::dmn::set_off(false); + s.dmn = crate::mind::dmn::State::Foraging; + "foraging" + } + }; + s.dmn_turns = 0; + drop(s); + let _ = ui_tx.send(UiMessage::Info(format!("DMN → {} (Ctrl+P to cycle)", label))); +} + +fn hotkey_adjust_sampling(mind: &crate::mind::Mind, param: usize, delta: f32) { + if let Ok(mut ag) = mind.agent.try_lock() { + match param { + 0 => ag.temperature = (ag.temperature + delta).clamp(0.0, 2.0), + 1 => ag.top_p = (ag.top_p + delta).clamp(0.0, 1.0), + 2 => ag.top_k = (ag.top_k as f32 + delta).max(0.0) as u32, + _ => {} + } + } +} + +pub fn send_context_info(config: &crate::config::SessionConfig, ui_tx: &ui_channel::UiSender) { + let context_groups = crate::config::get().context_groups.clone(); + let (instruction_files, memory_files) = crate::mind::identity::context_file_info( + &config.prompt_file, + config.app.memory_project.as_deref(), + &context_groups, + ); + let _ = ui_tx.send(UiMessage::Info(format!( + " context: {}K chars ({} config, {} memory files)", + config.context_parts.iter().map(|(_, c)| c.len()).sum::() / 1024, + instruction_files.len(), memory_files.len(), + ))); +} + +fn diff_mind_state( + cur: &crate::mind::MindState, + prev: &crate::mind::MindState, + ui_tx: &ui_channel::UiSender, + dirty: &mut bool, +) { + if cur.dmn.label() != prev.dmn.label() || cur.dmn_turns != prev.dmn_turns { + let _ = ui_tx.send(UiMessage::StatusUpdate(ui_channel::StatusInfo { + dmn_state: cur.dmn.label().to_string(), + dmn_turns: cur.dmn_turns, + dmn_max_turns: cur.max_dmn_turns, + prompt_tokens: 0, completion_tokens: 0, + model: String::new(), turn_tools: 0, + context_budget: String::new(), + })); + *dirty = true; + } + // Input consumed — Mind started a turn with it + if !prev.input.is_empty() && cur.input.is_empty() { + *dirty = true; + } + if cur.turn_active != prev.turn_active { + *dirty = true; + } + if cur.scoring_in_flight != prev.scoring_in_flight { + if !cur.scoring_in_flight && prev.scoring_in_flight { + let _ = ui_tx.send(UiMessage::Info("[scoring complete]".into())); + } + *dirty = true; + } + if cur.compaction_in_flight != prev.compaction_in_flight { + if !cur.compaction_in_flight && prev.compaction_in_flight { + let _ = ui_tx.send(UiMessage::Info("[compacted]".into())); + } + *dirty = true; + } +} + +pub async fn run( + mut app: tui::App, + mind: &crate::mind::Mind, + mind_tx: tokio::sync::mpsc::UnboundedSender, + ui_tx: ui_channel::UiSender, + mut ui_rx: ui_channel::UiReceiver, +) -> Result<()> { + let agent = &mind.agent; + let shared_mind = &mind.shared; + let turn_watch = mind.turn_watch(); + // UI-owned state + let mut idle_state = crate::thalamus::idle::State::new(); + idle_state.load(); + + let (channel_tx, mut channel_rx) = tokio::sync::mpsc::channel::>(4); + { + let tx = channel_tx.clone(); + tokio::spawn(async move { + let result = crate::thalamus::channels::fetch_all_channels().await; + let _ = tx.send(result).await; + }); + } + let notify_rx = crate::thalamus::channels::subscribe_all(); + + // InteractScreen held separately for UiMessage routing + let mut interact = crate::user::chat::InteractScreen::new( + mind.agent.clone(), mind.shared.clone(), mind_tx.clone(), ui_tx.clone(), + ); + // Overlay screens: F2=conscious, F3=subconscious, F4=unconscious, F5=thalamus + let mut screens: Vec> = vec![ + Box::new(crate::user::context::ConsciousScreen::new()), + Box::new(crate::user::subconscious::SubconsciousScreen::new()), + Box::new(crate::user::unconscious::UnconsciousScreen::new()), + Box::new(crate::user::thalamus::ThalamusScreen::new()), + ]; + let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay + tui::set_screen_legend(tui::screen_legend_from(&interact, &screens)); + + 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; + let mut prev_mind = shared_mind.lock().unwrap().clone(); + let mut pending_key: Option = None; + + terminal.hide_cursor()?; + + let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into())); + + // Initial render + terminal.draw(|f| { + let area = f.area(); + interact.tick(f, area, None, &mut app); + })?; + + // Replay conversation after Mind init completes (non-blocking check) + let mut startup_done = false; + + loop { + tokio::select! { + biased; + + maybe_event = reader.next() => { + match maybe_event { + Some(Ok(Event::Key(key))) => { + if key.kind != KeyEventKind::Press { continue; } + idle_state.user_activity(); + + // F-keys switch screens + if let ratatui::crossterm::event::KeyCode::F(n) = key.code { + active_screen = match n { + 1 => 0, // interact + n @ 2..=5 if (n as usize - 2) < screens.len() => n as usize - 1, + _ => active_screen, + }; + if active_screen == 4 { // thalamus — refresh channels + 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; + continue; + } + + // Global keys (Ctrl combos) — only pass to screen if not consumed + if !app.handle_global_key(key) { + pending_key = Some(key); + } + dirty = true; + } + Some(Ok(Event::Mouse(_mouse))) => { + // TODO: route to active screen + dirty = true; + } + Some(Ok(Event::Resize(_w, _h))) => { + terminal.clear()?; + dirty = true; + } + Some(Err(_)) => break, + None => break, + _ => continue, + } + } + + + _ = render_interval.tick() => { + idle_state.decay_ewma(); + app.update_idle(&idle_state); + + // One-time: replay conversation after Mind init + if !startup_done { + if let Ok(ag) = agent.try_lock() { + if !ag.entries().is_empty() { + ui_channel::replay_session_to_ui(ag.entries(), &ui_tx); + let _ = ui_tx.send(UiMessage::Info("--- restored from conversation log ---".into())); + } + let _ = ui_tx.send(UiMessage::Info(format!(" model: {}", ag.model()))); + startup_done = true; + dirty = true; + } + } + + // Diff MindState — generate UI messages from changes + { + let cur = shared_mind.lock().unwrap(); + diff_mind_state(&cur, &prev_mind, &ui_tx, &mut dirty); + prev_mind = cur.clone(); + } + + 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() => { + interact.handle_ui_message(&msg, &mut app); + dirty = true; + } + } + + // Handle hotkey actions + let actions: Vec = app.hotkey_actions.drain(..).collect(); + for action in actions { + match action { + HotkeyAction::CycleReasoning => hotkey_cycle_reasoning(mind, &ui_tx), + HotkeyAction::KillProcess => hotkey_kill_processes(mind, &ui_tx).await, + HotkeyAction::Interrupt => { let _ = mind_tx.send(MindCommand::Interrupt); } + HotkeyAction::CycleAutonomy => hotkey_cycle_autonomy(mind, &ui_tx), + HotkeyAction::AdjustSampling(param, delta) => hotkey_adjust_sampling(mind, param, delta), + } + } + + // Drain UiMessages to interact screen + while let Ok(msg) = ui_rx.try_recv() { + interact.handle_ui_message(&msg, &mut app); + dirty = true; + } + + if dirty { + let key = pending_key.take(); + let mut screen_action = None; + if active_screen == 0 { + terminal.draw(|f| { + let area = f.area(); + screen_action = interact.tick(f, area, key, &mut app); + })?; + } else { + let screen = &mut screens[active_screen - 1]; + terminal.draw(|f| { + let area = f.area(); + screen_action = screen.tick(f, area, key, &mut app); + })?; + } + if let Some(action) = screen_action { + match action { + tui::ScreenAction::Switch(i) => { active_screen = i; dirty = true; continue; } + tui::ScreenAction::Hotkey(h) => app.hotkey_actions.push(h), + } + } + dirty = false; + } + + if app.should_quit { + break; + } + } + + tui::restore_terminal(&mut terminal)?; + Ok(()) +} + // --- CLI --- use clap::{Parser, Subcommand}; @@ -260,7 +635,7 @@ pub async fn main() { return; } - if let Err(e) = crate::user::event_loop::start(cli).await { + if let Err(e) = start(cli).await { let _ = ratatui::crossterm::terminal::disable_raw_mode(); let _ = ratatui::crossterm::execute!( std::io::stdout(),