Skip to main content

minion_engine/cli/
display.rs

1// Public API — new items used after worktree merge; suppress dead_code lint until then
2#![allow(dead_code)]
3
4use std::time::Duration;
5
6use colored::Colorize;
7use indicatif::{ProgressBar, ProgressStyle};
8
9/// Controls how the CLI renders output
10#[derive(Debug, Clone, PartialEq, Eq, Default)]
11pub enum OutputMode {
12    #[default]
13    Normal,
14    Quiet,
15    Verbose,
16    Json,
17}
18
19pub fn step_start(name: &str, step_type: &str) -> ProgressBar {
20    let pb = ProgressBar::new_spinner();
21    pb.set_style(
22        ProgressStyle::default_spinner()
23            .template("{spinner:.cyan} {msg}")
24            .unwrap(),
25    );
26    pb.set_message(format!("{} [{}]", name, step_type.dimmed()));
27    pb.enable_steady_tick(Duration::from_millis(100));
28    pb
29}
30
31pub fn step_ok(pb: &ProgressBar, name: &str, duration: Duration) {
32    pb.finish_and_clear();
33    println!(
34        "  {} {} {}",
35        "✓".green(),
36        name,
37        format!("({:.1}s)", duration.as_secs_f64()).dimmed()
38    );
39}
40
41pub fn step_fail(pb: &ProgressBar, name: &str, message: &str) {
42    pb.finish_and_clear();
43    println!("  {} {} — {}", "✗".red(), name, message.red());
44}
45
46pub fn step_skip(pb: &ProgressBar, name: &str, message: &str) {
47    pb.finish_and_clear();
48    println!("  {} {} {}", "→".yellow(), name, message.dimmed());
49}
50
51pub fn iteration(current: usize, max: usize) {
52    println!(
53        "    {} Iteration {}/{}",
54        "↻".cyan(),
55        current + 1,
56        max
57    );
58}
59
60pub fn agent_progress(text: &str) {
61    if !text.is_empty() {
62        for line in text.lines().take(3) {
63            println!("    {}", line.dimmed());
64        }
65    }
66}
67
68pub fn tool_use(tool: &str, _input: &str) {
69    println!("    {} [tool: {}]", "⚙".blue(), tool);
70}
71
72pub fn workflow_start(name: &str) {
73    println!("{} {}", "▶".cyan().bold(), name.bold());
74}
75
76pub fn workflow_done(duration: Duration, step_count: usize) {
77    println!(
78        "\n{} Done — {} steps in {:.1}s",
79        "✓".green().bold(),
80        step_count,
81        duration.as_secs_f64()
82    );
83}
84
85pub fn workflow_failed(step_name: &str, message: &str) {
86    println!(
87        "\n{} Failed at step '{}': {}",
88        "✗".red().bold(),
89        step_name,
90        message
91    );
92}
93
94/// Display a map item position, e.g. "Item 2/5: filename.rs"
95pub fn map_item(current: usize, total: usize, name: &str) {
96    println!(
97        "  {} Item {}/{}: {}",
98        "◆".cyan(),
99        current,
100        total,
101        name.bold()
102    );
103}
104
105/// Display a parallel sub-step with indentation
106pub fn parallel_step(name: &str) {
107    println!("    {} {}", "⟶".blue(), name);
108}
109
110/// Display a rich workflow summary including token usage and cost
111pub fn workflow_summary(
112    steps: usize,
113    duration: Duration,
114    input_tokens: u64,
115    output_tokens: u64,
116    cost_usd: f64,
117) {
118    println!(
119        "\n{} Summary — {} steps in {:.1}s",
120        "✓".green().bold(),
121        steps,
122        duration.as_secs_f64()
123    );
124    println!(
125        "   {} Tokens: {} in / {} out",
126        "·".dimmed(),
127        input_tokens,
128        output_tokens
129    );
130    println!(
131        "   {} Cost:   ${:.4}",
132        "·".dimmed(),
133        cost_usd
134    );
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_output_mode_default_is_normal() {
143        let mode = OutputMode::default();
144        assert_eq!(mode, OutputMode::Normal);
145    }
146
147    #[test]
148    fn test_output_mode_variants() {
149        let modes = [
150            OutputMode::Normal,
151            OutputMode::Quiet,
152            OutputMode::Verbose,
153            OutputMode::Json,
154        ];
155        // Ensure Clone and PartialEq work
156        for m in &modes {
157            assert_eq!(m, &m.clone());
158        }
159    }
160
161    #[test]
162    fn test_map_item_does_not_panic() {
163        // Just verify it runs without panicking
164        map_item(1, 5, "some_file.rs");
165        map_item(5, 5, "last_file.rs");
166    }
167
168    #[test]
169    fn test_parallel_step_does_not_panic() {
170        parallel_step("compile");
171        parallel_step("lint");
172    }
173
174    #[test]
175    fn test_workflow_summary_does_not_panic() {
176        workflow_summary(
177            10,
178            Duration::from_secs(42),
179            1234,
180            567,
181            0.0023,
182        );
183    }
184}