From ef868cb98fc1b004b2a82098c2f55824ebcdcc87 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 7 Apr 2026 02:08:48 -0400 Subject: [PATCH] Subconscious screen: detail view with post-fork entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track fork point in run_forked(), capture entries added during the run. Subconscious screen shows these in a detail view (Enter to drill in, Esc to go back) — only the subconscious agent's own conversation, not the inherited conscious context. Co-Authored-By: Proof of Concept --- src/agent/oneshot.rs | 11 +++- src/mind/mod.rs | 3 + src/user/subconscious.rs | 119 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 3e0021b..6a6dd58 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -59,6 +59,9 @@ pub struct AutoAgent { /// Named outputs from the agent's output() tool calls. /// Collected per-run, read by Mind after completion. pub outputs: std::collections::HashMap, + /// Entries added during the last forked run (after the fork point). + /// The subconscious screen shows these. + pub last_run_entries: Vec, // Observable status pub current_phase: String, pub turn: usize, @@ -163,6 +166,7 @@ impl AutoAgent { priority, walked: Vec::new(), outputs: std::collections::HashMap::new(), + last_run_entries: Vec::new(), current_phase: String::new(), turn: 0, } @@ -198,9 +202,14 @@ impl AutoAgent { }).collect(); let orig_steps = std::mem::replace(&mut self.steps, resolved_steps); let forked = agent.fork(self.tools.clone()); + let fork_point = forked.context.entries.len(); let mut backend = Backend::Forked(forked); let result = self.run_with_backend(&mut backend, None).await; - self.steps = orig_steps; // restore templates + // Capture entries added during this run + if let Backend::Forked(ref agent) = backend { + self.last_run_entries = agent.context.entries[fork_point..].to_vec(); + } + self.steps = orig_steps; result } diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 5664fb4..6810ff0 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -100,6 +100,8 @@ pub struct SubconsciousSnapshot { pub turn: usize, pub walked_count: usize, pub last_run_secs_ago: Option, + /// Entries from the last forked run (after fork point). + pub last_run_entries: Vec, } impl SubconsciousAgent { @@ -111,6 +113,7 @@ impl SubconsciousAgent { turn: self.auto.turn, walked_count: self.auto.walked.len(), last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), + last_run_entries: self.auto.last_run_entries.clone(), } } } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 10407f8..cdd54db 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -10,15 +10,18 @@ use ratatui::{ }; use super::{App, ScreenView, screen_legend}; +use crate::agent::context::ConversationEntry; +use crate::agent::api::types::Role; pub(crate) struct SubconsciousScreen { selected: usize, + detail: bool, scroll: u16, } impl SubconsciousScreen { pub fn new() -> Self { - Self { selected: 0, scroll: 0 } + Self { selected: 0, detail: false, scroll: 0 } } } @@ -31,13 +34,26 @@ impl ScreenView for SubconsciousScreen { if let ratatui::crossterm::event::Event::Key(key) = event { if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; } match key.code { - KeyCode::Up => { + KeyCode::Up if !self.detail => { self.selected = self.selected.saturating_sub(1); } - KeyCode::Down => { + KeyCode::Down if !self.detail => { self.selected = (self.selected + 1) .min(app.agent_state.len().saturating_sub(1)); } + KeyCode::Enter | KeyCode::Right if !self.detail => { + self.detail = true; + self.scroll = 0; + } + KeyCode::Esc | KeyCode::Left if self.detail => { + self.detail = false; + } + KeyCode::Up if self.detail => { + self.scroll = self.scroll.saturating_sub(3); + } + KeyCode::Down if self.detail => { + self.scroll += 3; + } KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); } KeyCode::PageDown => { self.scroll += 20; } _ => {} @@ -45,13 +61,23 @@ impl ScreenView for SubconsciousScreen { } } + if self.detail { + self.draw_detail(frame, area, app); + } else { + self.draw_list(frame, area, app); + } + } +} + +impl SubconsciousScreen { + fn draw_list(&self, frame: &mut Frame, area: Rect, app: &App) { let mut lines: Vec = Vec::new(); let section = Style::default().fg(Color::Yellow); let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); lines.push(Line::raw("")); lines.push(Line::styled("── Subconscious Agents ──", section)); - lines.push(Line::styled(" (↑/↓ select)", hint)); + lines.push(Line::styled(" (↑/↓ select, Enter view log)", hint)); lines.push(Line::raw("")); if app.agent_state.is_empty() { @@ -87,6 +113,7 @@ impl ScreenView for SubconsciousScreen { else { format!("{:.1}h ago", s / 3600.0) } }) .unwrap_or_else(|| "never".to_string()); + let entries = snap.last_run_entries.len(); vec![ Span::styled( format!("{}{:<30}", prefix, snap.name), @@ -94,7 +121,8 @@ impl ScreenView for SubconsciousScreen { ), Span::styled("○ ", bg.fg(Color::DarkGray)), Span::styled( - format!("idle last: {} walked: {}", ago, snap.walked_count), + format!("idle last: {} entries: {} walked: {}", + ago, entries, snap.walked_count), bg.fg(Color::DarkGray), ), ] @@ -114,4 +142,85 @@ impl ScreenView for SubconsciousScreen { .scroll((self.scroll, 0)); frame.render_widget(para, area); } + + fn draw_detail(&self, frame: &mut Frame, area: Rect, app: &App) { + let snap = match app.agent_state.get(self.selected) { + Some(s) => s, + None => return, + }; + + let mut lines: Vec = Vec::new(); + let section = Style::default().fg(Color::Yellow); + let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); + + lines.push(Line::raw("")); + lines.push(Line::styled(format!("── {} ──", snap.name), section)); + lines.push(Line::styled(" (Esc/← back, ↑/↓/PgUp/PgDn scroll)", hint)); + lines.push(Line::raw("")); + + if snap.last_run_entries.is_empty() { + lines.push(Line::styled(" (no run data)", hint)); + } + + for entry in &snap.last_run_entries { + if entry.is_log() { + if let ConversationEntry::Log(text) = entry { + lines.push(Line::styled( + format!(" [log] {}", text), + Style::default().fg(Color::DarkGray), + )); + } + continue; + } + + let msg = entry.message(); + let (role_str, role_color) = match msg.role { + Role::User => ("user", Color::Cyan), + Role::Assistant => ("assistant", Color::Reset), + Role::Tool => ("tool", Color::DarkGray), + Role::System => ("system", Color::Yellow), + }; + + let text = msg.content_text(); + let tool_info = msg.tool_calls.as_ref().map(|tc| { + tc.iter().map(|c| c.function.name.as_str()) + .collect::>().join(", ") + }); + + // Role header + let header = match &tool_info { + Some(tools) => format!(" [{} → {}]", role_str, tools), + None => format!(" [{}]", role_str), + }; + lines.push(Line::styled(header, Style::default().fg(role_color))); + + // Content (truncated per line) + if !text.is_empty() { + for line in text.lines().take(20) { + lines.push(Line::styled( + format!(" {}", line), + Style::default().fg(Color::Gray), + )); + } + if text.lines().count() > 20 { + lines.push(Line::styled( + format!(" ... ({} more lines)", text.lines().count() - 20), + hint, + )); + } + } + } + + let block = Block::default() + .title_top(Line::from(screen_legend()).left_aligned()) + .title_top(Line::from(format!(" {} ", snap.name)).right_aligned()) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan)); + + let para = Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((self.scroll, 0)); + frame.render_widget(para, area); + } }