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