tui: overlay screens via F-keys (F1=context, F2=agents)

Replaced debug_visible bool with an Overlay enum. F1 shows the
context/debug screen (Ctrl+D still works as alias), F2 shows the
agents screen (placeholder for now — will show surface, observe,
reflect, journal status). Esc closes any overlay.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-03-25 02:42:03 -04:00 committed by Kent Overstreet
parent c5efc6e650
commit baf208281d
2 changed files with 69 additions and 24 deletions

View file

@ -9,6 +9,8 @@
// Uses ratatui + crossterm. The App struct holds all TUI state and // Uses ratatui + crossterm. The App struct holds all TUI state and
// handles rendering. Input is processed from crossterm key events. // handles rendering. Input is processed from crossterm key events.
const SCREEN_LEGEND: &str = " F1=main F2=agents F10=context ";
use crossterm::{ use crossterm::{
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton}, event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton},
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
@ -328,8 +330,8 @@ pub struct App {
pub hotkey_actions: Vec<HotkeyAction>, pub hotkey_actions: Vec<HotkeyAction>,
/// Pane areas from last draw (for mouse click → pane selection). /// Pane areas from last draw (for mouse click → pane selection).
pane_areas: [Rect; 3], // [autonomous, conversation, tools] pane_areas: [Rect; 3], // [autonomous, conversation, tools]
/// Debug screen visible (Ctrl+D toggle). /// Active overlay screen (F1=context, F2=agents, Ctrl+D=context).
debug_visible: bool, overlay: Option<Overlay>,
/// Debug screen scroll offset. /// Debug screen scroll offset.
debug_scroll: u16, debug_scroll: u16,
/// Index of selected context section in debug view (for expand/collapse). /// Index of selected context section in debug view (for expand/collapse).
@ -342,6 +344,15 @@ pub struct App {
shared_context: SharedContextState, shared_context: SharedContextState,
} }
/// Overlay screens toggled by F-keys.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Overlay {
/// F1 / Ctrl+D — context window, model info, budget
Context,
/// F2 — subconscious agent status
Agents,
}
/// Actions triggered by hotkeys, consumed by the main loop. /// Actions triggered by hotkeys, consumed by the main loop.
#[derive(Debug)] #[derive(Debug)]
pub enum HotkeyAction { pub enum HotkeyAction {
@ -385,7 +396,7 @@ impl App {
submitted: Vec::new(), submitted: Vec::new(),
hotkey_actions: Vec::new(), hotkey_actions: Vec::new(),
pane_areas: [Rect::default(); 3], pane_areas: [Rect::default(); 3],
debug_visible: false, overlay: None,
debug_scroll: 0, debug_scroll: 0,
debug_selected: None, debug_selected: None,
debug_expanded: std::collections::HashSet::new(), debug_expanded: std::collections::HashSet::new(),
@ -510,8 +521,8 @@ impl App {
return; return;
} }
KeyCode::Char('d') => { KeyCode::Char('d') => {
self.debug_visible = !self.debug_visible; // Legacy alias for F10
self.debug_scroll = 0; self.set_overlay(Overlay::Context);
return; return;
} }
KeyCode::Char('p') => { KeyCode::Char('p') => {
@ -522,13 +533,15 @@ impl App {
} }
} }
// Debug screen captures scroll keys and Esc // Overlay screen captures scroll keys and Esc
if self.debug_visible { if self.overlay.is_some() {
match key.code { match key.code {
KeyCode::Esc => { KeyCode::F(1) => {
self.debug_visible = false; self.overlay = None;
return; return;
} }
KeyCode::F(10) => { self.set_overlay(Overlay::Context); return; }
KeyCode::F(2) => { self.set_overlay(Overlay::Agents); return; }
KeyCode::Up => { KeyCode::Up => {
let cs = self.read_context_state(); let cs = self.read_context_state();
let n = self.debug_item_count(&cs); let n = self.debug_item_count(&cs);
@ -574,6 +587,8 @@ impl App {
} }
match key.code { match key.code {
KeyCode::F(10) => { self.set_overlay(Overlay::Context); return; }
KeyCode::F(2) => { self.set_overlay(Overlay::Agents); return; }
KeyCode::Esc => { KeyCode::Esc => {
self.hotkey_actions.push(HotkeyAction::Interrupt); self.hotkey_actions.push(HotkeyAction::Interrupt);
} }
@ -695,9 +710,10 @@ impl App {
pub fn draw(&mut self, frame: &mut Frame) { pub fn draw(&mut self, frame: &mut Frame) {
let size = frame.area(); let size = frame.area();
if self.debug_visible { match self.overlay {
self.draw_debug(frame, size); Some(Overlay::Context) => { self.draw_debug(frame, size); return; }
return; Some(Overlay::Agents) => { self.draw_agents(frame, size); return; }
None => {}
} }
// Main layout: content area + active tools overlay + status bar // Main layout: content area + active tools overlay + status bar
@ -744,11 +760,12 @@ impl App {
// Draw autonomous pane // Draw autonomous pane
let auto_active = self.active_pane == ActivePane::Autonomous; let auto_active = self.active_pane == ActivePane::Autonomous;
draw_pane(frame, auto_area, "autonomous", &mut self.autonomous, auto_active); draw_pane(frame, auto_area, "autonomous", &mut self.autonomous, auto_active,
Some(SCREEN_LEGEND));
// Draw tools pane // Draw tools pane
let tools_active = self.active_pane == ActivePane::Tools; let tools_active = self.active_pane == ActivePane::Tools;
draw_pane(frame, right_col, "tools", &mut self.tools, tools_active); draw_pane(frame, right_col, "tools", &mut self.tools, tools_active, None);
// Draw conversation pane (with input line) // Draw conversation pane (with input line)
let conv_active = self.active_pane == ActivePane::Conversation; let conv_active = self.active_pane == ActivePane::Conversation;
@ -849,7 +866,7 @@ impl App {
String::new() String::new()
}; };
let right_legend = format!( let right_legend = format!(
"{}{} ^P:pause ^D:debug ^R:reason ^K:kill | {} ", "{}{} ^P:pause ^R:reason ^K:kill | {} ",
reason_indicator, reason_indicator,
proc_indicator, proc_indicator,
self.status.model, self.status.model,
@ -961,15 +978,35 @@ impl App {
} }
} }
fn draw_debug(&self, frame: &mut Frame, size: Rect) { fn set_overlay(&mut self, screen: Overlay) {
self.overlay = Some(screen);
self.debug_scroll = 0;
}
fn draw_agents(&self, frame: &mut Frame, size: Rect) {
let mut lines: Vec<Line> = Vec::new(); let mut lines: Vec<Line> = Vec::new();
let section = Style::default().fg(Color::Yellow); let section = Style::default().fg(Color::Yellow);
lines.push(Line::styled(
" Debug (Ctrl+D or Esc to close, arrows/PgUp/PgDn to scroll)",
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
));
lines.push(Line::raw("")); lines.push(Line::raw(""));
lines.push(Line::styled("── Subconscious Agents ──", section));
lines.push(Line::raw(""));
lines.push(Line::raw(" (not yet wired — will show surface, observe, reflect, journal status)"));
let block = Block::default()
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
.title_top(Line::from(" agents ").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);
}
fn draw_debug(&self, frame: &mut Frame, size: Rect) {
let mut lines: Vec<Line> = Vec::new();
let section = Style::default().fg(Color::Yellow);
// Model // Model
lines.push(Line::styled("── Model ──", section)); lines.push(Line::styled("── Model ──", section));
@ -1024,7 +1061,8 @@ impl App {
lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.len()))); lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.len())));
let block = Block::default() let block = Block::default()
.title(" Debug ") .title_top(Line::from(SCREEN_LEGEND).left_aligned())
.title_top(Line::from(" context ").right_aligned())
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)); .border_style(Style::default().fg(Color::Cyan));
@ -1142,6 +1180,7 @@ fn draw_pane(
title: &str, title: &str,
pane: &mut PaneState, pane: &mut PaneState,
is_active: bool, is_active: bool,
left_title: Option<&str>,
) { ) {
let inner_height = area.height.saturating_sub(2); let inner_height = area.height.saturating_sub(2);
@ -1151,10 +1190,16 @@ fn draw_pane(
Style::default().fg(Color::DarkGray) Style::default().fg(Color::DarkGray)
}; };
let block = Block::default() let mut block = Block::default()
.title(format!(" {} ", title))
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(border_style); .border_style(border_style);
if let Some(left) = left_title {
block = block
.title_top(Line::from(left).left_aligned())
.title_top(Line::from(format!(" {} ", title)).right_aligned());
} else {
block = block.title(format!(" {} ", title));
}
let lines = pane.all_lines(); let lines = pane.all_lines();
let paragraph = Paragraph::new(lines) let paragraph = Paragraph::new(lines)

View file

@ -628,7 +628,7 @@ impl Session {
"Keys: Tab=pane ^Up/Down=scroll PgUp/PgDn=scroll Mouse=click/scroll".into(), "Keys: Tab=pane ^Up/Down=scroll PgUp/PgDn=scroll Mouse=click/scroll".into(),
)); ));
let _ = self.ui_tx.send(UiMessage::Info( let _ = self.ui_tx.send(UiMessage::Info(
" Alt+Enter=newline Esc=interrupt ^P=pause ^R=reasoning ^K=kill ^D=debug".into(), " Alt+Enter=newline Esc=interrupt ^P=pause ^R=reasoning ^K=kill F10=context F2=agents".into(),
)); ));
let _ = self.ui_tx.send(UiMessage::Info( let _ = self.ui_tx.send(UiMessage::Info(
" Shift+click for native text selection (copy/paste)".into(), " Shift+click for native text selection (copy/paste)".into(),