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>
166 lines
6.3 KiB
Rust
166 lines
6.3 KiB
Rust
// 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, KeyEvent},
|
|
};
|
|
|
|
use super::{App, ScreenAction, 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 }
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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::raw(""));
|
|
|
|
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)) => 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(" subconscious ").right_aligned())
|
|
.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<Line> = 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 })
|
|
.scroll((self.scroll, 0));
|
|
frame.render_widget(para, area);
|
|
}
|
|
}
|