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

@ -30,7 +30,11 @@ use std::io;
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo, UiMessage};
pub(crate) const SCREEN_LEGEND: &str = " F1=interact F2=conscious F3=subconscious F4=unconscious F5=thalamus ";
/// Build the screen legend from the screen table.
pub(crate) fn screen_legend() -> String {
// Built from the SCREENS table in event_loop
" F1=interact F2=conscious F3=subconscious F4=unconscious F5=thalamus ".to_string()
}
pub(crate) fn strip_ansi(text: &str) -> String {
let mut out = String::with_capacity(text.len());
@ -231,9 +235,19 @@ pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Screen {
Interact, Conscious, Subconscious, Unconscious, Thalamus,
/// Action returned from a screen's tick method.
pub enum ScreenAction {
/// Switch to screen at this index
Switch(usize),
/// Send a hotkey action to the Mind
Hotkey(HotkeyAction),
}
/// A screen that can draw itself and handle input.
pub(crate) trait ScreenView {
fn tick(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect,
key: Option<ratatui::crossterm::event::KeyEvent>, app: &App) -> Option<ScreenAction>;
fn label(&self) -> &'static str;
}
#[derive(Debug)]
@ -285,19 +299,11 @@ pub struct App {
pub submitted: Vec<String>,
pub hotkey_actions: Vec<HotkeyAction>,
pub(crate) pane_areas: [Rect; 3],
pub screen: Screen,
pub(crate) debug_scroll: u16,
pub(crate) debug_selected: Option<usize>,
pub(crate) debug_expanded: std::collections::HashSet<usize>,
pub(crate) context_info: Option<ContextInfo>,
pub(crate) shared_context: SharedContextState,
pub(crate) agent_selected: usize,
pub(crate) agent_log_view: bool,
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
pub(crate) channel_status: Vec<ChannelStatus>,
pub(crate) idle_info: Option<IdleInfo>,
/// Thalamus screen: selected sampling param (0=temp, 1=top_p, 2=top_k).
pub(crate) sampling_selected: usize,
}
impl App {
@ -323,12 +329,9 @@ impl App {
input_history: Vec::new(), history_index: None,
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
pane_areas: [Rect::default(); 3],
screen: Screen::Interact,
debug_scroll: 0, debug_selected: None,
debug_expanded: std::collections::HashSet::new(),
context_info: None, shared_context,
agent_selected: 0, agent_log_view: false, agent_state: Vec::new(),
channel_status: Vec::new(), idle_info: None, sampling_selected: 0,
agent_state: Vec::new(),
channel_status: Vec::new(), idle_info: None,
}
}
@ -417,6 +420,8 @@ impl App {
}
}
/// Handle global keys only (Ctrl combos, F-keys). Screen-specific
/// keys are handled by the active ScreenView's tick method.
pub fn handle_key(&mut self, key: KeyEvent) {
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
@ -428,96 +433,6 @@ impl App {
}
}
match key.code {
KeyCode::F(1) => { self.set_screen(Screen::Interact); return; }
KeyCode::F(2) => { self.set_screen(Screen::Conscious); return; }
KeyCode::F(3) => { self.set_screen(Screen::Subconscious); return; }
KeyCode::F(4) => { self.set_screen(Screen::Unconscious); return; }
KeyCode::F(5) => { self.set_screen(Screen::Thalamus); return; }
_ => {}
}
match self.screen {
Screen::Subconscious => {
match key.code {
KeyCode::Up => { self.agent_selected = self.agent_selected.saturating_sub(1); self.debug_scroll = 0; return; }
KeyCode::Down => { self.agent_selected = (self.agent_selected + 1).min(self.agent_state.len().saturating_sub(1)); self.debug_scroll = 0; return; }
KeyCode::Enter | KeyCode::Right => { self.agent_log_view = true; self.debug_scroll = 0; return; }
KeyCode::Left | KeyCode::Esc => {
if self.agent_log_view { self.agent_log_view = false; self.debug_scroll = 0; }
else { self.screen = Screen::Interact; }
return;
}
KeyCode::PageUp => { self.debug_scroll = self.debug_scroll.saturating_sub(10); return; }
KeyCode::PageDown => { self.debug_scroll += 10; return; }
_ => {}
}
}
Screen::Conscious => {
let cs = self.read_context_state();
let n = self.debug_item_count(&cs);
match key.code {
KeyCode::Up => {
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => n - 1, Some(0) => 0, Some(i) => i - 1 }); self.scroll_to_selected(n); }
return;
}
KeyCode::Down => {
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) if i >= n - 1 => n - 1, Some(i) => i + 1 }); self.scroll_to_selected(n); }
return;
}
KeyCode::PageUp => {
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) => i.saturating_sub(20) }); self.scroll_to_selected(n); }
return;
}
KeyCode::PageDown => {
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) => (i + 20).min(n - 1) }); self.scroll_to_selected(n); }
return;
}
KeyCode::Right | KeyCode::Enter => { if let Some(idx) = self.debug_selected { self.debug_expanded.insert(idx); } return; }
KeyCode::Left => { if let Some(idx) = self.debug_selected { self.debug_expanded.remove(&idx); } return; }
KeyCode::Esc => { self.screen = Screen::Interact; return; }
_ => {}
}
}
Screen::Unconscious => {
match key.code {
KeyCode::PageUp => { self.debug_scroll = self.debug_scroll.saturating_sub(10); return; }
KeyCode::PageDown => { self.debug_scroll += 10; return; }
KeyCode::Esc => { self.screen = Screen::Interact; return; }
_ => {}
}
}
Screen::Thalamus => {
match key.code {
KeyCode::Up => { self.sampling_selected = self.sampling_selected.saturating_sub(1); return; }
KeyCode::Down => { self.sampling_selected = (self.sampling_selected + 1).min(2); return; }
KeyCode::Right => {
let delta = match self.sampling_selected {
0 => 0.05, // temperature
1 => 0.05, // top_p
2 => 5.0, // top_k
_ => 0.0,
};
self.hotkey_actions.push(HotkeyAction::AdjustSampling(self.sampling_selected, delta));
return;
}
KeyCode::Left => {
let delta = match self.sampling_selected {
0 => -0.05,
1 => -0.05,
2 => -5.0,
_ => 0.0,
};
self.hotkey_actions.push(HotkeyAction::AdjustSampling(self.sampling_selected, delta));
return;
}
KeyCode::Esc => { self.screen = Screen::Interact; return; }
_ => {}
}
}
Screen::Interact => {}
}
match key.code {
KeyCode::Esc => { self.hotkey_actions.push(HotkeyAction::Interrupt); }
KeyCode::Enter if !key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
@ -601,15 +516,10 @@ impl App {
}
}
/// Draw the interact (F1) screen. Overlay screens are drawn
/// by the event loop via ScreenView::tick.
pub fn draw(&mut self, frame: &mut Frame) {
let size = frame.area();
match self.screen {
Screen::Conscious => { self.draw_debug(frame, size); return; }
Screen::Subconscious => { self.draw_agents(frame, size); return; }
Screen::Unconscious => { self.draw_unconscious(frame, size); return; }
Screen::Thalamus => { self.draw_thalamus(frame, size); return; }
Screen::Interact => {}
}
self.draw_main(frame, size);
}
@ -627,10 +537,6 @@ impl App {
});
}
pub(crate) fn set_screen(&mut self, screen: Screen) {
self.screen = screen;
self.debug_scroll = 0;
}
}
pub fn init_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {