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, PartialEq)]
27pub enum AgentDisplayStatus {
28 Active,
29 Waiting,
30 Complete,
31 CompleteInteractive,
33 Error(String),
34 Idle,
35 Paused,
39 Cancelled,
40 Stale,
47}
48
49impl std::fmt::Display for AgentDisplayStatus {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::Active => write!(f, "{}ACTIVE", GLYPH_ACTIVE),
53 Self::Waiting => write!(f, "{}WAITING", GLYPH_WAITING),
54 Self::Complete => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
55 Self::CompleteInteractive => write!(f, "{}COMPLETE", GLYPH_COMPLETE),
56 Self::Error(msg) => write!(f, "{}ERROR: {}", GLYPH_ERROR, msg),
57 Self::Idle => write!(f, "{}IDLE", GLYPH_PENDING),
58 Self::Paused => write!(f, "{}PAUSED", GLYPH_PENDING),
59 Self::Cancelled => write!(f, "⊘CANCEL"),
60 Self::Stale => write!(f, "{}STALE", GLYPH_ERROR),
61 }
62 }
63}
64
65impl AgentDisplayStatus {
66 pub(super) fn is_terminal(&self) -> bool {
68 matches!(
69 self,
70 Self::Complete | Self::CompleteInteractive | Self::Error(_) | Self::Cancelled
71 )
72 }
73
74 pub(super) fn is_killable(&self) -> bool {
78 !self.is_terminal()
79 }
80
81 pub(super) fn color(&self) -> Color {
82 match self {
83 Self::Active => C_ACTIVE,
84 Self::Waiting => C_WARN,
85 Self::Complete | Self::CompleteInteractive => C_SUCCESS,
86 Self::Error(_) => C_ERROR,
87 Self::Idle => C_DIM,
88 Self::Paused => C_WARN,
89 Self::Cancelled => C_DIM,
90 Self::Stale => C_WARN,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct DashboardAgent {
98 pub id: String,
99 pub blueprint_name: String,
100 pub stage: String,
101 pub stage_index: usize,
102 pub num_stages: usize,
103 pub status: AgentDisplayStatus,
104 pub tokens_in: usize,
106 pub tokens_out: usize,
108 pub cached_tokens: usize,
110 pub iteration: usize,
111 pub waiting_prompt: Option<String>,
112 pub pending_request: Option<interaction::InteractionRequest>,
114 pub last_answered_request_id: Option<String>,
117 pub context_snapshot: Option<runstate::ContextSnapshot>,
119 pub stages: Vec<StageRecord>,
121 pub workdir: String,
123 pub task: String,
125 pub title: Option<String>,
127 pub model: Option<String>,
129 pub parent_id: Option<String>,
131 pub depth: usize,
133 pub started_at: i64,
135 pub active_until: Option<i64>,
138 pub waiting_secs: u64,
141 pub(super) graph_info: Option<GraphTransitionInfo>,
143 pub accepts_messages: bool,
145 pub taint_summary: Vec<(String, String)>,
148}
149
150#[derive(Debug, Clone)]
152pub(super) struct LogEntry {
153 pub(super) timestamp: String,
154 pub(super) message: String,
155}
156
157#[derive(Debug, PartialEq)]
160pub(super) enum DaemonCommand {
161 Cancel { run_id: String },
163 Pause { run_id: String },
165 Resume { run_id: String },
167 Answer {
169 response: interaction::InteractionResponse,
170 },
171 Message { agent_id: String, content: String },
173}
174
175#[derive(Debug, PartialEq)]
181pub(super) struct DaemonOutcome {
182 pub(super) run_id: String,
184 pub(super) message: String,
186 pub(super) ok: bool,
188}
189
190#[derive(Debug, PartialEq)]
193pub(super) enum McpCommand {
194 Login { name: String },
196 Test { name: String },
198}
199
200#[derive(Debug, PartialEq)]
202pub(super) struct McpOutcome {
203 pub(super) message: String,
205 pub(super) ok: bool,
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub(super) struct McpRow {
212 pub(super) name: String,
213 pub(super) transport: String,
214 pub(super) endpoint: String,
215 pub(super) auth: String,
216}
217
218#[derive(Clone)]
221pub(super) struct McpContext {
222 pub(super) config_path: std::path::PathBuf,
223 pub(super) store_path: std::path::PathBuf,
224 pub(super) opener: leviath_mcp::BrowserOpener,
225 pub(super) clock: fn() -> u64,
226}
227
228#[derive(Debug, Clone)]
230pub(super) struct Toast {
231 pub(super) message: String,
232 pub(super) remaining_ticks: u32,
233 pub(super) level: ToastLevel,
234}
235
236#[derive(Debug, Clone, PartialEq)]
237pub(super) enum ToastLevel {
238 Info,
239 Warning,
240 Error,
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn agent_display_status_display() {
249 assert!(AgentDisplayStatus::Active.to_string().contains("ACTIVE"));
250 assert!(AgentDisplayStatus::Waiting.to_string().contains("WAITING"));
251 assert!(
252 AgentDisplayStatus::Complete
253 .to_string()
254 .contains("COMPLETE")
255 );
256 assert!(
257 AgentDisplayStatus::CompleteInteractive
258 .to_string()
259 .contains("COMPLETE")
260 );
261 assert!(
262 AgentDisplayStatus::Error("boom".to_string())
263 .to_string()
264 .contains("boom")
265 );
266 assert!(AgentDisplayStatus::Idle.to_string().contains("IDLE"));
267 assert!(AgentDisplayStatus::Paused.to_string().contains("PAUSED"));
268 assert!(AgentDisplayStatus::Cancelled.to_string().contains("CANCEL"));
269 assert!(AgentDisplayStatus::Stale.to_string().contains("STALE"));
270 }
271
272 #[test]
273 fn agent_display_status_colors_are_distinct() {
274 let active = AgentDisplayStatus::Active.color();
275 let error = AgentDisplayStatus::Error("x".to_string()).color();
276 let success = AgentDisplayStatus::Complete.color();
277 assert_ne!(active, error);
278 assert_ne!(error, success);
279 }
280
281 #[test]
282 fn agent_display_status_color_idle_and_cancelled() {
283 assert_eq!(AgentDisplayStatus::Idle.color(), C_DIM);
284 assert_eq!(AgentDisplayStatus::Cancelled.color(), C_DIM);
285 assert_eq!(AgentDisplayStatus::Stale.color(), C_WARN);
287 assert_eq!(AgentDisplayStatus::Paused.color(), C_WARN);
289 assert!(!AgentDisplayStatus::Paused.is_terminal());
290 assert!(AgentDisplayStatus::Paused.is_killable());
291 assert_eq!(AgentDisplayStatus::Waiting.color(), C_WARN);
292 assert_eq!(AgentDisplayStatus::CompleteInteractive.color(), C_SUCCESS);
293 }
294
295 #[test]
296 fn stage_content_mode_equality() {
297 assert_eq!(StageContentMode::Output, StageContentMode::Output);
298 assert_ne!(StageContentMode::Output, StageContentMode::Logs);
299 assert_ne!(StageContentMode::Logs, StageContentMode::Context);
300 }
301
302 #[test]
303 fn toast_level_debug() {
304 let toast = Toast {
305 message: "hello".to_string(),
306 remaining_ticks: 25,
307 level: ToastLevel::Info,
308 };
309 let dbg = format!("{:?}", toast);
310 assert!(dbg.contains("hello"));
311 assert!(dbg.contains("25"));
312 }
313
314 #[test]
315 fn daemon_command_debug_and_eq() {
316 let cmd = DaemonCommand::Cancel {
317 run_id: "run-123".to_string(),
318 };
319 let dbg = format!("{:?}", cmd);
320 assert!(dbg.contains("run-123"));
321 assert_eq!(
322 cmd,
323 DaemonCommand::Cancel {
324 run_id: "run-123".to_string()
325 }
326 );
327 assert_ne!(
328 cmd,
329 DaemonCommand::Message {
330 agent_id: "a".to_string(),
331 content: "b".to_string()
332 }
333 );
334 }
335
336 #[test]
337 fn log_entry_clone() {
338 let entry = LogEntry {
339 timestamp: "12:00:00".to_string(),
340 message: "started".to_string(),
341 };
342 let cloned = entry.clone();
343 assert_eq!(cloned.timestamp, "12:00:00");
344 assert_eq!(cloned.message, "started");
345 }
346
347 #[test]
348 fn dashboard_agent_clone() {
349 let agent = DashboardAgent {
350 id: "run-1".to_string(),
351 blueprint_name: "coder".to_string(),
352 stage: "plan".to_string(),
353 stage_index: 0,
354 num_stages: 2,
355 status: AgentDisplayStatus::Active,
356 tokens_in: 100,
357 tokens_out: 50,
358 cached_tokens: 0,
359 iteration: 1,
360 waiting_prompt: None,
361 pending_request: None,
362 last_answered_request_id: None,
363 context_snapshot: None,
364 stages: vec![],
365 workdir: "/tmp".to_string(),
366 task: "do stuff".to_string(),
367 title: Some("My Task".to_string()),
368 model: None,
369 parent_id: None,
370 depth: 0,
371 started_at: 1000,
372 active_until: None,
373 waiting_secs: 0,
374 graph_info: None,
375 accepts_messages: true,
376 taint_summary: vec![],
377 };
378 let cloned = agent.clone();
379 assert_eq!(cloned.id, "run-1");
380 assert_eq!(cloned.blueprint_name, "coder");
381 assert_eq!(cloned.stage, "plan");
382 assert_eq!(cloned.tokens_in, 100);
383 }
384
385 #[test]
386 fn agent_display_status_complete_interactive_shows_complete() {
387 let status = AgentDisplayStatus::CompleteInteractive;
388 let display = status.to_string();
389 assert!(display.contains("COMPLETE"));
390 assert_eq!(status.color(), C_SUCCESS);
391 }
392}