2026-04-03 17:25:59 -04:00
|
|
|
// subconscious_screen.rs — F3 subconscious agent overlay
|
2026-04-07 19:03:14 -04:00
|
|
|
//
|
|
|
|
|
// Three-pane layout:
|
|
|
|
|
// Top-left: Agent list (↑/↓ select)
|
|
|
|
|
// Bottom-left: Detail — outputs from selected agent's last run
|
|
|
|
|
// Right: Context tree from fork point (→/Enter expand, ← collapse)
|
2026-04-03 17:25:59 -04:00
|
|
|
|
|
|
|
|
use ratatui::{
|
2026-04-07 19:03:14 -04:00
|
|
|
layout::{Constraint, Layout, Rect},
|
2026-04-03 17:25:59 -04:00
|
|
|
style::{Color, Modifier, Style},
|
|
|
|
|
text::{Line, Span},
|
2026-04-07 19:07:00 -04:00
|
|
|
widgets::{List, ListItem, ListState},
|
2026-04-03 17:25:59 -04:00
|
|
|
Frame,
|
2026-04-06 19:33:18 -04:00
|
|
|
crossterm::event::KeyCode,
|
2026-04-03 17:25:59 -04:00
|
|
|
};
|
|
|
|
|
|
2026-04-06 19:33:18 -04:00
|
|
|
use super::{App, ScreenView, screen_legend};
|
2026-04-07 19:09:04 -04:00
|
|
|
use super::widgets::{SectionTree, pane_block_focused, render_scrollable, tree_legend, format_age, format_ts_age};
|
2026-04-07 20:15:31 -04:00
|
|
|
use crate::agent::context::{ContextSection, ContextEntry, ConversationEntry};
|
|
|
|
|
use crate::agent::api::Message;
|
2026-04-07 19:03:14 -04:00
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
|
|
|
enum Pane { Agents, Outputs, History, Context }
|
|
|
|
|
|
|
|
|
|
// Clockwise: top-left → right → bottom-left → middle-left
|
|
|
|
|
const PANE_ORDER: &[Pane] = &[Pane::Agents, Pane::Context, Pane::History, Pane::Outputs];
|
2026-04-03 17:25:59 -04:00
|
|
|
|
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>
2026-04-05 17:54:40 -04:00
|
|
|
pub(crate) struct SubconsciousScreen {
|
2026-04-07 19:03:14 -04:00
|
|
|
focus: Pane,
|
|
|
|
|
list_state: ListState,
|
|
|
|
|
output_tree: SectionTree,
|
|
|
|
|
context_tree: SectionTree,
|
|
|
|
|
history_scroll: u16,
|
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>
2026-04-05 17:54:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SubconsciousScreen {
|
|
|
|
|
pub fn new() -> Self {
|
2026-04-07 19:03:14 -04:00
|
|
|
let mut list_state = ListState::default();
|
|
|
|
|
list_state.select(Some(0));
|
|
|
|
|
Self {
|
|
|
|
|
focus: Pane::Agents,
|
|
|
|
|
list_state,
|
|
|
|
|
output_tree: SectionTree::new(),
|
|
|
|
|
context_tree: SectionTree::new(),
|
|
|
|
|
history_scroll: 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn selected(&self) -> usize {
|
|
|
|
|
self.list_state.selected().unwrap_or(0)
|
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>
2026-04-05 17:54:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ScreenView for SubconsciousScreen {
|
|
|
|
|
fn label(&self) -> &'static str { "subconscious" }
|
|
|
|
|
|
|
|
|
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
2026-04-05 23:04:10 -04:00
|
|
|
events: &[ratatui::crossterm::event::Event], app: &mut App) {
|
2026-04-07 19:03:14 -04:00
|
|
|
let context_sections = self.read_sections(app);
|
|
|
|
|
let output_sections = self.output_sections(app);
|
|
|
|
|
|
2026-04-05 23:04:10 -04:00
|
|
|
for event in events {
|
|
|
|
|
if let ratatui::crossterm::event::Event::Key(key) = event {
|
|
|
|
|
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
2026-04-07 01:59:09 -04:00
|
|
|
match key.code {
|
2026-04-07 19:03:14 -04:00
|
|
|
KeyCode::Tab => {
|
|
|
|
|
let idx = PANE_ORDER.iter().position(|p| *p == self.focus).unwrap_or(0);
|
|
|
|
|
self.focus = PANE_ORDER[(idx + 1) % PANE_ORDER.len()];
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
2026-04-07 19:03:14 -04:00
|
|
|
KeyCode::BackTab => {
|
|
|
|
|
let idx = PANE_ORDER.iter().position(|p| *p == self.focus).unwrap_or(0);
|
|
|
|
|
self.focus = PANE_ORDER[(idx + PANE_ORDER.len() - 1) % PANE_ORDER.len()];
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
2026-04-07 19:03:14 -04:00
|
|
|
code => match self.focus {
|
|
|
|
|
Pane::Agents => match code {
|
|
|
|
|
KeyCode::Up => {
|
|
|
|
|
self.list_state.select_previous();
|
|
|
|
|
self.reset_pane_state();
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Down => {
|
|
|
|
|
self.list_state.select_next();
|
|
|
|
|
self.reset_pane_state();
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
2026-04-07 19:07:00 -04:00
|
|
|
Pane::Outputs => self.output_tree.handle_nav(code, &output_sections, area.height),
|
2026-04-07 19:03:14 -04:00
|
|
|
Pane::History => match code {
|
|
|
|
|
KeyCode::Up => self.history_scroll = self.history_scroll.saturating_sub(3),
|
|
|
|
|
KeyCode::Down => self.history_scroll += 3,
|
|
|
|
|
KeyCode::PageUp => self.history_scroll = self.history_scroll.saturating_sub(20),
|
|
|
|
|
KeyCode::PageDown => self.history_scroll += 20,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
2026-04-07 19:07:00 -04:00
|
|
|
Pane::Context => self.context_tree.handle_nav(code, &context_sections, area.height),
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
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>
2026-04-05 17:54:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-03 17:25:59 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
// Layout: left column (38%) | right column (62%)
|
|
|
|
|
let [left, right] = Layout::horizontal([
|
|
|
|
|
Constraint::Percentage(38),
|
|
|
|
|
Constraint::Percentage(62),
|
|
|
|
|
]).areas(area);
|
|
|
|
|
|
|
|
|
|
// Left column: agent list (top) | outputs (middle) | history (bottom, main)
|
|
|
|
|
let agent_count = app.agent_state.len().max(1) as u16;
|
|
|
|
|
let list_height = (agent_count + 2).min(left.height / 4);
|
|
|
|
|
let output_lines = app.agent_state.get(self.selected())
|
|
|
|
|
.map(|s| s.state.values().map(|v| v.lines().count() + 1).sum::<usize>())
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
let output_height = (output_lines as u16 + 2).min(left.height / 4).max(3);
|
|
|
|
|
let [list_area, output_area, history_area] = Layout::vertical([
|
|
|
|
|
Constraint::Length(list_height),
|
|
|
|
|
Constraint::Length(output_height),
|
|
|
|
|
Constraint::Min(5),
|
|
|
|
|
]).areas(left);
|
|
|
|
|
|
|
|
|
|
self.draw_list(frame, list_area, app);
|
|
|
|
|
self.draw_outputs(frame, output_area, app);
|
|
|
|
|
self.draw_history(frame, history_area, app);
|
|
|
|
|
self.draw_context(frame, right, &context_sections, app);
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SubconsciousScreen {
|
2026-04-07 19:03:14 -04:00
|
|
|
fn reset_pane_state(&mut self) {
|
|
|
|
|
self.output_tree = SectionTree::new();
|
|
|
|
|
self.context_tree = SectionTree::new();
|
|
|
|
|
self.history_scroll = 0;
|
|
|
|
|
}
|
2026-04-07 01:59:09 -04:00
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
fn output_sections(&self, app: &App) -> Vec<ContextSection> {
|
|
|
|
|
let snap = match app.agent_state.get(self.selected()) {
|
|
|
|
|
Some(s) => s,
|
|
|
|
|
None => return Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
snap.state.iter().map(|(key, val)| {
|
2026-04-07 20:15:31 -04:00
|
|
|
let mut section = ContextSection::new(key.clone());
|
|
|
|
|
section.push(ContextEntry {
|
|
|
|
|
entry: ConversationEntry::Message(Message::user(val)),
|
2026-04-07 19:03:14 -04:00
|
|
|
tokens: 0,
|
2026-04-07 20:15:31 -04:00
|
|
|
timestamp: None,
|
|
|
|
|
});
|
|
|
|
|
section
|
2026-04-07 19:03:14 -04:00
|
|
|
}).collect()
|
|
|
|
|
}
|
2026-04-03 17:25:59 -04:00
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
fn read_sections(&self, app: &App) -> Vec<ContextSection> {
|
|
|
|
|
let snap = match app.agent_state.get(self.selected()) {
|
|
|
|
|
Some(s) => s,
|
|
|
|
|
None => return Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
snap.forked_agent.as_ref()
|
|
|
|
|
.and_then(|agent| agent.try_lock().ok())
|
2026-04-07 20:15:31 -04:00
|
|
|
.map(|ag| {
|
|
|
|
|
// Build a single section from the forked conversation entries
|
|
|
|
|
let entries = ag.conversation_entries_from(snap.fork_point);
|
|
|
|
|
let mut section = ContextSection::new("Conversation");
|
|
|
|
|
for e in entries {
|
|
|
|
|
section.push(e.clone());
|
|
|
|
|
}
|
|
|
|
|
vec![section]
|
|
|
|
|
})
|
2026-04-07 19:03:14 -04:00
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
|
|
|
|
let items: Vec<ListItem> = app.agent_state.iter().map(|snap| {
|
|
|
|
|
if snap.running {
|
|
|
|
|
ListItem::from(Line::from(vec![
|
|
|
|
|
Span::styled(&snap.name, Style::default().fg(Color::Green)),
|
|
|
|
|
Span::styled(" ● ", Style::default().fg(Color::Green)),
|
2026-04-07 01:59:09 -04:00
|
|
|
Span::styled(
|
2026-04-07 19:03:14 -04:00
|
|
|
format!("p:{} t:{}", snap.current_phase, snap.turn),
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
2026-04-07 01:59:09 -04:00
|
|
|
),
|
2026-04-07 19:03:14 -04:00
|
|
|
]))
|
2026-04-07 01:59:09 -04:00
|
|
|
} else {
|
|
|
|
|
let ago = snap.last_run_secs_ago
|
2026-04-07 19:03:14 -04:00
|
|
|
.map(|s| format_age(s))
|
|
|
|
|
.unwrap_or_else(|| "—".to_string());
|
2026-04-07 03:09:06 -04:00
|
|
|
let entries = snap.forked_agent.as_ref()
|
|
|
|
|
.and_then(|a| a.try_lock().ok())
|
2026-04-07 20:15:31 -04:00
|
|
|
.map(|ag| ag.context.conversation.len().saturating_sub(snap.fork_point))
|
2026-04-07 03:09:06 -04:00
|
|
|
.unwrap_or(0);
|
2026-04-07 19:03:14 -04:00
|
|
|
ListItem::from(Line::from(vec![
|
|
|
|
|
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
|
|
|
|
Span::styled(" ○ ", Style::default().fg(Color::DarkGray)),
|
2026-04-07 01:59:09 -04:00
|
|
|
Span::styled(
|
2026-04-07 19:03:14 -04:00
|
|
|
format!("{} {}e", ago, entries),
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
2026-04-07 01:59:09 -04:00
|
|
|
),
|
2026-04-07 19:03:14 -04:00
|
|
|
]))
|
|
|
|
|
}
|
|
|
|
|
}).collect();
|
2026-04-03 17:25:59 -04:00
|
|
|
|
2026-04-07 19:09:04 -04:00
|
|
|
let mut block = pane_block_focused("agents", self.focus == Pane::Agents)
|
|
|
|
|
.title_top(Line::from(screen_legend()).left_aligned());
|
|
|
|
|
if self.focus == Pane::Agents {
|
|
|
|
|
block = block.title_bottom(Line::styled(
|
|
|
|
|
" ↑↓:select Tab:next pane ",
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-07 19:03:14 -04:00
|
|
|
let list = List::new(items)
|
2026-04-07 19:09:04 -04:00
|
|
|
.block(block)
|
2026-04-07 19:03:14 -04:00
|
|
|
.highlight_symbol("▸ ")
|
|
|
|
|
.highlight_style(Style::default().bg(Color::DarkGray));
|
|
|
|
|
|
|
|
|
|
frame.render_stateful_widget(list, area, &mut self.list_state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw_outputs(&self, frame: &mut Frame, area: Rect, app: &App) {
|
|
|
|
|
let sections = self.output_sections(app);
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
|
|
|
|
|
|
if sections.is_empty() {
|
|
|
|
|
let dim = Style::default().fg(Color::DarkGray);
|
|
|
|
|
let snap = app.agent_state.get(self.selected());
|
|
|
|
|
let msg = if snap.is_some_and(|s| s.running) { "(running...)" } else { "—" };
|
|
|
|
|
lines.push(Line::styled(format!(" {}", msg), dim));
|
|
|
|
|
} else {
|
|
|
|
|
self.output_tree.render_sections(§ions, &mut lines);
|
|
|
|
|
}
|
2026-04-03 17:25:59 -04:00
|
|
|
|
2026-04-07 19:09:04 -04:00
|
|
|
let mut block = pane_block_focused("state", self.focus == Pane::Outputs);
|
|
|
|
|
if self.focus == Pane::Outputs { block = block.title_bottom(tree_legend()); }
|
|
|
|
|
render_scrollable(frame, area, lines, block, self.output_tree.scroll);
|
2026-04-03 17:25:59 -04:00
|
|
|
}
|
2026-04-07 02:08:48 -04:00
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
fn draw_history(&self, frame: &mut Frame, area: Rect, app: &App) {
|
|
|
|
|
let dim = Style::default().fg(Color::DarkGray);
|
|
|
|
|
let key_style = Style::default().fg(Color::Yellow);
|
2026-04-07 02:08:48 -04:00
|
|
|
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
2026-04-07 19:03:14 -04:00
|
|
|
let mut title = "memory store activity".to_string();
|
2026-04-07 03:09:06 -04:00
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
if let Some(snap) = app.agent_state.get(self.selected()) {
|
|
|
|
|
let short_name = snap.name.strip_prefix("subconscious-").unwrap_or(&snap.name);
|
|
|
|
|
title = format!("{} store activity", short_name);
|
2026-04-07 02:08:48 -04:00
|
|
|
|
2026-04-07 19:03:14 -04:00
|
|
|
if snap.history.is_empty() {
|
|
|
|
|
lines.push(Line::styled(" (no store activity)", dim));
|
|
|
|
|
} else {
|
|
|
|
|
for (key, ts) in &snap.history {
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
Span::styled(format!(" {:>6} ", format_ts_age(*ts)), dim),
|
|
|
|
|
Span::styled(key.as_str(), key_style),
|
|
|
|
|
]));
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 19:16:01 -04:00
|
|
|
if let Some(walked_str) = snap.state.get("walked") {
|
|
|
|
|
let walked: Vec<&str> = walked_str.lines()
|
|
|
|
|
.map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
|
|
|
|
|
if !walked.is_empty() {
|
|
|
|
|
lines.push(Line::raw(""));
|
|
|
|
|
lines.push(Line::styled(
|
|
|
|
|
format!(" walked ({}):", walked.len()),
|
|
|
|
|
Style::default().fg(Color::Cyan),
|
|
|
|
|
));
|
|
|
|
|
for key in &walked {
|
|
|
|
|
lines.push(Line::styled(format!(" {}", key), dim));
|
|
|
|
|
}
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 19:09:04 -04:00
|
|
|
let mut block = pane_block_focused(&title, self.focus == Pane::History);
|
|
|
|
|
if self.focus == Pane::History {
|
|
|
|
|
block = block.title_bottom(Line::styled(
|
|
|
|
|
" ↑↓:scroll PgUp/Dn ",
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
render_scrollable(frame, area, lines, block, self.history_scroll);
|
2026-04-07 19:03:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw_context(
|
|
|
|
|
&self,
|
|
|
|
|
frame: &mut Frame,
|
|
|
|
|
area: Rect,
|
|
|
|
|
sections: &[ContextSection],
|
|
|
|
|
app: &App,
|
|
|
|
|
) {
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
|
|
|
|
|
|
if sections.is_empty() {
|
|
|
|
|
lines.push(Line::styled(
|
|
|
|
|
" (no conversation data)",
|
|
|
|
|
Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC),
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
self.context_tree.render_sections(sections, &mut lines);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = app.agent_state.get(self.selected())
|
|
|
|
|
.map(|s| s.name.as_str())
|
|
|
|
|
.unwrap_or("—");
|
2026-04-07 02:08:48 -04:00
|
|
|
|
2026-04-07 19:09:04 -04:00
|
|
|
let mut block = pane_block_focused(title, self.focus == Pane::Context);
|
|
|
|
|
if self.focus == Pane::Context { block = block.title_bottom(tree_legend()); }
|
|
|
|
|
render_scrollable(frame, area, lines, block, self.context_tree.scroll);
|
2026-04-07 02:08:48 -04:00
|
|
|
}
|
2026-04-03 17:25:59 -04:00
|
|
|
}
|