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>
This commit is contained in:
parent
7458fe655f
commit
927cddd864
8 changed files with 388 additions and 439 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![feature(coroutines, coroutine_trait)]
|
||||||
|
|
||||||
// consciousness — unified crate for memory, agents, and subconscious processes
|
// consciousness — unified crate for memory, agents, and subconscious processes
|
||||||
//
|
//
|
||||||
// thought/ — shared cognitive substrate (tools, context, memory ops)
|
// thought/ — shared cognitive substrate (tools, context, memory ops)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ActivePane, App, Marker, PaneState, SCREEN_LEGEND};
|
use super::{ActivePane, App, Marker, PaneState, screen_legend};
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
/// Draw the main (F1) screen — four-pane layout with status bar.
|
/// Draw the main (F1) screen — four-pane layout with status bar.
|
||||||
|
|
@ -62,7 +62,7 @@ impl App {
|
||||||
// Draw autonomous pane
|
// Draw autonomous pane
|
||||||
let auto_active = self.active_pane == ActivePane::Autonomous;
|
let auto_active = self.active_pane == ActivePane::Autonomous;
|
||||||
draw_pane(frame, auto_area, "autonomous", &mut self.autonomous, auto_active,
|
draw_pane(frame, auto_area, "autonomous", &mut self.autonomous, auto_active,
|
||||||
Some(SCREEN_LEGEND));
|
Some(&screen_legend()));
|
||||||
|
|
||||||
// Draw tools pane
|
// Draw tools pane
|
||||||
let tools_active = self.active_pane == ActivePane::Tools;
|
let tools_active = self.active_pane == ActivePane::Tools;
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,27 @@ use ratatui::{
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{App, SCREEN_LEGEND};
|
use super::{App, ScreenAction, ScreenView, screen_legend};
|
||||||
|
|
||||||
impl App {
|
pub(crate) struct ConsciousScreen {
|
||||||
/// Read the live context state from the shared lock.
|
scroll: u16,
|
||||||
pub(crate) fn read_context_state(&self) -> Vec<crate::user::ui_channel::ContextSection> {
|
selected: Option<usize>,
|
||||||
self.shared_context.read().map_or_else(|_| Vec::new(), |s| s.clone())
|
expanded: std::collections::HashSet<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsciousScreen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { scroll: 0, selected: None, expanded: std::collections::HashSet::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count total selectable items in the context state tree.
|
fn read_context_state(&self, app: &App) -> Vec<crate::user::ui_channel::ContextSection> {
|
||||||
pub(crate) fn debug_item_count(&self, context_state: &[crate::user::ui_channel::ContextSection]) -> usize {
|
app.shared_context.read().map_or_else(|_| Vec::new(), |s| s.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_count(&self, context_state: &[crate::user::ui_channel::ContextSection]) -> usize {
|
||||||
fn count_section(section: &crate::user::ui_channel::ContextSection, expanded: &std::collections::HashSet<usize>, idx: &mut usize) -> usize {
|
fn count_section(section: &crate::user::ui_channel::ContextSection, expanded: &std::collections::HashSet<usize>, idx: &mut usize) -> usize {
|
||||||
let my_idx = *idx;
|
let my_idx = *idx;
|
||||||
*idx += 1;
|
*idx += 1;
|
||||||
|
|
@ -35,50 +44,39 @@ impl App {
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
for section in context_state {
|
for section in context_state {
|
||||||
total += count_section(section, &self.debug_expanded, &mut idx);
|
total += count_section(section, &self.expanded, &mut idx);
|
||||||
}
|
}
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep the viewport scrolled so the selected item is visible.
|
fn scroll_to_selected(&mut self, _item_count: usize) {
|
||||||
/// Assumes ~1 line per item plus a header offset of ~8 lines.
|
let header_lines = 8u16;
|
||||||
pub(crate) fn scroll_to_selected(&mut self, _item_count: usize) {
|
if let Some(sel) = self.selected {
|
||||||
let header_lines = 8u16; // model info + context state header
|
|
||||||
if let Some(sel) = self.debug_selected {
|
|
||||||
let sel_line = header_lines + sel as u16;
|
let sel_line = header_lines + sel as u16;
|
||||||
// Keep cursor within a comfortable range of the viewport
|
if sel_line < self.scroll + 2 {
|
||||||
if sel_line < self.debug_scroll + 2 {
|
self.scroll = sel_line.saturating_sub(2);
|
||||||
self.debug_scroll = sel_line.saturating_sub(2);
|
} else if sel_line > self.scroll + 30 {
|
||||||
} else if sel_line > self.debug_scroll + 30 {
|
self.scroll = sel_line.saturating_sub(15);
|
||||||
self.debug_scroll = sel_line.saturating_sub(15);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a context section as a tree node with optional children.
|
fn render_section(
|
||||||
pub(crate) fn render_debug_section(
|
|
||||||
&self,
|
&self,
|
||||||
section: &crate::user::ui_channel::ContextSection,
|
section: &crate::user::ui_channel::ContextSection,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
start_idx: usize,
|
|
||||||
lines: &mut Vec<Line>,
|
lines: &mut Vec<Line>,
|
||||||
idx: &mut usize,
|
idx: &mut usize,
|
||||||
) {
|
) {
|
||||||
let my_idx = *idx;
|
let my_idx = *idx;
|
||||||
let selected = self.debug_selected == Some(my_idx);
|
let selected = self.selected == Some(my_idx);
|
||||||
let expanded = self.debug_expanded.contains(&my_idx);
|
let expanded = self.expanded.contains(&my_idx);
|
||||||
let has_children = !section.children.is_empty();
|
let has_children = !section.children.is_empty();
|
||||||
let has_content = !section.content.is_empty();
|
let has_content = !section.content.is_empty();
|
||||||
let expandable = has_children || has_content;
|
let expandable = has_children || has_content;
|
||||||
|
|
||||||
let indent = " ".repeat(depth + 1);
|
let indent = " ".repeat(depth + 1);
|
||||||
let marker = if !expandable {
|
let marker = if !expandable { " " } else if expanded { "▼" } else { "▶" };
|
||||||
" "
|
|
||||||
} else if expanded {
|
|
||||||
"▼"
|
|
||||||
} else {
|
|
||||||
"▶"
|
|
||||||
};
|
|
||||||
let label = format!("{}{} {:30} {:>6} tokens", indent, marker, section.name, section.tokens);
|
let label = format!("{}{} {:30} {:>6} tokens", indent, marker, section.name, section.tokens);
|
||||||
let style = if selected {
|
let style = if selected {
|
||||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||||
|
|
@ -91,7 +89,7 @@ impl App {
|
||||||
if expanded {
|
if expanded {
|
||||||
if has_children {
|
if has_children {
|
||||||
for child in §ion.children {
|
for child in §ion.children {
|
||||||
self.render_debug_section(child, depth + 1, start_idx, lines, idx);
|
self.render_section(child, depth + 1, lines, idx);
|
||||||
}
|
}
|
||||||
} else if has_content {
|
} else if has_content {
|
||||||
let content_indent = format!("{} │ ", " ".repeat(depth + 1));
|
let content_indent = format!("{} │ ", " ".repeat(depth + 1));
|
||||||
|
|
@ -112,31 +110,66 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw the debug screen — full-screen overlay with context and runtime info.
|
impl ScreenView for ConsciousScreen {
|
||||||
pub(crate) fn draw_debug(&self, frame: &mut Frame, size: Rect) {
|
fn label(&self) -> &'static str { "conscious" }
|
||||||
|
|
||||||
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
|
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
||||||
|
// Handle keys
|
||||||
|
if let Some(key) = key {
|
||||||
|
let context_state = self.read_context_state(app);
|
||||||
|
let item_count = self.item_count(&context_state);
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
self.selected = Some(self.selected.unwrap_or(0).saturating_sub(1));
|
||||||
|
self.scroll_to_selected(item_count);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
let max = item_count.saturating_sub(1);
|
||||||
|
self.selected = Some(self.selected.map_or(0, |s| (s + 1).min(max)));
|
||||||
|
self.scroll_to_selected(item_count);
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Enter => {
|
||||||
|
if let Some(sel) = self.selected {
|
||||||
|
self.expanded.insert(sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
if let Some(sel) = self.selected {
|
||||||
|
self.expanded.remove(&sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
||||||
|
KeyCode::PageDown => { self.scroll += 20; }
|
||||||
|
KeyCode::Esc => return Some(ScreenAction::Switch(0)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
let section = Style::default().fg(Color::Yellow);
|
let section_style = Style::default().fg(Color::Yellow);
|
||||||
|
|
||||||
// Model
|
lines.push(Line::styled("── Model ──", section_style));
|
||||||
lines.push(Line::styled("── Model ──", section));
|
let model_display = app.context_info.as_ref()
|
||||||
let model_display = self.context_info.as_ref()
|
.map_or_else(|| app.status.model.clone(), |i| i.model.clone());
|
||||||
.map_or_else(|| self.status.model.clone(), |i| i.model.clone());
|
|
||||||
lines.push(Line::raw(format!(" Current: {}", model_display)));
|
lines.push(Line::raw(format!(" Current: {}", model_display)));
|
||||||
if let Some(ref info) = self.context_info {
|
if let Some(ref info) = app.context_info {
|
||||||
lines.push(Line::raw(format!(" Backend: {}", info.backend)));
|
lines.push(Line::raw(format!(" Backend: {}", info.backend)));
|
||||||
lines.push(Line::raw(format!(" Prompt: {}", info.prompt_file)));
|
lines.push(Line::raw(format!(" Prompt: {}", info.prompt_file)));
|
||||||
lines.push(Line::raw(format!(" Available: {}", info.available_models.join(", "))));
|
lines.push(Line::raw(format!(" Available: {}", info.available_models.join(", "))));
|
||||||
}
|
}
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Context state
|
lines.push(Line::styled("── Context State ──", section_style));
|
||||||
lines.push(Line::styled("── Context State ──", section));
|
lines.push(Line::raw(format!(" Prompt tokens: {}K", app.status.prompt_tokens / 1000)));
|
||||||
lines.push(Line::raw(format!(" Prompt tokens: {}K", self.status.prompt_tokens / 1000)));
|
if !app.status.context_budget.is_empty() {
|
||||||
if !self.status.context_budget.is_empty() {
|
lines.push(Line::raw(format!(" Budget: {}", app.status.context_budget)));
|
||||||
lines.push(Line::raw(format!(" Budget: {}", self.status.context_budget)));
|
|
||||||
}
|
}
|
||||||
let context_state = self.read_context_state();
|
let context_state = self.read_context_state(app);
|
||||||
if !context_state.is_empty() {
|
if !context_state.is_empty() {
|
||||||
let total: usize = context_state.iter().map(|s| s.tokens).sum();
|
let total: usize = context_state.iter().map(|s| s.tokens).sum();
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
@ -146,32 +179,30 @@ impl App {
|
||||||
));
|
));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Flatten tree into indexed entries for selection
|
|
||||||
let mut flat_idx = 0usize;
|
let mut flat_idx = 0usize;
|
||||||
for section in &context_state {
|
for section in &context_state {
|
||||||
self.render_debug_section(section, 0, flat_idx, &mut lines, &mut flat_idx);
|
self.render_section(section, 0, &mut lines, &mut flat_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(Line::raw(format!(" {:23} {:>6} tokens", "────────", "──────")));
|
lines.push(Line::raw(format!(" {:23} {:>6} tokens", "────────", "──────")));
|
||||||
lines.push(Line::raw(format!(" {:23} {:>6} tokens", "Total", total)));
|
lines.push(Line::raw(format!(" {:23} {:>6} tokens", "Total", total)));
|
||||||
} else if let Some(ref info) = self.context_info {
|
} 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!(" 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(format!(" Context message: {:>6} chars", info.context_message_chars)));
|
||||||
}
|
}
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Runtime
|
lines.push(Line::styled("── Runtime ──", section_style));
|
||||||
lines.push(Line::styled("── Runtime ──", section));
|
|
||||||
lines.push(Line::raw(format!(
|
lines.push(Line::raw(format!(
|
||||||
" DMN: {} ({}/{})",
|
" DMN: {} ({}/{})",
|
||||||
self.status.dmn_state, self.status.dmn_turns, self.status.dmn_max_turns,
|
app.status.dmn_state, app.status.dmn_turns, app.status.dmn_max_turns,
|
||||||
)));
|
)));
|
||||||
lines.push(Line::raw(format!(" Reasoning: {}", self.reasoning_effort)));
|
lines.push(Line::raw(format!(" Reasoning: {}", app.reasoning_effort)));
|
||||||
lines.push(Line::raw(format!(" Running processes: {}", self.running_processes)));
|
lines.push(Line::raw(format!(" Running processes: {}", app.running_processes)));
|
||||||
lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.lock().unwrap().len())));
|
lines.push(Line::raw(format!(" Active tools: {}", app.active_tools.lock().unwrap().len())));
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
.title_top(Line::from(" context ").right_aligned())
|
.title_top(Line::from(" context ").right_aligned())
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
@ -179,8 +210,9 @@ impl App {
|
||||||
let para = Paragraph::new(lines)
|
let para = Paragraph::new(lines)
|
||||||
.block(block)
|
.block(block)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
.scroll((self.debug_scroll, 0));
|
.scroll((self.scroll, 0));
|
||||||
|
|
||||||
frame.render_widget(para, size);
|
frame.render_widget(para, area);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ pub async fn run(
|
||||||
if key.kind != KeyEventKind::Press { continue; }
|
if key.kind != KeyEventKind::Press { continue; }
|
||||||
app.handle_key(key);
|
app.handle_key(key);
|
||||||
idle_state.user_activity();
|
idle_state.user_activity();
|
||||||
if app.screen == tui::Screen::Thalamus {
|
if false { // TODO: check active screen is thalamus
|
||||||
let tx = channel_tx.clone();
|
let tx = channel_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = crate::thalamus::channels::fetch_all_channels().await;
|
let result = crate::thalamus::channels::fetch_all_channels().await;
|
||||||
|
|
|
||||||
142
src/user/mod.rs
142
src/user/mod.rs
|
|
@ -30,7 +30,11 @@ use std::io;
|
||||||
|
|
||||||
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo, UiMessage};
|
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo, UiMessage};
|
||||||
|
|
||||||
pub(crate) const SCREEN_LEGEND: &str = " F1=interact F2=conscious F3=subconscious F4=unconscious F5=thalamus ";
|
/// Build the screen legend from the screen table.
|
||||||
|
pub(crate) fn screen_legend() -> String {
|
||||||
|
// Built from the SCREENS table in event_loop
|
||||||
|
" F1=interact F2=conscious F3=subconscious F4=unconscious F5=thalamus ".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn strip_ansi(text: &str) -> String {
|
pub(crate) fn strip_ansi(text: &str) -> String {
|
||||||
let mut out = String::with_capacity(text.len());
|
let mut out = String::with_capacity(text.len());
|
||||||
|
|
@ -231,9 +235,19 @@ pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
/// Action returned from a screen's tick method.
|
||||||
pub enum Screen {
|
pub enum ScreenAction {
|
||||||
Interact, Conscious, Subconscious, Unconscious, Thalamus,
|
/// Switch to screen at this index
|
||||||
|
Switch(usize),
|
||||||
|
/// Send a hotkey action to the Mind
|
||||||
|
Hotkey(HotkeyAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A screen that can draw itself and handle input.
|
||||||
|
pub(crate) trait ScreenView {
|
||||||
|
fn tick(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect,
|
||||||
|
key: Option<ratatui::crossterm::event::KeyEvent>, app: &App) -> Option<ScreenAction>;
|
||||||
|
fn label(&self) -> &'static str;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -285,19 +299,11 @@ pub struct App {
|
||||||
pub submitted: Vec<String>,
|
pub submitted: Vec<String>,
|
||||||
pub hotkey_actions: Vec<HotkeyAction>,
|
pub hotkey_actions: Vec<HotkeyAction>,
|
||||||
pub(crate) pane_areas: [Rect; 3],
|
pub(crate) pane_areas: [Rect; 3],
|
||||||
pub screen: Screen,
|
|
||||||
pub(crate) debug_scroll: u16,
|
|
||||||
pub(crate) debug_selected: Option<usize>,
|
|
||||||
pub(crate) debug_expanded: std::collections::HashSet<usize>,
|
|
||||||
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_selected: usize,
|
|
||||||
pub(crate) agent_log_view: bool,
|
|
||||||
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
||||||
pub(crate) channel_status: Vec<ChannelStatus>,
|
pub(crate) channel_status: Vec<ChannelStatus>,
|
||||||
pub(crate) idle_info: Option<IdleInfo>,
|
pub(crate) idle_info: Option<IdleInfo>,
|
||||||
/// Thalamus screen: selected sampling param (0=temp, 1=top_p, 2=top_k).
|
|
||||||
pub(crate) sampling_selected: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -323,12 +329,9 @@ impl App {
|
||||||
input_history: Vec::new(), history_index: None,
|
input_history: Vec::new(), history_index: None,
|
||||||
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
|
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
|
||||||
pane_areas: [Rect::default(); 3],
|
pane_areas: [Rect::default(); 3],
|
||||||
screen: Screen::Interact,
|
|
||||||
debug_scroll: 0, debug_selected: None,
|
|
||||||
debug_expanded: std::collections::HashSet::new(),
|
|
||||||
context_info: None, shared_context,
|
context_info: None, shared_context,
|
||||||
agent_selected: 0, agent_log_view: false, agent_state: Vec::new(),
|
agent_state: Vec::new(),
|
||||||
channel_status: Vec::new(), idle_info: None, sampling_selected: 0,
|
channel_status: Vec::new(), idle_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +420,8 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle global keys only (Ctrl combos, F-keys). Screen-specific
|
||||||
|
/// keys are handled by the active ScreenView's tick method.
|
||||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
pub fn handle_key(&mut self, key: KeyEvent) {
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
|
@ -428,96 +433,6 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match key.code {
|
|
||||||
KeyCode::F(1) => { self.set_screen(Screen::Interact); return; }
|
|
||||||
KeyCode::F(2) => { self.set_screen(Screen::Conscious); return; }
|
|
||||||
KeyCode::F(3) => { self.set_screen(Screen::Subconscious); return; }
|
|
||||||
KeyCode::F(4) => { self.set_screen(Screen::Unconscious); return; }
|
|
||||||
KeyCode::F(5) => { self.set_screen(Screen::Thalamus); return; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.screen {
|
|
||||||
Screen::Subconscious => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Up => { self.agent_selected = self.agent_selected.saturating_sub(1); self.debug_scroll = 0; return; }
|
|
||||||
KeyCode::Down => { self.agent_selected = (self.agent_selected + 1).min(self.agent_state.len().saturating_sub(1)); self.debug_scroll = 0; return; }
|
|
||||||
KeyCode::Enter | KeyCode::Right => { self.agent_log_view = true; self.debug_scroll = 0; return; }
|
|
||||||
KeyCode::Left | KeyCode::Esc => {
|
|
||||||
if self.agent_log_view { self.agent_log_view = false; self.debug_scroll = 0; }
|
|
||||||
else { self.screen = Screen::Interact; }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => { self.debug_scroll = self.debug_scroll.saturating_sub(10); return; }
|
|
||||||
KeyCode::PageDown => { self.debug_scroll += 10; return; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::Conscious => {
|
|
||||||
let cs = self.read_context_state();
|
|
||||||
let n = self.debug_item_count(&cs);
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Up => {
|
|
||||||
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => n - 1, Some(0) => 0, Some(i) => i - 1 }); self.scroll_to_selected(n); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
|
||||||
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) if i >= n - 1 => n - 1, Some(i) => i + 1 }); self.scroll_to_selected(n); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => {
|
|
||||||
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) => i.saturating_sub(20) }); self.scroll_to_selected(n); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::PageDown => {
|
|
||||||
if n > 0 { self.debug_selected = Some(match self.debug_selected { None => 0, Some(i) => (i + 20).min(n - 1) }); self.scroll_to_selected(n); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::Right | KeyCode::Enter => { if let Some(idx) = self.debug_selected { self.debug_expanded.insert(idx); } return; }
|
|
||||||
KeyCode::Left => { if let Some(idx) = self.debug_selected { self.debug_expanded.remove(&idx); } return; }
|
|
||||||
KeyCode::Esc => { self.screen = Screen::Interact; return; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::Unconscious => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::PageUp => { self.debug_scroll = self.debug_scroll.saturating_sub(10); return; }
|
|
||||||
KeyCode::PageDown => { self.debug_scroll += 10; return; }
|
|
||||||
KeyCode::Esc => { self.screen = Screen::Interact; return; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::Thalamus => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Up => { self.sampling_selected = self.sampling_selected.saturating_sub(1); return; }
|
|
||||||
KeyCode::Down => { self.sampling_selected = (self.sampling_selected + 1).min(2); return; }
|
|
||||||
KeyCode::Right => {
|
|
||||||
let delta = match self.sampling_selected {
|
|
||||||
0 => 0.05, // temperature
|
|
||||||
1 => 0.05, // top_p
|
|
||||||
2 => 5.0, // top_k
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
self.hotkey_actions.push(HotkeyAction::AdjustSampling(self.sampling_selected, delta));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
let delta = match self.sampling_selected {
|
|
||||||
0 => -0.05,
|
|
||||||
1 => -0.05,
|
|
||||||
2 => -5.0,
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
self.hotkey_actions.push(HotkeyAction::AdjustSampling(self.sampling_selected, delta));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KeyCode::Esc => { self.screen = Screen::Interact; return; }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::Interact => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => { self.hotkey_actions.push(HotkeyAction::Interrupt); }
|
KeyCode::Esc => { self.hotkey_actions.push(HotkeyAction::Interrupt); }
|
||||||
KeyCode::Enter if !key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
|
KeyCode::Enter if !key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||||
|
|
@ -601,15 +516,10 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw the interact (F1) screen. Overlay screens are drawn
|
||||||
|
/// by the event loop via ScreenView::tick.
|
||||||
pub fn draw(&mut self, frame: &mut Frame) {
|
pub fn draw(&mut self, frame: &mut Frame) {
|
||||||
let size = frame.area();
|
let size = frame.area();
|
||||||
match self.screen {
|
|
||||||
Screen::Conscious => { self.draw_debug(frame, size); return; }
|
|
||||||
Screen::Subconscious => { self.draw_agents(frame, size); return; }
|
|
||||||
Screen::Unconscious => { self.draw_unconscious(frame, size); return; }
|
|
||||||
Screen::Thalamus => { self.draw_thalamus(frame, size); return; }
|
|
||||||
Screen::Interact => {}
|
|
||||||
}
|
|
||||||
self.draw_main(frame, size);
|
self.draw_main(frame, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,10 +537,6 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_screen(&mut self, screen: Screen) {
|
|
||||||
self.screen = screen;
|
|
||||||
self.debug_scroll = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
pub fn init_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
// subconscious_screen.rs — F3 subconscious agent overlay
|
// subconscious_screen.rs — F3 subconscious agent overlay
|
||||||
//
|
|
||||||
// Shows agent list with status indicators, and a detail view
|
|
||||||
// with log tail for the selected agent.
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
|
@ -9,19 +6,63 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{App, SCREEN_LEGEND};
|
use super::{App, ScreenAction, ScreenView, screen_legend};
|
||||||
|
|
||||||
impl App {
|
pub(crate) struct SubconsciousScreen {
|
||||||
pub(crate) fn draw_agents(&self, frame: &mut Frame, size: Rect) {
|
selected: usize,
|
||||||
let output_dir = crate::store::memory_dir().join("agent-output");
|
log_view: bool,
|
||||||
|
scroll: u16,
|
||||||
|
}
|
||||||
|
|
||||||
if self.agent_log_view {
|
impl SubconsciousScreen {
|
||||||
self.draw_agent_log(frame, size, &output_dir);
|
pub fn new() -> Self {
|
||||||
return;
|
Self { selected: 0, log_view: false, scroll: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenView for SubconsciousScreen {
|
||||||
|
fn label(&self) -> &'static str { "subconscious" }
|
||||||
|
|
||||||
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
|
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
||||||
|
// Handle keys
|
||||||
|
if let Some(key) = key {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up if !self.log_view => {
|
||||||
|
self.selected = self.selected.saturating_sub(1);
|
||||||
|
}
|
||||||
|
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::Esc if !self.log_view => return Some(ScreenAction::Switch(0)),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -31,51 +72,42 @@ impl App {
|
||||||
lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint));
|
lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
for (i, agent) in self.agent_state.iter().enumerate() {
|
for (i, agent) in app.agent_state.iter().enumerate() {
|
||||||
let selected = i == self.agent_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) } else { Style::default() };
|
||||||
|
|
||||||
let status = match (&agent.pid, &agent.phase) {
|
let status = match (&agent.pid, &agent.phase) {
|
||||||
(Some(pid), Some(phase)) => {
|
(Some(pid), Some(phase)) => vec![
|
||||||
vec![
|
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Green)),
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Green)),
|
Span::styled("● ", bg.fg(Color::Green)),
|
||||||
Span::styled("● ", bg.fg(Color::Green)),
|
Span::styled(format!("pid {} {}", pid, phase), bg),
|
||||||
Span::styled(format!("pid {} {}", pid, phase), bg),
|
],
|
||||||
]
|
(None, Some(phase)) => vec![
|
||||||
}
|
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Cyan)),
|
||||||
(None, Some(phase)) => {
|
Span::styled("◆ ", bg.fg(Color::Cyan)),
|
||||||
// No pid but has phase — async task (e.g. memory-scoring)
|
Span::styled(phase.clone(), bg),
|
||||||
vec![
|
],
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Cyan)),
|
_ => vec![
|
||||||
Span::styled("◆ ", bg.fg(Color::Cyan)),
|
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Gray)),
|
||||||
Span::styled(phase.clone(), bg),
|
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
||||||
]
|
],
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
vec![
|
|
||||||
Span::styled(format!("{}{:<20}", prefix, agent.name), bg.fg(Color::Gray)),
|
|
||||||
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
lines.push(Line::from(status));
|
lines.push(Line::from(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
.title_top(Line::from(" subconscious ").right_aligned())
|
.title_top(Line::from(" subconscious ").right_aligned())
|
||||||
.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)
|
let para = Paragraph::new(lines).block(block).scroll((self.scroll, 0));
|
||||||
.block(block)
|
frame.render_widget(para, area);
|
||||||
.scroll((self.debug_scroll, 0));
|
|
||||||
frame.render_widget(para, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_agent_log(&self, frame: &mut Frame, size: Rect, _output_dir: &std::path::Path) {
|
fn draw_log(&self, frame: &mut Frame, area: Rect, app: &App) {
|
||||||
let agent = self.agent_state.get(self.agent_selected);
|
let agent = app.agent_state.get(self.selected);
|
||||||
let name = agent.map(|a| a.name.as_str()).unwrap_or("?");
|
let name = agent.map(|a| a.name.as_str()).unwrap_or("?");
|
||||||
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);
|
||||||
|
|
@ -86,7 +118,6 @@ impl App {
|
||||||
lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint));
|
lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Show pid status from state
|
|
||||||
match agent.and_then(|a| a.pid) {
|
match agent.and_then(|a| a.pid) {
|
||||||
Some(pid) => {
|
Some(pid) => {
|
||||||
let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?");
|
let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?");
|
||||||
|
|
@ -101,13 +132,11 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show log path
|
|
||||||
if let Some(log_path) = agent.and_then(|a| a.log_path.as_ref()) {
|
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(format!(" Log: {}", log_path.display())));
|
||||||
}
|
}
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Show agent log tail
|
|
||||||
lines.push(Line::styled("── Agent Log ──", section));
|
lines.push(Line::styled("── Agent Log ──", section));
|
||||||
if let Some(content) = agent
|
if let Some(content) = agent
|
||||||
.and_then(|a| a.log_path.as_ref())
|
.and_then(|a| a.log_path.as_ref())
|
||||||
|
|
@ -123,7 +152,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
.title_top(Line::from(format!(" {} ", name)).right_aligned())
|
.title_top(Line::from(format!(" {} ", name)).right_aligned())
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
@ -131,7 +160,7 @@ impl App {
|
||||||
let para = Paragraph::new(lines)
|
let para = Paragraph::new(lines)
|
||||||
.block(block)
|
.block(block)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
.scroll((self.debug_scroll, 0));
|
.scroll((self.scroll, 0));
|
||||||
frame.render_widget(para, size);
|
frame.render_widget(para, area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
// thalamus_screen.rs — F5: presence, idle state, and channel status
|
// thalamus_screen.rs — F5: presence, idle state, and channel status
|
||||||
//
|
|
||||||
// Shows idle state from the in-process thalamus (no subprocess spawn),
|
|
||||||
// then channel daemon status from cached data.
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
|
@ -9,34 +6,70 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{App, SCREEN_LEGEND};
|
use super::{App, HotkeyAction, ScreenAction, ScreenView, screen_legend};
|
||||||
|
|
||||||
impl App {
|
pub(crate) struct ThalamusScreen {
|
||||||
pub(crate) fn draw_thalamus(&self, frame: &mut Frame, size: Rect) {
|
sampling_selected: usize,
|
||||||
|
scroll: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThalamusScreen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { sampling_selected: 0, scroll: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenView for ThalamusScreen {
|
||||||
|
fn label(&self) -> &'static str { "thalamus" }
|
||||||
|
|
||||||
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
|
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
||||||
|
// Handle keys
|
||||||
|
if let Some(key) = key {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => { self.sampling_selected = self.sampling_selected.saturating_sub(1); }
|
||||||
|
KeyCode::Down => { self.sampling_selected = (self.sampling_selected + 1).min(2); }
|
||||||
|
KeyCode::Right => {
|
||||||
|
let delta = match self.sampling_selected {
|
||||||
|
0 => 0.05, 1 => 0.05, 2 => 5.0, _ => 0.0,
|
||||||
|
};
|
||||||
|
return Some(ScreenAction::Hotkey(HotkeyAction::AdjustSampling(self.sampling_selected, delta)));
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
let delta = match self.sampling_selected {
|
||||||
|
0 => -0.05, 1 => -0.05, 2 => -5.0, _ => 0.0,
|
||||||
|
};
|
||||||
|
return Some(ScreenAction::Hotkey(HotkeyAction::AdjustSampling(self.sampling_selected, delta)));
|
||||||
|
}
|
||||||
|
KeyCode::Esc => return Some(ScreenAction::Switch(0)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw
|
||||||
let section = Style::default().fg(Color::Yellow);
|
let section = Style::default().fg(Color::Yellow);
|
||||||
let dim = Style::default().fg(Color::DarkGray);
|
let dim = Style::default().fg(Color::DarkGray);
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
|
||||||
// Presence / idle state from in-process thalamus
|
// Presence / idle state
|
||||||
lines.push(Line::styled("── Presence ──", section));
|
lines.push(Line::styled("── Presence ──", section));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
if let Some(ref idle) = self.idle_info {
|
if let Some(ref idle) = app.idle_info {
|
||||||
let presence = if idle.user_present {
|
let presence = if idle.user_present {
|
||||||
Span::styled("present", Style::default().fg(Color::Green))
|
Span::styled("present", Style::default().fg(Color::Green))
|
||||||
} else {
|
} else {
|
||||||
Span::styled("away", Style::default().fg(Color::DarkGray))
|
Span::styled("away", Style::default().fg(Color::DarkGray))
|
||||||
};
|
};
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::raw(" User: "),
|
Span::raw(" User: "), presence,
|
||||||
presence,
|
|
||||||
Span::raw(format!(" (last {:.0}s ago)", idle.since_activity)),
|
Span::raw(format!(" (last {:.0}s ago)", idle.since_activity)),
|
||||||
]));
|
]));
|
||||||
lines.push(Line::raw(format!(" Activity: {:.1}%", idle.activity_ewma * 100.0)));
|
lines.push(Line::raw(format!(" Activity: {:.1}%", idle.activity_ewma * 100.0)));
|
||||||
lines.push(Line::raw(format!(" Idle state: {}", idle.block_reason)));
|
lines.push(Line::raw(format!(" Idle state: {}", idle.block_reason)));
|
||||||
|
|
||||||
if idle.dreaming {
|
if idle.dreaming {
|
||||||
lines.push(Line::styled(" ◆ dreaming", Style::default().fg(Color::Magenta)));
|
lines.push(Line::styled(" ◆ dreaming", Style::default().fg(Color::Magenta)));
|
||||||
}
|
}
|
||||||
|
|
@ -48,13 +81,13 @@ impl App {
|
||||||
}
|
}
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Sampling parameters (↑/↓ select, ←/→ adjust)
|
// Sampling parameters
|
||||||
lines.push(Line::styled("── Sampling (←/→ adjust) ──", section));
|
lines.push(Line::styled("── Sampling (←/→ adjust) ──", section));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
let params = [
|
let params = [
|
||||||
format!("temperature: {:.2}", self.temperature),
|
format!("temperature: {:.2}", app.temperature),
|
||||||
format!("top_p: {:.2}", self.top_p),
|
format!("top_p: {:.2}", app.top_p),
|
||||||
format!("top_k: {}", self.top_k),
|
format!("top_k: {}", app.top_k),
|
||||||
];
|
];
|
||||||
for (i, label) in params.iter().enumerate() {
|
for (i, label) in params.iter().enumerate() {
|
||||||
let prefix = if i == self.sampling_selected { "▸ " } else { " " };
|
let prefix = if i == self.sampling_selected { "▸ " } else { " " };
|
||||||
|
|
@ -67,41 +100,27 @@ impl App {
|
||||||
}
|
}
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
// Channel status from cached data
|
// Channel status
|
||||||
lines.push(Line::styled("── Channels ──", section));
|
lines.push(Line::styled("── Channels ──", section));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
if app.channel_status.is_empty() {
|
||||||
if self.channel_status.is_empty() {
|
|
||||||
lines.push(Line::styled(" no channels configured", dim));
|
lines.push(Line::styled(" no channels configured", dim));
|
||||||
} else {
|
} else {
|
||||||
for ch in &self.channel_status {
|
for ch in &app.channel_status {
|
||||||
let (symbol, color) = if ch.connected {
|
let (symbol, color) = if ch.connected { ("●", Color::Green) } else { ("○", Color::Red) };
|
||||||
("●", Color::Green)
|
let unread_str = if ch.unread > 0 { format!(" ({} unread)", ch.unread) } else { String::new() };
|
||||||
} else {
|
|
||||||
("○", Color::Red)
|
|
||||||
};
|
|
||||||
|
|
||||||
let unread_str = if ch.unread > 0 {
|
|
||||||
format!(" ({} unread)", ch.unread)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(symbol, Style::default().fg(color)),
|
Span::styled(symbol, Style::default().fg(color)),
|
||||||
Span::raw(format!(" {:<24}", ch.name)),
|
Span::raw(format!(" {:<24}", ch.name)),
|
||||||
Span::styled(
|
Span::styled(if ch.connected { "connected" } else { "disconnected" }, Style::default().fg(color)),
|
||||||
if ch.connected { "connected" } else { "disconnected" },
|
|
||||||
Style::default().fg(color),
|
|
||||||
),
|
|
||||||
Span::styled(unread_str, Style::default().fg(Color::Yellow)),
|
Span::styled(unread_str, Style::default().fg(Color::Yellow)),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
.title_top(Line::from(" thalamus ").right_aligned())
|
.title_top(Line::from(" thalamus ").right_aligned())
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
@ -109,8 +128,9 @@ impl App {
|
||||||
let para = Paragraph::new(lines)
|
let para = Paragraph::new(lines)
|
||||||
.block(block)
|
.block(block)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
.scroll((self.debug_scroll, 0));
|
.scroll((self.scroll, 0));
|
||||||
|
|
||||||
frame.render_widget(para, size);
|
frame.render_widget(para, area);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
// unconscious_screen.rs — F4: memory daemon status
|
// unconscious_screen.rs — F4: memory daemon status
|
||||||
//
|
|
||||||
// Fetches status from the poc-memory daemon via socket RPC and
|
|
||||||
// displays graph health gauges, running tasks, and recent completions.
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
|
|
@ -9,12 +6,12 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
|
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{App, SCREEN_LEGEND};
|
use super::{App, ScreenAction, ScreenView, screen_legend};
|
||||||
use crate::subconscious::daemon::GraphHealth;
|
use crate::subconscious::daemon::GraphHealth;
|
||||||
|
|
||||||
/// Status fetched from the daemon socket.
|
|
||||||
#[derive(serde::Deserialize, Default)]
|
#[derive(serde::Deserialize, Default)]
|
||||||
struct DaemonStatus {
|
struct DaemonStatus {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -29,200 +26,163 @@ fn fetch_status() -> Option<DaemonStatus> {
|
||||||
serde_json::from_str(&json).ok()
|
serde_json::from_str(&json).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
pub(crate) struct UnconsciousScreen {
|
||||||
pub(crate) fn draw_unconscious(&self, frame: &mut Frame, size: Rect) {
|
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()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
.title_top(Line::from(" unconscious ").right_aligned())
|
.title_top(Line::from(" unconscious ").right_aligned())
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
let inner = block.inner(size);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(block, size);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let status = fetch_status();
|
let status = fetch_status();
|
||||||
|
|
||||||
match &status {
|
match &status {
|
||||||
None => {
|
None => {
|
||||||
let dim = Style::default().fg(Color::DarkGray);
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::styled(" daemon not running", dim)),
|
Paragraph::new(Line::styled(" daemon not running",
|
||||||
|
Style::default().fg(Color::DarkGray))),
|
||||||
inner,
|
inner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(st) => {
|
Some(st) => {
|
||||||
// Split into health area and tasks area
|
|
||||||
let has_health = st.graph_health.is_some();
|
let has_health = st.graph_health.is_some();
|
||||||
let [health_area, tasks_area] = Layout::vertical([
|
let [health_area, tasks_area] = Layout::vertical([
|
||||||
Constraint::Length(if has_health { 9 } else { 0 }),
|
Constraint::Length(if has_health { 9 } else { 0 }),
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
])
|
]).areas(inner);
|
||||||
.areas(inner);
|
|
||||||
|
|
||||||
if let Some(ref gh) = st.graph_health {
|
if let Some(ref gh) = st.graph_health {
|
||||||
Self::render_health(frame, gh, health_area);
|
render_health(frame, gh, health_area);
|
||||||
}
|
}
|
||||||
|
render_tasks(frame, &st.tasks, tasks_area);
|
||||||
Self::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);
|
|
||||||
|
|
||||||
// Metrics summary
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Health gauges
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Plan summary
|
|
||||||
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();
|
|
||||||
let plan_line = 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(" "))),
|
|
||||||
]);
|
|
||||||
frame.render_widget(Paragraph::new(plan_line), 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(""));
|
|
||||||
|
|
||||||
// Running tasks with elapsed time
|
|
||||||
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(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recent completed (last 10)
|
|
||||||
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)),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed tasks
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue