#![allow(dead_code)]
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
use chrono::{DateTime, Utc};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use darq_core::types::{Run, RunStatus};
use ratatui::prelude::*;
use ratatui::widgets::*;
use crate::daemon::client::DaemonEvent;
use crate::tui::TuiEvent;
pub const WATERFALL_CAP: usize = 200;
pub const SCOPE_WIDTH: usize = 200;
pub const SCOPE_DECAY: f32 = 0.985;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentPhase {
Idle,
Thinking,
Producing,
}
#[derive(Debug, Clone, Copy)]
pub struct CarrierProfile {
pub base: f32,
pub amp: f32,
pub period_ms: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventKind {
Heartbeat,
StateTransition,
ToolUse,
SessionUpdate,
Artifact,
BlueprintsInjected,
Routing,
Warning,
Error,
Success,
}
#[derive(Debug, Clone)]
pub struct EventDisplayed {
pub ts: DateTime<Utc>,
pub kind: EventKind,
pub text: String,
pub run_id: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct SatPersonaState {
pub current: f32, pub target: f32, pub last_updated: Option<DateTime<Utc>>,
pub note: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StationState {
Pending,
Active,
Completed,
Failed,
NotApplicable,
}
impl StationState {
pub fn glyph(self) -> &'static str {
match self {
Self::Pending => "○",
Self::Active => "●",
Self::Completed => "✓",
Self::Failed => "✖",
Self::NotApplicable => "·",
}
}
}
#[derive(Debug, Clone)]
pub struct ChainState {
pub stations: [StationState; 7],
pub elapsed_ms: [Option<u64>; 7],
}
impl Default for ChainState {
fn default() -> Self {
Self {
stations: [StationState::Pending; 7],
elapsed_ms: [None; 7],
}
}
}
pub fn workflow_kind_to_station(kind: &str) -> Option<usize> {
match kind {
"plan_issue" | "plan" => Some(0),
"implement_issue" | "implement" => Some(1),
"review_pr" | "review" => Some(2),
"fix_review" | "fix" => Some(3),
"merge_pr" | "merge" => Some(4),
"sat_verify" | "sat" => Some(5),
"learn_update" | "learn" => Some(6),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PanelMode {
#[default]
SatDials,
Shifts,
Blueprints,
Backlog,
Judges,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WaterfallFilter {
#[default]
All,
Errors,
Tools,
Routing,
}
impl WaterfallFilter {
pub fn next(self) -> Self {
match self {
Self::All => Self::Errors,
Self::Errors => Self::Tools,
Self::Tools => Self::Routing,
Self::Routing => Self::All,
}
}
pub fn label(self) -> &'static str {
match self {
Self::All => "all",
Self::Errors => "errors",
Self::Tools => "tools",
Self::Routing => "routing",
}
}
pub fn matches(self, kind: EventKind) -> bool {
match self {
Self::All => true,
Self::Errors => matches!(kind, EventKind::Error | EventKind::Warning),
Self::Tools => matches!(kind, EventKind::ToolUse),
Self::Routing => matches!(kind, EventKind::Routing | EventKind::StateTransition),
}
}
}
#[derive(Debug, Clone)]
pub struct AgentHeartbeatState {
pub elapsed_secs: u64,
pub output_chars: u64,
pub last_frame_at: Instant,
pub prev_output_chars: u64,
pub prev_frame_at: Instant,
}
impl AgentHeartbeatState {
pub fn rate_per_sec(&self) -> u64 {
let dt = self
.last_frame_at
.duration_since(self.prev_frame_at)
.as_secs_f32();
if dt < 0.1 {
return 0;
}
let delta = self.output_chars.saturating_sub(self.prev_output_chars) as f32;
(delta / dt) as u64
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompletionTrail {
pub station_idx: usize,
pub at: Instant,
}
#[derive(Debug, Clone)]
pub struct TerminalCard {
pub run_id: String,
pub final_status: String, pub duration_secs: u64,
pub verdict: Option<String>, pub total_events: usize,
pub blueprints: usize,
pub tool_count: u32,
}
#[derive(Debug, Clone)]
pub struct BlueprintMatch {
pub run_id: String,
pub utility_name: String,
pub similarity: f32,
pub kind: BlueprintKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlueprintKind {
Reuse,
Avoid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Panel {
Milestone,
Runs,
Approvals,
PrState,
SatStatus,
Learnings,
}
impl Panel {
pub fn next(self) -> Self {
match self {
Self::Milestone => Self::Runs,
Self::Runs => Self::Approvals,
Self::Approvals => Self::PrState,
Self::PrState => Self::SatStatus,
Self::SatStatus => Self::Learnings,
Self::Learnings => Self::Milestone,
}
}
pub fn prev(self) -> Self {
match self {
Self::Milestone => Self::Learnings,
Self::Runs => Self::Milestone,
Self::Approvals => Self::Runs,
Self::PrState => Self::Approvals,
Self::SatStatus => Self::PrState,
Self::Learnings => Self::SatStatus,
}
}
}
pub enum Action {
Quit,
ApproveRun(String),
TriggerNext,
}
pub struct RunInfo {
pub id: String,
pub status: String,
pub issue: String,
pub duration: String,
}
pub struct PrInfo {
pub number: u64,
pub title: String,
pub status: String,
}
pub struct Confirmation {
pub action: Action,
pub prompt: String,
}
pub struct App {
pub focused: Panel,
pub tick_count: u64,
pub runs_state: TableState,
pub approvals_state: ListState,
pub milestone_name: Option<String>,
pub runs: Vec<RunInfo>,
pub approvals: Vec<RunInfo>,
pub prs: Vec<PrInfo>,
pub sat_summary: String,
pub learnings: Vec<String>,
pub confirmation: Option<Confirmation>,
pub status_message: String,
pub waterfall: VecDeque<EventDisplayed>,
pub focus_run_id: Option<String>,
pub chain_state: ChainState,
pub sat_scores: HashMap<String, SatPersonaState>,
pub sat_verdict: Option<String>,
pub tool_count: u32,
pub blueprints: Vec<BlueprintMatch>,
pub throughput_window: VecDeque<u32>,
pub events_this_tick: u32,
pub start_time: Instant,
pub last_render: Instant,
pub glitch: Option<(usize, Instant)>,
pub daemon_uptime_secs: u64,
pub daemon_blueprint_count: u64,
pub mode: PanelMode,
pub waterfall_filter: WaterfallFilter,
pub reduce_motion: bool,
pub failure_dot_until: Option<Instant>,
pub run_started_at: HashMap<String, DateTime<Utc>>,
pub run_completed_at: HashMap<String, DateTime<Utc>>,
pub agent_heartbeat: Option<AgentHeartbeatState>,
pub completion_trail: Option<CompletionTrail>,
pub is_replay: bool,
pub terminal_card: Option<TerminalCard>,
pub show_help: bool,
pub scope_trace: VecDeque<f32>,
pub last_tool: Option<String>,
pub last_tool_at: Option<Instant>,
pub event_status: String,
pub event_count: u64,
}
impl App {
pub fn new() -> Self {
let mut runs_state = TableState::default();
runs_state.select(Some(0));
let mut approvals_state = ListState::default();
approvals_state.select(Some(0));
Self {
focused: Panel::Milestone,
tick_count: 0,
runs_state,
approvals_state,
milestone_name: Some("v1.0".into()),
runs: vec![],
approvals: vec![],
prs: vec![],
sat_summary: "No SAT runs yet".into(),
learnings: vec![],
confirmation: None,
status_message: "Starting...".into(),
event_status: "Events: not subscribed".into(),
event_count: 0,
waterfall: VecDeque::with_capacity(WATERFALL_CAP),
focus_run_id: None,
chain_state: ChainState::default(),
sat_scores: HashMap::new(),
sat_verdict: None,
tool_count: 0,
blueprints: Vec::new(),
throughput_window: VecDeque::with_capacity(32),
events_this_tick: 0,
start_time: Instant::now(),
last_render: Instant::now(),
glitch: None,
daemon_uptime_secs: 0,
daemon_blueprint_count: 0,
mode: PanelMode::default(),
waterfall_filter: WaterfallFilter::default(),
reduce_motion: std::env::var("DARQ_TUI_REDUCE_MOTION")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
failure_dot_until: None,
run_started_at: HashMap::new(),
run_completed_at: HashMap::new(),
agent_heartbeat: None,
completion_trail: None,
is_replay: false,
terminal_card: None,
show_help: false,
scope_trace: VecDeque::with_capacity(SCOPE_WIDTH),
last_tool: None,
last_tool_at: None,
}
}
pub fn agent_phase(&self) -> AgentPhase {
if let Some(t) = self.last_tool_at
&& t.elapsed().as_secs() < 5
{
return AgentPhase::Producing;
}
if let Some(hb) = &self.agent_heartbeat
&& hb.last_frame_at.elapsed().as_secs() <= 30
{
return AgentPhase::Thinking;
}
if self.active_station_idx().is_some() {
return AgentPhase::Thinking;
}
AgentPhase::Idle
}
pub fn active_station_idx(&self) -> Option<usize> {
self.chain_state
.stations
.iter()
.position(|s| matches!(s, StationState::Active))
}
pub fn station_carrier_profile(&self) -> CarrierProfile {
match self.active_station_idx() {
Some(0) => CarrierProfile {
base: 0.20,
amp: 0.15,
period_ms: 6000.0,
}, Some(1) => CarrierProfile {
base: 0.40,
amp: 0.25,
period_ms: 3000.0,
}, Some(2) => CarrierProfile {
base: 0.30,
amp: 0.20,
period_ms: 4500.0,
}, Some(3) => CarrierProfile {
base: 0.35,
amp: 0.20,
period_ms: 3500.0,
}, Some(4) => CarrierProfile {
base: 0.15,
amp: 0.05,
period_ms: 8000.0,
}, Some(5) => CarrierProfile {
base: 0.40,
amp: 0.30,
period_ms: 5000.0,
}, Some(6) => CarrierProfile {
base: 0.25,
amp: 0.10,
period_ms: 7000.0,
}, _ => CarrierProfile {
base: 0.25,
amp: 0.25,
period_ms: 8000.0,
}, }
}
fn scope_spike(&mut self, intensity: f32) {
while self.scope_trace.len() < SCOPE_WIDTH {
self.scope_trace.push_back(0.0);
}
if let Some(last) = self.scope_trace.back_mut() {
*last = (*last + intensity).min(1.0);
}
}
fn scope_step(&mut self) {
if self.scope_trace.is_empty() {
for _ in 0..SCOPE_WIDTH {
self.scope_trace.push_back(0.0);
}
}
for s in self.scope_trace.iter_mut() {
*s *= SCOPE_DECAY;
}
if self.scope_trace.len() >= SCOPE_WIDTH {
self.scope_trace.pop_front();
}
let carrier = self.thinking_carrier_sample();
self.scope_trace.push_back(carrier);
}
fn thinking_carrier_sample(&self) -> f32 {
let phase = self.agent_phase();
if phase == AgentPhase::Idle {
return 0.0;
}
let prof = self.station_carrier_profile();
use std::f32::consts::PI;
let p = (self.elapsed_ms() as f32 / prof.period_ms) * 2.0 * PI;
prof.base + prof.amp * p.sin()
}
fn refresh_durations(&mut self) {
let now = Utc::now();
for run in self.runs.iter_mut().chain(self.approvals.iter_mut()) {
let Some(started) = self.run_started_at.get(&run.id) else {
continue;
};
let end = self.run_completed_at.get(&run.id).copied().unwrap_or(now);
let secs = (end - *started).num_seconds().max(0) as u64;
run.duration = format_short_secs(secs);
}
}
pub fn toggle_mode(&mut self, target: PanelMode) {
self.mode = if self.mode == target {
PanelMode::SatDials
} else {
target
};
}
pub fn elapsed_ms(&self) -> u64 {
self.start_time.elapsed().as_millis() as u64
}
pub fn step_animations(&mut self) {
let now = Instant::now();
let dt = now.duration_since(self.last_render).as_secs_f32();
self.last_render = now;
if self.reduce_motion {
for state in self.sat_scores.values_mut() {
state.current = state.target;
}
} else {
const RATE: f32 = 7.1;
let factor = 1.0 - (-dt * RATE).exp();
for state in self.sat_scores.values_mut() {
state.current += (state.target - state.current) * factor;
}
}
if let Some((_, expires_at)) = self.glitch
&& now >= expires_at
{
self.glitch = None;
}
if let Some(until) = self.failure_dot_until
&& now >= until
{
self.failure_dot_until = None;
}
if let Some(trail) = self.completion_trail
&& now.duration_since(trail.at) > std::time::Duration::from_millis(250)
{
self.completion_trail = None;
}
}
fn push_waterfall(&mut self, mut ev: EventDisplayed) {
if self.is_replay {
ev.ts = Utc::now();
}
if self.waterfall.len() >= WATERFALL_CAP {
self.waterfall.pop_front();
}
self.waterfall.push_back(ev);
}
fn refocus(&mut self, run_id: &str) {
if self.focus_run_id.as_deref() != Some(run_id) {
self.focus_run_id = Some(run_id.to_string());
self.chain_state = ChainState::default();
self.sat_scores.clear();
self.sat_verdict = None;
self.tool_count = 0;
self.blueprints.clear();
self.agent_heartbeat = None;
self.completion_trail = None;
self.last_tool = None;
self.last_tool_at = None;
}
}
pub fn handle_event(&mut self, event: TuiEvent) -> Option<Action> {
match event {
TuiEvent::Key(key) => {
if self.confirmation.is_some() {
return self.handle_confirmation(key);
}
self.handle_key(key)
}
TuiEvent::Tick => {
self.tick_count += 1;
if self.throughput_window.len() >= 32 {
self.throughput_window.pop_front();
}
self.throughput_window.push_back(self.events_this_tick);
self.events_this_tick = 0;
self.refresh_durations();
self.status_message = format!(
"Tick #{} | Runs: {} | Waterfall: {} | {}",
self.tick_count,
self.runs.len(),
self.waterfall.len(),
self.event_status
);
None
}
TuiEvent::Render => None,
TuiEvent::DaemonEvent(event) => {
self.event_count += 1;
self.event_status = format!("Events: {} received", self.event_count);
self.status_message = format!("Last event: {}", event.event);
self.handle_daemon_event(event);
None
}
}
}
fn handle_daemon_event(&mut self, event: DaemonEvent) {
if event.event != "run_event" {
return;
}
let data = &event.data;
let event_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
let run_id = data.get("run_id").and_then(|v| v.as_str()).unwrap_or("");
let ts = data
.get("timestamp")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<DateTime<Utc>>().ok())
.unwrap_or_else(Utc::now);
self.events_this_tick = self.events_this_tick.saturating_add(1);
let intensity = match event_type {
"tool_use" => 0.85,
"agent_heartbeat" => 0.55,
"sat_persona_score" => 0.7,
"blueprints_injected" => 0.9,
"artifact_added" => 0.6,
"step_started" | "step_completed" | "step_failed" => 0.4,
"log" => match data.get("level").and_then(|v| v.as_str()) {
Some("error") => 1.0,
Some("warn") => 0.7,
_ => 0.25,
},
"sat_verdict" => 1.0,
_ => 0.2,
};
self.scope_spike(intensity);
match event_type {
"status_changed" => {
let from = data.get("from").and_then(|v| v.as_str()).unwrap_or("?");
let to = data.get("to").and_then(|v| v.as_str()).unwrap_or("?");
if to == "running" && !run_id.is_empty() {
self.run_started_at.entry(run_id.to_string()).or_insert(ts);
}
if matches!(
to,
"completed" | "failed" | "cancelled" | "merged" | "cooled"
) && !run_id.is_empty()
{
self.run_completed_at.insert(run_id.to_string(), ts);
if self.focus_run_id.as_deref() == Some(run_id) {
let started = self.run_started_at.get(run_id).copied();
let dur = match started {
Some(s) => (ts - s).num_seconds().max(0) as u64,
None => 0,
};
self.terminal_card = Some(TerminalCard {
run_id: run_id.to_string(),
final_status: to.to_string(),
duration_secs: dur,
verdict: self.sat_verdict.clone(),
total_events: self.waterfall.len(),
blueprints: self.blueprints.len(),
tool_count: self.tool_count,
});
}
}
if let Some(run) = self.runs.iter_mut().find(|r| r.id == run_id) {
run.status = to.to_string();
} else if let Some(idx) = self.approvals.iter().position(|r| r.id == run_id) {
let mut info = self.approvals.remove(idx);
info.status = to.to_string();
self.runs.push(info);
} else if !run_id.is_empty() {
let issue_num = data.get("issue_number").and_then(|v| v.as_u64());
let issue_title = data
.get("issue_title")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let issue_label = match (issue_num, issue_title.as_deref()) {
(Some(n), Some(t)) => format!("#{n} {t}"),
(Some(n), None) => format!("#{n}"),
(None, Some(t)) => t.to_string(),
_ => format!("Run {}", &run_id[..8.min(run_id.len())]),
};
self.runs.push(RunInfo {
id: run_id.to_string(),
status: to.to_string(),
issue: issue_label,
duration: "0s".into(),
});
}
if to == "running" && self.focus_run_id.is_none() {
self.refocus(run_id);
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::StateTransition,
text: format!("status · {from} → {to}"),
run_id: Some(run_id.to_string()),
});
}
"step_started" => {
let workflow_kind = data
.get("workflow_kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let workflow_name = data
.get("workflow_name")
.and_then(|v| v.as_str())
.unwrap_or(workflow_kind);
if self.focus_run_id.as_deref() == Some(run_id)
&& let Some(idx) = workflow_kind_to_station(workflow_kind)
{
self.chain_state.stations[idx] = StationState::Active;
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Routing,
text: format!("▸ station started · {workflow_name}"),
run_id: Some(run_id.to_string()),
});
}
"step_completed" => {
let workflow_kind = data
.get("workflow_kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let workflow_name = data
.get("workflow_name")
.and_then(|v| v.as_str())
.unwrap_or(workflow_kind);
let summary = data.get("summary").and_then(|v| v.as_str()).unwrap_or("");
let duration_ms = data
.get("duration_ms")
.and_then(|v| v.as_u64())
.unwrap_or(0);
if self.focus_run_id.as_deref() == Some(run_id)
&& let Some(idx) = workflow_kind_to_station(workflow_kind)
{
self.chain_state.stations[idx] = StationState::Completed;
self.chain_state.elapsed_ms[idx] = Some(duration_ms);
self.completion_trail = Some(CompletionTrail {
station_idx: idx,
at: Instant::now(),
});
self.agent_heartbeat = None;
}
if workflow_kind == "learn_update"
&& let Some(total) = parse_total_in_store(summary)
{
self.daemon_blueprint_count = total;
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Success,
text: format!("✓ {workflow_name} · {summary}"),
run_id: Some(run_id.to_string()),
});
}
"step_failed" => {
let workflow_kind = data
.get("workflow_kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let error = data.get("error").and_then(|v| v.as_str()).unwrap_or("?");
if self.focus_run_id.as_deref() == Some(run_id)
&& let Some(idx) = workflow_kind_to_station(workflow_kind)
{
self.chain_state.stations[idx] = StationState::Failed;
if !self.reduce_motion {
self.glitch =
Some((idx, Instant::now() + std::time::Duration::from_millis(120)));
}
self.failure_dot_until =
Some(Instant::now() + std::time::Duration::from_secs(5));
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Error,
text: format!("✖ {workflow_kind} cooled · {error}"),
run_id: Some(run_id.to_string()),
});
}
"contract_violation" => {
let workflow_kind = data
.get("workflow_kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let error = data.get("error").and_then(|v| v.as_str()).unwrap_or("?");
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Error,
text: format!("✖ contract violation · {workflow_kind} · {error}"),
run_id: Some(run_id.to_string()),
});
}
"agent_heartbeat" => {
let elapsed = data
.get("elapsed_secs")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let chars = data
.get("output_chars")
.and_then(|v| v.as_u64())
.unwrap_or(0);
if self.focus_run_id.as_deref() == Some(run_id) {
let now = Instant::now();
let prev = self.agent_heartbeat.as_ref();
let (prev_chars, prev_at) = match prev {
Some(s) => (s.output_chars, s.last_frame_at),
None => (chars, now),
};
self.agent_heartbeat = Some(AgentHeartbeatState {
elapsed_secs: elapsed,
output_chars: chars,
last_frame_at: now,
prev_output_chars: prev_chars,
prev_frame_at: prev_at,
});
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Heartbeat,
text: format!("▸ heartbeat · {elapsed}s · {chars} chars"),
run_id: Some(run_id.to_string()),
});
}
"log" => {
let level = data.get("level").and_then(|v| v.as_str()).unwrap_or("info");
let message = data.get("message").and_then(|v| v.as_str()).unwrap_or("");
let kind = match level {
"error" => EventKind::Error,
"warn" | "warning" => EventKind::Warning,
_ => EventKind::StateTransition,
};
let glyph = match level {
"error" => "✖",
"warn" | "warning" => "⚠",
_ => "▸",
};
self.push_waterfall(EventDisplayed {
ts,
kind,
text: format!("{glyph} {message}"),
run_id: Some(run_id.to_string()),
});
}
"artifact_added" => {
let artifact_kind = data.get("kind").and_then(|v| v.as_str()).unwrap_or("?");
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Artifact,
text: format!("▲ artifact · {artifact_kind}"),
run_id: Some(run_id.to_string()),
});
}
"approval_recorded" => {
let decision = data.get("decision").and_then(|v| v.as_str()).unwrap_or("?");
if let Some(idx) = self.approvals.iter().position(|r| r.id == run_id) {
let mut info = self.approvals.remove(idx);
info.status = "running".into();
self.runs.push(info);
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::Routing,
text: format!("↺ approval · {decision}"),
run_id: Some(run_id.to_string()),
});
}
"tool_use" => {
let title = data.get("title").and_then(|v| v.as_str()).unwrap_or("");
let kind = data.get("kind").and_then(|v| v.as_str()).unwrap_or("");
if self.focus_run_id.as_deref() == Some(run_id) {
self.tool_count = self.tool_count.saturating_add(1);
self.last_tool = Some(if !title.is_empty() {
title.to_string()
} else {
kind.to_string()
});
self.last_tool_at = Some(Instant::now());
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::ToolUse,
text: format!("⚡ tool_use · {kind} · {title}"),
run_id: Some(run_id.to_string()),
});
}
"blueprints_injected" => {
let matches = data
.get("matches")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if self.focus_run_id.as_deref() == Some(run_id) {
self.blueprints.clear();
for m in &matches {
if let (Some(rid), Some(util), Some(sim)) = (
m.get("run_id").and_then(|v| v.as_str()),
m.get("utility_name").and_then(|v| v.as_str()),
m.get("similarity").and_then(|v| v.as_f64()),
) {
let kind_str =
m.get("kind").and_then(|v| v.as_str()).unwrap_or("reuse");
let bk = if kind_str == "avoid" {
BlueprintKind::Avoid
} else {
BlueprintKind::Reuse
};
self.blueprints.push(BlueprintMatch {
run_id: rid.into(),
utility_name: util.into(),
similarity: sim as f32,
kind: bk,
});
}
}
}
let top = matches
.iter()
.filter_map(|m| m.get("similarity").and_then(|v| v.as_f64()))
.fold(0.0_f64, f64::max);
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::BlueprintsInjected,
text: format!(
"▼ blueprints injected · top {:.2} · {} matches",
top,
matches.len()
),
run_id: Some(run_id.to_string()),
});
}
"sat_started" => {
let parent = data
.get("parent_run_id")
.and_then(|v| v.as_str())
.unwrap_or("?");
self.sat_verdict = Some("… judging".into());
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::SessionUpdate,
text: format!("◆ sat started · parent {}", &parent[..8.min(parent.len())]),
run_id: Some(run_id.to_string()),
});
}
"sat_persona_score" => {
let persona = data
.get("persona")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_lowercase();
let score = data.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let entry = self.sat_scores.entry(persona.clone()).or_default();
entry.target = score;
entry.last_updated = Some(ts);
if self.mode == PanelMode::Blueprints {
self.mode = PanelMode::SatDials;
}
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::SessionUpdate,
text: format!("◆ sat_persona · {persona} · score {score:.1}"),
run_id: Some(run_id.to_string()),
});
}
"sat_verdict" => {
let verdict = data
.get("verdict")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_string();
self.sat_verdict = Some(verdict.clone());
let kind = if verdict.to_uppercase().contains("PASS") {
EventKind::Success
} else {
EventKind::Error
};
let glyph = if verdict.to_uppercase().contains("PASS") {
"✓"
} else {
"✖"
};
self.push_waterfall(EventDisplayed {
ts,
kind,
text: format!("{glyph} sat verdict · {verdict}"),
run_id: Some(run_id.to_string()),
});
}
_ => {
self.push_waterfall(EventDisplayed {
ts,
kind: EventKind::StateTransition,
text: format!("? unknown · {event_type}"),
run_id: Some(run_id.to_string()),
});
}
}
}
fn handle_confirmation(&mut self, key: KeyEvent) -> Option<Action> {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
let conf = self.confirmation.take().unwrap();
match conf.action {
Action::ApproveRun(id) => Some(Action::ApproveRun(id)),
Action::TriggerNext => Some(Action::TriggerNext),
Action::Quit => Some(Action::Quit),
}
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
self.confirmation = None;
None
}
_ => None,
}
}
fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return Some(Action::Quit);
}
match key.code {
KeyCode::Char('q') => Some(Action::Quit),
KeyCode::Char('?') => {
self.show_help = !self.show_help;
None
}
KeyCode::Esc if self.show_help => {
self.show_help = false;
None
}
KeyCode::Char('s') => {
self.toggle_mode(PanelMode::Shifts);
None
}
KeyCode::Char('b') => {
self.toggle_mode(PanelMode::Blueprints);
None
}
KeyCode::Char('k') => {
self.toggle_mode(PanelMode::Backlog);
None
}
KeyCode::Char('j') => {
self.toggle_mode(PanelMode::Judges);
None
}
KeyCode::Char('d') => {
self.mode = PanelMode::SatDials;
None
}
KeyCode::Char('f') => {
self.waterfall_filter = self.waterfall_filter.next();
None
}
KeyCode::Enter => {
if let Some(rid) = &self.focus_run_id
&& self.approvals.iter().any(|r| r.id == *rid)
{
return Some(Action::ApproveRun(rid.clone()));
}
None
}
KeyCode::Tab => {
self.focused = self.focused.next();
None
}
KeyCode::BackTab => {
self.focused = self.focused.prev();
None
}
KeyCode::Up => {
self.move_selection(-1);
None
}
KeyCode::Down => {
self.move_selection(1);
None
}
KeyCode::Esc => {
self.deselect();
None
}
KeyCode::Char('a') if self.focused == Panel::Approvals => {
if let Some(idx) = self.approvals_state.selected()
&& let Some(run) = self.approvals.get(idx)
{
self.confirmation = Some(Confirmation {
action: Action::ApproveRun(run.id.clone()),
prompt: format!("Approve run {}? [y/n]", &run.id[..8.min(run.id.len())]),
});
}
None
}
KeyCode::Char('n') => {
self.confirmation = Some(Confirmation {
action: Action::TriggerNext,
prompt: "Trigger next action? [y/n]".into(),
});
None
}
_ => None,
}
}
fn move_selection(&mut self, delta: i32) {
match self.focused {
Panel::Runs => {
if self.runs.is_empty() {
return;
}
let current = self.runs_state.selected().unwrap_or(0) as i32;
let next = (current + delta).clamp(0, self.runs.len() as i32 - 1) as usize;
self.runs_state.select(Some(next));
}
Panel::Approvals => {
if self.approvals.is_empty() {
return;
}
let current = self.approvals_state.selected().unwrap_or(0) as i32;
let next = (current + delta).clamp(0, self.approvals.len() as i32 - 1) as usize;
self.approvals_state.select(Some(next));
}
_ => {}
}
}
fn deselect(&mut self) {
match self.focused {
Panel::Runs => self.runs_state.select(None),
Panel::Approvals => self.approvals_state.select(None),
_ => {}
}
}
pub fn update_from_runs(&mut self, runs: Vec<Run>) {
self.runs.clear();
self.approvals.clear();
for run in &runs {
let issue = match (&run.issue_number, &run.issue_title) {
(Some(num), Some(title)) => format!("#{} \u{2014} {}", num, title),
(Some(num), None) => format!("#{}", num),
_ => "-".into(),
};
if let Some(t) = run.started_at {
self.run_started_at.entry(run.id.clone()).or_insert(t);
}
if let Some(t) = run.completed_at {
self.run_completed_at.insert(run.id.clone(), t);
}
let duration = format_duration(run.started_at, run.completed_at);
let info = RunInfo {
id: run.id.clone(),
status: run.status.to_string(),
issue,
duration,
};
match run.status {
RunStatus::AwaitingApproval => self.approvals.push(info),
_ => self.runs.push(info),
}
}
if !self.runs.is_empty() && self.runs_state.selected().is_none() {
self.runs_state.select(Some(0));
}
if !self.approvals.is_empty() && self.approvals_state.selected().is_none() {
self.approvals_state.select(Some(0));
}
}
pub fn render(&mut self, frame: &mut Frame) {
self.step_animations();
self.scope_step();
crate::tui::panels::render_all(frame, self);
}
}
fn format_duration(start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> String {
let start = match start {
Some(s) => s,
None => return "-".into(),
};
let end = end.unwrap_or_else(Utc::now);
let secs = (end - start).num_seconds().max(0) as u64;
format_short_secs(secs)
}
fn parse_total_in_store(summary: &str) -> Option<u64> {
let after = summary.split(',').nth(1)?.trim();
let n_str = after.split_whitespace().next()?;
n_str.parse::<u64>().ok()
}
fn format_short_secs(secs: u64) -> String {
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m{:02}s", secs / 60, secs % 60)
} else {
format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60)
}
}