diff --git a/src/bin/consciousness.rs b/src/bin/consciousness.rs index c369cc0..d014b1c 100644 --- a/src/bin/consciousness.rs +++ b/src/bin/consciousness.rs @@ -810,6 +810,10 @@ async fn run(cli: cli::CliArgs) -> Result<()> { channel_supervisor.load_config(); channel_supervisor.ensure_running(); + // Initialize idle state machine + let mut idle_state = poc_memory::thalamus::idle::State::new(); + idle_state.load(); + // Create UI channel let (ui_tx, mut ui_rx) = ui_channel::channel(); @@ -935,6 +939,7 @@ async fn run(cli: cli::CliArgs) -> Result<()> { continue; } app.handle_key(key); + idle_state.user_activity(); dirty = true; } Some(Ok(Event::Mouse(mouse))) => { @@ -961,16 +966,20 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // Turn completed in background task Some((result, target)) = turn_rx.recv() => { session.handle_turn_result(result, target).await; + idle_state.response_activity(); dirty = true; } - // Render tick + // Render tick — update periodic state _ = render_interval.tick() => { let new_count = session.process_tracker.list().await.len() as u32; if new_count != app.running_processes { app.running_processes = new_count; dirty = true; } + // Update idle state for F5 screen + idle_state.decay_ewma(); + app.update_idle(&idle_state); } // DMN timer (only when no turn is running) diff --git a/src/user/tui/mod.rs b/src/user/tui/mod.rs index 6b9b122..458ae51 100644 --- a/src/user/tui/mod.rs +++ b/src/user/tui/mod.rs @@ -370,6 +370,19 @@ pub struct App { pub(crate) agent_state: Vec, /// Cached channel info for F5 screen (refreshed on status tick). pub(crate) channel_status: Vec, + /// Cached idle state for F5 screen. + pub(crate) idle_info: Option, +} + +/// Snapshot of thalamus idle state for display. +#[derive(Clone)] +pub(crate) struct IdleInfo { + pub user_present: bool, + pub since_activity: f64, + pub activity_ewma: f64, + pub block_reason: String, + pub dreaming: bool, + pub sleeping: bool, } /// Channel info for display on F5 screen. @@ -450,6 +463,7 @@ impl App { agent_log_view: false, agent_state: Vec::new(), channel_status: Vec::new(), + idle_info: None, } } @@ -885,12 +899,25 @@ impl App { self.channel_status = status; } + /// Snapshot idle state for F5 display. + pub fn update_idle(&mut self, state: &crate::thalamus::idle::State) { + self.idle_info = Some(IdleInfo { + user_present: state.user_present(), + since_activity: state.since_activity(), + activity_ewma: state.activity_ewma, + block_reason: state.block_reason().to_string(), + dreaming: state.dreaming, + sleeping: state.sleep_until.is_some(), + }); + } + pub(crate) fn set_screen(&mut self, screen: Screen) { self.screen = screen; self.debug_scroll = 0; // Refresh data for status screens on entry match screen { Screen::Thalamus => self.refresh_channels(), + // idle_info is updated from the event loop, not here _ => {} } } diff --git a/src/user/tui/thalamus_screen.rs b/src/user/tui/thalamus_screen.rs index c080f9b..c17288d 100644 --- a/src/user/tui/thalamus_screen.rs +++ b/src/user/tui/thalamus_screen.rs @@ -1,7 +1,7 @@ -// thalamus_screen.rs — F5: attention routing and channel status +// thalamus_screen.rs — F5: presence, idle state, and channel status // -// Shows presence/idle/activity status, then channel status. -// Channel data is cached on App and refreshed on screen entry. +// Shows idle state from the in-process thalamus (no subprocess spawn), +// then channel daemon status from cached data. use ratatui::{ layout::Rect, @@ -13,38 +13,40 @@ use ratatui::{ use super::{App, SCREEN_LEGEND}; -fn fetch_daemon_status() -> Vec { - std::process::Command::new("poc-daemon") - .arg("status") - .output() - .ok() - .and_then(|o| { - if o.status.success() { - String::from_utf8(o.stdout).ok() - } else { - None - } - }) - .map(|s| s.lines().map(String::from).collect()) - .unwrap_or_default() -} - impl App { pub(crate) fn draw_thalamus(&self, frame: &mut Frame, size: Rect) { let section = Style::default().fg(Color::Yellow); let dim = Style::default().fg(Color::DarkGray); let mut lines: Vec = Vec::new(); - // Presence status - let daemon_status = fetch_daemon_status(); - if !daemon_status.is_empty() { - lines.push(Line::styled("── Presence ──", section)); - lines.push(Line::raw("")); - for line in &daemon_status { - lines.push(Line::raw(format!(" {}", line))); + // Presence / idle state from in-process thalamus + lines.push(Line::styled("── Presence ──", section)); + lines.push(Line::raw("")); + + if let Some(ref idle) = self.idle_info { + let presence = if idle.user_present { + Span::styled("present", Style::default().fg(Color::Green)) + } else { + Span::styled("away", Style::default().fg(Color::DarkGray)) + }; + lines.push(Line::from(vec![ + Span::raw(" User: "), + presence, + Span::raw(format!(" (last {:.0}s ago)", idle.since_activity)), + ])); + lines.push(Line::raw(format!(" Activity: {:.1}%", idle.activity_ewma * 100.0))); + lines.push(Line::raw(format!(" Idle state: {}", idle.block_reason))); + + if idle.dreaming { + lines.push(Line::styled(" ◆ dreaming", Style::default().fg(Color::Magenta))); } - lines.push(Line::raw("")); + if idle.sleeping { + lines.push(Line::styled(" ◆ sleeping", Style::default().fg(Color::Blue))); + } + } else { + lines.push(Line::styled(" not initialized", dim)); } + lines.push(Line::raw("")); // Channel status from cached data lines.push(Line::styled("── Channels ──", section));