Skip to main content

sr_ai/ui/
mod.rs

1use crate::ai::AiUsage;
2use crate::commands::commit::CommitPlan;
3use crossterm::style::Stylize;
4use std::collections::HashMap;
5
6// Re-export generic UI primitives from agentspec-ui.
7pub use agentspec_ui::{
8    confirm, format_tokens, header, info, is_tty, phase_ok, spinner, spinner_done, tool_call, warn,
9};
10
11/// Display token usage and cost.
12pub fn usage(usage: &AiUsage) {
13    agentspec_ui::usage(usage.input_tokens, usage.output_tokens, usage.cost_usd);
14}
15
16/// Display the commit plan with file statuses and optional cache label.
17pub 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
88/// Print commit execution header.
89pub 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
98/// Print a file staging result.
99pub 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
107/// Print commit created with short SHA.
108pub fn commit_created(sha: &str) {
109    println!("    {} {}", "→".green().bold(), sha.green());
110}
111
112/// Print commit skipped notice.
113pub fn commit_skipped() {
114    println!("    {} {}", "−".yellow(), "skipped (no staged files)".dim());
115}
116
117/// Print commit failed notice.
118pub fn commit_failed(reason: &str) {
119    println!(
120        "    {} {} {}",
121        "✗".red().bold(),
122        "failed:".red(),
123        reason.dim()
124    );
125}
126
127/// Print final summary with commit list.
128pub 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
144/// Display invalid commit messages found during pre-validation.
145pub 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
168/// Display commits that failed during execution.
169pub 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}