From 578be807e77105dcac14c7e87832054ec8f6d409 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 7 Apr 2026 19:09:04 -0400 Subject: [PATCH] Add expand/collapse all, per-pane key legends SectionTree: - 'e': expand all nodes - 'c': collapse all nodes - Home/End already wired from previous commit Key legend shown at bottom border of each focused pane: - Tree panes: nav, expand/collapse, expand/collapse all, paging - Agent list: select, tab - History: scroll, paging Legend only appears on the focused pane to avoid clutter. Co-Authored-By: Proof of Concept --- src/user/context.rs | 5 +++-- src/user/subconscious.rs | 36 ++++++++++++++++++++++++------------ src/user/widgets.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/user/context.rs b/src/user/context.rs index 96af080..f088313 100644 --- a/src/user/context.rs +++ b/src/user/context.rs @@ -11,7 +11,7 @@ use ratatui::{ }; use super::{App, ScreenView, screen_legend}; -use super::widgets::{SectionTree, pane_block, render_scrollable}; +use super::widgets::{SectionTree, pane_block, render_scrollable, tree_legend}; pub(crate) struct ConsciousScreen { agent: std::sync::Arc>, @@ -94,7 +94,8 @@ impl ScreenView for ConsciousScreen { lines.push(Line::raw(format!(" Active tools: {}", app.active_tools.lock().unwrap().len()))); let block = pane_block("context") - .title_top(Line::from(screen_legend()).left_aligned()); + .title_top(Line::from(screen_legend()).left_aligned()) + .title_bottom(tree_legend()); render_scrollable(frame, area, lines, block, self.tree.scroll); } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 8734589..4c82d32 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -15,7 +15,7 @@ use ratatui::{ }; use super::{App, ScreenView, screen_legend}; -use super::widgets::{SectionTree, pane_block_focused, render_scrollable, format_age, format_ts_age}; +use super::widgets::{SectionTree, pane_block_focused, render_scrollable, tree_legend, format_age, format_ts_age}; use crate::agent::context::ContextSection; #[derive(Clone, Copy, PartialEq)] @@ -185,9 +185,16 @@ impl SubconsciousScreen { } }).collect(); + 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), + )); + } let list = List::new(items) - .block(pane_block_focused("agents", self.focus == Pane::Agents) - .title_top(Line::from(screen_legend()).left_aligned())) + .block(block) .highlight_symbol("▸ ") .highlight_style(Style::default().bg(Color::DarkGray)); @@ -207,9 +214,9 @@ impl SubconsciousScreen { self.output_tree.render_sections(§ions, &mut lines); } - render_scrollable(frame, area, lines, - pane_block_focused("state", self.focus == Pane::Outputs), - self.output_tree.scroll); + 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); } fn draw_history(&self, frame: &mut Frame, area: Rect, app: &App) { @@ -246,9 +253,14 @@ impl SubconsciousScreen { } } - render_scrollable(frame, area, lines, - pane_block_focused(&title, self.focus == Pane::History), - self.history_scroll); + 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); } fn draw_context( @@ -273,8 +285,8 @@ impl SubconsciousScreen { .map(|s| s.name.as_str()) .unwrap_or("—"); - render_scrollable(frame, area, lines, - pane_block_focused(title, self.focus == Pane::Context), - self.context_tree.scroll); + 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); } } diff --git a/src/user/widgets.rs b/src/user/widgets.rs index 7e10d90..d873cb4 100644 --- a/src/user/widgets.rs +++ b/src/user/widgets.rs @@ -48,6 +48,14 @@ pub fn format_ts_age(ts: i64) -> String { format_age((now - ts).max(0) as f64) } +/// Key legend for SectionTree panes. +pub fn tree_legend() -> Line<'static> { + Line::styled( + " ↑↓:nav →/Enter:expand ←:collapse e:expand all c:collapse all PgUp/Dn Home/End ", + Style::default().fg(Color::DarkGray), + ) +} + /// Render a paragraph with a vertical scrollbar. pub fn render_scrollable( frame: &mut Frame, @@ -90,6 +98,14 @@ impl SectionTree { Self { selected: None, expanded: std::collections::HashSet::new(), scroll: 0 } } + /// Total nodes in the tree (regardless of expand state). + fn total_nodes(&self, sections: &[ContextSection]) -> usize { + fn count_all(section: &ContextSection) -> usize { + 1 + section.children.iter().map(|c| count_all(c)).sum::() + } + sections.iter().map(|s| count_all(s)).sum() + } + pub fn item_count(&self, sections: &[ContextSection]) -> usize { fn count(section: &ContextSection, expanded: &std::collections::HashSet, idx: &mut usize) -> usize { let my_idx = *idx; @@ -146,6 +162,17 @@ impl SectionTree { self.expanded.remove(&sel); } } + KeyCode::Char('e') => { + // Expand all + let total = self.total_nodes(sections); + for i in 0..total { + self.expanded.insert(i); + } + } + KeyCode::Char('c') => { + // Collapse all + self.expanded.clear(); + } _ => {} } self.scroll_to_selected(height);