Subconscious screen: show AutoAgent state
F3 screen now displays SubconsciousSnapshot from Mind's AutoAgents instead of the old process-based AgentSnapshot. Shows running status (phase + turn), last run time, and walked key count. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
94ddf7b189
commit
85aafd206c
3 changed files with 86 additions and 107 deletions
|
|
@ -91,6 +91,30 @@ impl SubconsciousAgent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lightweight snapshot of subconscious agent state for the TUI.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct SubconsciousSnapshot {
|
||||||
|
pub name: String,
|
||||||
|
pub running: bool,
|
||||||
|
pub current_phase: String,
|
||||||
|
pub turn: usize,
|
||||||
|
pub walked_count: usize,
|
||||||
|
pub last_run_secs_ago: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubconsciousAgent {
|
||||||
|
fn snapshot(&self) -> SubconsciousSnapshot {
|
||||||
|
SubconsciousSnapshot {
|
||||||
|
name: self.auto.name.clone(),
|
||||||
|
running: self.is_running(),
|
||||||
|
current_phase: self.auto.current_phase.clone(),
|
||||||
|
turn: self.auto.turn,
|
||||||
|
walked_count: self.auto.walked.len(),
|
||||||
|
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Which pane streaming text should go to.
|
/// Which pane streaming text should go to.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum StreamTarget {
|
pub enum StreamTarget {
|
||||||
|
|
@ -316,6 +340,10 @@ impl Mind {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize — restore log, start daemons and background agents.
|
/// Initialize — restore log, start daemons and background agents.
|
||||||
|
pub async fn subconscious_snapshots(&self) -> Vec<SubconsciousSnapshot> {
|
||||||
|
self.subconscious.lock().await.iter().map(|s| s.snapshot()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init(&self) {
|
pub async fn init(&self) {
|
||||||
// Restore conversation
|
// Restore conversation
|
||||||
let mut ag = self.agent.lock().await;
|
let mut ag = self.agent.lock().await;
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ pub struct App {
|
||||||
pub submitted: Vec<String>,
|
pub submitted: Vec<String>,
|
||||||
pub(crate) context_info: Option<ContextInfo>,
|
pub(crate) context_info: Option<ContextInfo>,
|
||||||
pub(crate) shared_context: SharedContextState,
|
pub(crate) shared_context: SharedContextState,
|
||||||
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
pub(crate) agent_state: Vec<crate::mind::SubconsciousSnapshot>,
|
||||||
pub(crate) channel_status: Vec<ChannelStatus>,
|
pub(crate) channel_status: Vec<ChannelStatus>,
|
||||||
pub(crate) idle_info: Option<IdleInfo>,
|
pub(crate) idle_info: Option<IdleInfo>,
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +406,7 @@ pub async fn run(
|
||||||
// State sync on every wake
|
// State sync on every wake
|
||||||
idle_state.decay_ewma();
|
idle_state.decay_ewma();
|
||||||
app.update_idle(&idle_state);
|
app.update_idle(&idle_state);
|
||||||
|
app.agent_state = mind.subconscious_snapshots().await;
|
||||||
if !startup_done {
|
if !startup_done {
|
||||||
if let Ok(mut ag) = agent.try_lock() {
|
if let Ok(mut ag) = agent.try_lock() {
|
||||||
let model = ag.model().to_string();
|
let model = ag.model().to_string();
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,12 @@ use super::{App, ScreenView, screen_legend};
|
||||||
|
|
||||||
pub(crate) struct SubconsciousScreen {
|
pub(crate) struct SubconsciousScreen {
|
||||||
selected: usize,
|
selected: usize,
|
||||||
log_view: bool,
|
|
||||||
scroll: u16,
|
scroll: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubconsciousScreen {
|
impl SubconsciousScreen {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { selected: 0, log_view: false, scroll: 0 }
|
Self { selected: 0, scroll: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,73 +27,79 @@ impl ScreenView for SubconsciousScreen {
|
||||||
|
|
||||||
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
events: &[ratatui::crossterm::event::Event], app: &mut App) {
|
events: &[ratatui::crossterm::event::Event], app: &mut App) {
|
||||||
// Handle keys
|
|
||||||
for event in events {
|
for event in events {
|
||||||
if let ratatui::crossterm::event::Event::Key(key) = event {
|
if let ratatui::crossterm::event::Event::Key(key) = event {
|
||||||
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up if !self.log_view => {
|
KeyCode::Up => {
|
||||||
self.selected = self.selected.saturating_sub(1);
|
self.selected = self.selected.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
self.selected = (self.selected + 1)
|
||||||
|
.min(app.agent_state.len().saturating_sub(1));
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
||||||
|
KeyCode::PageDown => { self.scroll += 20; }
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
KeyCode::Down if !self.log_view => {
|
|
||||||
self.selected = (self.selected + 1).min(app.agent_state.len().saturating_sub(1));
|
|
||||||
}
|
|
||||||
KeyCode::Enter | KeyCode::Right if !self.log_view => {
|
|
||||||
self.log_view = true;
|
|
||||||
self.scroll = 0;
|
|
||||||
}
|
|
||||||
KeyCode::Esc | KeyCode::Left if self.log_view => {
|
|
||||||
self.log_view = false;
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
|
||||||
KeyCode::PageDown => { self.scroll += 20; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw
|
|
||||||
if self.log_view {
|
|
||||||
self.draw_log(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<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
let section = Style::default().fg(Color::Yellow);
|
let section = Style::default().fg(Color::Yellow);
|
||||||
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
||||||
|
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
lines.push(Line::styled("── Subconscious Agents ──", section));
|
lines.push(Line::styled("── Subconscious Agents ──", section));
|
||||||
lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint));
|
lines.push(Line::styled(" (↑/↓ select)", hint));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
for (i, agent) in app.agent_state.iter().enumerate() {
|
if app.agent_state.is_empty() {
|
||||||
|
lines.push(Line::styled(" (no agents loaded)", hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, snap) in app.agent_state.iter().enumerate() {
|
||||||
let selected = i == self.selected;
|
let selected = i == self.selected;
|
||||||
let prefix = if selected { "▸ " } else { " " };
|
let prefix = if selected { "▸ " } else { " " };
|
||||||
let bg = if selected { Style::default().bg(Color::DarkGray) } else { Style::default() };
|
let bg = if selected {
|
||||||
|
Style::default().bg(Color::DarkGray)
|
||||||
let status = match (&agent.pid, &agent.phase) {
|
} else {
|
||||||
(Some(pid), Some(phase)) => vec![
|
Style::default()
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Green)),
|
|
||||||
Span::styled("● ", bg.fg(Color::Green)),
|
|
||||||
Span::styled(format!("pid {} {}", pid, phase), bg),
|
|
||||||
],
|
|
||||||
(None, Some(phase)) => vec![
|
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Cyan)),
|
|
||||||
Span::styled("◆ ", bg.fg(Color::Cyan)),
|
|
||||||
Span::styled(phase.clone(), bg),
|
|
||||||
],
|
|
||||||
_ => vec![
|
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Gray)),
|
|
||||||
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
lines.push(Line::from(status));
|
|
||||||
|
let status_spans = if snap.running {
|
||||||
|
vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:<30}", prefix, snap.name),
|
||||||
|
bg.fg(Color::Green),
|
||||||
|
),
|
||||||
|
Span::styled("● ", bg.fg(Color::Green)),
|
||||||
|
Span::styled(
|
||||||
|
format!("phase: {} turn: {}", snap.current_phase, snap.turn),
|
||||||
|
bg,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
let ago = snap.last_run_secs_ago
|
||||||
|
.map(|s| {
|
||||||
|
if s < 60.0 { format!("{:.0}s ago", s) }
|
||||||
|
else if s < 3600.0 { format!("{:.0}m ago", s / 60.0) }
|
||||||
|
else { format!("{:.1}h ago", s / 3600.0) }
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "never".to_string());
|
||||||
|
vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:<30}", prefix, snap.name),
|
||||||
|
bg.fg(Color::Gray),
|
||||||
|
),
|
||||||
|
Span::styled("○ ", bg.fg(Color::DarkGray)),
|
||||||
|
Span::styled(
|
||||||
|
format!("idle last: {} walked: {}", ago, snap.walked_count),
|
||||||
|
bg.fg(Color::DarkGray),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
lines.push(Line::from(status_spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
|
|
@ -103,61 +108,6 @@ impl SubconsciousScreen {
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
||||||
let para = Paragraph::new(lines).block(block).scroll((self.scroll, 0));
|
|
||||||
frame.render_widget(para, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_log(&self, frame: &mut Frame, area: Rect, app: &App) {
|
|
||||||
let agent = app.agent_state.get(self.selected);
|
|
||||||
let name = agent.map(|a| a.name.as_str()).unwrap_or("?");
|
|
||||||
let mut lines: Vec<Line> = 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!("── {} ──", name), section));
|
|
||||||
lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint));
|
|
||||||
lines.push(Line::raw(""));
|
|
||||||
|
|
||||||
match agent.and_then(|a| a.pid) {
|
|
||||||
Some(pid) => {
|
|
||||||
let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?");
|
|
||||||
lines.push(Line::from(vec![
|
|
||||||
Span::styled(" Status: ", Style::default()),
|
|
||||||
Span::styled(format!("● running pid {} phase: {}", pid, phase),
|
|
||||||
Style::default().fg(Color::Green)),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
lines.push(Line::styled(" Status: idle", Style::default().fg(Color::DarkGray)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(log_path) = agent.and_then(|a| a.log_path.as_ref()) {
|
|
||||||
lines.push(Line::raw(format!(" Log: {}", log_path.display())));
|
|
||||||
}
|
|
||||||
lines.push(Line::raw(""));
|
|
||||||
|
|
||||||
lines.push(Line::styled("── Agent Log ──", section));
|
|
||||||
if let Some(content) = agent
|
|
||||||
.and_then(|a| a.log_path.as_ref())
|
|
||||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
|
||||||
{
|
|
||||||
let log_lines: Vec<&str> = content.lines().collect();
|
|
||||||
let start = log_lines.len().saturating_sub(40);
|
|
||||||
for line in &log_lines[start..] {
|
|
||||||
lines.push(Line::raw(format!(" {}", line)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines.push(Line::styled(" (no log available)", hint));
|
|
||||||
}
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.title_top(Line::from(screen_legend()).left_aligned())
|
|
||||||
.title_top(Line::from(format!(" {} ", name)).right_aligned())
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
|
||||||
|
|
||||||
let para = Paragraph::new(lines)
|
let para = Paragraph::new(lines)
|
||||||
.block(block)
|
.block(block)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue