diff --git a/src/user/context.rs b/src/user/context.rs index cd61c92..2ae623e 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, tree_legend}; +use super::widgets::{SectionTree, SectionView, section_to_view, pane_block, render_scrollable, tree_legend}; pub(crate) struct ConsciousScreen { agent: std::sync::Arc>, @@ -23,42 +23,53 @@ impl ConsciousScreen { Self { agent, tree: SectionTree::new() } } - fn read_context_sections(&self) -> Vec { - use crate::agent::context::{ContextSection, ContextEntry, ConversationEntry}; - use crate::agent::api::Message; + fn read_context_views(&self) -> Vec { + use crate::agent::context::ConversationEntry; let ag = match self.agent.try_lock() { Ok(ag) => ag, Err(_) => return Vec::new(), }; - let mut sections: Vec = ag.context_sections() - .iter().map(|s| (*s).clone()).collect(); + let mut views: Vec = Vec::new(); - // Build a synthetic "Memory nodes" section from conversation entries - let mut mem_section = ContextSection::new("Memory nodes"); + // System, Identity, Journal — simple section-to-view + views.push(section_to_view(&ag.context.system)); + views.push(section_to_view(&ag.context.identity)); + views.push(section_to_view(&ag.context.journal)); + + // Memory nodes — extracted from conversation, shown as children + let mut mem_children: Vec = Vec::new(); let mut scored = 0usize; let mut unscored = 0usize; for ce in ag.context.conversation.entries() { if let ConversationEntry::Memory { key, score, .. } = &ce.entry { let label = match score { - Some(s) => { scored += 1; format!("{} (score:{:.2})", key, s) } + Some(s) => { scored += 1; format!("{} (score:{:.1})", key, s) } None => { unscored += 1; key.clone() } }; - mem_section.push(ContextEntry { - entry: ConversationEntry::Message(Message::user(&label)), + mem_children.push(SectionView { + name: label, tokens: ce.tokens, - timestamp: ce.timestamp, + content: ce.entry.message().content_text().to_string(), + children: Vec::new(), }); } } - if !mem_section.is_empty() { - mem_section.name = format!("Memory nodes ({} scored, {} unscored)", - scored, unscored); - sections.insert(sections.len() - 1, mem_section); // before conversation + if !mem_children.is_empty() { + let mem_tokens: usize = mem_children.iter().map(|c| c.tokens).sum(); + views.push(SectionView { + name: format!("Memory nodes ({} scored, {} unscored)", scored, unscored), + tokens: mem_tokens, + content: String::new(), + children: mem_children, + }); } - sections + // Conversation — each entry as a child + views.push(section_to_view(&ag.context.conversation)); + + views } } @@ -70,7 +81,7 @@ impl ScreenView for ConsciousScreen { for event in events { if let ratatui::crossterm::event::Event::Key(key) = event { if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; } - let context_state = self.read_context_sections(); + let context_state = self.read_context_views(); self.tree.handle_nav(key.code, &context_state, area.height); } } @@ -95,9 +106,9 @@ impl ScreenView for ConsciousScreen { if !app.status.context_budget.is_empty() { lines.push(Line::raw(format!(" Budget: {}", app.status.context_budget))); } - let context_state = self.read_context_sections(); + let context_state = self.read_context_views(); if !context_state.is_empty() { - let total: usize = context_state.iter().map(|s| s.tokens()).sum(); + let total: usize = context_state.iter().map(|s| s.tokens).sum(); lines.push(Line::raw("")); lines.push(Line::styled( " (↑/↓ select, →/Enter expand, ← collapse, PgUp/PgDn scroll)", diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 9f1a7cd..e2662c2 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -15,9 +15,7 @@ use ratatui::{ }; use super::{App, ScreenView, screen_legend}; -use super::widgets::{SectionTree, pane_block_focused, render_scrollable, tree_legend, format_age, format_ts_age}; -use crate::agent::context::{ContextSection, ContextEntry, ConversationEntry}; -use crate::agent::api::Message; +use super::widgets::{SectionTree, SectionView, section_to_view, pane_block_focused, render_scrollable, tree_legend, format_age, format_ts_age}; #[derive(Clone, Copy, PartialEq)] enum Pane { Agents, Outputs, History, Context } @@ -130,23 +128,22 @@ impl SubconsciousScreen { self.history_scroll = 0; } - fn output_sections(&self, app: &App) -> Vec { + fn output_sections(&self, app: &App) -> Vec { let snap = match app.agent_state.get(self.selected()) { Some(s) => s, None => return Vec::new(), }; snap.state.iter().map(|(key, val)| { - let mut section = ContextSection::new(key.clone()); - section.push(ContextEntry { - entry: ConversationEntry::Message(Message::user(val)), + SectionView { + name: key.clone(), tokens: 0, - timestamp: None, - }); - section + content: val.clone(), + children: Vec::new(), + } }).collect() } - fn read_sections(&self, app: &App) -> Vec { + fn read_sections(&self, app: &App) -> Vec { let snap = match app.agent_state.get(self.selected()) { Some(s) => s, None => return Vec::new(), @@ -154,13 +151,12 @@ impl SubconsciousScreen { snap.forked_agent.as_ref() .and_then(|agent| agent.try_lock().ok()) .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] + let conv = ag.context.conversation.clone(); + // Only show entries from fork point onward + let mut view = section_to_view(&conv); + let fork = snap.fork_point.min(view.children.len()); + view.children = view.children.split_off(fork); + vec![view] }) .unwrap_or_default() } @@ -281,7 +277,7 @@ impl SubconsciousScreen { &self, frame: &mut Frame, area: Rect, - sections: &[ContextSection], + sections: &[SectionView], app: &App, ) { let mut lines: Vec = Vec::new(); diff --git a/src/user/widgets.rs b/src/user/widgets.rs index d4d6416..00734f0 100644 --- a/src/user/widgets.rs +++ b/src/user/widgets.rs @@ -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, +} + +/// 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 = 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::() } - 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, 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) { + pub fn render_sections(&self, sections: &[SectionView], lines: &mut Vec) { 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, 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), + )); } } }