consciousness/src/user/unconscious.rs
Kent Overstreet 68f115b880 user: InteractScreen extracted, all screens use ScreenView trait
InteractScreen in chat.rs owns conversation/autonomous/tools panes,
textarea, input history, scroll state. App is now just shared state
(status, sampling params, agent_state, channel_status, idle_info).

Event loop holds InteractScreen separately for UiMessage routing.
Overlay screens (F2-F5) in screens vec. F-key switching preserves
state across screen changes.

handle_ui_message moved from App to InteractScreen.
handle_key split: global keys on App, screen keys in tick().
draw dispatch eliminated — each screen draws itself.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 18:57:54 -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: &mut 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);
}