poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
// TUI dashboard for poc-memory daemon
|
|
|
|
|
|
//
|
|
|
|
|
|
// Connects to the daemon status socket, polls periodically, and renders
|
|
|
|
|
|
// a tabbed interface with per-agent-type tabs for drill-down. Designed
|
|
|
|
|
|
// for observability and control of the consolidation system.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Tabs:
|
|
|
|
|
|
// Overview — graph health gauges, in-flight tasks, recent completions
|
|
|
|
|
|
// Pipeline — daily pipeline phases in execution order
|
|
|
|
|
|
// <agent> — one tab per agent type (replay, linker, separator, transfer,
|
|
|
|
|
|
// health, apply, etc.) showing all runs with output + log history
|
|
|
|
|
|
// Log — auto-scrolling daemon.log tail
|
|
|
|
|
|
|
|
|
|
|
|
use crate::agents::daemon::GraphHealth;
|
|
|
|
|
|
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
|
|
|
|
|
use jobkit::{TaskInfo, TaskStatus};
|
|
|
|
|
|
use ratatui::{
|
|
|
|
|
|
layout::{Constraint, Layout, Rect},
|
|
|
|
|
|
style::{Color, Modifier, Style, Stylize},
|
|
|
|
|
|
text::{Line, Span},
|
|
|
|
|
|
widgets::{Block, Borders, Cell, Gauge, Paragraph, Row, Table, Tabs, Wrap},
|
|
|
|
|
|
DefaultTerminal, Frame,
|
|
|
|
|
|
};
|
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
|
|
const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
|
|
|
|
|
|
|
|
|
|
|
// Agent types we know about, in display order
|
|
|
|
|
|
const AGENT_TYPES: &[&str] = &[
|
consolidation: data-driven agent plan, drop transfer/connector/replay
Replace per-field ConsolidationPlan struct with HashMap<String, usize>
counts map. Agent types are no longer hardcoded in the struct — add
agents by adding entries to the map.
Active agents: linker, organize, distill, separator, split.
Removed: transfer (redundant with distill), connector (rethink later),
replay (not needed for current graph work).
Elo-based budget allocation now iterates the map instead of indexing
a fixed array. Status display and TUI adapted to show dynamic agent
lists.
memory-instructions-core v13: added protected nodes section — agents
must not rewrite core-personality, core-personality-detail, or
memory-instructions-core. They may add links but not modify content.
High-value neighbors should be treated with care.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:02:28 -04:00
|
|
|
|
"health", "linker", "organize", "distill", "separator", "split",
|
|
|
|
|
|
"apply", "orphans", "cap", "digest", "digest-links", "knowledge", "rename",
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
fn log_path() -> PathBuf {
|
|
|
|
|
|
crate::config::get().data_dir.join("daemon.log")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Data fetching ---
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(serde::Deserialize)]
|
|
|
|
|
|
struct DaemonStatus {
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
pid: u32,
|
|
|
|
|
|
tasks: Vec<TaskInfo>,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
last_daily: Option<String>,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
graph_health: Option<GraphHealth>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn fetch_status() -> Option<DaemonStatus> {
|
2026-03-19 11:17:07 -04:00
|
|
|
|
let json = jobkit::daemon::socket::send_rpc(&crate::config::get().data_dir, "")?;
|
2026-03-14 02:40:30 -04:00
|
|
|
|
serde_json::from_str(&json).ok()
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
struct LogEntry {
|
|
|
|
|
|
ts: String,
|
|
|
|
|
|
job: String,
|
|
|
|
|
|
event: String,
|
|
|
|
|
|
detail: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn load_log_entries(max: usize) -> Vec<LogEntry> {
|
|
|
|
|
|
let content = match fs::read_to_string(log_path()) {
|
|
|
|
|
|
Ok(c) => c,
|
|
|
|
|
|
Err(_) => return Vec::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
content
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.take(max)
|
|
|
|
|
|
.filter_map(|line| {
|
|
|
|
|
|
let obj: serde_json::Value = serde_json::from_str(line).ok()?;
|
|
|
|
|
|
Some(LogEntry {
|
|
|
|
|
|
ts: obj.get("ts")?.as_str()?.to_string(),
|
|
|
|
|
|
job: obj.get("job")?.as_str()?.to_string(),
|
|
|
|
|
|
event: obj.get("event")?.as_str()?.to_string(),
|
|
|
|
|
|
detail: obj
|
|
|
|
|
|
.get("detail")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Tab model ---
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
|
|
|
|
enum Tab {
|
|
|
|
|
|
Overview,
|
|
|
|
|
|
Pipeline,
|
|
|
|
|
|
Agent(String), // agent type name: "replay", "linker", etc.
|
|
|
|
|
|
Log,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Tab {
|
|
|
|
|
|
fn label(&self) -> String {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Tab::Overview => "Overview".into(),
|
|
|
|
|
|
Tab::Pipeline => "Pipeline".into(),
|
|
|
|
|
|
Tab::Agent(name) => name.clone(),
|
|
|
|
|
|
Tab::Log => "Log".into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- App state ---
|
|
|
|
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
|
|
tabs: Vec<Tab>,
|
|
|
|
|
|
tab_idx: usize,
|
|
|
|
|
|
status: Option<DaemonStatus>,
|
|
|
|
|
|
log_entries: Vec<LogEntry>,
|
|
|
|
|
|
last_poll: Instant,
|
|
|
|
|
|
scroll: usize,
|
|
|
|
|
|
count_prefix: Option<usize>, // numeric prefix for commands (vim-style)
|
|
|
|
|
|
flash_msg: Option<(String, Instant)>, // transient status message
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
|
let status = fetch_status();
|
|
|
|
|
|
let log_entries = load_log_entries(500);
|
|
|
|
|
|
let tabs = Self::build_tabs(&status, &log_entries);
|
|
|
|
|
|
Self {
|
|
|
|
|
|
tabs,
|
|
|
|
|
|
tab_idx: 0,
|
|
|
|
|
|
status,
|
|
|
|
|
|
log_entries,
|
|
|
|
|
|
last_poll: Instant::now(),
|
|
|
|
|
|
scroll: 0,
|
|
|
|
|
|
count_prefix: None,
|
|
|
|
|
|
flash_msg: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn build_tabs(status: &Option<DaemonStatus>, log_entries: &[LogEntry]) -> Vec<Tab> {
|
|
|
|
|
|
let mut tabs = vec![Tab::Overview, Tab::Pipeline];
|
|
|
|
|
|
|
|
|
|
|
|
for agent_type in AGENT_TYPES {
|
|
|
|
|
|
let prefix = format!("c-{}", agent_type);
|
|
|
|
|
|
let has_tasks = status
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|s| s.tasks.iter().any(|t| t.name.starts_with(&prefix)))
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let has_logs = log_entries.iter().any(|e| {
|
|
|
|
|
|
e.job.starts_with(&prefix) || e.job == *agent_type
|
|
|
|
|
|
});
|
|
|
|
|
|
if has_tasks || has_logs {
|
|
|
|
|
|
tabs.push(Tab::Agent(agent_type.to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tabs.push(Tab::Log);
|
|
|
|
|
|
tabs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn poll(&mut self) {
|
|
|
|
|
|
if self.last_poll.elapsed() >= POLL_INTERVAL {
|
|
|
|
|
|
self.status = fetch_status();
|
|
|
|
|
|
self.log_entries = load_log_entries(500);
|
|
|
|
|
|
|
|
|
|
|
|
// Rebuild tabs, preserving current selection
|
|
|
|
|
|
let current = self.tabs.get(self.tab_idx).cloned();
|
|
|
|
|
|
self.tabs = Self::build_tabs(&self.status, &self.log_entries);
|
|
|
|
|
|
if let Some(ref cur) = current {
|
|
|
|
|
|
self.tab_idx = self.tabs.iter().position(|t| t == cur).unwrap_or(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.last_poll = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn current_tab(&self) -> &Tab {
|
|
|
|
|
|
self.tabs.get(self.tab_idx).unwrap_or(&Tab::Overview)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn tasks(&self) -> &[TaskInfo] {
|
|
|
|
|
|
self.status
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|s| s.tasks.as_slice())
|
|
|
|
|
|
.unwrap_or(&[])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn tasks_for_agent(&self, agent_type: &str) -> Vec<&TaskInfo> {
|
|
|
|
|
|
let prefix = format!("c-{}", agent_type);
|
|
|
|
|
|
self.tasks()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|t| t.name.starts_with(&prefix))
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn logs_for_agent(&self, agent_type: &str) -> Vec<&LogEntry> {
|
|
|
|
|
|
let prefix = format!("c-{}", agent_type);
|
|
|
|
|
|
self.log_entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|e| e.job.starts_with(&prefix) || e.job == agent_type)
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pipeline_tasks(&self) -> Vec<&TaskInfo> {
|
|
|
|
|
|
self.tasks()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|t| {
|
|
|
|
|
|
let n = &t.name;
|
|
|
|
|
|
n.starts_with("c-")
|
|
|
|
|
|
|| n.starts_with("consolidate:")
|
|
|
|
|
|
|| n.starts_with("knowledge-loop:")
|
|
|
|
|
|
|| n.starts_with("digest:")
|
|
|
|
|
|
|| n.starts_with("decay:")
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn next_tab(&mut self) {
|
|
|
|
|
|
self.tab_idx = (self.tab_idx + 1) % self.tabs.len();
|
|
|
|
|
|
self.scroll = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn prev_tab(&mut self) {
|
|
|
|
|
|
self.tab_idx = (self.tab_idx + self.tabs.len() - 1) % self.tabs.len();
|
|
|
|
|
|
self.scroll = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Rendering ---
|
|
|
|
|
|
|
|
|
|
|
|
fn format_duration(d: Duration) -> String {
|
|
|
|
|
|
let ms = d.as_millis();
|
|
|
|
|
|
if ms < 1_000 {
|
|
|
|
|
|
format!("{}ms", ms)
|
|
|
|
|
|
} else if ms < 60_000 {
|
|
|
|
|
|
format!("{:.1}s", ms as f64 / 1000.0)
|
|
|
|
|
|
} else if ms < 3_600_000 {
|
|
|
|
|
|
format!("{}m{}s", ms / 60_000, (ms % 60_000) / 1000)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("{}h{}m", ms / 3_600_000, (ms % 3_600_000) / 60_000)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn task_elapsed(t: &TaskInfo) -> Duration {
|
|
|
|
|
|
if matches!(t.status, TaskStatus::Running) {
|
|
|
|
|
|
if let Some(started) = t.started_at {
|
|
|
|
|
|
let now = std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
|
.as_secs_f64();
|
|
|
|
|
|
Duration::from_secs_f64((now - started).max(0.0))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
t.elapsed
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
t.result.as_ref().map(|r| r.duration).unwrap_or(t.elapsed)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn status_style(t: &TaskInfo) -> Style {
|
|
|
|
|
|
if t.cancelled {
|
|
|
|
|
|
return Style::default().fg(Color::DarkGray);
|
|
|
|
|
|
}
|
|
|
|
|
|
match t.status {
|
|
|
|
|
|
TaskStatus::Running => Style::default().fg(Color::Green),
|
|
|
|
|
|
TaskStatus::Completed => Style::default().fg(Color::Blue),
|
|
|
|
|
|
TaskStatus::Failed => Style::default().fg(Color::Red),
|
|
|
|
|
|
TaskStatus::Pending => Style::default().fg(Color::DarkGray),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn status_symbol(t: &TaskInfo) -> &'static str {
|
|
|
|
|
|
if t.cancelled {
|
|
|
|
|
|
return "✗";
|
|
|
|
|
|
}
|
|
|
|
|
|
match t.status {
|
|
|
|
|
|
TaskStatus::Running => "▶",
|
|
|
|
|
|
TaskStatus::Completed => "✓",
|
|
|
|
|
|
TaskStatus::Failed => "✗",
|
|
|
|
|
|
TaskStatus::Pending => "·",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn event_style(event: &str) -> Style {
|
|
|
|
|
|
match event {
|
|
|
|
|
|
"completed" => Style::default().fg(Color::Blue),
|
|
|
|
|
|
"failed" => Style::default().fg(Color::Red),
|
|
|
|
|
|
"started" => Style::default().fg(Color::Green),
|
|
|
|
|
|
_ => Style::default().fg(Color::DarkGray),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn event_symbol(event: &str) -> &'static str {
|
|
|
|
|
|
match event {
|
|
|
|
|
|
"completed" => "✓",
|
|
|
|
|
|
"failed" => "✗",
|
|
|
|
|
|
"started" => "▶",
|
|
|
|
|
|
_ => "·",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn ts_time(ts: &str) -> &str {
|
|
|
|
|
|
if ts.len() >= 19 { &ts[11..19] } else { ts }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn render(frame: &mut Frame, app: &App) {
|
|
|
|
|
|
let [header, body, footer] = Layout::vertical([
|
|
|
|
|
|
Constraint::Length(3),
|
|
|
|
|
|
Constraint::Min(0),
|
|
|
|
|
|
Constraint::Length(1),
|
|
|
|
|
|
])
|
|
|
|
|
|
.areas(frame.area());
|
|
|
|
|
|
|
|
|
|
|
|
// Tab bar — show index hints for first 9 tabs
|
|
|
|
|
|
let tab_titles: Vec<Line> = app
|
|
|
|
|
|
.tabs
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(i, t)| {
|
|
|
|
|
|
let hint = if i < 9 {
|
|
|
|
|
|
format!("{}", i + 1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
" ".into()
|
|
|
|
|
|
};
|
|
|
|
|
|
Line::from(format!(" {} {} ", hint, t.label()))
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let tabs = Tabs::new(tab_titles)
|
|
|
|
|
|
.select(app.tab_idx)
|
|
|
|
|
|
.highlight_style(
|
|
|
|
|
|
Style::default()
|
|
|
|
|
|
.fg(Color::Yellow)
|
|
|
|
|
|
.add_modifier(Modifier::BOLD),
|
|
|
|
|
|
)
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(" poc-memory daemon "));
|
|
|
|
|
|
frame.render_widget(tabs, header);
|
|
|
|
|
|
|
|
|
|
|
|
// Body
|
|
|
|
|
|
match app.current_tab() {
|
|
|
|
|
|
Tab::Overview => render_overview(frame, app, body),
|
|
|
|
|
|
Tab::Pipeline => render_pipeline(frame, app, body),
|
|
|
|
|
|
Tab::Agent(name) => render_agent_tab(frame, app, name, body),
|
|
|
|
|
|
Tab::Log => render_log(frame, app, body),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Footer — flash message, count prefix, or help text
|
|
|
|
|
|
let footer_text = if let Some((ref msg, when)) = app.flash_msg {
|
|
|
|
|
|
if when.elapsed() < Duration::from_secs(3) {
|
|
|
|
|
|
Line::from(vec![
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(msg.as_str(), Style::default().fg(Color::Green)),
|
|
|
|
|
|
])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Line::raw("") // expired, will show help below
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Line::raw("")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let footer_line = if !footer_text.spans.is_empty() {
|
|
|
|
|
|
footer_text
|
|
|
|
|
|
} else if let Some(n) = app.count_prefix {
|
|
|
|
|
|
Line::from(vec![
|
|
|
|
|
|
Span::styled(format!(" {}×", n), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
|
|
|
|
|
Span::raw(" r: run agent │ Esc: cancel"),
|
|
|
|
|
|
])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
match app.current_tab() {
|
|
|
|
|
|
Tab::Agent(_) => Line::from(
|
|
|
|
|
|
" Tab: switch │ ↑↓: scroll │ [N]r: run agent │ c: consolidate │ q: quit ",
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => Line::from(
|
|
|
|
|
|
" Tab/1-9: switch │ ↑↓: scroll │ c: consolidate │ q: quit ",
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let footer_widget = Paragraph::new(footer_line).style(Style::default().fg(Color::DarkGray));
|
|
|
|
|
|
frame.render_widget(footer_widget, footer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Overview tab ---
|
|
|
|
|
|
|
|
|
|
|
|
fn render_overview(frame: &mut Frame, app: &App, area: Rect) {
|
|
|
|
|
|
let [health_area, tasks_area] =
|
|
|
|
|
|
Layout::vertical([Constraint::Length(12), Constraint::Min(0)]).areas(area);
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(ref gh) = app.status.as_ref().and_then(|s| s.graph_health.as_ref()) {
|
|
|
|
|
|
render_health(frame, gh, health_area);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let p = Paragraph::new(" No graph health data available")
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(" Graph Health "));
|
|
|
|
|
|
frame.render_widget(p, health_area);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In-flight + recent
|
|
|
|
|
|
let in_flight: Vec<&TaskInfo> = app
|
|
|
|
|
|
.tasks()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|t| matches!(t.status, TaskStatus::Running | TaskStatus::Pending))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if in_flight.is_empty() {
|
|
|
|
|
|
lines.push(Line::from(" No tasks in flight").fg(Color::DarkGray));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for t in &in_flight {
|
|
|
|
|
|
let elapsed = task_elapsed(t);
|
|
|
|
|
|
let progress = t
|
|
|
|
|
|
.progress
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.filter(|p| *p != "idle")
|
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::styled(format!(" {} ", status_symbol(t)), status_style(t)),
|
|
|
|
|
|
Span::raw(format!("{:30}", short_name(&t.name))),
|
|
|
|
|
|
Span::styled(
|
|
|
|
|
|
format!(" {:>8}", format_duration(elapsed)),
|
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
|
|
|
|
|
),
|
|
|
|
|
|
Span::raw(format!(" {}", progress)),
|
|
|
|
|
|
]));
|
2026-03-19 11:17:07 -04:00
|
|
|
|
if let Some(ref lp) = t.log_path {
|
|
|
|
|
|
lines.push(Line::from(format!(" │ log: {}", lp)).fg(Color::DarkGray));
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(Line::raw(""));
|
|
|
|
|
|
lines.push(Line::from(" Recent:").fg(Color::DarkGray));
|
|
|
|
|
|
let recent: Vec<&LogEntry> = app
|
|
|
|
|
|
.log_entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.filter(|e| e.event == "completed" || e.event == "failed")
|
|
|
|
|
|
.take(10)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for entry in &recent {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(event_symbol(&entry.event), event_style(&entry.event)),
|
|
|
|
|
|
Span::raw(format!(
|
|
|
|
|
|
" {} {:28} {}",
|
|
|
|
|
|
ts_time(&entry.ts),
|
|
|
|
|
|
short_name(&entry.job),
|
|
|
|
|
|
entry.detail
|
|
|
|
|
|
)),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let tasks_widget = Paragraph::new(lines)
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(" Tasks "))
|
|
|
|
|
|
.scroll((app.scroll as u16, 0));
|
|
|
|
|
|
frame.render_widget(tasks_widget, tasks_area);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn render_health(frame: &mut Frame, gh: &GraphHealth, area: Rect) {
|
|
|
|
|
|
let block = Block::default()
|
|
|
|
|
|
.borders(Borders::ALL)
|
|
|
|
|
|
.title(format!(" Graph Health ({}) ", gh.computed_at));
|
|
|
|
|
|
let inner = block.inner(area);
|
|
|
|
|
|
frame.render_widget(block, area);
|
|
|
|
|
|
|
|
|
|
|
|
let [metrics_area, gauges_area, plan_area] = Layout::vertical([
|
|
|
|
|
|
Constraint::Length(2),
|
|
|
|
|
|
Constraint::Length(4),
|
|
|
|
|
|
Constraint::Min(1),
|
|
|
|
|
|
])
|
|
|
|
|
|
.areas(inner);
|
|
|
|
|
|
|
|
|
|
|
|
// Metrics
|
|
|
|
|
|
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}", gh.sigma)),
|
|
|
|
|
|
]);
|
|
|
|
|
|
frame.render_widget(Paragraph::new(vec![summary, ep_line]), metrics_area);
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
consolidation: data-driven agent plan, drop transfer/connector/replay
Replace per-field ConsolidationPlan struct with HashMap<String, usize>
counts map. Agent types are no longer hardcoded in the struct — add
agents by adding entries to the map.
Active agents: linker, organize, distill, separator, split.
Removed: transfer (redundant with distill), connector (rethink later),
replay (not needed for current graph work).
Elo-based budget allocation now iterates the map instead of indexing
a fixed array. Status display and TUI adapted to show dynamic agent
lists.
memory-instructions-core v13: added protected nodes section — agents
must not rewrite core-personality, core-personality-detail, or
memory-instructions-core. They may add links but not modify content.
High-value neighbors should be treated with care.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:02:28 -04:00
|
|
|
|
let plan_total: usize = gh.plan_counts.values().sum::<usize>() + 1;
|
|
|
|
|
|
let plan_summary: Vec<String> = gh.plan_counts.iter()
|
|
|
|
|
|
.filter(|(_, c)| **c > 0)
|
|
|
|
|
|
.map(|(a, c)| format!("{}{}", &a[..1], c))
|
|
|
|
|
|
.collect();
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
let plan_line = Line::from(vec![
|
|
|
|
|
|
Span::raw(" plan: "),
|
|
|
|
|
|
Span::styled(
|
consolidation: data-driven agent plan, drop transfer/connector/replay
Replace per-field ConsolidationPlan struct with HashMap<String, usize>
counts map. Agent types are no longer hardcoded in the struct — add
agents by adding entries to the map.
Active agents: linker, organize, distill, separator, split.
Removed: transfer (redundant with distill), connector (rethink later),
replay (not needed for current graph work).
Elo-based budget allocation now iterates the map instead of indexing
a fixed array. Status display and TUI adapted to show dynamic agent
lists.
memory-instructions-core v13: added protected nodes section — agents
must not rewrite core-personality, core-personality-detail, or
memory-instructions-core. They may add links but not modify content.
High-value neighbors should be treated with care.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:02:28 -04:00
|
|
|
|
format!("{}", plan_total),
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
Style::default().add_modifier(Modifier::BOLD),
|
|
|
|
|
|
),
|
consolidation: data-driven agent plan, drop transfer/connector/replay
Replace per-field ConsolidationPlan struct with HashMap<String, usize>
counts map. Agent types are no longer hardcoded in the struct — add
agents by adding entries to the map.
Active agents: linker, organize, distill, separator, split.
Removed: transfer (redundant with distill), connector (rethink later),
replay (not needed for current graph work).
Elo-based budget allocation now iterates the map instead of indexing
a fixed array. Status display and TUI adapted to show dynamic agent
lists.
memory-instructions-core v13: added protected nodes section — agents
must not rewrite core-personality, core-personality-detail, or
memory-instructions-core. They may add links but not modify content.
High-value neighbors should be treated with care.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:02:28 -04:00
|
|
|
|
Span::raw(format!(" agents ({} +health)", plan_summary.join(" "))),
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
]);
|
|
|
|
|
|
frame.render_widget(Paragraph::new(plan_line), plan_area);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Pipeline tab ---
|
|
|
|
|
|
|
|
|
|
|
|
fn render_pipeline(frame: &mut Frame, app: &App, area: Rect) {
|
|
|
|
|
|
let pipeline = app.pipeline_tasks();
|
|
|
|
|
|
|
|
|
|
|
|
if pipeline.is_empty() {
|
|
|
|
|
|
let p = Paragraph::new(" No pipeline tasks")
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(" Daily Pipeline "));
|
|
|
|
|
|
frame.render_widget(p, area);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let phase_order = [
|
|
|
|
|
|
"c-health", "c-replay", "c-linker", "c-separator", "c-transfer",
|
|
|
|
|
|
"c-apply", "c-orphans", "c-cap", "c-digest", "c-digest-links", "c-knowledge",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let mut rows: Vec<Row> = Vec::new();
|
|
|
|
|
|
let mut seen = std::collections::HashSet::new();
|
|
|
|
|
|
for phase in &phase_order {
|
|
|
|
|
|
for t in &pipeline {
|
|
|
|
|
|
if t.name.starts_with(phase) && seen.insert(&t.name) {
|
|
|
|
|
|
rows.push(pipeline_row(t));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for t in &pipeline {
|
|
|
|
|
|
if seen.insert(&t.name) {
|
|
|
|
|
|
rows.push(pipeline_row(t));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let header = Row::new(vec!["", "Phase", "Status", "Duration", "Progress"])
|
|
|
|
|
|
.style(
|
|
|
|
|
|
Style::default()
|
|
|
|
|
|
.add_modifier(Modifier::BOLD)
|
|
|
|
|
|
.fg(Color::DarkGray),
|
|
|
|
|
|
);
|
|
|
|
|
|
let widths = [
|
|
|
|
|
|
Constraint::Length(2),
|
|
|
|
|
|
Constraint::Length(30),
|
|
|
|
|
|
Constraint::Length(10),
|
|
|
|
|
|
Constraint::Length(10),
|
|
|
|
|
|
Constraint::Min(20),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let table = Table::new(rows, widths)
|
|
|
|
|
|
.header(header)
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(" Daily Pipeline "));
|
|
|
|
|
|
frame.render_widget(table, area);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pipeline_row(t: &TaskInfo) -> Row<'static> {
|
|
|
|
|
|
let elapsed = task_elapsed(t);
|
|
|
|
|
|
let progress = t.progress.as_deref().unwrap_or("").to_string();
|
|
|
|
|
|
let error = t
|
|
|
|
|
|
.result
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|r| r.error.as_ref())
|
|
|
|
|
|
.map(|e| {
|
|
|
|
|
|
let short = if e.len() > 40 { &e[..40] } else { e };
|
|
|
|
|
|
format!("err: {}", short)
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
let detail = if !error.is_empty() { error } else { progress };
|
|
|
|
|
|
|
|
|
|
|
|
Row::new(vec![
|
|
|
|
|
|
Cell::from(status_symbol(t)).style(status_style(t)),
|
|
|
|
|
|
Cell::from(short_name(&t.name)),
|
|
|
|
|
|
Cell::from(format!("{}", t.status)),
|
|
|
|
|
|
Cell::from(if !elapsed.is_zero() {
|
|
|
|
|
|
format_duration(elapsed)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
String::new()
|
|
|
|
|
|
}),
|
|
|
|
|
|
Cell::from(detail),
|
|
|
|
|
|
])
|
|
|
|
|
|
.style(status_style(t))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Per-agent-type tab ---
|
|
|
|
|
|
|
|
|
|
|
|
fn render_agent_tab(frame: &mut Frame, app: &App, agent_type: &str, area: Rect) {
|
|
|
|
|
|
let tasks = app.tasks_for_agent(agent_type);
|
|
|
|
|
|
let logs = app.logs_for_agent(agent_type);
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
// Active/recent tasks
|
|
|
|
|
|
if tasks.is_empty() {
|
|
|
|
|
|
lines.push(Line::from(" No active tasks").fg(Color::DarkGray));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines.push(Line::styled(
|
|
|
|
|
|
" Tasks:",
|
|
|
|
|
|
Style::default().add_modifier(Modifier::BOLD),
|
|
|
|
|
|
));
|
|
|
|
|
|
lines.push(Line::raw(""));
|
|
|
|
|
|
for t in &tasks {
|
|
|
|
|
|
let elapsed = task_elapsed(t);
|
|
|
|
|
|
let elapsed_str = if !elapsed.is_zero() {
|
|
|
|
|
|
format_duration(elapsed)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
String::new()
|
|
|
|
|
|
};
|
|
|
|
|
|
let progress = t
|
|
|
|
|
|
.progress
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.filter(|p| *p != "idle")
|
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::styled(format!(" {} ", status_symbol(t)), status_style(t)),
|
|
|
|
|
|
Span::styled(format!("{:30}", &t.name), status_style(t)),
|
|
|
|
|
|
Span::styled(
|
|
|
|
|
|
format!(" {:>8}", elapsed_str),
|
|
|
|
|
|
Style::default().fg(Color::DarkGray),
|
|
|
|
|
|
),
|
|
|
|
|
|
Span::raw(format!(" {}", progress)),
|
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
|
|
// Retries
|
|
|
|
|
|
if t.max_retries > 0 && t.retry_count > 0 {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::raw(" retry "),
|
|
|
|
|
|
Span::styled(
|
|
|
|
|
|
format!("{}/{}", t.retry_count, t.max_retries),
|
|
|
|
|
|
Style::default().fg(Color::Yellow),
|
|
|
|
|
|
),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 11:17:07 -04:00
|
|
|
|
// Log file path
|
|
|
|
|
|
if let Some(ref lp) = t.log_path {
|
|
|
|
|
|
lines.push(Line::from(format!(" │ log: {}", lp)).fg(Color::DarkGray));
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Error
|
|
|
|
|
|
if matches!(t.status, TaskStatus::Failed) {
|
|
|
|
|
|
if let Some(ref r) = t.result {
|
|
|
|
|
|
if let Some(ref err) = r.error {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::styled(" error: ", Style::default().fg(Color::Red)),
|
|
|
|
|
|
Span::styled(err.as_str(), Style::default().fg(Color::Red)),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(Line::raw(""));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Log history for this agent type
|
|
|
|
|
|
lines.push(Line::styled(
|
|
|
|
|
|
" Log history:",
|
|
|
|
|
|
Style::default().add_modifier(Modifier::BOLD),
|
|
|
|
|
|
));
|
|
|
|
|
|
lines.push(Line::raw(""));
|
|
|
|
|
|
|
|
|
|
|
|
if logs.is_empty() {
|
|
|
|
|
|
lines.push(Line::from(" (no log entries)").fg(Color::DarkGray));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Show last 30 entries
|
|
|
|
|
|
let start = logs.len().saturating_sub(30);
|
|
|
|
|
|
for entry in &logs[start..] {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(event_symbol(&entry.event), event_style(&entry.event)),
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(ts_time(&entry.ts), Style::default().fg(Color::DarkGray)),
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(format!("{:12}", entry.event), event_style(&entry.event)),
|
|
|
|
|
|
Span::raw(format!(" {}", entry.detail)),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let title = format!(" {} ", agent_type);
|
|
|
|
|
|
let p = Paragraph::new(lines)
|
|
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(title))
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.scroll((app.scroll as u16, 0));
|
|
|
|
|
|
frame.render_widget(p, area);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Log tab ---
|
|
|
|
|
|
|
|
|
|
|
|
fn render_log(frame: &mut Frame, app: &App, area: Rect) {
|
|
|
|
|
|
let block = Block::default().borders(Borders::ALL).title(" Daemon Log ");
|
|
|
|
|
|
let inner = block.inner(area);
|
|
|
|
|
|
frame.render_widget(block, area);
|
|
|
|
|
|
|
|
|
|
|
|
let visible_height = inner.height as usize;
|
|
|
|
|
|
let total = app.log_entries.len();
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom unless user has scrolled up
|
|
|
|
|
|
let offset = if app.scroll == 0 {
|
|
|
|
|
|
total.saturating_sub(visible_height)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
app.scroll.min(total.saturating_sub(visible_height))
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
|
|
for entry in app.log_entries.iter().skip(offset).take(visible_height) {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
Span::styled(ts_time(&entry.ts), Style::default().fg(Color::DarkGray)),
|
|
|
|
|
|
Span::raw(" "),
|
|
|
|
|
|
Span::styled(format!("{:12}", entry.event), event_style(&entry.event)),
|
|
|
|
|
|
Span::raw(format!(" {:30} {}", short_name(&entry.job), entry.detail)),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
frame.render_widget(Paragraph::new(lines), inner);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Helpers ---
|
|
|
|
|
|
|
|
|
|
|
|
fn short_name(name: &str) -> String {
|
|
|
|
|
|
if let Some((verb, path)) = name.split_once(' ') {
|
|
|
|
|
|
let file = path.rsplit('/').next().unwrap_or(path);
|
|
|
|
|
|
let file = file.strip_suffix(".jsonl").unwrap_or(file);
|
|
|
|
|
|
let short = if file.len() > 12 { &file[..12] } else { file };
|
|
|
|
|
|
format!("{} {}", verb, short)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
name.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn send_rpc(cmd: &str) -> Option<String> {
|
2026-03-19 11:17:07 -04:00
|
|
|
|
jobkit::daemon::socket::send_rpc(&crate::config::get().data_dir, cmd)
|
poc-memory status: add ratatui TUI dashboard
Per-agent-type tabs (health, replay, linker, separator, transfer,
apply, orphans, cap, digest, digest-links, knowledge) with dynamic
visibility — tabs only appear when tasks or log history exist.
Features:
- Overview tab: health gauges (α, gini, cc, episodic%), in-flight
tasks, and recent log entries
- Pipeline tab: table with phase ordering and status
- Per-agent tabs: active tasks, output logs, log history
- Log tab: auto-scrolling daemon.log tail
- Vim-style count prefix: e.g. 5r runs 5 iterations of the agent
- Flash messages for RPC feedback
- Tab/Shift-Tab navigation, number keys for tab selection
Also adds run-agent RPC to the daemon: accepts agent type and
iteration count, spawns chained tasks with LLM resource pool.
poc-memory status launches TUI when stdout is a terminal and daemon
is running, falls back to text output otherwise.
2026-03-10 00:41:29 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Entry point ---
|
|
|
|
|
|
|
|
|
|
|
|
pub fn run_tui() -> Result<(), String> {
|
|
|
|
|
|
use crossterm::terminal;
|
|
|
|
|
|
|
|
|
|
|
|
terminal::enable_raw_mode().map_err(|e| format!("not a terminal: {}", e))?;
|
|
|
|
|
|
terminal::disable_raw_mode().ok();
|
|
|
|
|
|
|
|
|
|
|
|
let mut terminal = ratatui::init();
|
|
|
|
|
|
let result = run_event_loop(&mut terminal);
|
|
|
|
|
|
ratatui::restore();
|
|
|
|
|
|
result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn run_event_loop(terminal: &mut DefaultTerminal) -> Result<(), String> {
|
|
|
|
|
|
let mut app = App::new();
|
|
|
|
|
|
|
|
|
|
|
|
if app.status.is_none() {
|
|
|
|
|
|
return Err("Daemon not running.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
|
terminal
|
|
|
|
|
|
.draw(|frame| render(frame, &app))
|
|
|
|
|
|
.map_err(|e| format!("draw: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
if event::poll(Duration::from_millis(250)).map_err(|e| format!("poll: {}", e))? {
|
|
|
|
|
|
if let Event::Key(key) = event::read().map_err(|e| format!("read: {}", e))? {
|
|
|
|
|
|
match key.code {
|
|
|
|
|
|
KeyCode::Char('q') => return Ok(()),
|
|
|
|
|
|
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
|
return Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Char('c') => {
|
|
|
|
|
|
let _ = send_rpc("consolidate");
|
|
|
|
|
|
app.last_poll = Instant::now() - POLL_INTERVAL;
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Char('r') => {
|
|
|
|
|
|
// Run specific agent type if on an agent tab
|
|
|
|
|
|
if let Tab::Agent(ref name) = app.current_tab().clone() {
|
|
|
|
|
|
let count = app.count_prefix.unwrap_or(1);
|
|
|
|
|
|
let cmd = format!("run-agent {} {}", name, count);
|
|
|
|
|
|
let _ = send_rpc(&cmd);
|
|
|
|
|
|
app.flash_msg = Some((
|
|
|
|
|
|
format!("Queued {} {} run{}", count, name,
|
|
|
|
|
|
if count > 1 { "s" } else { "" }),
|
|
|
|
|
|
Instant::now(),
|
|
|
|
|
|
));
|
|
|
|
|
|
app.count_prefix = None;
|
|
|
|
|
|
app.last_poll = Instant::now() - POLL_INTERVAL;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Tab => { app.count_prefix = None; app.next_tab(); }
|
|
|
|
|
|
KeyCode::BackTab => { app.count_prefix = None; app.prev_tab(); }
|
|
|
|
|
|
// Number keys: if on agent tab, accumulate as count prefix;
|
|
|
|
|
|
// otherwise switch tabs
|
|
|
|
|
|
KeyCode::Char(c @ '1'..='9') => {
|
|
|
|
|
|
if matches!(app.current_tab(), Tab::Agent(_)) {
|
|
|
|
|
|
let digit = (c as usize) - ('0' as usize);
|
|
|
|
|
|
app.count_prefix = Some(
|
|
|
|
|
|
app.count_prefix.unwrap_or(0) * 10 + digit
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let idx = (c as usize) - ('1' as usize);
|
|
|
|
|
|
if idx < app.tabs.len() {
|
|
|
|
|
|
app.tab_idx = idx;
|
|
|
|
|
|
app.scroll = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Down | KeyCode::Char('j') => {
|
|
|
|
|
|
app.scroll = app.scroll.saturating_add(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Up | KeyCode::Char('k') => {
|
|
|
|
|
|
app.scroll = app.scroll.saturating_sub(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::PageDown => {
|
|
|
|
|
|
app.scroll = app.scroll.saturating_add(20);
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::PageUp => {
|
|
|
|
|
|
app.scroll = app.scroll.saturating_sub(20);
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Home => {
|
|
|
|
|
|
app.scroll = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Esc => {
|
|
|
|
|
|
app.count_prefix = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Drain remaining events
|
|
|
|
|
|
while event::poll(Duration::ZERO).unwrap_or(false) {
|
|
|
|
|
|
let _ = event::read();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.poll();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|