From 85aafd206cbf8942b1314850a7ce7e8e67c95cb7 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 7 Apr 2026 01:59:09 -0400 Subject: [PATCH] Subconscious screen: show AutoAgent state F3 screen now displays SubconsciousSnapshot from Mind's AutoAgents instead of the old process-based AgentSnapshot. Shows running status (phase + turn), last run time, and walked key count. Co-Authored-By: Proof of Concept --- src/mind/mod.rs | 28 +++++++ src/user/mod.rs | 3 +- src/user/subconscious.rs | 162 ++++++++++++++------------------------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index aac6e7c..2505660 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -91,6 +91,30 @@ impl SubconsciousAgent { } } +/// Lightweight snapshot of subconscious agent state for the TUI. +#[derive(Clone, Default)] +pub struct SubconsciousSnapshot { + pub name: String, + pub running: bool, + pub current_phase: String, + pub turn: usize, + pub walked_count: usize, + pub last_run_secs_ago: Option, +} + +impl SubconsciousAgent { + fn snapshot(&self) -> SubconsciousSnapshot { + SubconsciousSnapshot { + name: self.auto.name.clone(), + running: self.is_running(), + current_phase: self.auto.current_phase.clone(), + turn: self.auto.turn, + walked_count: self.auto.walked.len(), + last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), + } + } +} + /// Which pane streaming text should go to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StreamTarget { @@ -316,6 +340,10 @@ impl Mind { } /// Initialize — restore log, start daemons and background agents. + pub async fn subconscious_snapshots(&self) -> Vec { + self.subconscious.lock().await.iter().map(|s| s.snapshot()).collect() + } + pub async fn init(&self) { // Restore conversation let mut ag = self.agent.lock().await; diff --git a/src/user/mod.rs b/src/user/mod.rs index 888a9d3..ea814d0 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -127,7 +127,7 @@ pub struct App { pub submitted: Vec, pub(crate) context_info: Option, pub(crate) shared_context: SharedContextState, - pub(crate) agent_state: Vec, + pub(crate) agent_state: Vec, pub(crate) channel_status: Vec, pub(crate) idle_info: Option, } @@ -406,6 +406,7 @@ pub async fn run( // State sync on every wake idle_state.decay_ewma(); app.update_idle(&idle_state); + app.agent_state = mind.subconscious_snapshots().await; if !startup_done { if let Ok(mut ag) = agent.try_lock() { let model = ag.model().to_string(); diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 43af16e..10407f8 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -13,13 +13,12 @@ use super::{App, ScreenView, screen_legend}; pub(crate) struct SubconsciousScreen { selected: usize, - log_view: bool, scroll: u16, } impl SubconsciousScreen { pub fn new() -> Self { - Self { selected: 0, log_view: false, scroll: 0 } + Self { selected: 0, scroll: 0 } } } @@ -28,73 +27,79 @@ impl ScreenView for SubconsciousScreen { fn tick(&mut self, frame: &mut Frame, area: Rect, events: &[ratatui::crossterm::event::Event], app: &mut App) { - // Handle keys for event in events { if let ratatui::crossterm::event::Event::Key(key) = event { if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; } - match key.code { - KeyCode::Up if !self.log_view => { - self.selected = self.selected.saturating_sub(1); + match key.code { + KeyCode::Up => { + self.selected = self.selected.saturating_sub(1); + } + KeyCode::Down => { + self.selected = (self.selected + 1) + .min(app.agent_state.len().saturating_sub(1)); + } + KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); } + KeyCode::PageDown => { self.scroll += 20; } + _ => {} } - KeyCode::Down if !self.log_view => { - self.selected = (self.selected + 1).min(app.agent_state.len().saturating_sub(1)); - } - KeyCode::Enter | KeyCode::Right if !self.log_view => { - self.log_view = true; - self.scroll = 0; - } - KeyCode::Esc | KeyCode::Left if self.log_view => { - self.log_view = false; - } - KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); } - KeyCode::PageDown => { self.scroll += 20; } - _ => {} - } } } - // Draw - if self.log_view { - self.draw_log(frame, area, app); - } else { - self.draw_list(frame, area, app); - } - } -} - -impl SubconsciousScreen { - fn draw_list(&self, frame: &mut Frame, area: Rect, app: &App) { let mut lines: Vec = Vec::new(); let section = Style::default().fg(Color::Yellow); let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); lines.push(Line::raw("")); lines.push(Line::styled("── Subconscious Agents ──", section)); - lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint)); + lines.push(Line::styled(" (↑/↓ select)", hint)); lines.push(Line::raw("")); - for (i, agent) in app.agent_state.iter().enumerate() { + if app.agent_state.is_empty() { + lines.push(Line::styled(" (no agents loaded)", hint)); + } + + for (i, snap) in app.agent_state.iter().enumerate() { let selected = i == self.selected; let prefix = if selected { "▸ " } else { " " }; - let bg = if selected { Style::default().bg(Color::DarkGray) } else { Style::default() }; - - let status = match (&agent.pid, &agent.phase) { - (Some(pid), Some(phase)) => vec![ - Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Green)), - Span::styled("● ", bg.fg(Color::Green)), - Span::styled(format!("pid {} {}", pid, phase), bg), - ], - (None, Some(phase)) => vec![ - Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Cyan)), - Span::styled("◆ ", bg.fg(Color::Cyan)), - Span::styled(phase.clone(), bg), - ], - _ => vec![ - Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Gray)), - Span::styled("○ idle", bg.fg(Color::DarkGray)), - ], + let bg = if selected { + Style::default().bg(Color::DarkGray) + } else { + Style::default() }; - lines.push(Line::from(status)); + + let status_spans = if snap.running { + vec![ + Span::styled( + format!("{}{:<30}", prefix, snap.name), + bg.fg(Color::Green), + ), + Span::styled("● ", bg.fg(Color::Green)), + Span::styled( + format!("phase: {} turn: {}", snap.current_phase, snap.turn), + bg, + ), + ] + } else { + let ago = snap.last_run_secs_ago + .map(|s| { + if s < 60.0 { format!("{:.0}s ago", s) } + else if s < 3600.0 { format!("{:.0}m ago", s / 60.0) } + else { format!("{:.1}h ago", s / 3600.0) } + }) + .unwrap_or_else(|| "never".to_string()); + vec![ + Span::styled( + format!("{}{:<30}", prefix, snap.name), + bg.fg(Color::Gray), + ), + Span::styled("○ ", bg.fg(Color::DarkGray)), + Span::styled( + format!("idle last: {} walked: {}", ago, snap.walked_count), + bg.fg(Color::DarkGray), + ), + ] + }; + lines.push(Line::from(status_spans)); } let block = Block::default() @@ -103,61 +108,6 @@ impl SubconsciousScreen { .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)); - let para = Paragraph::new(lines).block(block).scroll((self.scroll, 0)); - frame.render_widget(para, area); - } - - fn draw_log(&self, frame: &mut Frame, area: Rect, app: &App) { - let agent = app.agent_state.get(self.selected); - let name = agent.map(|a| a.name.as_str()).unwrap_or("?"); - let mut lines: Vec = Vec::new(); - let section = Style::default().fg(Color::Yellow); - let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); - - lines.push(Line::raw("")); - lines.push(Line::styled(format!("── {} ──", name), section)); - lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint)); - lines.push(Line::raw("")); - - match agent.and_then(|a| a.pid) { - Some(pid) => { - let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?"); - lines.push(Line::from(vec![ - Span::styled(" Status: ", Style::default()), - Span::styled(format!("● running pid {} phase: {}", pid, phase), - Style::default().fg(Color::Green)), - ])); - } - None => { - lines.push(Line::styled(" Status: idle", Style::default().fg(Color::DarkGray))); - } - } - - if let Some(log_path) = agent.and_then(|a| a.log_path.as_ref()) { - lines.push(Line::raw(format!(" Log: {}", log_path.display()))); - } - lines.push(Line::raw("")); - - lines.push(Line::styled("── Agent Log ──", section)); - if let Some(content) = agent - .and_then(|a| a.log_path.as_ref()) - .and_then(|p| std::fs::read_to_string(p).ok()) - { - let log_lines: Vec<&str> = content.lines().collect(); - let start = log_lines.len().saturating_sub(40); - for line in &log_lines[start..] { - lines.push(Line::raw(format!(" {}", line))); - } - } else { - lines.push(Line::styled(" (no log available)", hint)); - } - - let block = Block::default() - .title_top(Line::from(screen_legend()).left_aligned()) - .title_top(Line::from(format!(" {} ", name)).right_aligned()) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); - let para = Paragraph::new(lines) .block(block) .wrap(Wrap { trim: false })