use clap::Args;
use super::graph::GraphTransitionInfo;
use super::theme::{C_ACTIVE, C_DIM, C_ERROR, C_SUCCESS, C_WARN};
use super::theme::{GLYPH_ACTIVE, GLYPH_COMPLETE, GLYPH_ERROR, GLYPH_PENDING, GLYPH_WAITING};
use crate::runstate::{self, StageRecord};
use leviath_core::interaction;
use ratatui::style::Color;
#[derive(Args)]
pub struct DashboardArgs {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum StageContentMode {
Output,
Logs,
Context,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SortMode {
StartedAt,
RecentActivity,
StatusGrouped,
}
impl SortMode {
pub(super) fn next(self) -> Self {
match self {
Self::StartedAt => Self::RecentActivity,
Self::RecentActivity => Self::StatusGrouped,
Self::StatusGrouped => Self::StartedAt,
}
}
pub(super) fn label(self) -> &'static str {
match self {
Self::StartedAt => "started",
Self::RecentActivity => "activity",
Self::StatusGrouped => "status",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum MainPane {
RunList,
LogPane,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PaneId {
RunTable,
LogPanel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ExplorerTab {
Graph,
Timeline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ExplorerState {
pub(super) tab: ExplorerTab,
pub(super) show_unvisited: bool,
pub(super) scroll: usize,
pub(super) timeline_selected: usize,
}
impl ExplorerState {
pub(super) fn new() -> Self {
Self {
tab: ExplorerTab::Graph,
show_unvisited: true,
scroll: 0,
timeline_selected: 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub(super) struct ContextTreeState {
pub(super) collapsed_regions: std::collections::HashSet<String>,
pub(super) expanded_entries: std::collections::HashSet<(String, usize)>,
pub(super) cursor: usize,
pub(super) follow_cursor: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) enum ConfirmAction {
Kill { run_id: String },
Delete { run_id: String },
McpRemove { name: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum AgentDisplayStatus {
Active,
Waiting,
Complete,
CompleteInteractive,
Error(String),
Idle,
Paused,
Cancelled,
Stale,
}
impl std::fmt::Display for AgentDisplayStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Active => write!(f, "{}ACTIVE", GLYPH_ACTIVE),
Self::Waiting => write!(f, "{}WAITING", GLYPH_WAITING),
Self::Complete => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
Self::CompleteInteractive => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
Self::Error(msg) => write!(f, "{}ERROR: {}", GLYPH_ERROR, msg),
Self::Idle => write!(f, "{}IDLE", GLYPH_PENDING),
Self::Paused => write!(f, "{}PAUSED", GLYPH_PENDING),
Self::Cancelled => write!(f, "⊘CANCEL"),
Self::Stale => write!(f, "{}STALE", GLYPH_ERROR),
}
}
}
impl AgentDisplayStatus {
pub(super) fn is_terminal(&self) -> bool {
matches!(
self,
Self::Complete | Self::CompleteInteractive | Self::Error(_) | Self::Cancelled
)
}
pub(super) fn is_killable(&self) -> bool {
!self.is_terminal()
}
pub(super) fn color(&self) -> Color {
match self {
Self::Active => C_ACTIVE,
Self::Waiting => C_WARN,
Self::Complete | Self::CompleteInteractive => C_SUCCESS,
Self::Error(_) => C_ERROR,
Self::Idle => C_DIM,
Self::Paused => C_WARN,
Self::Cancelled => C_DIM,
Self::Stale => C_WARN,
}
}
}
#[derive(Debug, Clone)]
pub struct DashboardAgent {
pub id: String,
pub blueprint_name: String,
pub stage: String,
pub stage_index: usize,
pub num_stages: usize,
pub status: AgentDisplayStatus,
pub tokens_in: usize,
pub tokens_out: usize,
pub cached_tokens: usize,
pub iteration: usize,
pub waiting_prompt: Option<String>,
pub pending_request: Option<interaction::InteractionRequest>,
pub last_answered_request_id: Option<String>,
pub context_snapshot: Option<std::sync::Arc<runstate::ContextSnapshot>>,
pub stages: Vec<StageRecord>,
pub workdir: String,
pub task: String,
pub title: Option<String>,
pub model: Option<String>,
pub parent_id: Option<String>,
pub depth: usize,
pub started_at: i64,
pub last_progress_at: Option<i64>,
pub active_until: Option<i64>,
pub waiting_secs: u64,
pub(super) graph_info: Option<GraphTransitionInfo>,
pub accepts_messages: bool,
pub taint_summary: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
pub(super) struct LogEntry {
pub(super) timestamp: String,
pub(super) message: String,
}
#[derive(Debug, PartialEq)]
pub(super) enum DaemonCommand {
Cancel { run_id: String },
Pause { run_id: String },
Resume { run_id: String },
Answer {
response: interaction::InteractionResponse,
},
Message { agent_id: String, content: String },
}
#[derive(Debug, PartialEq)]
pub(super) struct DaemonOutcome {
pub(super) run_id: String,
pub(super) message: String,
pub(super) ok: bool,
}
#[derive(Debug, PartialEq)]
pub(super) enum McpCommand {
Login { name: String },
Test { name: String },
}
#[derive(Debug, PartialEq)]
pub(super) struct McpOutcome {
pub(super) message: String,
pub(super) ok: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) struct McpRow {
pub(super) name: String,
pub(super) transport: String,
pub(super) endpoint: String,
pub(super) auth: String,
}
#[derive(Clone)]
pub(super) struct McpContext {
pub(super) config_path: std::path::PathBuf,
pub(super) store_path: std::path::PathBuf,
pub(super) opener: leviath_mcp::BrowserOpener,
pub(super) clock: fn() -> u64,
}
#[derive(Debug, Clone)]
pub(super) struct Toast {
pub(super) message: String,
pub(super) remaining_ticks: u32,
pub(super) level: ToastLevel,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) enum ToastLevel {
Info,
Warning,
Error,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_display_status_display() {
assert!(AgentDisplayStatus::Active.to_string().contains("ACTIVE"));
assert!(AgentDisplayStatus::Waiting.to_string().contains("WAITING"));
assert!(
AgentDisplayStatus::Complete
.to_string()
.contains("COMPLETE")
);
assert!(
AgentDisplayStatus::CompleteInteractive
.to_string()
.contains("COMPLETE")
);
assert!(
AgentDisplayStatus::Error("boom".to_string())
.to_string()
.contains("boom")
);
assert!(AgentDisplayStatus::Idle.to_string().contains("IDLE"));
assert!(AgentDisplayStatus::Paused.to_string().contains("PAUSED"));
assert!(AgentDisplayStatus::Cancelled.to_string().contains("CANCEL"));
assert!(AgentDisplayStatus::Stale.to_string().contains("STALE"));
}
#[test]
fn agent_display_status_colors_are_distinct() {
let active = AgentDisplayStatus::Active.color();
let error = AgentDisplayStatus::Error("x".to_string()).color();
let success = AgentDisplayStatus::Complete.color();
assert_ne!(active, error);
assert_ne!(error, success);
}
#[test]
fn agent_display_status_color_idle_and_cancelled() {
assert_eq!(AgentDisplayStatus::Idle.color(), C_DIM);
assert_eq!(AgentDisplayStatus::Cancelled.color(), C_DIM);
assert_eq!(AgentDisplayStatus::Stale.color(), C_WARN);
assert_eq!(AgentDisplayStatus::Paused.color(), C_WARN);
assert!(!AgentDisplayStatus::Paused.is_terminal());
assert!(AgentDisplayStatus::Paused.is_killable());
assert_eq!(AgentDisplayStatus::Waiting.color(), C_WARN);
assert_eq!(AgentDisplayStatus::CompleteInteractive.color(), C_SUCCESS);
}
#[test]
fn stage_content_mode_equality() {
assert_eq!(StageContentMode::Output, StageContentMode::Output);
assert_ne!(StageContentMode::Output, StageContentMode::Logs);
assert_ne!(StageContentMode::Logs, StageContentMode::Context);
}
#[test]
fn toast_level_debug() {
let toast = Toast {
message: "hello".to_string(),
remaining_ticks: 25,
level: ToastLevel::Info,
};
let dbg = format!("{:?}", toast);
assert!(dbg.contains("hello"));
assert!(dbg.contains("25"));
}
#[test]
fn daemon_command_debug_and_eq() {
let cmd = DaemonCommand::Cancel {
run_id: "run-123".to_string(),
};
let dbg = format!("{:?}", cmd);
assert!(dbg.contains("run-123"));
assert_eq!(
cmd,
DaemonCommand::Cancel {
run_id: "run-123".to_string()
}
);
assert_ne!(
cmd,
DaemonCommand::Message {
agent_id: "a".to_string(),
content: "b".to_string()
}
);
}
#[test]
fn log_entry_clone() {
let entry = LogEntry {
timestamp: "12:00:00".to_string(),
message: "started".to_string(),
};
let cloned = entry.clone();
assert_eq!(cloned.timestamp, "12:00:00");
assert_eq!(cloned.message, "started");
}
#[test]
fn dashboard_agent_clone() {
let agent = DashboardAgent {
id: "run-1".to_string(),
blueprint_name: "coder".to_string(),
stage: "plan".to_string(),
stage_index: 0,
num_stages: 2,
status: AgentDisplayStatus::Active,
tokens_in: 100,
tokens_out: 50,
cached_tokens: 0,
iteration: 1,
waiting_prompt: None,
pending_request: None,
last_answered_request_id: None,
context_snapshot: None,
stages: vec![],
workdir: "/tmp".to_string(),
task: "do stuff".to_string(),
title: Some("My Task".to_string()),
model: None,
parent_id: None,
depth: 0,
started_at: 1000,
last_progress_at: None,
active_until: None,
waiting_secs: 0,
graph_info: None,
accepts_messages: true,
taint_summary: vec![],
};
let cloned = agent.clone();
assert_eq!(cloned.id, "run-1");
assert_eq!(cloned.blueprint_name, "coder");
assert_eq!(cloned.stage, "plan");
assert_eq!(cloned.tokens_in, 100);
}
#[test]
fn agent_display_status_complete_interactive_shows_complete() {
let status = AgentDisplayStatus::CompleteInteractive;
let display = status.to_string();
assert!(display.contains("COMPLETE"));
assert_eq!(status.color(), C_SUCCESS);
}
}