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