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)]
15pub struct DashboardArgs {}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub(super) enum StageContentMode {
20 Output,
21 Logs,
22 Context,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(super) enum SortMode {
30 StartedAt,
33 RecentActivity,
36 StatusGrouped,
38}
39
40impl SortMode {
41 pub(super) fn next(self) -> Self {
42 match self {
43 Self::StartedAt => Self::RecentActivity,
44 Self::RecentActivity => Self::StatusGrouped,
45 Self::StatusGrouped => Self::StartedAt,
46 }
47 }
48
49 pub(super) fn label(self) -> &'static str {
51 match self {
52 Self::StartedAt => "started",
53 Self::RecentActivity => "activity",
54 Self::StatusGrouped => "status",
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(super) enum MainPane {
62 RunList,
63 LogPane,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(super) enum PaneId {
71 RunTable,
72 LogPanel,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(super) enum ExplorerTab {
78 Graph,
79 Timeline,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
86pub(super) struct ExplorerState {
87 pub(super) tab: ExplorerTab,
88 pub(super) show_unvisited: bool,
90 pub(super) scroll: usize,
92 pub(super) timeline_selected: usize,
94}
95
96impl ExplorerState {
97 pub(super) fn new() -> Self {
98 Self {
99 tab: ExplorerTab::Graph,
100 show_unvisited: true,
101 scroll: 0,
102 timeline_selected: 0,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Default)]
113pub(super) struct ContextTreeState {
114 pub(super) collapsed_regions: std::collections::HashSet<String>,
116 pub(super) expanded_entries: std::collections::HashSet<(String, usize)>,
118 pub(super) cursor: usize,
120 pub(super) follow_cursor: bool,
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub(super) enum ConfirmAction {
128 Kill { run_id: String },
130 Delete { run_id: String },
132 McpRemove { name: String },
134}
135
136#[derive(Debug, Clone, PartialEq)]
138pub enum AgentDisplayStatus {
139 Active,
140 Waiting,
141 Complete,
142 CompleteInteractive,
144 Error(String),
145 Idle,
146 Paused,
150 Cancelled,
151 Stale,
158}
159
160impl std::fmt::Display for AgentDisplayStatus {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Self::Active => write!(f, "{}ACTIVE", GLYPH_ACTIVE),
164 Self::Waiting => write!(f, "{}WAITING", GLYPH_WAITING),
165 Self::Complete => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
166 Self::CompleteInteractive => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
167 Self::Error(msg) => write!(f, "{}ERROR: {}", GLYPH_ERROR, msg),
168 Self::Idle => write!(f, "{}IDLE", GLYPH_PENDING),
169 Self::Paused => write!(f, "{}PAUSED", GLYPH_PENDING),
170 Self::Cancelled => write!(f, "⊘CANCEL"),
171 Self::Stale => write!(f, "{}STALE", GLYPH_ERROR),
172 }
173 }
174}
175
176impl AgentDisplayStatus {
177 pub(super) fn is_terminal(&self) -> bool {
179 matches!(
180 self,
181 Self::Complete | Self::CompleteInteractive | Self::Error(_) | Self::Cancelled
182 )
183 }
184
185 pub(super) fn is_killable(&self) -> bool {
189 !self.is_terminal()
190 }
191
192 pub(super) fn color(&self) -> Color {
193 match self {
194 Self::Active => C_ACTIVE,
195 Self::Waiting => C_WARN,
196 Self::Complete | Self::CompleteInteractive => C_SUCCESS,
197 Self::Error(_) => C_ERROR,
198 Self::Idle => C_DIM,
199 Self::Paused => C_WARN,
200 Self::Cancelled => C_DIM,
201 Self::Stale => C_WARN,
202 }
203 }
204}
205
206#[derive(Debug, Clone)]
208pub struct DashboardAgent {
209 pub id: String,
210 pub blueprint_name: String,
211 pub stage: String,
212 pub stage_index: usize,
213 pub num_stages: usize,
214 pub status: AgentDisplayStatus,
215 pub tokens_in: usize,
217 pub tokens_out: usize,
219 pub cached_tokens: usize,
221 pub iteration: usize,
222 pub waiting_prompt: Option<String>,
223 pub pending_request: Option<interaction::InteractionRequest>,
225 pub last_answered_request_id: Option<String>,
228 pub context_snapshot: Option<runstate::ContextSnapshot>,
230 pub stages: Vec<StageRecord>,
232 pub workdir: String,
234 pub task: String,
236 pub title: Option<String>,
238 pub model: Option<String>,
240 pub parent_id: Option<String>,
242 pub depth: usize,
244 pub started_at: i64,
246 pub last_progress_at: Option<i64>,
249 pub active_until: Option<i64>,
252 pub waiting_secs: u64,
255 pub(super) graph_info: Option<GraphTransitionInfo>,
257 pub accepts_messages: bool,
259 pub taint_summary: Vec<(String, String)>,
262}
263
264#[derive(Debug, Clone)]
266pub(super) struct LogEntry {
267 pub(super) timestamp: String,
268 pub(super) message: String,
269}
270
271#[derive(Debug, PartialEq)]
274pub(super) enum DaemonCommand {
275 Cancel { run_id: String },
277 Pause { run_id: String },
279 Resume { run_id: String },
281 Answer {
283 response: interaction::InteractionResponse,
284 },
285 Message { agent_id: String, content: String },
287}
288
289#[derive(Debug, PartialEq)]
295pub(super) struct DaemonOutcome {
296 pub(super) run_id: String,
298 pub(super) message: String,
300 pub(super) ok: bool,
302}
303
304#[derive(Debug, PartialEq)]
307pub(super) enum McpCommand {
308 Login { name: String },
310 Test { name: String },
312}
313
314#[derive(Debug, PartialEq)]
316pub(super) struct McpOutcome {
317 pub(super) message: String,
319 pub(super) ok: bool,
321}
322
323#[derive(Debug, Clone, PartialEq)]
325pub(super) struct McpRow {
326 pub(super) name: String,
327 pub(super) transport: String,
328 pub(super) endpoint: String,
329 pub(super) auth: String,
330}
331
332#[derive(Clone)]
335pub(super) struct McpContext {
336 pub(super) config_path: std::path::PathBuf,
337 pub(super) store_path: std::path::PathBuf,
338 pub(super) opener: leviath_mcp::BrowserOpener,
339 pub(super) clock: fn() -> u64,
340}
341
342#[derive(Debug, Clone)]
344pub(super) struct Toast {
345 pub(super) message: String,
346 pub(super) remaining_ticks: u32,
347 pub(super) level: ToastLevel,
348}
349
350#[derive(Debug, Clone, PartialEq)]
351pub(super) enum ToastLevel {
352 Info,
353 Warning,
354 Error,
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn agent_display_status_display() {
363 assert!(AgentDisplayStatus::Active.to_string().contains("ACTIVE"));
364 assert!(AgentDisplayStatus::Waiting.to_string().contains("WAITING"));
365 assert!(
366 AgentDisplayStatus::Complete
367 .to_string()
368 .contains("COMPLETE")
369 );
370 assert!(
371 AgentDisplayStatus::CompleteInteractive
372 .to_string()
373 .contains("COMPLETE")
374 );
375 assert!(
376 AgentDisplayStatus::Error("boom".to_string())
377 .to_string()
378 .contains("boom")
379 );
380 assert!(AgentDisplayStatus::Idle.to_string().contains("IDLE"));
381 assert!(AgentDisplayStatus::Paused.to_string().contains("PAUSED"));
382 assert!(AgentDisplayStatus::Cancelled.to_string().contains("CANCEL"));
383 assert!(AgentDisplayStatus::Stale.to_string().contains("STALE"));
384 }
385
386 #[test]
387 fn agent_display_status_colors_are_distinct() {
388 let active = AgentDisplayStatus::Active.color();
389 let error = AgentDisplayStatus::Error("x".to_string()).color();
390 let success = AgentDisplayStatus::Complete.color();
391 assert_ne!(active, error);
392 assert_ne!(error, success);
393 }
394
395 #[test]
396 fn agent_display_status_color_idle_and_cancelled() {
397 assert_eq!(AgentDisplayStatus::Idle.color(), C_DIM);
398 assert_eq!(AgentDisplayStatus::Cancelled.color(), C_DIM);
399 assert_eq!(AgentDisplayStatus::Stale.color(), C_WARN);
401 assert_eq!(AgentDisplayStatus::Paused.color(), C_WARN);
403 assert!(!AgentDisplayStatus::Paused.is_terminal());
404 assert!(AgentDisplayStatus::Paused.is_killable());
405 assert_eq!(AgentDisplayStatus::Waiting.color(), C_WARN);
406 assert_eq!(AgentDisplayStatus::CompleteInteractive.color(), C_SUCCESS);
407 }
408
409 #[test]
410 fn stage_content_mode_equality() {
411 assert_eq!(StageContentMode::Output, StageContentMode::Output);
412 assert_ne!(StageContentMode::Output, StageContentMode::Logs);
413 assert_ne!(StageContentMode::Logs, StageContentMode::Context);
414 }
415
416 #[test]
417 fn toast_level_debug() {
418 let toast = Toast {
419 message: "hello".to_string(),
420 remaining_ticks: 25,
421 level: ToastLevel::Info,
422 };
423 let dbg = format!("{:?}", toast);
424 assert!(dbg.contains("hello"));
425 assert!(dbg.contains("25"));
426 }
427
428 #[test]
429 fn daemon_command_debug_and_eq() {
430 let cmd = DaemonCommand::Cancel {
431 run_id: "run-123".to_string(),
432 };
433 let dbg = format!("{:?}", cmd);
434 assert!(dbg.contains("run-123"));
435 assert_eq!(
436 cmd,
437 DaemonCommand::Cancel {
438 run_id: "run-123".to_string()
439 }
440 );
441 assert_ne!(
442 cmd,
443 DaemonCommand::Message {
444 agent_id: "a".to_string(),
445 content: "b".to_string()
446 }
447 );
448 }
449
450 #[test]
451 fn log_entry_clone() {
452 let entry = LogEntry {
453 timestamp: "12:00:00".to_string(),
454 message: "started".to_string(),
455 };
456 let cloned = entry.clone();
457 assert_eq!(cloned.timestamp, "12:00:00");
458 assert_eq!(cloned.message, "started");
459 }
460
461 #[test]
462 fn dashboard_agent_clone() {
463 let agent = DashboardAgent {
464 id: "run-1".to_string(),
465 blueprint_name: "coder".to_string(),
466 stage: "plan".to_string(),
467 stage_index: 0,
468 num_stages: 2,
469 status: AgentDisplayStatus::Active,
470 tokens_in: 100,
471 tokens_out: 50,
472 cached_tokens: 0,
473 iteration: 1,
474 waiting_prompt: None,
475 pending_request: None,
476 last_answered_request_id: None,
477 context_snapshot: None,
478 stages: vec![],
479 workdir: "/tmp".to_string(),
480 task: "do stuff".to_string(),
481 title: Some("My Task".to_string()),
482 model: None,
483 parent_id: None,
484 depth: 0,
485 started_at: 1000,
486 last_progress_at: None,
487 active_until: None,
488 waiting_secs: 0,
489 graph_info: None,
490 accepts_messages: true,
491 taint_summary: vec![],
492 };
493 let cloned = agent.clone();
494 assert_eq!(cloned.id, "run-1");
495 assert_eq!(cloned.blueprint_name, "coder");
496 assert_eq!(cloned.stage, "plan");
497 assert_eq!(cloned.tokens_in, 100);
498 }
499
500 #[test]
501 fn agent_display_status_complete_interactive_shows_complete() {
502 let status = AgentDisplayStatus::CompleteInteractive;
503 let display = status.to_string();
504 assert!(display.contains("COMPLETE"));
505 assert_eq!(status.color(), C_SUCCESS);
506 }
507}