1use crate::ai::AiUsage;
2use crate::commands::commit::CommitPlan;
3use crossterm::style::Stylize;
4use std::collections::HashMap;
5
6pub use agentspec_ui::{
8 confirm, format_tokens, header, info, is_tty, phase_ok, spinner, spinner_done, tool_call, warn,
9};
10
11pub fn usage(usage: &AiUsage) {
13 agentspec_ui::usage(usage.input_tokens, usage.output_tokens, usage.cost_usd);
14}
15
16pub fn display_plan(
18 plan: &CommitPlan,
19 statuses: &HashMap<String, char>,
20 cache_label: Option<&str>,
21) {
22 let count = plan.commits.len();
23 let count_str = format!("{count} commit{}", if count == 1 { "" } else { "s" });
24 let label = match cache_label {
25 Some(l) => format!("{count_str} · {l}"),
26 None => count_str,
27 };
28
29 println!();
30 println!(" {} {}", "COMMIT PLAN".bold(), format!("· {label}").dim());
31 let rule = "─".repeat(50);
32 println!(" {}", rule.as_str().dim());
33
34 for (i, commit) in plan.commits.iter().enumerate() {
35 let order = commit.order.unwrap_or(i as u32 + 1);
36 let idx = format!("[{order}]");
37
38 println!();
39 println!(
40 " {} {}",
41 idx.as_str().cyan().bold(),
42 commit.message.as_str().bold()
43 );
44
45 if let Some(body) = &commit.body
46 && !body.is_empty()
47 {
48 for line in body.lines() {
49 println!(" {} {}", "│".dim(), line.dim());
50 }
51 }
52
53 if let Some(footer) = &commit.footer
54 && !footer.is_empty()
55 {
56 println!(" {}", "│".dim());
57 for line in footer.lines() {
58 println!(" {} {}", "│".dim(), line.yellow());
59 }
60 }
61
62 println!(" {}", "│".dim());
63
64 let fc = commit.files.len();
65 if fc == 0 {
66 println!(" {} {}", "└─".dim(), "(no files)".dim());
67 } else {
68 for (j, file) in commit.files.iter().enumerate() {
69 let is_last = j == fc - 1;
70 let connector = if is_last { "└─" } else { "├─" };
71 let status_char = statuses.get(file).copied().unwrap_or('~');
72 let status_styled = match status_char {
73 'A' => format!("{}", "A".green()),
74 'D' => format!("{}", "D".red()),
75 'M' => format!("{}", "M".yellow()),
76 'R' => format!("{}", "R".blue()),
77 _ => format!("{}", "·".dim()),
78 };
79 println!(" {} {} {}", connector.dim(), status_styled, file);
80 }
81 }
82 }
83
84 println!();
85 println!(" {}", rule.as_str().dim());
86}
87
88pub fn commit_start(index: usize, total: usize, message: &str) {
90 println!();
91 println!(
92 " {} {}",
93 format!("[{index}/{total}]").as_str().cyan().bold(),
94 message.bold()
95 );
96}
97
98pub fn file_staged(file: &str, success: bool) {
100 if success {
101 println!(" {} {}", "✓".green(), file.dim());
102 } else {
103 println!(" {} {} {}", "⚠".yellow(), file, "(not found)".dim());
104 }
105}
106
107pub fn commit_created(sha: &str) {
109 println!(" {} {}", "→".green().bold(), sha.green());
110}
111
112pub fn commit_skipped() {
114 println!(" {} {}", "−".yellow(), "skipped (no staged files)".dim());
115}
116
117pub fn commit_failed(reason: &str) {
119 println!(
120 " {} {} {}",
121 "✗".red().bold(),
122 "failed:".red(),
123 reason.dim()
124 );
125}
126
127pub fn summary(commits: &[(String, String)]) {
129 let count = commits.len();
130 println!();
131 println!(
132 " {} {} commit{} created",
133 "✓".green().bold(),
134 count.to_string().as_str().bold(),
135 if count == 1 { "" } else { "s" }
136 );
137 println!();
138 for (sha, msg) in commits {
139 println!(" {} {}", sha.as_str().dim(), msg);
140 }
141 println!();
142}
143
144pub fn invalid_messages(invalid: &[(usize, String, String)]) {
146 println!();
147 println!(
148 " {} {}",
149 "⚠".yellow().bold(),
150 format!(
151 "{} commit message{} failed validation:",
152 invalid.len(),
153 if invalid.len() == 1 { "" } else { "s" }
154 )
155 .yellow()
156 );
157 for (idx, msg, reason) in invalid {
158 println!(
159 " {} {} — {}",
160 format!("[{idx}]").cyan(),
161 msg,
162 reason.as_str().dim()
163 );
164 }
165 println!();
166}
167
168pub fn failed_commits(failed: &[(usize, String, String)]) {
170 println!(
171 " {} {}",
172 "⚠".yellow().bold(),
173 format!(
174 "{} commit{} failed:",
175 failed.len(),
176 if failed.len() == 1 { "" } else { "s" }
177 )
178 .yellow()
179 );
180 for (idx, msg, reason) in failed {
181 println!(
182 " {} {} — {}",
183 format!("[{idx}]").cyan(),
184 msg,
185 reason.as_str().dim()
186 );
187 }
188 println!();
189}