Restore context tree display with SectionView UI type
Introduced SectionView {name, tokens, content, children} as a
UI-only tree node, separate from the data ContextSection. The widget
SectionTree renders SectionView with the old recursive expand/collapse
behavior — children for sub-sections, content for text expansion.
section_to_view() converts data sections to UI views, using
ConversationEntry::label() for names and content_text() for
expandable content.
read_context_views() builds the same tree the old context_state_summary
did: System, Identity, Journal, Memory nodes (scored/unscored counts,
expandable to show content), Conversation entries.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
613704720b
commit
07b400c95c
3 changed files with 132 additions and 105 deletions
|
|
@ -10,6 +10,40 @@ use ratatui::{
|
|||
};
|
||||
use crate::agent::context::ContextSection;
|
||||
|
||||
/// UI-only tree node for the section tree display.
|
||||
/// Built from ContextSection data; not used for budgeting.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SectionView {
|
||||
pub name: String,
|
||||
pub tokens: usize,
|
||||
pub content: String,
|
||||
pub children: Vec<SectionView>,
|
||||
}
|
||||
|
||||
/// Build a SectionView tree from a ContextSection.
|
||||
/// Each entry becomes a child with label + expandable content.
|
||||
pub fn section_to_view(section: &ContextSection) -> SectionView {
|
||||
let children: Vec<SectionView> = section.entries().iter().map(|ce| {
|
||||
let content = if ce.entry.is_log() {
|
||||
String::new()
|
||||
} else {
|
||||
ce.entry.message().content_text().to_string()
|
||||
};
|
||||
SectionView {
|
||||
name: ce.entry.label(),
|
||||
tokens: ce.tokens,
|
||||
content,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}).collect();
|
||||
SectionView {
|
||||
name: section.name.clone(),
|
||||
tokens: section.tokens(),
|
||||
content: String::new(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -98,30 +132,32 @@ impl SectionTree {
|
|||
Self { selected: None, expanded: std::collections::HashSet::new(), scroll: 0 }
|
||||
}
|
||||
|
||||
/// Total nodes in the tree (regardless of expand state).
|
||||
/// Each section is 1 node, each entry within is 1 node.
|
||||
fn total_nodes(&self, sections: &[ContextSection]) -> usize {
|
||||
sections.iter().map(|s| 1 + s.entries().len()).sum()
|
||||
}
|
||||
|
||||
pub fn item_count(&self, sections: &[ContextSection]) -> usize {
|
||||
let mut idx = 0;
|
||||
let mut total = 0;
|
||||
for section in sections {
|
||||
let my_idx = idx;
|
||||
idx += 1;
|
||||
total += 1;
|
||||
if self.expanded.contains(&my_idx) {
|
||||
total += section.entries().len();
|
||||
idx += section.entries().len();
|
||||
}
|
||||
fn total_nodes(&self, sections: &[SectionView]) -> usize {
|
||||
fn count_all(s: &SectionView) -> usize {
|
||||
1 + s.children.iter().map(|c| count_all(c)).sum::<usize>()
|
||||
}
|
||||
total
|
||||
sections.iter().map(|s| count_all(s)).sum()
|
||||
}
|
||||
|
||||
pub fn handle_nav(&mut self, code: KeyCode, sections: &[ContextSection], height: u16) {
|
||||
pub fn item_count(&self, sections: &[SectionView]) -> usize {
|
||||
fn count(section: &SectionView, expanded: &std::collections::HashSet<usize>, idx: &mut usize) -> usize {
|
||||
let my_idx = *idx;
|
||||
*idx += 1;
|
||||
let mut total = 1;
|
||||
if expanded.contains(&my_idx) {
|
||||
for child in §ion.children {
|
||||
total += count(child, expanded, idx);
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
let mut idx = 0;
|
||||
sections.iter().map(|s| count(s, &self.expanded, &mut idx)).sum()
|
||||
}
|
||||
|
||||
pub fn handle_nav(&mut self, code: KeyCode, sections: &[SectionView], height: u16) {
|
||||
let item_count = self.item_count(sections);
|
||||
let page = height.saturating_sub(2) as usize; // account for border
|
||||
let page = height.saturating_sub(2) as usize;
|
||||
match code {
|
||||
KeyCode::Up => {
|
||||
self.selected = Some(self.selected.unwrap_or(0).saturating_sub(1));
|
||||
|
|
@ -134,7 +170,7 @@ impl SectionTree {
|
|||
let sel = self.selected.unwrap_or(0);
|
||||
self.selected = Some(sel.saturating_sub(page));
|
||||
self.scroll = self.scroll.saturating_sub(page as u16);
|
||||
return; // skip scroll_to_selected — we moved both together
|
||||
return;
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
let max = item_count.saturating_sub(1);
|
||||
|
|
@ -160,14 +196,12 @@ impl SectionTree {
|
|||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -178,7 +212,7 @@ impl SectionTree {
|
|||
fn scroll_to_selected(&mut self, height: u16) {
|
||||
if let Some(sel) = self.selected {
|
||||
let sel_line = sel as u16;
|
||||
let visible = height.saturating_sub(2); // border
|
||||
let visible = height.saturating_sub(2);
|
||||
if sel_line < self.scroll {
|
||||
self.scroll = sel_line;
|
||||
} else if sel_line >= self.scroll + visible {
|
||||
|
|
@ -187,27 +221,30 @@ impl SectionTree {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render_sections(&self, sections: &[ContextSection], lines: &mut Vec<Line>) {
|
||||
pub fn render_sections(&self, sections: &[SectionView], lines: &mut Vec<Line>) {
|
||||
let mut idx = 0;
|
||||
for section in sections {
|
||||
self.render_one(section, lines, &mut idx);
|
||||
self.render_one(section, 0, lines, &mut idx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_one(
|
||||
&self,
|
||||
section: &ContextSection,
|
||||
section: &SectionView,
|
||||
depth: usize,
|
||||
lines: &mut Vec<Line>,
|
||||
idx: &mut usize,
|
||||
) {
|
||||
let my_idx = *idx;
|
||||
let selected = self.selected == Some(my_idx);
|
||||
let expanded = self.expanded.contains(&my_idx);
|
||||
let expandable = !section.is_empty();
|
||||
let has_children = !section.children.is_empty();
|
||||
let has_content = !section.content.is_empty();
|
||||
let expandable = has_children || has_content;
|
||||
|
||||
let indent = " ".repeat(depth + 1);
|
||||
let marker = if !expandable { " " } else if expanded { "▼" } else { "▶" };
|
||||
let label = format!(" {} {:30} {:>6} tokens",
|
||||
marker, format!("{} ({})", section.name, section.len()), section.tokens());
|
||||
let label = format!("{}{} {:30} {:>6} tokens", indent, marker, section.name, section.tokens);
|
||||
let style = if selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
|
|
@ -217,42 +254,25 @@ impl SectionTree {
|
|||
*idx += 1;
|
||||
|
||||
if expanded {
|
||||
for ce in section.entries() {
|
||||
let entry_selected = self.selected == Some(*idx);
|
||||
let entry_expanded = self.expanded.contains(idx);
|
||||
let text = if ce.entry.is_log() {
|
||||
String::new()
|
||||
} else {
|
||||
ce.entry.message().content_text().to_string()
|
||||
};
|
||||
let has_content = text.len() > 0;
|
||||
let entry_marker = if has_content {
|
||||
if entry_expanded { "▼" } else { "▶" }
|
||||
} else { " " };
|
||||
let entry_label = format!(" {} {:>6} {}", entry_marker, ce.tokens, ce.entry.label());
|
||||
let style = if entry_selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
lines.push(Line::styled(entry_label, style));
|
||||
*idx += 1;
|
||||
|
||||
if entry_expanded {
|
||||
let content_lines: Vec<&str> = text.lines().collect();
|
||||
let show = content_lines.len().min(50);
|
||||
for line in &content_lines[..show] {
|
||||
lines.push(Line::styled(
|
||||
format!(" │ {}", line),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
if content_lines.len() > 50 {
|
||||
lines.push(Line::styled(
|
||||
format!(" ... ({} more lines)", content_lines.len() - 50),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
if has_children {
|
||||
for child in §ion.children {
|
||||
self.render_one(child, depth + 1, lines, idx);
|
||||
}
|
||||
} else if has_content {
|
||||
let content_indent = format!("{} │ ", " ".repeat(depth + 1));
|
||||
let content_lines: Vec<&str> = section.content.lines().collect();
|
||||
let show = content_lines.len().min(50);
|
||||
for line in &content_lines[..show] {
|
||||
lines.push(Line::styled(
|
||||
format!("{}{}", content_indent, line),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
if content_lines.len() > 50 {
|
||||
lines.push(Line::styled(
|
||||
format!("{}... ({} more lines)", content_indent, content_lines.len() - 50),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue