1use clap::Args;
4
5use super::graph::GraphTransitionInfo;
6use super::theme::{C_ACTIVE, C_DIM, C_ERROR, C_SUCCESS, C_WARN};
7use super::theme::{GLYPH_ACTIVE, GLYPH_COMPLETE, GLYPH_ERROR, GLYPH_PENDING, GLYPH_WAITING};
8
9use crate::runstate::{self, StageRecord};
10use leviath_core::interaction;
11
12use ratatui::style::Color;
13
14#[derive(Args)]
16pub struct DashboardArgs {}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub(super) enum StageContentMode {
21 Output,
22 Logs,
23 Context,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(super) enum SortMode {
31 StartedAt,
34 RecentActivity,
37 StatusGrouped,
39}
40
41impl SortMode {
42 pub(super) fn next(self) -> Self {
43 match self {
44 Self::StartedAt => Self::RecentActivity,
45 Self::RecentActivity => Self::StatusGrouped,
46 Self::StatusGrouped => Self::StartedAt,
47 }
48 }
49
50 pub(super) fn label(self) -> &'static str {
52 match self {
53 Self::StartedAt => "started",
54 Self::RecentActivity => "activity",
55 Self::StatusGrouped => "status",
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(super) enum MainPane {
63 RunList,
64 LogPane,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(super) enum PaneId {
72 RunTable,
73 LogPanel,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(super) enum ExplorerTab {
79 Graph,
80 Timeline,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
87pub(super) struct ExplorerState {
88 pub(super) tab: ExplorerTab,
89 pub(super) show_unvisited: bool,
91 pub(super) scroll: usize,
93 pub(super) timeline_selected: usize,
95}
96
97impl ExplorerState {
98 pub(super) fn new() -> Self {
99 Self {
100 tab: ExplorerTab::Graph,
101 show_unvisited: true,
102 scroll: 0,
103 timeline_selected: 0,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Default)]
114pub(super) struct ContextTreeState {
115 pub(super) collapsed_regions: std::collections::HashSet<String>,
117 pub(super) expanded_entries: std::collections::HashSet<(String, usize)>,
119 pub(super) cursor: usize,
121 pub(super) follow_cursor: bool,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub(super) enum ConfirmAction {
129 Kill { run_id: String },
131 Delete { run_id: String },
133 McpRemove { name: String },
135}
136
137#[derive(Debug, Clone, PartialEq)]
139pub enum AgentDisplayStatus {
140 Active,
142 Waiting,
144 Complete,
146 CompleteInteractive,
148 Error(String),
150 Idle,
152 Paused,
156 Cancelled,
158 Stale,
165}
166
167impl std::fmt::Display for AgentDisplayStatus {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 Self::Active => write!(f, "{}ACTIVE", GLYPH_ACTIVE),
171 Self::Waiting => write!(f, "{}WAITING", GLYPH_WAITING),
172 Self::Complete => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
173 Self::CompleteInteractive => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
174 Self::Error(msg) => write!(f, "{}ERROR: {}", GLYPH_ERROR, msg),
175 Self::Idle => write!(f, "{}IDLE", GLYPH_PENDING),
176 Self::Paused => write!(f, "{}PAUSED", GLYPH_PENDING),
177 Self::Cancelled => write!(f, "⊘CANCEL"),
178 Self::Stale => write!(f, "{}STALE", GLYPH_ERROR),
179 }
180 }
181}
182
183impl AgentDisplayStatus {
184 pub(super) fn is_terminal(&self) -> bool {
186 matches!(
187 self,
188 Self::Complete | Self::CompleteInteractive | Self::Error(_) | Self::Cancelled
189 )
190 }
191
192 pub(super) fn is_killable(&self) -> bool {
196 !self.is_terminal()
197 }
198
199 pub(super) fn color(&self) -> Color {
200 match self {
201 Self::Active => C_ACTIVE,
202 Self::Waiting => C_WARN,
203 Self::Complete | Self::CompleteInteractive => C_SUCCESS,
204 Self::Error(_) => C_ERROR,
205 Self::Idle => C_DIM,
206 Self::Paused => C_WARN,
207 Self::Cancelled => C_DIM,
208 Self::Stale => C_WARN,
209 }
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct DashboardAgent {
216 pub id: String,
218 pub blueprint_name: String,
220 pub stage: String,
222 pub stage_index: usize,
224 pub num_stages: usize,
226 pub status: AgentDisplayStatus,
228 pub tokens_in: usize,
230 pub tokens_out: usize,
232 pub cached_tokens: usize,
234 pub iteration: usize,
236 pub waiting_prompt: Option<String>,
238 pub pending_request: Option<interaction::InteractionRequest>,
240 pub last_answered_request_id: Option<String>,
243 pub context_snapshot: Option<std::sync::Arc<runstate::ContextSnapshot>>,
248 pub stages: Vec<StageRecord>,
250 pub workdir: String,
252 pub task: String,
254 pub title: Option<String>,
256 pub model: Option<String>,
258 pub parent_id: Option<String>,
260 pub depth: usize,
262 pub started_at: i64,
264 pub last_progress_at: Option<i64>,
267 pub active_until: Option<i64>,
270 pub waiting_secs: u64,
273 pub(super) graph_info: Option<GraphTransitionInfo>,
275 pub accepts_messages: bool,
277 pub taint_summary: Vec<(String, String)>,
280}
281
282#[derive(Debug, Clone)]
284pub(super) struct LogEntry {
285 pub(super) timestamp: String,
286 pub(super) message: String,
287}
288
289#[derive(Debug, PartialEq)]
292pub(super) enum DaemonCommand {
293 Cancel { run_id: String },
295 Pause { run_id: String },
297 Resume { run_id: String },
299 Answer {
301 response: interaction::InteractionResponse,
302 },
303 Message { agent_id: String, content: String },
305}
306
307#[derive(Debug, PartialEq)]
313pub(super) struct DaemonOutcome {
314 pub(super) run_id: String,
316 pub(super) message: String,
318 pub(super) ok: bool,
320}
321
322#[derive(Debug, PartialEq)]
325pub(super) enum McpCommand {
326 Login { name: String },
328 Test { name: String },
330}
331
332#[derive(Debug, PartialEq)]
334pub(super) struct McpOutcome {
335 pub(super) message: String,
337 pub(super) ok: bool,
339}
340
341#[derive(Debug, Clone, PartialEq)]
343pub(super) struct McpRow {
344 pub(super) name: String,
345 pub(super) transport: String,
346 pub(super) endpoint: String,
347 pub(super) auth: String,
348}
349
350#[derive(Clone)]
353pub(super) struct McpContext {
354 pub(super) config_path: std::path::PathBuf,
355 pub(super) store_path: std::path::PathBuf,
356 pub(super) opener: leviath_mcp::BrowserOpener,
357 pub(super) clock: fn() -> u64,
358}
359
360#[derive(Debug, Clone)]
362pub(super) struct Toast {
363 pub(super) message: String,
364 pub(super) remaining_ticks: u32,
365 pub(super) level: ToastLevel,
366}
367
368#[derive(Debug, Clone, PartialEq)]
369pub(super) enum ToastLevel {
370 Info,
371 Warning,
372 Error,
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn agent_display_status_display() {
381 assert!(AgentDisplayStatus::Active.to_string().contains("ACTIVE"));
382 assert!(AgentDisplayStatus::Waiting.to_string().contains("WAITING"));
383 assert!(
384 AgentDisplayStatus::Complete
385 .to_string()
386 .contains("COMPLETE")
387 );
388 assert!(
389 AgentDisplayStatus::CompleteInteractive
390 .to_string()
391 .contains("COMPLETE")
392 );
393 assert!(
394 AgentDisplayStatus::Error("boom".to_string())
395 .to_string()
396 .contains("boom")
397 );
398 assert!(AgentDisplayStatus::Idle.to_string().contains("IDLE"));
399 assert!(AgentDisplayStatus::Paused.to_string().contains("PAUSED"));
400 assert!(AgentDisplayStatus::Cancelled.to_string().contains("CANCEL"));
401 assert!(AgentDisplayStatus::Stale.to_string().contains("STALE"));
402 }
403
404 #[test]
405 fn agent_display_status_colors_are_distinct() {
406 let active = AgentDisplayStatus::Active.color();
407 let error = AgentDisplayStatus::Error("x".to_string()).color();
408 let success = AgentDisplayStatus::Complete.color();
409 assert_ne!(active, error);
410 assert_ne!(error, success);
411 }
412
413 #[test]
414 fn agent_display_status_color_idle_and_cancelled() {
415 assert_eq!(AgentDisplayStatus::Idle.color(), C_DIM);
416 assert_eq!(AgentDisplayStatus::Cancelled.color(), C_DIM);
417 assert_eq!(AgentDisplayStatus::Stale.color(), C_WARN);
419 assert_eq!(AgentDisplayStatus::Paused.color(), C_WARN);
421 assert!(!AgentDisplayStatus::Paused.is_terminal());
422 assert!(AgentDisplayStatus::Paused.is_killable());
423 assert_eq!(AgentDisplayStatus::Waiting.color(), C_WARN);
424 assert_eq!(AgentDisplayStatus::CompleteInteractive.color(), C_SUCCESS);
425 }
426
427 #[test]
428 fn stage_content_mode_equality() {
429 assert_eq!(StageContentMode::Output, StageContentMode::Output);
430 assert_ne!(StageContentMode::Output, StageContentMode::Logs);
431 assert_ne!(StageContentMode::Logs, StageContentMode::Context);
432 }
433
434 #[test]
435 fn toast_level_debug() {
436 let toast = Toast {
437 message: "hello".to_string(),
438 remaining_ticks: 25,
439 level: ToastLevel::Info,
440 };
441 let dbg = format!("{:?}", toast);
442 assert!(dbg.contains("hello"));
443 assert!(dbg.contains("25"));
444 }
445
446 #[test]
447 fn daemon_command_debug_and_eq() {
448 let cmd = DaemonCommand::Cancel {
449 run_id: "run-123".to_string(),
450 };
451 let dbg = format!("{:?}", cmd);
452 assert!(dbg.contains("run-123"));
453 assert_eq!(
454 cmd,
455 DaemonCommand::Cancel {
456 run_id: "run-123".to_string()
457 }
458 );
459 assert_ne!(
460 cmd,
461 DaemonCommand::Message {
462 agent_id: "a".to_string(),
463 content: "b".to_string()
464 }
465 );
466 }
467
468 #[test]
469 fn log_entry_clone() {
470 let entry = LogEntry {
471 timestamp: "12:00:00".to_string(),
472 message: "started".to_string(),
473 };
474 let cloned = entry.clone();
475 assert_eq!(cloned.timestamp, "12:00:00");
476 assert_eq!(cloned.message, "started");
477 }
478
479 #[test]
480 fn dashboard_agent_clone() {
481 let agent = DashboardAgent {
482 id: "run-1".to_string(),
483 blueprint_name: "coder".to_string(),
484 stage: "plan".to_string(),
485 stage_index: 0,
486 num_stages: 2,
487 status: AgentDisplayStatus::Active,
488 tokens_in: 100,
489 tokens_out: 50,
490 cached_tokens: 0,
491 iteration: 1,
492 waiting_prompt: None,
493 pending_request: None,
494 last_answered_request_id: None,
495 context_snapshot: None,
496 stages: vec![],
497 workdir: "/tmp".to_string(),
498 task: "do stuff".to_string(),
499 title: Some("My Task".to_string()),
500 model: None,
501 parent_id: None,
502 depth: 0,
503 started_at: 1000,
504 last_progress_at: None,
505 active_until: None,
506 waiting_secs: 0,
507 graph_info: None,
508 accepts_messages: true,
509 taint_summary: vec![],
510 };
511 let cloned = agent.clone();
512 assert_eq!(cloned.id, "run-1");
513 assert_eq!(cloned.blueprint_name, "coder");
514 assert_eq!(cloned.stage, "plan");
515 assert_eq!(cloned.tokens_in, 100);
516 }
517
518 #[test]
519 fn agent_display_status_complete_interactive_shows_complete() {
520 let status = AgentDisplayStatus::CompleteInteractive;
521 let display = status.to_string();
522 assert!(display.contains("COMPLETE"));
523 assert_eq!(status.color(), C_SUCCESS);
524 }
525}