user: InteractScreen extracted, all screens use ScreenView trait

InteractScreen in chat.rs owns conversation/autonomous/tools panes,
textarea, input history, scroll state. App is now just shared state
(status, sampling params, agent_state, channel_status, idle_info).

Event loop holds InteractScreen separately for UiMessage routing.
Overlay screens (F2-F5) in screens vec. F-key switching preserves
state across screen changes.

handle_ui_message moved from App to InteractScreen.
handle_key split: global keys on App, screen keys in tick().
draw dispatch eliminated — each screen draws itself.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 18:57:54 -04:00
commit 68f115b880
8 changed files with 276 additions and 259 deletions

View file

@ -14,7 +14,7 @@ use tokio::sync::Mutex;
use crate::agent::Agent;
use crate::agent::api::ApiClient;
use crate::mind::MindCommand;
use crate::user::{self as tui, HotkeyAction};
use crate::user::{self as tui, HotkeyAction, ScreenView};
use crate::user::ui_channel::{self, UiMessage};
// ── Slash commands ─────────────────────────────────────────────
@ -381,14 +381,16 @@ pub async fn run(
}
let notify_rx = crate::thalamus::channels::subscribe_all();
// Overlay screens (F2-F5). Index 0 = no overlay (interact).
// InteractScreen held separately for UiMessage routing
let mut interact = crate::user::chat::InteractScreen::new();
// Overlay screens: F2=conscious, F3=subconscious, F4=unconscious, F5=thalamus
let mut screens: Vec<Box<dyn tui::ScreenView>> = vec![
Box::new(crate::user::context::ConsciousScreen::new()),
Box::new(crate::user::subconscious::SubconsciousScreen::new()),
Box::new(crate::user::unconscious::UnconsciousScreen::new()),
Box::new(crate::user::thalamus::ThalamusScreen::new()),
];
let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay index + 1
let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay
let mut terminal = tui::init_terminal()?;
let mut reader = EventStream::new();
@ -403,9 +405,11 @@ pub async fn run(
let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into()));
// Initial render — don't wait for Mind to init
app.drain_messages(&mut ui_rx);
terminal.draw(|f| app.draw(f))?;
// Initial render
terminal.draw(|f| {
let area = f.area();
interact.tick(f, area, None, &mut app);
})?;
// Replay conversation after Mind init completes (non-blocking check)
let mut startup_done = false;
@ -423,8 +427,8 @@ pub async fn run(
// F-keys switch screens
if let ratatui::crossterm::event::KeyCode::F(n) = key.code {
active_screen = match n {
1 => 0,
n @ 2..=5 => n as usize - 1,
1 => 0, // interact
n @ 2..=5 if (n as usize - 2) < screens.len() => n as usize - 1,
_ => active_screen,
};
if active_screen == 4 { // thalamus — refresh channels
@ -438,25 +442,18 @@ pub async fn run(
continue;
}
// App handles global keys (Ctrl combos) + interact keys
if active_screen == 0 {
app.handle_key(key);
} else {
// Global keys only
app.handle_key(key);
// Screen gets the key on next render tick
}
// Global keys (Ctrl combos)
app.handle_global_key(key);
// Store pending key for active overlay screen
pending_key = Some(key);
dirty = true;
}
Some(Ok(Event::Mouse(mouse))) => {
app.handle_mouse(mouse);
Some(Ok(Event::Mouse(_mouse))) => {
// TODO: route to active screen
dirty = true;
}
Some(Ok(Event::Resize(w, h))) => {
app.handle_resize(w, h);
Some(Ok(Event::Resize(_w, _h))) => {
terminal.clear()?;
dirty = true;
}
@ -506,7 +503,7 @@ pub async fn run(
}
Some(msg) = ui_rx.recv() => {
app.handle_ui_message(msg);
interact.handle_ui_message(&msg, &mut app);
dirty = true;
}
}
@ -540,7 +537,9 @@ pub async fn run(
}
}
if app.drain_messages(&mut ui_rx) {
// Drain UiMessages to interact screen
while let Ok(msg) = ui_rx.try_recv() {
interact.handle_ui_message(&msg, &mut app);
dirty = true;
}
@ -548,12 +547,15 @@ pub async fn run(
let key = pending_key.take();
let mut screen_action = None;
if active_screen == 0 {
terminal.draw(|f| app.draw(f))?;
terminal.draw(|f| {
let area = f.area();
screen_action = interact.tick(f, area, key, &mut app);
})?;
} else {
let screen = &mut screens[active_screen - 1];
terminal.draw(|f| {
let area = f.area();
screen_action = screen.tick(f, area, key, &app);
screen_action = screen.tick(f, area, key, &mut app);
})?;
}
if let Some(action) = screen_action {