// subconscious_screen.rs — F3 subconscious agent overlay use ratatui::{ layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, crossterm::event::KeyCode, }; use super::{App, ScreenView, screen_legend}; use crate::agent::context::ConversationEntry; use crate::agent::api::Role; pub(crate) struct SubconsciousScreen { selected: usize, detail: bool, scroll: u16, } impl SubconsciousScreen { pub fn new() -> Self { Self { selected: 0, detail: false, scroll: 0 } } } impl ScreenView for SubconsciousScreen { fn label(&self) -> &'static str { "subconscious" } fn tick(&mut self, frame: &mut Frame, area: Rect, events: &[ratatui::crossterm::event::Event], app: &mut App) { 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.detail => { self.selected = self.selected.saturating_sub(1); } KeyCode::Down if !self.detail => { self.selected = (self.selected + 1) .min(app.agent_state.len().saturating_sub(1)); } KeyCode::Enter | KeyCode::Right if !self.detail => { self.detail = true; self.scroll = 0; } KeyCode::Esc | KeyCode::Left if self.detail => { self.detail = false; } KeyCode::Up if self.detail => { self.scroll = self.scroll.saturating_sub(3); } KeyCode::Down if self.detail => { self.scroll += 3; } KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); } KeyCode::PageDown => { self.scroll += 20; } _ => {} } } } if self.detail { self.draw_detail(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("")); let walked = app.walked_count; lines.push(Line::styled( format!("── Subconscious Agents ── walked: {}", walked), section)); lines.push(Line::styled(" (↑/↓ select, Enter view log)", hint)); lines.push(Line::raw("")); 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_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()); let entries = snap.forked_agent.as_ref() .and_then(|a| a.try_lock().ok()) .map(|ag| ag.context.entries.len().saturating_sub(snap.fork_point)) .unwrap_or(0); vec![ Span::styled( format!("{}{:<30}", prefix, snap.name), bg.fg(Color::Gray), ), Span::styled("○ ", bg.fg(Color::DarkGray)), Span::styled( format!("idle last: {} entries: {}", ago, entries), bg.fg(Color::DarkGray), ), ] }; lines.push(Line::from(status_spans)); } let block = Block::default() .title_top(Line::from(screen_legend()).left_aligned()) .title_top(Line::from(" subconscious ").right_aligned()) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)); let para = Paragraph::new(lines) .block(block) .wrap(Wrap { trim: false }) .scroll((self.scroll, 0)); frame.render_widget(para, area); } fn draw_detail(&self, frame: &mut Frame, area: Rect, app: &App) { let snap = match app.agent_state.get(self.selected) { Some(s) => s, None => return, }; 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!("── {} ──", snap.name), section)); lines.push(Line::styled(" (Esc/← back, ↑/↓/PgUp/PgDn scroll)", hint)); lines.push(Line::raw("")); // Read entries from the forked agent (from fork point onward) let entries: Vec = snap.forked_agent.as_ref() .and_then(|agent| agent.try_lock().ok()) .map(|ag| ag.context.entries.get(snap.fork_point..).unwrap_or(&[]).to_vec()) .unwrap_or_default(); if entries.is_empty() { lines.push(Line::styled(" (no run data)", hint)); } for entry in &entries { if entry.is_log() { if let ConversationEntry::Log(text) = entry { lines.push(Line::styled( format!(" [log] {}", text), Style::default().fg(Color::DarkGray), )); } continue; } let msg = entry.message(); let (role_str, role_color) = match msg.role { Role::User => ("user", Color::Cyan), Role::Assistant => ("assistant", Color::Reset), Role::Tool => ("tool", Color::DarkGray), Role::System => ("system", Color::Yellow), }; let text = msg.content_text(); let tool_info = msg.tool_calls.as_ref().map(|tc| { tc.iter().map(|c| c.function.name.as_str()) .collect::>().join(", ") }); let header = match &tool_info { Some(tools) => format!(" [{} → {}]", role_str, tools), None => format!(" [{}]", role_str), }; lines.push(Line::styled(header, Style::default().fg(role_color))); if !text.is_empty() { for line in text.lines().take(20) { lines.push(Line::styled( format!(" {}", line), Style::default().fg(Color::Gray), )); } if text.lines().count() > 20 { lines.push(Line::styled( format!(" ... ({} more lines)", text.lines().count() - 20), hint, )); } } } let block = Block::default() .title_top(Line::from(screen_legend()).left_aligned()) .title_top(Line::from(format!(" {} ", snap.name)).right_aligned()) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)); let para = Paragraph::new(lines) .block(block) .wrap(Wrap { trim: false }) .scroll((self.scroll, 0)); frame.render_widget(para, area); } }