pub mod cli {
use crate::constants::status_keywords;
use colored::{ColoredString, Colorize};
pub fn format_reasoning(content: &str) -> String {
format!("{} {}", "[Reasoning]".bright_blue().bold(), content)
}
pub fn format_tool_call(name: &str, details: Option<&str>) -> String {
let prefix = "[Tool]".bright_cyan().bold();
let name = name.bright_cyan();
match details {
Some(d) => match split_status_details(d) {
Some((status, rest)) => {
let status = format_status_paren(&status);
if rest.is_empty() {
format!("{} {} {}", prefix, name, status)
} else {
format!("{} {} {} {}", prefix, name, status, rest.dimmed())
}
}
None => format!("{} {} {}", prefix, name, d.dimmed()),
},
None => format!("{} {}", prefix, name),
}
}
pub fn format_command(name: &str, status: Option<&str>) -> String {
let prefix = "[Command]".bright_magenta().bold();
let name = name.bright_magenta();
match status {
Some(s) => format!("{} {} {}", prefix, name, format_status_paren(s)),
None => format!("{} {}", prefix, name),
}
}
pub fn format_permission_denied(tool_name: &str) -> String {
format!("[Permission denied: {}]", tool_name.red())
}
#[derive(Clone, Copy, Debug)]
enum StatusTone {
Success,
Warning,
Error,
Neutral,
}
fn format_status_paren(status: &str) -> ColoredString {
let display = format!("({})", status);
match classify_status(status) {
StatusTone::Success => display.green(),
StatusTone::Warning => display.yellow(),
StatusTone::Error => display.red(),
StatusTone::Neutral => display.dimmed(),
}
}
fn classify_status(status: &str) -> StatusTone {
if let Some(code) = extract_exit_code(status) {
return if code == 0 {
StatusTone::Success
} else {
StatusTone::Error
};
}
let status_lower = status.to_ascii_lowercase();
if contains_any(&status_lower, status_keywords::ERROR) {
return StatusTone::Error;
}
if contains_any(&status_lower, status_keywords::IN_PROGRESS) {
return StatusTone::Warning;
}
if contains_any(&status_lower, status_keywords::SUCCESS) {
return StatusTone::Success;
}
StatusTone::Neutral
}
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| haystack.contains(needle))
}
fn extract_exit_code(status: &str) -> Option<i32> {
let tokens: Vec<&str> = status
.split(|ch: char| !ch.is_ascii_alphanumeric())
.filter(|token| !token.is_empty())
.collect();
for pair in tokens.windows(2) {
if pair[0].eq_ignore_ascii_case("exit")
&& let Ok(code) = pair[1].parse::<i32>()
{
return Some(code);
}
}
None
}
fn split_status_details(details: &str) -> Option<(String, String)> {
let trimmed = details.trim_start();
let inner = trimmed.strip_prefix('(')?;
let end = inner.find(')')?;
let status = inner[..end].trim();
if status.is_empty() {
return None;
}
let remainder = inner[end + 1..].trim_start();
Some((status.to_string(), remainder.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use colored::Colorize;
#[test]
fn cli_format_reasoning_includes_prefix() {
let formatted = cli::format_reasoning("test reasoning");
assert!(formatted.contains("[Reasoning]"));
assert!(formatted.contains("test reasoning"));
}
#[test]
fn cli_format_reasoning_only_prefix_colored() {
colored::control::set_override(true);
let formatted = cli::format_reasoning("test reasoning");
let prefix_colored = "[Reasoning]".bright_blue().bold().to_string();
assert!(formatted.starts_with(&prefix_colored));
assert!(
formatted.contains("\x1B[0m test reasoning"),
"reset code should be followed by plain content"
);
let after_last_reset = formatted.rfind("\x1B[0m").map(|i| i + 4).unwrap_or(0);
let content_part = &formatted[after_last_reset..];
assert!(
!content_part.contains("\x1B["),
"content should not contain any ANSI codes"
);
assert_eq!(content_part, " test reasoning");
assert!(formatted.contains("test reasoning"));
assert!(formatted.ends_with("test reasoning"));
colored::control::unset_override();
}
#[test]
fn cli_format_tool_call_with_details() {
let formatted = cli::format_tool_call("read_file", Some("path=foo.rs"));
assert!(formatted.contains("[Tool]"));
assert!(formatted.contains("read_file"));
assert!(formatted.contains("path=foo.rs"));
}
#[test]
fn cli_format_tool_call_without_details() {
let formatted = cli::format_tool_call("read_file", None);
assert!(formatted.contains("[Tool]"));
assert!(formatted.contains("read_file"));
}
#[test]
fn cli_format_command_with_status() {
let formatted = cli::format_command("cargo test", Some("running"));
assert!(formatted.contains("[Command]"));
assert!(formatted.contains("cargo test"));
assert!(formatted.contains("(running)"));
}
#[test]
fn cli_format_command_without_status() {
let formatted = cli::format_command("cargo test", None);
assert!(formatted.contains("[Command]"));
assert!(formatted.contains("cargo test"));
}
#[test]
fn cli_format_tool_call_colors_status() {
colored::control::set_override(true);
let formatted = cli::format_tool_call("read_file", Some("(completed) path=foo.rs"));
let expected_status = "(completed)".green().to_string();
assert!(formatted.contains(&expected_status));
assert!(formatted.contains("path=foo.rs"));
let error_formatted = cli::format_tool_call("read_file", Some("(error)"));
let expected_error = "(error)".red().to_string();
assert!(error_formatted.contains(&expected_error));
colored::control::unset_override();
}
#[test]
fn cli_format_command_colors_status() {
colored::control::set_override(true);
let formatted = cli::format_command("cargo test", Some("running"));
let expected_status = "(running)".yellow().to_string();
assert!(formatted.contains(&expected_status));
let failed = cli::format_command("cargo test", Some("exit 2"));
let expected_failed = "(exit 2)".red().to_string();
assert!(failed.contains(&expected_failed));
colored::control::unset_override();
}
#[test]
fn cli_format_permission_denied() {
let formatted = cli::format_permission_denied("bash");
assert!(formatted.contains("Permission denied"));
assert!(formatted.contains("bash"));
}
}