consciousness/src/user/unconscious.rs
Kent Overstreet 927cddd864 user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.

- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll

Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.

App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.

InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00

188 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// unconscious_screen.rs — F4: memory daemon status
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
Frame,
crossterm::event::{KeyCode, KeyEvent},
};
use super::{App, ScreenAction, ScreenView, screen_legend};
use crate::subconscious::daemon::GraphHealth;
#[derive(serde::Deserialize, Default)]
struct DaemonStatus {
#[allow(dead_code)]
pid: u32,
tasks: Vec<jobkit::TaskInfo>,
#[serde(default)]
graph_health: Option<GraphHealth>,
}
fn fetch_status() -> Option<DaemonStatus> {
let json = jobkit::daemon::socket::send_rpc(&crate::config::get().data_dir, "")?;
serde_json::from_str(&json).ok()
}
pub(crate) struct UnconsciousScreen {
scroll: u16,
}
impl UnconsciousScreen {
pub fn new() -> Self { Self { scroll: 0 } }
}
impl ScreenView for UnconsciousScreen {
fn label(&self) -> &'static str { "unconscious" }
fn tick(&mut self, frame: &mut Frame, area: Rect,
key: Option<KeyEvent>, _app: &App) -> Option<ScreenAction> {
if let Some(key) = key {
match key.code {
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
KeyCode::PageDown => { self.scroll += 20; }
KeyCode::Esc => return Some(ScreenAction::Switch(0)),
_ => {}
}
}
let block = Block::default()
.title_top(Line::from(screen_legend()).left_aligned())
.title_top(Line::from(" unconscious ").right_aligned())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let inner = block.inner(area);
frame.render_widget(block, area);
let status = fetch_status();
match &status {
None => {
frame.render_widget(
Paragraph::new(Line::styled(" daemon not running",
Style::default().fg(Color::DarkGray))),
inner,
);
}
Some(st) => {
let has_health = st.graph_health.is_some();
let [health_area, tasks_area] = Layout::vertical([
Constraint::Length(if has_health { 9 } else { 0 }),
Constraint::Min(1),
]).areas(inner);
if let Some(ref gh) = st.graph_health {
render_health(frame, gh, health_area);
}
render_tasks(frame, &st.tasks, tasks_area);
}
}
None
}
}
fn render_health(frame: &mut Frame, gh: &GraphHealth, area: Rect) {
let [metrics_area, gauges_area, plan_area] = Layout::vertical([
Constraint::Length(2), Constraint::Length(4), Constraint::Min(1),
]).areas(area);
let summary = Line::from(format!(
" {} nodes {} edges {} communities", gh.nodes, gh.edges, gh.communities
));
let ep_line = Line::from(vec![
Span::raw(" episodic: "),
Span::styled(format!("{:.0}%", gh.episodic_ratio * 100.0),
if gh.episodic_ratio < 0.4 { Style::default().fg(Color::Green) }
else { Style::default().fg(Color::Red) }),
Span::raw(format!(" σ={:.1} interference={}", gh.sigma, gh.interference)),
]);
frame.render_widget(Paragraph::new(vec![summary, ep_line]), metrics_area);
let [g1, g2, g3] = Layout::horizontal([
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
]).areas(gauges_area);
let alpha_color = if gh.alpha >= 2.5 { Color::Green } else { Color::Red };
frame.render_widget(Gauge::default()
.block(Block::default().borders(Borders::ALL).title(" α (≥2.5) "))
.gauge_style(Style::default().fg(alpha_color))
.ratio((gh.alpha / 5.0).clamp(0.0, 1.0) as f64)
.label(format!("{:.2}", gh.alpha)), g1);
let gini_color = if gh.gini <= 0.4 { Color::Green } else { Color::Red };
frame.render_widget(Gauge::default()
.block(Block::default().borders(Borders::ALL).title(" gini (≤0.4) "))
.gauge_style(Style::default().fg(gini_color))
.ratio(gh.gini.clamp(0.0, 1.0) as f64)
.label(format!("{:.3}", gh.gini)), g2);
let cc_color = if gh.avg_cc >= 0.2 { Color::Green } else { Color::Red };
frame.render_widget(Gauge::default()
.block(Block::default().borders(Borders::ALL).title(" cc (≥0.2) "))
.gauge_style(Style::default().fg(cc_color))
.ratio(gh.avg_cc.clamp(0.0, 1.0) as f64)
.label(format!("{:.3}", gh.avg_cc)), g3);
let plan_total: usize = gh.plan_counts.values().sum::<usize>() + 1;
let mut plan_items: Vec<_> = gh.plan_counts.iter().filter(|(_, c)| **c > 0).collect();
plan_items.sort_by(|a, b| a.0.cmp(b.0));
let plan_summary: Vec<String> = plan_items.iter().map(|(a, c)| format!("{}{}", &a[..1], c)).collect();
frame.render_widget(Paragraph::new(Line::from(vec![
Span::raw(" plan: "),
Span::styled(format!("{}", plan_total), Style::default().add_modifier(Modifier::BOLD)),
Span::raw(format!(" agents ({} +health)", plan_summary.join(" "))),
])), plan_area);
}
fn render_tasks(frame: &mut Frame, tasks: &[jobkit::TaskInfo], area: Rect) {
let mut lines: Vec<Line> = Vec::new();
let section = Style::default().fg(Color::Yellow);
let dim = Style::default().fg(Color::DarkGray);
let running: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Running)).collect();
let completed: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Completed)).collect();
let failed: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Failed)).collect();
lines.push(Line::styled("── Tasks ──", section));
lines.push(Line::raw(format!(" Running: {} Completed: {} Failed: {}", running.len(), completed.len(), failed.len())));
lines.push(Line::raw(""));
if !running.is_empty() {
for task in &running {
let elapsed = task.started_at.map(|s| {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs_f64();
format!("{}s", (now - s) as u64)
}).unwrap_or_default();
lines.push(Line::from(vec![
Span::raw(" "), Span::styled("", Style::default().fg(Color::Green)),
Span::raw(format!(" {} ({})", task.name, elapsed)),
]));
}
lines.push(Line::raw(""));
}
if !completed.is_empty() {
lines.push(Line::styled(" Recent:", dim));
for task in completed.iter().rev().take(10) {
lines.push(Line::from(vec![
Span::raw(" "), Span::styled("", Style::default().fg(Color::Green)),
Span::raw(format!(" {}", task.name)),
]));
}
}
if !failed.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(" Failed:", Style::default().fg(Color::Red)));
for task in failed.iter().rev().take(5) {
lines.push(Line::from(vec![
Span::raw(" "), Span::styled("", Style::default().fg(Color::Red)),
Span::raw(format!(" {}", task.name)),
]));
}
}
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
}