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    let _ = print_welcome_banner_to(colors, developer_agent, reviewer_agent, std::io::stdout());
69}
70
71pub fn print_welcome_banner_to<W: std::io::Write>(
72    colors: Colors,
73    developer_agent: &str,
74    reviewer_agent: &str,
75    output: W,
76) -> std::io::Result<()> {
77    let content = build_welcome_banner_content(colors, developer_agent, reviewer_agent);
78    write_banner_to(output, &content)
79}
80
81fn build_welcome_banner_content(
82    colors: Colors,
83    developer_agent: &str,
84    reviewer_agent: &str,
85) -> String {
86    let lines = [
87        "",
88        &format!(
89            "{}{}╭────────────────────────────────────────────────────────────╮{}",
90            colors.bold(),
91            colors.cyan(),
92            colors.reset()
93        ),
94        &format!(
95            "{}{}│{}  {}{}🤖 Ralph{} {}─ PROMPT-driven agent orchestrator{}              {}{}│{}",
96            colors.bold(),
97            colors.cyan(),
98            colors.reset(),
99            colors.bold(),
100            colors.white(),
101            colors.reset(),
102            colors.dim(),
103            colors.reset(),
104            colors.bold(),
105            colors.cyan(),
106            colors.reset()
107        ),
108        &format!(
109            "{}{}│{}  {}{} × {} pipeline for autonomous development{}                 {}{}│{}",
110            colors.bold(),
111            colors.cyan(),
112            colors.reset(),
113            colors.dim(),
114            developer_agent,
115            reviewer_agent,
116            colors.reset(),
117            colors.bold(),
118            colors.cyan(),
119            colors.reset()
120        ),
121        &format!(
122            "{}{}╰────────────────────────────────────────────────────────────╯{}",
123            colors.bold(),
124            colors.cyan(),
125            colors.reset()
126        ),
127        "",
128        "",
129    ];
130    lines.join("\n")
131}
132
133/// Print the final summary after pipeline completion.
134///
135/// Displays statistics about the pipeline run including timing, run counts,
136/// and review metrics if available.
137///
138/// # Arguments
139///
140/// * `colors` - Color configuration for terminal output
141/// * `summary` - Pipeline summary data
142/// * `logger` - Logger for final success message (via Loggable trait)
143pub fn print_final_summary<L: Loggable>(colors: Colors, summary: &PipelineSummary, logger: &L) {
144    let _ = print_final_summary_to(colors, summary, logger, std::io::stdout());
145}
146
147pub fn print_final_summary_to<L: Loggable, W: std::io::Write>(
148    colors: Colors,
149    summary: &PipelineSummary,
150    logger: &L,
151    output: W,
152) -> std::io::Result<()> {
153    logger.header("Pipeline Complete", crate::logger::Colors::green);
154
155    let content = build_final_summary_content(colors, summary);
156
157    write_banner_to(output, &content)?;
158
159    // Use the Loggable trait's success method
160    logger.success("Ralph pipeline completed successfully!");
161
162    // Log additional status messages via Loggable trait
163    if summary.review_runs > 0 {
164        logger.info(&format!("Completed {} review run(s)", summary.review_runs));
165    }
166    if summary.changes_detected > 0 {
167        logger.info(&format!("Detected {} change(s)", summary.changes_detected));
168    }
169    if summary.isolation_mode {
170        logger.info("Running in isolation mode");
171    }
172
173    // Log warnings for unresolved issues if present
174    if let Some(ref review) = summary.review_summary {
175        if review.unresolved_count > 0 {
176            logger.warn(&format!(
177                "{} unresolved issue(s) remaining",
178                review.unresolved_count
179            ));
180        }
181        if review.blocking_count > 0 {
182            logger.error(&format!(
183                "{} blocking issue(s) unresolved",
184                review.blocking_count
185            ));
186        }
187    }
188
189    Ok(())
190}
191
192fn write_banner_to<W: std::io::Write>(mut output: W, content: &str) -> std::io::Result<()> {
193    output.write_all(content.as_bytes())
194}
195
196fn build_final_summary_content(colors: Colors, summary: &PipelineSummary) -> String {
197    let base_lines: Vec<String> = vec![
198        "".to_string(),
199        format!(
200            "{}{}📊 Summary{}",
201            colors.bold(),
202            colors.white(),
203            colors.reset()
204        ),
205        format!(
206            "{}──────────────────────────────────{}",
207            colors.dim(),
208            colors.reset()
209        ),
210        format!(
211            "  {}⏱{}  Total time:      {}{}{}",
212            colors.cyan(),
213            colors.reset(),
214            colors.bold(),
215            summary.total_time,
216            colors.reset()
217        ),
218        format!(
219            "  {}🔄{}  Dev runs:        {}{}{}/{}",
220            colors.blue(),
221            colors.reset(),
222            colors.bold(),
223            summary.dev_runs_completed,
224            colors.reset(),
225            summary.dev_runs_total
226        ),
227        format!(
228            "  {}🔍{}  Review passes:   {}{}{}/{}",
229            colors.magenta(),
230            colors.reset(),
231            colors.bold(),
232            summary.review_passes_completed,
233            colors.reset(),
234            summary.review_passes_total
235        ),
236        format!(
237            "  {}📝{}  Changes detected: {}{}{}",
238            colors.green(),
239            colors.reset(),
240            colors.bold(),
241            summary.changes_detected,
242            colors.reset()
243        ),
244    ];
245
246    let verbose_line: Vec<String> = if summary.verbose {
247        vec![format!(
248            "  {}  {}  (Total runs:     {}{}{}){}",
249            colors.dim(),
250            colors.magenta(),
251            colors.bold(),
252            summary.review_runs,
253            colors.reset(),
254            colors.reset()
255        )]
256    } else {
257        Vec::new()
258    };
259
260    let review_lines: Vec<String> = summary
261        .review_summary
262        .as_ref()
263        .map_or(Vec::new(), |review| {
264            build_review_summary_content(colors, summary.verbose, review)
265                .lines()
266                .map(String::from)
267                .collect()
268        });
269
270    let output_files_lines: Vec<String> =
271        build_output_files_content(colors, summary.isolation_mode)
272            .lines()
273            .map(String::from)
274            .collect();
275
276    base_lines
277        .into_iter()
278        .chain(verbose_line)
279        .chain(std::iter::once("".to_string()))
280        .chain(review_lines)
281        .chain(std::iter::once("".to_string()))
282        .chain(output_files_lines)
283        .collect::<Vec<_>>()
284        .join("\n")
285}
286
287fn build_review_summary_content(colors: Colors, verbose: bool, review: &ReviewSummary) -> String {
288    if review.unresolved_count == 0 && review.blocking_count == 0 {
289        return [format!(
290            "  {}✓{}   Review result:   {}{}{}",
291            colors.green(),
292            colors.reset(),
293            colors.bold(),
294            review.summary,
295            colors.reset()
296        )]
297        .join("\n");
298    }
299
300    let base_line: Vec<String> = vec![format!(
301        "  {}🔎{}  Review summary:  {}{}{}",
302        colors.yellow(),
303        colors.reset(),
304        colors.bold(),
305        review.summary,
306        colors.reset()
307    )];
308
309    let unresolved_line: Vec<String> = if review.unresolved_count > 0 {
310        vec![format!(
311            "  {}⚠{}   Unresolved:      {}{}{} issues remaining",
312            colors.red(),
313            colors.reset(),
314            colors.bold(),
315            review.unresolved_count,
316            colors.reset()
317        )]
318    } else {
319        Vec::new()
320    };
321
322    let verbose_lines: Vec<String> = if verbose {
323        let breakdown_lines: Vec<String> =
324            review
325                .detailed_breakdown
326                .as_ref()
327                .map_or_else(Vec::<String>::new, |breakdown| {
328                    let line_strs: Vec<&str> = breakdown.lines().collect();
329                    let dimmed: Vec<String> = line_strs
330                        .iter()
331                        .map(|line| {
332                            format!("      {}{}{}", colors.dim(), line.trim(), colors.reset())
333                        })
334                        .collect();
335                    std::iter::once(format!(
336                        "  {}📊{}  Breakdown:",
337                        colors.dim(),
338                        colors.reset()
339                    ))
340                    .chain(dimmed)
341                    .collect()
342                });
343
344        let sample_lines: Vec<String> = if !review.samples.is_empty() {
345            std::iter::once(format!(
346                "  {}🧾{}  Unresolved samples:",
347                colors.dim(),
348                colors.reset()
349            ))
350            .chain(
351                review
352                    .samples
353                    .iter()
354                    .map(|s| format!("      {}- {}{}", colors.dim(), s, colors.reset())),
355            )
356            .collect()
357        } else {
358            Vec::new()
359        };
360
361        breakdown_lines.into_iter().chain(sample_lines).collect()
362    } else {
363        Vec::new()
364    };
365
366    let blocking_line: Vec<String> = if review.blocking_count > 0 {
367        vec![format!(
368            "  {}🚨{}  BLOCKING:        {}{}{} critical/high issues unresolved",
369            colors.red(),
370            colors.reset(),
371            colors.bold(),
372            review.blocking_count,
373            colors.reset()
374        )]
375    } else {
376        Vec::new()
377    };
378
379    base_line
380        .into_iter()
381        .chain(unresolved_line)
382        .chain(verbose_lines)
383        .chain(blocking_line)
384        .collect::<Vec<_>>()
385        .join("\n")
386}
387
388fn build_output_files_content(colors: Colors, isolation_mode: bool) -> String {
389    let base_lines: Vec<String> = vec![
390        format!(
391            "{}{}📁 Output Files{}",
392            colors.bold(),
393            colors.white(),
394            colors.reset()
395        ),
396        format!(
397            "{}──────────────────────────────────{}",
398            colors.dim(),
399            colors.reset()
400        ),
401        format!(
402            "  → {}PROMPT.md{}           Goal definition",
403            colors.cyan(),
404            colors.reset()
405        ),
406        format!(
407            "  → {}.agent/STATUS.md{}    Current status",
408            colors.cyan(),
409            colors.reset()
410        ),
411    ];
412
413    let isolation_lines: Vec<String> = if !isolation_mode {
414        vec![
415            format!(
416                "  → {}.agent/ISSUES.md{}    Review findings",
417                colors.cyan(),
418                colors.reset()
419            ),
420            format!(
421                "  → {}.agent/NOTES.md{}     Progress notes",
422                colors.cyan(),
423                colors.reset()
424            ),
425        ]
426    } else {
427        Vec::new()
428    };
429
430    base_lines
431        .into_iter()
432        .chain(isolation_lines)
433        .chain(std::iter::once(format!(
434            "  → {}.agent/logs/{}        Detailed logs",
435            colors.cyan(),
436            colors.reset()
437        )))
438        .collect::<Vec<_>>()
439        .join("\n")
440}