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 EnableYolo,
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub enum AgentDisplayStatus {
142 Active,
144 Waiting,
146 Complete,
148 CompleteInteractive,
150 Error(String),
152 Idle,
154 Paused,
158 Cancelled,
160 Stale,
167}
168
169impl std::fmt::Display for AgentDisplayStatus {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 Self::Active => write!(f, "{}ACTIVE", GLYPH_ACTIVE),
173 Self::Waiting => write!(f, "{}WAITING", GLYPH_WAITING),
174 Self::Complete => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
175 Self::CompleteInteractive => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
176 Self::Error(msg) => write!(f, "{}ERROR: {}", GLYPH_ERROR, msg),
177 Self::Idle => write!(f, "{}IDLE", GLYPH_PENDING),
178 Self::Paused => write!(f, "{}PAUSED", GLYPH_PENDING),
179 Self::Cancelled => write!(f, "⊘CANCEL"),
180 Self::Stale => write!(f, "{}STALE", GLYPH_ERROR),
181 }
182 }
183}
184
185impl AgentDisplayStatus {
186 pub(super) fn is_terminal(&self) -> bool {
188 matches!(
189 self,
190 Self::Complete | Self::CompleteInteractive | Self::Error(_) | Self::Cancelled
191 )
192 }
193
194 pub(super) fn is_killable(&self) -> bool {
198 !self.is_terminal()
199 }
200
201 pub(super) fn color(&self) -> Color {
202 match self {
203 Self::Active => C_ACTIVE,
204 Self::Waiting => C_WARN,
205 Self::Complete | Self::CompleteInteractive => C_SUCCESS,
206 Self::Error(_) => C_ERROR,
207 Self::Idle => C_DIM,
208 Self::Paused => C_WARN,
209 Self::Cancelled => C_DIM,
210 Self::Stale => C_WARN,
211 }
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct DashboardAgent {
218 pub id: String,
220 pub blueprint_name: String,
222 pub stage: String,
224 pub stage_index: usize,
226 pub num_stages: usize,
228 pub status: AgentDisplayStatus,
230 pub tokens_in: usize,
232 pub tokens_out: usize,
234 pub cached_tokens: usize,
236 pub iteration: usize,
238 pub waiting_prompt: Option<String>,
240 pub pending_request: Option<interaction::InteractionRequest>,
242 pub last_answered_request_id: Option<String>,
245 pub context_snapshot: Option<std::sync::Arc<runstate::ContextSnapshot>>,
250 pub stages: Vec<StageRecord>,
252 pub workdir: String,
254 pub task: String,
256 pub title: Option<String>,
258 pub model: Option<String>,
260 pub parent_id: Option<String>,
262 pub depth: usize,
264 pub started_at: i64,
266 pub last_progress_at: Option<i64>,
269 pub active_until: Option<i64>,
272 pub waiting_secs: u64,
275 pub(super) graph_info: Option<GraphTransitionInfo>,
277 pub accepts_messages: bool,
279 pub taint_summary: Vec<(String, String)>,
282}
283
284#[derive(Debug, Clone)]
286pub(super) struct LogEntry {
287 pub(super) timestamp: String,
288 pub(super) message: String,
289}
290
291#[derive(Debug, PartialEq)]
294pub(super) enum DaemonCommand {
295 Cancel { run_id: String },
297 Pause { run_id: String },
299 Resume { run_id: String },
301 Answer {
303 response: interaction::InteractionResponse,
304 },
305 Message { agent_id: String, content: String },
307}
308
309#[derive(Debug, PartialEq)]
315pub(super) struct DaemonOutcome {
316 pub(super) run_id: String,
318 pub(super) message: String,
320 pub(super) ok: bool,
322}
323
324#[derive(Debug, PartialEq)]
327pub(super) enum McpCommand {
328 Login { name: String },
330 Test { name: String },
332}
333
334#[derive(Debug, PartialEq)]
336pub(super) struct McpOutcome {
337 pub(super) message: String,
339 pub(super) ok: bool,
341}
342
343#[derive(Debug, Clone, PartialEq)]
345pub(super) struct McpRow {
346 pub(super) name: String,
347 pub(super) transport: String,
348 pub(super) endpoint: String,
349 pub(super) auth: String,
350}
351
352#[derive(Clone)]
355pub(super) struct McpContext {
356 pub(super) config_path: std::path::PathBuf,
357 pub(super) store_path: std::path::PathBuf,
358 pub(super) opener: leviath_mcp::BrowserOpener,
359 pub(super) clock: fn() -> u64,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub(super) enum NewRunPane {
365 Agents,
366 Task,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371pub(super) struct NewRunAgent {
372 pub(super) name: String,
373 pub(super) source: String,
375 pub(super) description: String,
376 pub(super) path: String,
380}
381
382#[derive(Clone)]
386pub(super) struct NewRunContext {
387 pub(super) agents_dir: std::path::PathBuf,
389 pub(super) config_path: std::path::PathBuf,
391 pub(super) workdir: std::path::PathBuf,
394}
395
396#[derive(Debug, PartialEq, Eq)]
401pub(super) struct SpawnCommand {
402 pub(super) agent_path: String,
404 pub(super) task: String,
406 pub(super) workdir: String,
408 pub(super) yolo: bool,
410}
411
412#[derive(Debug, PartialEq, Eq)]
414pub(super) struct SpawnOutcome {
415 pub(super) message: String,
417 pub(super) ok: bool,
419 pub(super) run_id: Option<String>,
421}
422
423#[derive(Debug, Clone)]
425pub(super) struct Toast {
426 pub(super) message: String,
427 pub(super) remaining_ticks: u32,
428 pub(super) level: ToastLevel,
429}
430
431#[derive(Debug, Clone, PartialEq)]
432pub(super) enum ToastLevel {
433 Info,
434 Warning,
435 Error,
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn agent_display_status_display() {
444 assert!(AgentDisplayStatus::Active.to_string().contains("ACTIVE"));
445 assert!(AgentDisplayStatus::Waiting.to_string().contains("WAITING"));
446 assert!(
447 AgentDisplayStatus::Complete
448 .to_string()
449 .contains("COMPLETE")
450 );
451 assert!(
452 AgentDisplayStatus::CompleteInteractive
453 .to_string()
454 .contains("COMPLETE")
455 );
456 assert!(
457 AgentDisplayStatus::Error("boom".to_string())
458 .to_string()
459 .contains("boom")
460 );
461 assert!(AgentDisplayStatus::Idle.to_string().contains("IDLE"));
462 assert!(AgentDisplayStatus::Paused.to_string().contains("PAUSED"));
463 assert!(AgentDisplayStatus::Cancelled.to_string().contains("CANCEL"));
464 assert!(AgentDisplayStatus::Stale.to_string().contains("STALE"));
465 }
466
467 #[test]
468 fn agent_display_status_colors_are_distinct() {
469 let active = AgentDisplayStatus::Active.color();
470 let error = AgentDisplayStatus::Error("x".to_string()).color();
471 let success = AgentDisplayStatus::Complete.color();
472 assert_ne!(active, error);
473 assert_ne!(error, success);
474 }
475
476 #[test]
477 fn agent_display_status_color_idle_and_cancelled() {
478 assert_eq!(AgentDisplayStatus::Idle.color(), C_DIM);
479 assert_eq!(AgentDisplayStatus::Cancelled.color(), C_DIM);
480 assert_eq!(AgentDisplayStatus::Stale.color(), C_WARN);
482 assert_eq!(AgentDisplayStatus::Paused.color(), C_WARN);
484 assert!(!AgentDisplayStatus::Paused.is_terminal());
485 assert!(AgentDisplayStatus::Paused.is_killable());
486 assert_eq!(AgentDisplayStatus::Waiting.color(), C_WARN);
487 assert_eq!(AgentDisplayStatus::CompleteInteractive.color(), C_SUCCESS);
488 }
489
490 #[test]
491 fn stage_content_mode_equality() {
492 assert_eq!(StageContentMode::Output, StageContentMode::Output);
493 assert_ne!(StageContentMode::Output, StageContentMode::Logs);
494 assert_ne!(StageContentMode::Logs, StageContentMode::Context);
495 }
496
497 #[test]
498 fn toast_level_debug() {
499 let toast = Toast {
500 message: "hello".to_string(),
501 remaining_ticks: 25,
502 level: ToastLevel::Info,
503 };
504 let dbg = format!("{:?}", toast);
505 assert!(dbg.contains("hello"));
506 assert!(dbg.contains("25"));
507 }
508
509 #[test]
510 fn daemon_command_debug_and_eq() {
511 let cmd = DaemonCommand::Cancel {
512 run_id: "run-123".to_string(),
513 };
514 let dbg = format!("{:?}", cmd);
515 assert!(dbg.contains("run-123"));
516 assert_eq!(
517 cmd,
518 DaemonCommand::Cancel {
519 run_id: "run-123".to_string()
520 }
521 );
522 assert_ne!(
523 cmd,
524 DaemonCommand::Message {
525 agent_id: "a".to_string(),
526 content: "b".to_string()
527 }
528 );
529 }
530
531 #[test]
532 fn log_entry_clone() {
533 let entry = LogEntry {
534 timestamp: "12:00:00".to_string(),
535 message: "started".to_string(),
536 };
537 let cloned = entry.clone();
538 assert_eq!(cloned.timestamp, "12:00:00");
539 assert_eq!(cloned.message, "started");
540 }
541
542 #[test]
543 fn dashboard_agent_clone() {
544 let agent = DashboardAgent {
545 id: "run-1".to_string(),
546 blueprint_name: "coder".to_string(),
547 stage: "plan".to_string(),
548 stage_index: 0,
549 num_stages: 2,
550 status: AgentDisplayStatus::Active,
551 tokens_in: 100,
552 tokens_out: 50,
553 cached_tokens: 0,
554 iteration: 1,
555 waiting_prompt: None,
556 pending_request: None,
557 last_answered_request_id: None,
558 context_snapshot: None,
559 stages: vec![],
560 workdir: "/tmp".to_string(),
561 task: "do stuff".to_string(),
562 title: Some("My Task".to_string()),
563 model: None,
564 parent_id: None,
565 depth: 0,
566 started_at: 1000,
567 last_progress_at: None,
568 active_until: None,
569 waiting_secs: 0,
570 graph_info: None,
571 accepts_messages: true,
572 taint_summary: vec![],
573 };
574 let cloned = agent.clone();
575 assert_eq!(cloned.id, "run-1");
576 assert_eq!(cloned.blueprint_name, "coder");
577 assert_eq!(cloned.stage, "plan");
578 assert_eq!(cloned.tokens_in, 100);
579 }
580
581 #[test]
582 fn agent_display_status_complete_interactive_shows_complete() {
583 let status = AgentDisplayStatus::CompleteInteractive;
584 let display = status.to_string();
585 assert!(display.contains("COMPLETE"));
586 assert_eq!(status.color(), C_SUCCESS);
587 }
588}