// context_screen.rs — F2 context/debug overlay // // Full-screen overlay showing model info, context window breakdown, // and runtime state. Uses SectionTree for the expand/collapse tree. use ratatui::{ layout::Rect, style::{Color, Style}, text::Line, Frame, }; use super::{App, ScreenView, screen_legend}; use super::widgets::{SectionTree, pane_block, render_scrollable, tree_legend}; pub(crate) struct ConsciousScreen { agent: std::sync::Arc>, tree: SectionTree, } impl ConsciousScreen { pub fn new(agent: std::sync::Arc>) -> Self { Self { agent, tree: SectionTree::new() } } fn read_context_sections(&self) -> Vec { use crate::agent::context::{ContextSection, ContextEntry, ConversationEntry}; use crate::agent::api::Message; 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(); // Build a synthetic "Memory nodes" section from conversation entries let mut mem_section = ContextSection::new("Memory nodes"); 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) } None => { unscored += 1; key.clone() } }; mem_section.push(ContextEntry { entry: ConversationEntry::Message(Message::user(&label)), tokens: ce.tokens, timestamp: ce.timestamp, }); } } if !mem_section.is_empty() { mem_section.name = format!("Memory nodes ({} scored, {} unscored)", scored, unscored); sections.insert(sections.len() - 1, mem_section); // before conversation } sections } } impl ScreenView for ConsciousScreen { fn label(&self) -> &'static str { "conscious" } fn tick(&mut self, frame: &mut Frame, area: Rect, events: &[ratatui::crossterm::event::Event], app: &mut App) { 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(); self.tree.handle_nav(key.code, &context_state, area.height); } } // Draw let mut lines: Vec = Vec::new(); let section_style = Style::default().fg(Color::Yellow); lines.push(Line::styled("── Model ──", section_style)); let model_display = app.context_info.as_ref() .map_or_else(|| app.status.model.clone(), |i| i.model.clone()); lines.push(Line::raw(format!(" Current: {}", model_display))); if let Some(ref info) = app.context_info { lines.push(Line::raw(format!(" Backend: {}", info.backend))); lines.push(Line::raw(format!(" Prompt: {}", info.prompt_file))); lines.push(Line::raw(format!(" Available: {}", info.available_models.join(", ")))); } lines.push(Line::raw("")); lines.push(Line::styled("── Context State ──", section_style)); lines.push(Line::raw(format!(" Prompt tokens: {}K", app.status.prompt_tokens / 1000))); if !app.status.context_budget.is_empty() { lines.push(Line::raw(format!(" Budget: {}", app.status.context_budget))); } let context_state = self.read_context_sections(); if !context_state.is_empty() { 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)", Style::default().fg(Color::DarkGray), )); lines.push(Line::raw("")); self.tree.render_sections(&context_state, &mut lines); lines.push(Line::raw(format!(" {:23} {:>6} tokens", "────────", "──────"))); lines.push(Line::raw(format!(" {:23} {:>6} tokens", "Total", total))); } else if let Some(ref info) = app.context_info { lines.push(Line::raw(format!(" System prompt: {:>6} chars", info.system_prompt_chars))); lines.push(Line::raw(format!(" Context message: {:>6} chars", info.context_message_chars))); } lines.push(Line::raw("")); lines.push(Line::styled("── Runtime ──", section_style)); lines.push(Line::raw(format!( " DMN: {} ({}/{})", app.status.dmn_state, app.status.dmn_turns, app.status.dmn_max_turns, ))); lines.push(Line::raw(format!(" Reasoning: {}", app.reasoning_effort))); lines.push(Line::raw(format!(" Running processes: {}", app.running_processes))); 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_bottom(tree_legend()); render_scrollable(frame, area, lines, block, self.tree.scroll); } }