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