Skip to main content

autom8/output/
progress.rs

1//! Progress display and run summary.
2//!
3//! Provides progress bars, story completion tracking, and run summaries.
4
5use crate::progress::{format_tokens, Breadcrumb};
6
7use super::colors::*;
8
9/// Result information for a completed story.
10#[derive(Debug, Clone)]
11pub struct StoryResult {
12    pub id: String,
13    pub title: String,
14    pub passed: bool,
15    pub duration_secs: u64,
16}
17
18/// Make a progress bar string.
19pub fn make_progress_bar(completed: usize, total: usize, width: usize) -> String {
20    if total == 0 {
21        return " ".repeat(width);
22    }
23    let filled = (completed * width) / total;
24    let empty = width - filled;
25    format!(
26        "{GREEN}{}{RESET}{GRAY}{}{RESET}",
27        "█".repeat(filled),
28        "░".repeat(empty)
29    )
30}
31
32/// Print story complete message.
33pub fn print_story_complete(story_id: &str, duration_secs: u64) {
34    let mins = duration_secs / 60;
35    let secs = duration_secs % 60;
36    println!();
37    println!("{GRAY}{}{RESET}", "-".repeat(57));
38    println!(
39        "{GREEN}{BOLD}{} completed{RESET} in {}m {}s",
40        story_id, mins, secs
41    );
42    println!("{GRAY}{}{RESET}", "-".repeat(57));
43    println!();
44}
45
46/// Print all complete message.
47pub fn print_all_complete() {
48    println!();
49    println!("{GREEN}{BOLD}All stories completed!{RESET}");
50    println!();
51}
52
53/// Print the final run completion message with duration and optional token count.
54///
55/// Format: `✓ Run completed in 1h 2m 26s - 1,234,567 total tokens`
56///
57/// If total_tokens is None, the token portion is omitted:
58/// Format: `✓ Run completed in 1h 2m 26s`
59pub fn print_run_completed(duration_secs: u64, total_tokens: Option<u64>) {
60    let duration = format_run_duration(duration_secs);
61    let token_suffix = total_tokens
62        .map(|t| format!(" - {} total tokens", format_tokens(t)))
63        .unwrap_or_default();
64
65    println!();
66    println!(
67        "{GREEN}\u{2714} Run completed in {}{}{RESET}",
68        duration, token_suffix
69    );
70    println!();
71}
72
73/// Format duration for run completion: "Xh Ym Zs" for hours, "Xm Ys" for minutes, "Xs" for seconds
74fn format_run_duration(secs: u64) -> String {
75    if secs >= 3600 {
76        let hours = secs / 3600;
77        let mins = (secs % 3600) / 60;
78        let remaining_secs = secs % 60;
79        format!("{}h {}m {}s", hours, mins, remaining_secs)
80    } else if secs >= 60 {
81        let mins = secs / 60;
82        let remaining_secs = secs % 60;
83        format!("{}m {}s", mins, remaining_secs)
84    } else {
85        format!("{}s", secs)
86    }
87}
88
89/// Print reviewing message.
90pub fn print_reviewing(iteration: u32, max_iterations: u32) {
91    println!();
92    println!("{GRAY}{}{RESET}", "-".repeat(57));
93    println!(
94        "{YELLOW}Reviewing changes (review {}/{})...{RESET}",
95        iteration, max_iterations
96    );
97    println!("{GRAY}{}{RESET}", "-".repeat(57));
98    println!();
99}
100
101/// Print skip review message.
102pub fn print_skip_review() {
103    println!();
104    println!("{GRAY}{}{RESET}", "-".repeat(57));
105    println!("{YELLOW}Skipping review (--skip-review flag set){RESET}");
106    println!("{GRAY}{}{RESET}", "-".repeat(57));
107    println!();
108}
109
110/// Print review passed message.
111pub fn print_review_passed() {
112    println!();
113    println!("{GRAY}{}{RESET}", "-".repeat(57));
114    println!("{GREEN}{BOLD}Review passed! Proceeding to commit.{RESET}");
115    println!("{GRAY}{}{RESET}", "-".repeat(57));
116    println!();
117}
118
119/// Print issues found message.
120pub fn print_issues_found(iteration: u32, max_iterations: u32) {
121    println!();
122    println!("{GRAY}{}{RESET}", "-".repeat(57));
123    println!(
124        "{YELLOW}Issues found. Running corrector (attempt {}/{})...{RESET}",
125        iteration, max_iterations
126    );
127    println!("{GRAY}{}{RESET}", "-".repeat(57));
128    println!();
129}
130
131/// Print max review iterations message.
132pub fn print_max_review_iterations() {
133    println!();
134    println!("{GRAY}{}{RESET}", "-".repeat(57));
135    println!("{RED}{BOLD}Review failed after 3 attempts.{RESET}");
136    println!("{GRAY}{}{RESET}", "-".repeat(57));
137    println!();
138}
139
140/// Print a progress bar showing task (story) completion status.
141pub fn print_tasks_progress(completed: usize, total: usize) {
142    let progress_bar = make_progress_bar(completed, total, 12);
143    println!(
144        "{BLUE}Tasks:{RESET}   [{}] {}/{} complete",
145        progress_bar, completed, total
146    );
147}
148
149/// Print a progress bar showing review iteration status.
150pub fn print_review_progress(current: u32, max: u32) {
151    let progress_bar = make_progress_bar(current as usize, max as usize, 8);
152    println!(
153        "{BLUE}Review:{RESET}  [{}] {}/{}",
154        progress_bar, current, max
155    );
156}
157
158/// Print both tasks progress and review progress.
159pub fn print_full_progress(
160    tasks_completed: usize,
161    tasks_total: usize,
162    review_current: u32,
163    review_max: u32,
164) {
165    print_tasks_progress(tasks_completed, tasks_total);
166    print_review_progress(review_current, review_max);
167}
168
169/// Print run summary.
170pub fn print_run_summary(
171    total_stories: usize,
172    completed_stories: usize,
173    total_iterations: u32,
174    total_duration_secs: u64,
175    story_results: &[StoryResult],
176) {
177    let hours = total_duration_secs / 3600;
178    let mins = (total_duration_secs % 3600) / 60;
179    let secs = total_duration_secs % 60;
180
181    println!();
182    println!("{CYAN}{BOLD}Run Summary{RESET}");
183    println!("{GRAY}{}{RESET}", "-".repeat(57));
184    println!(
185        "{BLUE}Stories:{RESET}    {}/{} completed",
186        completed_stories, total_stories
187    );
188    println!("{BLUE}Tasks:{RESET}      {}", total_iterations);
189    println!(
190        "{BLUE}Total time:{RESET} {:02}:{:02}:{:02}",
191        hours, mins, secs
192    );
193    println!();
194
195    if !story_results.is_empty() {
196        println!("{BOLD}Per-story breakdown:{RESET}");
197        for result in story_results {
198            let status = if result.passed {
199                format!("{GREEN}PASS{RESET}")
200            } else {
201                format!("{RED}FAIL{RESET}")
202            };
203            let story_mins = result.duration_secs / 60;
204            let story_secs = result.duration_secs % 60;
205            println!(
206                "  [{}] {}: {} ({}m {}s)",
207                status, result.id, result.title, story_mins, story_secs
208            );
209        }
210        println!();
211    }
212    println!("{GRAY}{}{RESET}", "-".repeat(57));
213}
214
215/// Print a breadcrumb trail showing the workflow journey.
216pub fn print_breadcrumb_trail(breadcrumb: &Breadcrumb) {
217    breadcrumb.print();
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    // ========================================================================
225    // US-007: Run duration formatting tests
226    // ========================================================================
227
228    #[test]
229    fn test_format_run_duration_seconds() {
230        assert_eq!(format_run_duration(0), "0s");
231        assert_eq!(format_run_duration(1), "1s");
232        assert_eq!(format_run_duration(30), "30s");
233        assert_eq!(format_run_duration(59), "59s");
234    }
235
236    #[test]
237    fn test_format_run_duration_minutes() {
238        assert_eq!(format_run_duration(60), "1m 0s");
239        assert_eq!(format_run_duration(90), "1m 30s");
240        assert_eq!(format_run_duration(125), "2m 5s");
241        assert_eq!(format_run_duration(3599), "59m 59s");
242    }
243
244    #[test]
245    fn test_format_run_duration_hours() {
246        assert_eq!(format_run_duration(3600), "1h 0m 0s");
247        assert_eq!(format_run_duration(3661), "1h 1m 1s");
248        assert_eq!(format_run_duration(7326), "2h 2m 6s");
249        assert_eq!(format_run_duration(3723), "1h 2m 3s");
250    }
251
252    #[test]
253    fn test_format_run_duration_large() {
254        // 10 hours, 30 minutes, 45 seconds
255        assert_eq!(format_run_duration(37845), "10h 30m 45s");
256    }
257}