user: ScreenView trait, overlay screens extracted from App

Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.

- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll

Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.

App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.

InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 17:54:40 -04:00
parent 7458fe655f
commit 927cddd864
8 changed files with 388 additions and 439 deletions

View file

@ -1,7 +1,4 @@
// subconscious_screen.rs — F3 subconscious agent overlay
//
// Shows agent list with status indicators, and a detail view
// with log tail for the selected agent.
use ratatui::{
layout::Rect,
@ -9,19 +6,63 @@ use ratatui::{
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
crossterm::event::{KeyCode, KeyEvent},
};
use super::{App, SCREEN_LEGEND};
use super::{App, ScreenAction, ScreenView, screen_legend};
impl App {
pub(crate) fn draw_agents(&self, frame: &mut Frame, size: Rect) {
let output_dir = crate::store::memory_dir().join("agent-output");
pub(crate) struct SubconsciousScreen {
selected: usize,
log_view: bool,
scroll: u16,
}
if self.agent_log_view {
self.draw_agent_log(frame, size, &output_dir);
return;
impl SubconsciousScreen {
pub fn new() -> Self {
Self { selected: 0, log_view: false, scroll: 0 }
}
}
impl ScreenView for SubconsciousScreen {
fn label(&self) -> &'static str { "subconscious" }
fn tick(&mut self, frame: &mut Frame, area: Rect,
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
// Handle keys
if let Some(key) = key {
match key.code {
KeyCode::Up if !self.log_view => {
self.selected = self.selected.saturating_sub(1);
}
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::Esc if !self.log_view => return Some(ScreenAction::Switch(0)),
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);
}
None
}
}
impl SubconsciousScreen {
fn draw_list(&self, frame: &mut Frame, area: Rect, app: &App) {
let mut lines: Vec<Line> = Vec::new();
let section = Style::default().fg(Color::Yellow);
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
@ -31,51 +72,42 @@ impl App {
lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint));
lines.push(Line::raw(""));
for (i, agent) in self.agent_state.iter().enumerate() {
let selected = i == self.agent_selected;
for (i, agent) 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)) => {
// No pid but has phase — async task (e.g. memory-scoring)
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)),
]
}
(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)),
],
};
lines.push(Line::from(status));
}
let block = Block::default()
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
.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)
.scroll((self.debug_scroll, 0));
frame.render_widget(para, size);
let para = Paragraph::new(lines).block(block).scroll((self.scroll, 0));
frame.render_widget(para, area);
}
fn draw_agent_log(&self, frame: &mut Frame, size: Rect, _output_dir: &std::path::Path) {
let agent = self.agent_state.get(self.agent_selected);
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<Line> = Vec::new();
let section = Style::default().fg(Color::Yellow);
@ -86,7 +118,6 @@ impl App {
lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint));
lines.push(Line::raw(""));
// Show pid status from state
match agent.and_then(|a| a.pid) {
Some(pid) => {
let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?");
@ -101,13 +132,11 @@ impl App {
}
}
// Show log path
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(""));
// Show agent log tail
lines.push(Line::styled("── Agent Log ──", section));
if let Some(content) = agent
.and_then(|a| a.log_path.as_ref())
@ -123,7 +152,7 @@ impl App {
}
let block = Block::default()
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
.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));
@ -131,7 +160,7 @@ impl App {
let para = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false })
.scroll((self.debug_scroll, 0));
frame.render_widget(para, size);
.scroll((self.scroll, 0));
frame.render_widget(para, area);
}
}