Skip to main content

ralph_workflow/
banner.rs

1//! Banner and UI output utilities.
2//!
3//! This module contains presentation logic for the pipeline's visual output,
4//! including the welcome banner and the final summary display.
5
6use crate::logger::Colors;
7use crate::logger::Loggable;
8
9/// Summary data for pipeline completion display.
10///
11/// Decouples the banner presentation logic from the actual pipeline types.
12pub struct PipelineSummary {
13    /// Total elapsed time formatted as "Xm YYs"
14    pub total_time: String,
15    /// Number of developer runs completed
16    pub dev_runs_completed: usize,
17    /// Total configured developer iterations
18    pub dev_runs_total: usize,
19    /// Number of reviewer runs completed
20    pub review_runs: usize,
21    /// Number of changes detected during pipeline
22    pub changes_detected: usize,
23    /// Whether isolation mode is enabled
24    pub isolation_mode: bool,
25    /// Whether to show verbose output
26    pub verbose: bool,
27    /// Optional review metrics summary
28    pub review_summary: Option<ReviewSummary>,
29}
30
31/// Review metrics summary for display.
32pub struct ReviewSummary {
33    /// One-line summary of review results
34    pub summary: String,
35    /// Number of unresolved issues
36    pub unresolved_count: usize,
37    /// Number of unresolved blocking issues
38    pub blocking_count: usize,
39    /// Optional detailed breakdown (for verbose mode)
40    pub detailed_breakdown: Option<String>,
41    /// Optional sample unresolved issues (for verbose mode)
42    pub samples: Vec<String>,
43}
44
45/// Print the welcome banner for the Ralph pipeline.
46///
47/// Displays a styled ASCII box with the pipeline name and agent information.
48///
49/// # Arguments
50///
51/// * `colors` - Color configuration for terminal output
52/// * `developer_agent` - Name of the developer agent
53/// * `reviewer_agent` - Name of the reviewer agent
54pub fn print_welcome_banner(colors: Colors, developer_agent: &str, reviewer_agent: &str) {
55    println!();
56    println!(
57        "{}{}╭────────────────────────────────────────────────────────────╮{}",
58        colors.bold(),
59        colors.cyan(),
60        colors.reset()
61    );
62    println!(
63        "{}{}│{}  {}{}🤖 Ralph{} {}─ PROMPT-driven agent orchestrator{}              {}{}│{}",
64        colors.bold(),
65        colors.cyan(),
66        colors.reset(),
67        colors.bold(),
68        colors.white(),
69        colors.reset(),
70        colors.dim(),
71        colors.reset(),
72        colors.bold(),
73        colors.cyan(),
74        colors.reset()
75    );
76    println!(
77        "{}{}│{}  {}{} × {} pipeline for autonomous development{}                 {}{}│{}",
78        colors.bold(),
79        colors.cyan(),
80        colors.reset(),
81        colors.dim(),
82        developer_agent,
83        reviewer_agent,
84        colors.reset(),
85        colors.bold(),
86        colors.cyan(),
87        colors.reset()
88    );
89    println!(
90        "{}{}╰────────────────────────────────────────────────────────────╯{}",
91        colors.bold(),
92        colors.cyan(),
93        colors.reset()
94    );
95    println!();
96}
97
98/// Print the final summary after pipeline completion.
99///
100/// Displays statistics about the pipeline run including timing, run counts,
101/// and review metrics if available.
102///
103/// # Arguments
104///
105/// * `colors` - Color configuration for terminal output
106/// * `summary` - Pipeline summary data
107/// * `logger` - Logger for final success message (via Loggable trait)
108pub fn print_final_summary<L: Loggable>(colors: Colors, summary: &PipelineSummary, logger: &L) {
109    logger.header("Pipeline Complete", crate::logger::Colors::green);
110
111    println!();
112    println!(
113        "{}{}📊 Summary{}",
114        colors.bold(),
115        colors.white(),
116        colors.reset()
117    );
118    println!(
119        "{}──────────────────────────────────{}",
120        colors.dim(),
121        colors.reset()
122    );
123    println!(
124        "  {}⏱{}  Total time:      {}{}{}",
125        colors.cyan(),
126        colors.reset(),
127        colors.bold(),
128        summary.total_time,
129        colors.reset()
130    );
131    println!(
132        "  {}🔄{}  Dev runs:        {}{}{}/{}",
133        colors.blue(),
134        colors.reset(),
135        colors.bold(),
136        summary.dev_runs_completed,
137        colors.reset(),
138        summary.dev_runs_total
139    );
140    println!(
141        "  {}🔍{}  Review runs:     {}{}{}",
142        colors.magenta(),
143        colors.reset(),
144        colors.bold(),
145        summary.review_runs,
146        colors.reset()
147    );
148    println!(
149        "  {}📝{}  Changes detected: {}{}{}",
150        colors.green(),
151        colors.reset(),
152        colors.bold(),
153        summary.changes_detected,
154        colors.reset()
155    );
156
157    // Review metrics
158    if let Some(ref review) = summary.review_summary {
159        print_review_summary(colors, summary.verbose, review);
160    }
161    println!();
162
163    print_output_files(colors, summary.isolation_mode);
164
165    // Use the Loggable trait's success method
166    logger.success("Ralph pipeline completed successfully!");
167
168    // Log additional status messages via Loggable trait
169    if summary.review_runs > 0 {
170        logger.info(&format!("Completed {} review run(s)", summary.review_runs));
171    }
172    if summary.changes_detected > 0 {
173        logger.info(&format!("Detected {} change(s)", summary.changes_detected));
174    }
175    if summary.isolation_mode {
176        logger.info("Running in isolation mode");
177    }
178
179    // Log warnings for unresolved issues if present
180    if let Some(ref review) = summary.review_summary {
181        if review.unresolved_count > 0 {
182            logger.warn(&format!(
183                "{} unresolved issue(s) remaining",
184                review.unresolved_count
185            ));
186        }
187        if review.blocking_count > 0 {
188            logger.error(&format!(
189                "{} blocking issue(s) unresolved",
190                review.blocking_count
191            ));
192        }
193    }
194}
195
196/// Print review metrics summary.
197fn print_review_summary(colors: Colors, verbose: bool, review: &ReviewSummary) {
198    // No issues case
199    if review.unresolved_count == 0 && review.blocking_count == 0 {
200        println!(
201            "  {}✓{}   Review result:   {}{}{}",
202            colors.green(),
203            colors.reset(),
204            colors.bold(),
205            review.summary,
206            colors.reset()
207        );
208        return;
209    }
210
211    // Issues present
212    println!(
213        "  {}🔎{}  Review summary:  {}{}{}",
214        colors.yellow(),
215        colors.reset(),
216        colors.bold(),
217        review.summary,
218        colors.reset()
219    );
220
221    // Show unresolved count
222    if review.unresolved_count > 0 {
223        println!(
224            "  {}⚠{}   Unresolved:      {}{}{} issues remaining",
225            colors.red(),
226            colors.reset(),
227            colors.bold(),
228            review.unresolved_count,
229            colors.reset()
230        );
231    }
232
233    // Show detailed breakdown in verbose mode
234    if verbose {
235        if let Some(ref breakdown) = review.detailed_breakdown {
236            println!("  {}📊{}  Breakdown:", colors.dim(), colors.reset());
237            for line in breakdown.lines() {
238                println!("      {}{}{}", colors.dim(), line.trim(), colors.reset());
239            }
240        }
241        // Show sample unresolved issues
242        if !review.samples.is_empty() {
243            println!(
244                "  {}🧾{}  Unresolved samples:",
245                colors.dim(),
246                colors.reset()
247            );
248            for s in &review.samples {
249                println!("      {}- {}{}", colors.dim(), s, colors.reset());
250            }
251        }
252    }
253
254    // Highlight blocking issues
255    if review.blocking_count > 0 {
256        println!(
257            "  {}🚨{}  BLOCKING:        {}{}{} critical/high issues unresolved",
258            colors.red(),
259            colors.reset(),
260            colors.bold(),
261            review.blocking_count,
262            colors.reset()
263        );
264    }
265}
266
267/// Print the output files list.
268fn print_output_files(colors: Colors, isolation_mode: bool) {
269    println!(
270        "{}{}📁 Output Files{}",
271        colors.bold(),
272        colors.white(),
273        colors.reset()
274    );
275    println!(
276        "{}──────────────────────────────────{}",
277        colors.dim(),
278        colors.reset()
279    );
280    println!(
281        "  → {}PROMPT.md{}           Goal definition",
282        colors.cyan(),
283        colors.reset()
284    );
285    println!(
286        "  → {}.agent/STATUS.md{}    Current status",
287        colors.cyan(),
288        colors.reset()
289    );
290    // Only show ISSUES.md and NOTES.md when NOT in isolation mode
291    if !isolation_mode {
292        println!(
293            "  → {}.agent/ISSUES.md{}    Review findings",
294            colors.cyan(),
295            colors.reset()
296        );
297        println!(
298            "  → {}.agent/NOTES.md{}     Progress notes",
299            colors.cyan(),
300            colors.reset()
301        );
302    }
303    println!(
304        "  → {}.agent/logs/{}        Detailed logs",
305        colors.cyan(),
306        colors.reset()
307    );
308    println!();
309}