use std::sync::OnceLock;
static COLOR_DISABLED: OnceLock<bool> = OnceLock::new();
pub fn disable_color() {
let _ = COLOR_DISABLED.set(true);
}
fn color_enabled() -> bool {
!*COLOR_DISABLED.get_or_init(|| std::env::var("NO_COLOR").is_ok())
}
pub struct Color(pub &'static str);
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if color_enabled() {
f.write_str(self.0)
} else {
Ok(())
}
}
}
pub static RESET: Color = Color("\x1b[0m");
pub static BOLD: Color = Color("\x1b[1m");
pub static DIM: Color = Color("\x1b[2m");
pub static GREEN: Color = Color("\x1b[32m");
pub static YELLOW: Color = Color("\x1b[33m");
pub static CYAN: Color = Color("\x1b[36m");
pub static RED: Color = Color("\x1b[31m");
pub static MAGENTA: Color = Color("\x1b[35m");
pub static BOLD_CYAN: Color = Color("\x1b[1;36m");
pub static BOLD_YELLOW: Color = Color("\x1b[1;33m");
fn normalize_lang(lang: &str) -> Option<&'static str> {
match lang.to_lowercase().as_str() {
"rust" | "rs" => Some("rust"),
"python" | "py" => Some("python"),
"javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => Some("js"),
"go" | "golang" => Some("go"),
"sh" | "bash" | "shell" | "zsh" => Some("shell"),
"c" | "cpp" | "c++" | "cc" | "cxx" | "h" | "hpp" => Some("c"),
"json" | "jsonc" => Some("json"),
"yaml" | "yml" => Some("yaml"),
"toml" => Some("toml"),
_ => None,
}
}
fn lang_keywords(lang: &str) -> &'static [&'static str] {
match lang {
"rust" => &[
"fn",
"let",
"mut",
"if",
"else",
"for",
"while",
"loop",
"match",
"return",
"use",
"mod",
"pub",
"struct",
"enum",
"impl",
"trait",
"where",
"async",
"await",
"move",
"self",
"super",
"crate",
"const",
"static",
"type",
"as",
"in",
"ref",
"true",
"false",
"Some",
"None",
"Ok",
"Err",
"unsafe",
"dyn",
"macro_rules",
],
"python" => &[
"def", "class", "if", "elif", "else", "for", "while", "return", "import", "from", "as",
"with", "try", "except", "finally", "raise", "yield", "lambda", "pass", "break",
"continue", "and", "or", "not", "in", "is", "None", "True", "False", "self", "async",
"await", "del", "global", "nonlocal", "assert",
],
"js" => &[
"function",
"const",
"let",
"var",
"if",
"else",
"for",
"while",
"return",
"import",
"export",
"from",
"class",
"new",
"this",
"async",
"await",
"try",
"catch",
"finally",
"throw",
"typeof",
"instanceof",
"true",
"false",
"null",
"undefined",
"switch",
"case",
"default",
"break",
"continue",
"interface",
"type",
"enum",
"of",
"in",
"yield",
"delete",
"void",
"super",
"extends",
"implements",
"static",
"get",
"set",
],
"go" => &[
"func",
"var",
"const",
"if",
"else",
"for",
"range",
"return",
"import",
"package",
"type",
"struct",
"interface",
"map",
"chan",
"go",
"defer",
"select",
"case",
"switch",
"default",
"break",
"continue",
"nil",
"true",
"false",
"fallthrough",
"goto",
],
"shell" => &[
"if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac",
"function", "return", "exit", "echo", "export", "local", "readonly", "set", "unset",
"in", "true", "false", "source", "alias", "cd", "test",
],
"c" => &[
"if",
"else",
"for",
"while",
"do",
"switch",
"case",
"default",
"break",
"continue",
"return",
"goto",
"struct",
"union",
"enum",
"typedef",
"sizeof",
"static",
"extern",
"const",
"volatile",
"inline",
"void",
"int",
"char",
"float",
"double",
"long",
"short",
"unsigned",
"signed",
"auto",
"register",
"class",
"public",
"private",
"protected",
"virtual",
"template",
"namespace",
"using",
"new",
"delete",
"try",
"catch",
"throw",
"nullptr",
"true",
"false",
"bool",
"include",
"define",
"ifdef",
"ifndef",
"endif",
"pragma",
],
"toml" | "yaml" => &["true", "false", "null", "yes", "no", "on", "off"],
_ => &[],
}
}
fn lang_types(lang: &str) -> &'static [&'static str] {
match lang {
"rust" => &[
"String",
"Vec",
"Option",
"Result",
"Box",
"Rc",
"Arc",
"HashMap",
"HashSet",
"BTreeMap",
"BTreeSet",
"VecDeque",
"LinkedList",
"BinaryHeap",
"Cell",
"RefCell",
"Mutex",
"RwLock",
"Cow",
"Pin",
"PhantomData",
"i8",
"i16",
"i32",
"i64",
"i128",
"isize",
"u8",
"u16",
"u32",
"u64",
"u128",
"usize",
"f32",
"f64",
"bool",
"char",
"str",
"Self",
],
"go" => &[
"int",
"int8",
"int16",
"int32",
"int64",
"uint",
"uint8",
"uint16",
"uint32",
"uint64",
"uintptr",
"float32",
"float64",
"complex64",
"complex128",
"string",
"bool",
"byte",
"rune",
"error",
],
"c" => &[
"size_t",
"ssize_t",
"ptrdiff_t",
"intptr_t",
"uintptr_t",
"int8_t",
"int16_t",
"int32_t",
"int64_t",
"uint8_t",
"uint16_t",
"uint32_t",
"uint64_t",
"FILE",
"string",
"vector",
"map",
"set",
"pair",
"tuple",
"shared_ptr",
"unique_ptr",
],
_ => &[],
}
}
fn comment_prefix(lang: &str) -> &'static str {
match lang {
"python" | "shell" | "yaml" | "toml" => "#",
"c" | "rust" | "js" | "go" => "//",
_ => "//",
}
}
pub fn highlight_code_line(lang: &str, line: &str) -> String {
let norm = match normalize_lang(lang) {
Some(n) => n,
None => return format!("{DIM}{line}{RESET}"),
};
let cp = comment_prefix(norm);
let trimmed = line.trim_start();
if trimmed.starts_with(cp) {
return format!("{DIM}{line}{RESET}");
}
if norm == "json" {
return highlight_json_line(line);
}
if norm == "yaml" {
return highlight_yaml_line(line);
}
if norm == "toml" {
return highlight_toml_line(line);
}
let keywords = lang_keywords(norm);
let types = lang_types(norm);
let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let mut result = String::with_capacity(line.len() + 64);
let mut i = 0;
while i < len {
let ch = chars[i];
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' && cp == "//" {
let rest: String = chars[i..].iter().collect();
result.push_str(&format!("{DIM}{rest}{RESET}"));
break;
}
if ch == '#' && cp == "#" {
let rest: String = chars[i..].iter().collect();
result.push_str(&format!("{DIM}{rest}{RESET}"));
break;
}
if ch == '"' || ch == '\'' {
let quote = ch;
let mut s = String::new();
s.push(ch);
i += 1;
while i < len {
let c = chars[i];
s.push(c);
i += 1;
if c == '\\' && i < len {
s.push(chars[i]);
i += 1;
} else if c == quote {
break;
}
}
result.push_str(&format!("{GREEN}{s}{RESET}"));
continue;
}
if ch.is_ascii_digit()
&& (i == 0 || !chars[i - 1].is_ascii_alphanumeric() && chars[i - 1] != '_')
{
let mut num = String::new();
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.' || chars[i] == '_') {
num.push(chars[i]);
i += 1;
}
if i < len && (chars[i].is_ascii_alphabetic() || chars[i] == '_') {
result.push_str(&num);
} else {
result.push_str(&format!("{YELLOW}{num}{RESET}"));
}
continue;
}
if ch.is_ascii_alphabetic() || ch == '_' {
let mut word = String::new();
let start = i;
while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
word.push(chars[i]);
i += 1;
}
let before_ok = start == 0
|| (!chars[start - 1].is_ascii_alphanumeric() && chars[start - 1] != '_');
let after_ok = i >= len || (!chars[i].is_ascii_alphanumeric() && chars[i] != '_');
if before_ok && after_ok {
if keywords.contains(&word.as_str()) {
result.push_str(&format!("{BOLD_CYAN}{word}{RESET}"));
} else if types.contains(&word.as_str()) {
result.push_str(&format!("{MAGENTA}{word}{RESET}"));
} else {
result.push_str(&word);
}
} else {
result.push_str(&word);
}
continue;
}
result.push(ch);
i += 1;
}
result
}
fn highlight_json_line(line: &str) -> String {
let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let mut result = String::with_capacity(line.len() + 64);
let mut i = 0;
let mut expecting_value = false;
while i < len {
let ch = chars[i];
if ch == '"' {
let mut s = String::new();
s.push(ch);
i += 1;
while i < len {
let c = chars[i];
s.push(c);
i += 1;
if c == '\\' && i < len {
s.push(chars[i]);
i += 1;
} else if c == '"' {
break;
}
}
let rest_trimmed: String = chars[i..].iter().collect();
if !expecting_value && rest_trimmed.trim_start().starts_with(':') {
result.push_str(&format!("{CYAN}{s}{RESET}"));
} else {
result.push_str(&format!("{GREEN}{s}{RESET}"));
}
continue;
}
if ch == ':' {
expecting_value = true;
result.push(ch);
i += 1;
continue;
}
if ch == ',' || ch == '{' || ch == '[' {
expecting_value = false;
result.push(ch);
i += 1;
continue;
}
if ch.is_ascii_digit() || (ch == '-' && i + 1 < len && chars[i + 1].is_ascii_digit()) {
let mut num = String::new();
num.push(ch);
i += 1;
while i < len
&& (chars[i].is_ascii_digit()
|| chars[i] == '.'
|| chars[i] == 'e'
|| chars[i] == 'E'
|| chars[i] == '+'
|| chars[i] == '-')
{
num.push(chars[i]);
i += 1;
}
result.push_str(&format!("{YELLOW}{num}{RESET}"));
continue;
}
if ch.is_ascii_alphabetic() {
let mut word = String::new();
while i < len && chars[i].is_ascii_alphabetic() {
word.push(chars[i]);
i += 1;
}
match word.as_str() {
"true" | "false" | "null" => {
result.push_str(&format!("{BOLD_CYAN}{word}{RESET}"));
}
_ => result.push_str(&word),
}
continue;
}
result.push(ch);
i += 1;
}
result
}
fn highlight_yaml_line(line: &str) -> String {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
return format!("{DIM}{line}{RESET}");
}
if trimmed.starts_with("---") || trimmed.starts_with("...") {
return format!("{DIM}{line}{RESET}");
}
if let Some(colon_pos) = trimmed.find(':') {
let key_part = &trimmed[..colon_pos];
if !key_part.contains(' ') || key_part.starts_with("- ") || key_part.starts_with('-') {
let indent = &line[..line.len() - trimmed.len()];
let value_part = &trimmed[colon_pos + 1..];
let value_highlighted = highlight_yaml_value(value_part);
return format!("{indent}{BOLD_YELLOW}{key_part}{RESET}:{value_highlighted}");
}
}
if let Some(rest) = trimmed.strip_prefix("- ") {
let indent = &line[..line.len() - trimmed.len()];
return format!("{indent}- {}", highlight_yaml_value(rest));
}
line.to_string()
}
fn highlight_yaml_value(value: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return value.to_string();
}
if let Some(comment_pos) = trimmed.find(" #") {
let before = &trimmed[..comment_pos];
let after = &trimmed[comment_pos..];
return format!(" {}{DIM}{after}{RESET}", highlight_yaml_value_inner(before));
}
format!(" {}", highlight_yaml_value_inner(trimmed))
}
fn highlight_yaml_value_inner(value: &str) -> String {
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
return format!("{GREEN}{value}{RESET}");
}
match value {
"true" | "false" | "yes" | "no" | "on" | "off" | "null" | "~" => {
return format!("{BOLD_CYAN}{value}{RESET}");
}
_ => {}
}
if value.parse::<f64>().is_ok() {
return format!("{YELLOW}{value}{RESET}");
}
value.to_string()
}
fn highlight_toml_line(line: &str) -> String {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
return format!("{DIM}{line}{RESET}");
}
if trimmed.starts_with('[') {
return format!("{BOLD}{CYAN}{line}{RESET}");
}
if let Some(eq_pos) = trimmed.find('=') {
let key_part = trimmed[..eq_pos].trim();
let value_part = trimmed[eq_pos + 1..].trim();
let indent = &line[..line.len() - trimmed.len()];
let value_highlighted = highlight_toml_value(value_part);
return format!("{indent}{BOLD_YELLOW}{key_part}{RESET} = {value_highlighted}");
}
line.to_string()
}
fn highlight_toml_value(value: &str) -> String {
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
return format!("{GREEN}{value}{RESET}");
}
match value {
"true" | "false" => return format!("{BOLD_CYAN}{value}{RESET}"),
_ => {}
}
if value.parse::<f64>().is_ok() {
return format!("{YELLOW}{value}{RESET}");
}
value.to_string()
}
fn model_pricing(model: &str) -> Option<(f64, f64, f64, f64)> {
let model = model
.strip_prefix("anthropic/")
.or_else(|| model.strip_prefix("openai/"))
.or_else(|| model.strip_prefix("google/"))
.or_else(|| model.strip_prefix("deepseek/"))
.or_else(|| model.strip_prefix("mistralai/"))
.or_else(|| model.strip_prefix("x-ai/"))
.or_else(|| model.strip_prefix("meta-llama/"))
.unwrap_or(model);
if model.contains("opus") {
if model.contains("4-6")
|| model.contains("4-5")
|| model.contains("4.6")
|| model.contains("4.5")
{
return Some((5.0, 6.25, 0.50, 25.0));
} else {
return Some((15.0, 18.75, 1.50, 75.0));
}
}
if model.contains("sonnet") {
return Some((3.0, 3.75, 0.30, 15.0));
}
if model.contains("haiku") {
if model.contains("4-5") || model.contains("4.5") {
return Some((1.0, 1.25, 0.10, 5.0));
} else {
return Some((0.80, 1.0, 0.08, 4.0));
}
}
if model.starts_with("gpt-4.1") {
if model.contains("mini") {
return Some((0.40, 0.0, 0.0, 1.60)); } else if model.contains("nano") {
return Some((0.10, 0.0, 0.0, 0.40)); } else {
return Some((2.00, 0.0, 0.0, 8.00)); }
}
if model.starts_with("gpt-4o") {
if model.contains("mini") {
return Some((0.15, 0.0, 0.0, 0.60)); } else {
return Some((2.50, 0.0, 0.0, 10.00)); }
}
if model.starts_with("o4-mini") {
return Some((1.10, 0.0, 0.0, 4.40));
}
if model.starts_with("o3-mini") {
return Some((1.10, 0.0, 0.0, 4.40));
}
if model == "o3" {
return Some((2.00, 0.0, 0.0, 8.00));
}
if model.contains("gemini-2.5-pro") {
return Some((1.25, 0.0, 0.0, 10.00));
}
if model.contains("gemini-2.5-flash") {
return Some((0.15, 0.0, 0.0, 0.60));
}
if model.contains("gemini-2.0-flash") {
return Some((0.10, 0.0, 0.0, 0.40));
}
if model.contains("deepseek-chat") || model.contains("deepseek-v3") {
return Some((0.27, 0.0, 0.0, 1.10));
}
if model.contains("deepseek-reasoner") || model.contains("deepseek-r1") {
return Some((0.55, 0.0, 0.0, 2.19));
}
if model.contains("mistral-large") {
return Some((2.00, 0.0, 0.0, 6.00));
}
if model.contains("mistral-small") || model.contains("mistral-latest") {
return Some((0.10, 0.0, 0.0, 0.30));
}
if model.contains("codestral") {
return Some((0.30, 0.0, 0.0, 0.90));
}
if model.contains("grok-3") {
if model.contains("mini") {
return Some((0.30, 0.0, 0.0, 0.50));
} else {
return Some((3.00, 0.0, 0.0, 15.00));
}
}
if model.contains("grok-2") {
return Some((2.00, 0.0, 0.0, 10.00));
}
if model.contains("glm-4-plus") || model.contains("glm-4.7") {
return Some((0.70, 0.0, 0.0, 0.70));
}
if model.contains("glm-4-air") || model.contains("glm-4.5-air") {
return Some((0.07, 0.0, 0.0, 0.07));
}
if model.contains("glm-4-flash") || model.contains("glm-4.5-flash") {
return Some((0.01, 0.0, 0.0, 0.01));
}
if model.contains("glm-4-long") {
return Some((0.14, 0.0, 0.0, 0.14));
}
if model.contains("glm-5") {
return Some((0.70, 0.0, 0.0, 0.70));
}
if model.contains("llama-3.3-70b") || model.contains("llama3-70b") {
return Some((0.59, 0.0, 0.0, 0.79));
}
if model.contains("llama-3.1-8b") || model.contains("llama3-8b") {
return Some((0.05, 0.0, 0.0, 0.08));
}
if model.contains("mixtral-8x7b") {
return Some((0.24, 0.0, 0.0, 0.24));
}
if model.contains("gemma2-9b") {
return Some((0.20, 0.0, 0.0, 0.20));
}
None
}
pub fn estimate_cost(usage: &yoagent::Usage, model: &str) -> Option<f64> {
let (input_cost, cw_cost, cr_cost, output_cost) = cost_breakdown(usage, model)?;
Some(input_cost + cw_cost + cr_cost + output_cost)
}
pub fn cost_breakdown(usage: &yoagent::Usage, model: &str) -> Option<(f64, f64, f64, f64)> {
let (input_per_m, cache_write_per_m, cache_read_per_m, output_per_m) = model_pricing(model)?;
let input_cost = usage.input as f64 * input_per_m / 1_000_000.0;
let cache_write_cost = usage.cache_write as f64 * cache_write_per_m / 1_000_000.0;
let cache_read_cost = usage.cache_read as f64 * cache_read_per_m / 1_000_000.0;
let output_cost = usage.output as f64 * output_per_m / 1_000_000.0;
Some((input_cost, cache_write_cost, cache_read_cost, output_cost))
}
pub fn format_cost(cost: f64) -> String {
if cost < 0.01 {
format!("${:.4}", cost)
} else if cost < 1.0 {
format!("${:.3}", cost)
} else {
format!("${:.2}", cost)
}
}
pub fn format_duration(d: std::time::Duration) -> String {
let ms = d.as_millis();
if ms < 1000 {
format!("{ms}ms")
} else if ms < 60_000 {
format!("{:.1}s", ms as f64 / 1000.0)
} else {
let mins = ms / 60_000;
let secs = (ms % 60_000) / 1000;
format!("{mins}m {secs}s")
}
}
pub fn format_token_count(count: u64) -> String {
if count < 1000 {
format!("{count}")
} else if count < 1_000_000 {
format!("{:.1}k", count as f64 / 1000.0)
} else {
format!("{:.1}M", count as f64 / 1_000_000.0)
}
}
pub fn context_bar(used: u64, max: u64) -> String {
let pct = if max == 0 {
0.0
} else {
(used as f64 / max as f64).min(1.0)
};
let width = 20;
let filled = (pct * width as f64).round() as usize;
let empty = width - filled;
let bar: String = "█".repeat(filled) + &"░".repeat(empty);
format!("{bar} {:.0}%", pct * 100.0)
}
pub fn pluralize<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
if count == 1 {
singular
} else {
plural
}
}
pub fn truncate_with_ellipsis(s: &str, max: usize) -> String {
match s.char_indices().nth(max) {
Some((idx, _)) => format!("{}…", &s[..idx]),
None => s.to_string(),
}
}
pub const TOOL_OUTPUT_MAX_CHARS: usize = 30_000;
const TRUNCATION_HEAD_LINES: usize = 100;
const TRUNCATION_TAIL_LINES: usize = 50;
pub fn truncate_tool_output(output: &str, max_chars: usize) -> String {
if output.len() <= max_chars {
return output.to_string();
}
let lines: Vec<&str> = output.lines().collect();
let total_lines = lines.len();
if total_lines <= TRUNCATION_HEAD_LINES + TRUNCATION_TAIL_LINES {
return output.to_string();
}
let head = &lines[..TRUNCATION_HEAD_LINES];
let tail = &lines[total_lines - TRUNCATION_TAIL_LINES..];
let omitted = total_lines - TRUNCATION_HEAD_LINES - TRUNCATION_TAIL_LINES;
let mut result = String::with_capacity(max_chars);
for line in head {
result.push_str(line);
result.push('\n');
}
result.push_str(&format!(
"\n[... truncated {omitted} {} ...]\n\n",
pluralize(omitted, "line", "lines")
));
for (i, line) in tail.iter().enumerate() {
result.push_str(line);
if i < tail.len() - 1 {
result.push('\n');
}
}
result
}
#[cfg(test)]
pub fn truncate(s: &str, max: usize) -> &str {
match s.char_indices().nth(max) {
Some((idx, _)) => &s[..idx],
None => s,
}
}
const MAX_DIFF_LINES: usize = 20;
pub fn format_edit_diff(old_text: &str, new_text: &str) -> String {
let mut lines: Vec<String> = Vec::new();
if !old_text.is_empty() {
for line in old_text.lines() {
lines.push(format!("{RED} - {line}{RESET}"));
}
}
if !new_text.is_empty() {
for line in new_text.lines() {
lines.push(format!("{GREEN} + {line}{RESET}"));
}
}
if lines.is_empty() {
return String::new();
}
if lines.len() > MAX_DIFF_LINES {
let remaining = lines.len() - MAX_DIFF_LINES;
lines.truncate(MAX_DIFF_LINES);
lines.push(format!("{DIM} ... ({remaining} more lines){RESET}"));
}
lines.join("\n")
}
pub fn format_tool_summary(tool_name: &str, args: &serde_json::Value) -> String {
match tool_name {
"bash" => {
let cmd = args
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("...");
let line_count = cmd.lines().count();
let first_line = cmd.lines().next().unwrap_or("...");
if line_count > 1 {
format!(
"$ {} ({line_count} lines)",
truncate_with_ellipsis(first_line, 60)
)
} else {
format!("$ {}", truncate_with_ellipsis(cmd, 80))
}
}
"read_file" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("?");
let offset = args.get("offset").and_then(|v| v.as_u64());
let limit = args.get("limit").and_then(|v| v.as_u64());
match (offset, limit) {
(Some(off), Some(lim)) => {
format!("read {path}:{off}..{}", off + lim)
}
(Some(off), None) => {
format!("read {path}:{off}..")
}
(None, Some(lim)) => {
let word = pluralize(lim as usize, "line", "lines");
format!("read {path} ({lim} {word})")
}
(None, None) => {
format!("read {path}")
}
}
}
"write_file" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("?");
let line_info = args
.get("content")
.and_then(|v| v.as_str())
.map(|c| {
let count = c.lines().count();
let word = pluralize(count, "line", "lines");
format!(" ({count} {word})")
})
.unwrap_or_default();
format!("write {path}{line_info}")
}
"edit_file" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("?");
let old_text = args.get("old_text").and_then(|v| v.as_str());
let new_text = args.get("new_text").and_then(|v| v.as_str());
match (old_text, new_text) {
(Some(old), Some(new)) => {
let old_lines = old.lines().count();
let new_lines = new.lines().count();
format!("edit {path} ({old_lines} → {new_lines} lines)")
}
_ => format!("edit {path}"),
}
}
"list_files" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let pattern = args.get("pattern").and_then(|v| v.as_str());
match pattern {
Some(pat) => format!("ls {path} ({pat})"),
None => format!("ls {path}"),
}
}
"search" => {
let pat = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("?");
let search_path = args.get("path").and_then(|v| v.as_str());
let include = args.get("include").and_then(|v| v.as_str());
let mut summary = format!("search '{}'", truncate_with_ellipsis(pat, 60));
if let Some(p) = search_path {
summary.push_str(&format!(" in {p}"));
}
if let Some(inc) = include {
summary.push_str(&format!(" ({inc})"));
}
summary
}
_ => tool_name.to_string(),
}
}
pub fn print_usage(
usage: &yoagent::Usage,
total: &yoagent::Usage,
model: &str,
elapsed: std::time::Duration,
) {
if usage.input > 0 || usage.output > 0 {
let cache_info = if usage.cache_read > 0 || usage.cache_write > 0 {
format!(
" [cache: {} read, {} write]",
usage.cache_read, usage.cache_write
)
} else {
String::new()
};
let cost_info = estimate_cost(usage, model)
.map(|c| format!(" cost: {}", format_cost(c)))
.unwrap_or_default();
let total_cost_info = estimate_cost(total, model)
.map(|c| format!(" total: {}", format_cost(c)))
.unwrap_or_default();
let elapsed_str = format_duration(elapsed);
println!(
"\n{DIM} tokens: {} in / {} out{cache_info} (session: {} in / {} out){cost_info}{total_cost_info} ⏱ {elapsed_str}{RESET}",
usage.input, usage.output, total.input, total.output
);
}
}
pub struct MarkdownRenderer {
in_code_block: bool,
code_lang: Option<String>,
line_buffer: String,
line_start: bool,
}
const LINE_START_RESOLVE_THRESHOLD: usize = 4;
impl MarkdownRenderer {
pub fn new() -> Self {
Self {
in_code_block: false,
code_lang: None,
line_buffer: String::new(),
line_start: true,
}
}
pub fn render_delta(&mut self, delta: &str) -> String {
let mut output = String::new();
if !self.line_start && !self.in_code_block {
if let Some(newline_pos) = delta.find('\n') {
let mid_line_part = &delta[..newline_pos];
if !mid_line_part.is_empty() {
output.push_str(&self.render_inline(mid_line_part));
}
output.push('\n');
self.line_start = true;
let rest = &delta[newline_pos + 1..];
if !rest.is_empty() {
output.push_str(&self.render_delta_buffered(rest));
}
} else {
output.push_str(&self.render_inline(delta));
}
return output;
}
output.push_str(&self.render_delta_buffered(delta));
output
}
fn render_delta_buffered(&mut self, delta: &str) -> String {
let mut output = String::new();
self.line_buffer.push_str(delta);
while let Some(newline_pos) = self.line_buffer.find('\n') {
let line = self.line_buffer[..newline_pos].to_string();
self.line_buffer = self.line_buffer[newline_pos + 1..].to_string();
output.push_str(&self.render_line(&line));
output.push('\n');
self.line_start = true;
}
if self.line_start && !self.line_buffer.is_empty() && !self.in_code_block {
let trimmed = self.line_buffer.trim_start();
let could_be_fence =
trimmed.is_empty() || trimmed.starts_with('`') || "`".starts_with(trimmed);
let could_be_header =
trimmed.is_empty() || trimmed.starts_with('#') || "#".starts_with(trimmed);
if trimmed.len() >= LINE_START_RESOLVE_THRESHOLD && !could_be_fence && !could_be_header
{
let buf = std::mem::take(&mut self.line_buffer);
output.push_str(&self.render_inline(&buf));
self.line_start = false;
} else if !could_be_fence && !could_be_header {
let buf = std::mem::take(&mut self.line_buffer);
output.push_str(&self.render_inline(&buf));
self.line_start = false;
}
}
output
}
pub fn flush(&mut self) -> String {
if self.line_buffer.is_empty() {
return String::new();
}
let line = std::mem::take(&mut self.line_buffer);
self.line_start = true;
self.render_line(&line)
}
fn render_line(&mut self, line: &str) -> String {
let trimmed = line.trim();
self.line_start = true;
if let Some(after_fence) = trimmed.strip_prefix("```") {
if self.in_code_block {
self.in_code_block = false;
self.code_lang = None;
return format!("{DIM}{line}{RESET}");
} else {
self.in_code_block = true;
let lang = after_fence.trim();
self.code_lang = if lang.is_empty() {
None
} else {
Some(lang.to_string())
};
return format!("{DIM}{line}{RESET}");
}
}
if self.in_code_block {
return if let Some(ref lang) = self.code_lang {
highlight_code_line(lang, line)
} else {
format!("{DIM}{line}{RESET}")
};
}
if trimmed.starts_with('#') {
return format!("{BOLD}{CYAN}{line}{RESET}");
}
self.render_inline(line)
}
fn render_inline(&self, line: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if i + 1 < len && chars[i] == '*' && chars[i + 1] == '*' {
if let Some(close) = self.find_double_star(&chars, i + 2) {
let inner: String = chars[i + 2..close].iter().collect();
result.push_str(&format!("{BOLD}{inner}{RESET}"));
i = close + 2;
continue;
}
}
if chars[i] == '`' {
if let Some(close) = self.find_backtick(&chars, i + 1) {
let inner: String = chars[i + 1..close].iter().collect();
result.push_str(&format!("{CYAN}{inner}{RESET}"));
i = close + 1;
continue;
}
}
result.push(chars[i]);
i += 1;
}
result
}
fn find_double_star(&self, chars: &[char], from: usize) -> Option<usize> {
let len = chars.len();
let mut j = from;
while j + 1 < len {
if chars[j] == '*' && chars[j + 1] == '*' {
return Some(j);
}
j += 1;
}
None
}
fn find_backtick(&self, chars: &[char], from: usize) -> Option<usize> {
(from..chars.len()).find(|&j| chars[j] == '`')
}
}
impl Default for MarkdownRenderer {
fn default() -> Self {
Self::new()
}
}
pub const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
pub fn spinner_frame(tick: usize) -> char {
SPINNER_FRAMES[tick % SPINNER_FRAMES.len()]
}
pub struct Spinner {
cancel: tokio::sync::watch::Sender<bool>,
handle: Option<tokio::task::JoinHandle<()>>,
}
impl Spinner {
pub fn start() -> Self {
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
let handle = tokio::spawn(async move {
let mut tick: usize = 0;
loop {
if *cancel_rx.borrow() {
eprint!("\r\x1b[K");
break;
}
let frame = spinner_frame(tick);
eprint!("\r{DIM} {frame} thinking...{RESET}");
tick = tick.wrapping_add(1);
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
_ = cancel_rx.changed() => {
eprint!("\r\x1b[K");
break;
}
}
}
});
Self {
cancel: cancel_tx,
handle: Some(handle),
}
}
pub fn stop(mut self) {
let _ = self.cancel.send(true);
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}
impl Drop for Spinner {
fn drop(&mut self) {
let _ = self.cancel.send(true);
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn test_truncate_exact_length() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn test_truncate_long_string() {
assert_eq!(truncate("hello world", 5), "hello");
}
#[test]
fn test_truncate_unicode() {
assert_eq!(truncate("héllo wörld", 5), "héllo");
}
#[test]
fn test_truncate_empty() {
assert_eq!(truncate("", 5), "");
}
#[test]
fn test_truncate_adds_ellipsis() {
assert_eq!(truncate_with_ellipsis("hello world", 5), "hello…");
assert_eq!(truncate_with_ellipsis("hi", 5), "hi");
assert_eq!(truncate_with_ellipsis("hello", 5), "hello");
}
#[test]
fn test_format_token_count() {
assert_eq!(format_token_count(0), "0");
assert_eq!(format_token_count(999), "999");
assert_eq!(format_token_count(1000), "1.0k");
assert_eq!(format_token_count(1500), "1.5k");
assert_eq!(format_token_count(10000), "10.0k");
assert_eq!(format_token_count(150000), "150.0k");
assert_eq!(format_token_count(1000000), "1.0M");
assert_eq!(format_token_count(2500000), "2.5M");
}
#[test]
fn test_context_bar() {
let bar = context_bar(50000, 200000);
assert!(bar.contains('█'));
assert!(bar.contains("25%"));
let bar_empty = context_bar(0, 200000);
assert!(bar_empty.contains("0%"));
let bar_full = context_bar(200000, 200000);
assert!(bar_full.contains("100%"));
}
#[test]
fn test_format_cost() {
assert_eq!(format_cost(0.0001), "$0.0001");
assert_eq!(format_cost(0.0042), "$0.0042");
assert_eq!(format_cost(0.05), "$0.050");
assert_eq!(format_cost(0.123), "$0.123");
assert_eq!(format_cost(1.5), "$1.50");
assert_eq!(format_cost(12.345), "$12.35");
}
#[test]
fn test_format_duration_ms() {
assert_eq!(
format_duration(std::time::Duration::from_millis(50)),
"50ms"
);
assert_eq!(
format_duration(std::time::Duration::from_millis(999)),
"999ms"
);
}
#[test]
fn test_format_duration_seconds() {
assert_eq!(
format_duration(std::time::Duration::from_millis(1000)),
"1.0s"
);
assert_eq!(
format_duration(std::time::Duration::from_millis(1500)),
"1.5s"
);
assert_eq!(
format_duration(std::time::Duration::from_millis(30000)),
"30.0s"
);
}
#[test]
fn test_format_duration_minutes() {
assert_eq!(
format_duration(std::time::Duration::from_millis(60000)),
"1m 0s"
);
assert_eq!(
format_duration(std::time::Duration::from_millis(90000)),
"1m 30s"
);
assert_eq!(
format_duration(std::time::Duration::from_millis(125000)),
"2m 5s"
);
}
#[test]
fn test_estimate_cost_opus() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "claude-opus-4-6").unwrap();
assert!((cost - 7.5).abs() < 0.001);
}
#[test]
fn test_estimate_cost_sonnet() {
let usage = yoagent::Usage {
input: 500_000,
output: 50_000,
cache_read: 200_000,
cache_write: 100_000,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "claude-sonnet-4-6").unwrap();
assert!((cost - 2.685).abs() < 0.001);
}
#[test]
fn test_estimate_cost_haiku() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 500_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "claude-haiku-4-5").unwrap();
assert!((cost - 3.5).abs() < 0.001);
}
#[test]
fn test_estimate_cost_unknown_model() {
let usage = yoagent::Usage {
input: 1000,
output: 1000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
assert!(estimate_cost(&usage, "unknown-model-xyz").is_none());
}
#[test]
fn test_cost_breakdown_opus() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 500_000,
cache_write: 200_000,
total_tokens: 0,
};
let (input, cw, cr, output) = cost_breakdown(&usage, "claude-opus-4-6").unwrap();
assert!((input - 5.0).abs() < 0.001);
assert!((output - 2.5).abs() < 0.001);
assert!((cr - 0.25).abs() < 0.001);
assert!((cw - 1.25).abs() < 0.001);
let total = input + cw + cr + output;
let expected = estimate_cost(&usage, "claude-opus-4-6").unwrap();
assert!((total - expected).abs() < 0.001);
}
#[test]
fn test_cost_breakdown_unknown_model() {
let usage = yoagent::Usage {
input: 1000,
output: 1000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
assert!(cost_breakdown(&usage, "unknown-model-xyz").is_none());
}
#[test]
fn test_estimate_cost_gpt4o() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gpt-4o").unwrap();
assert!((cost - 3.5).abs() < 0.001, "gpt-4o cost: {cost}");
}
#[test]
fn test_estimate_cost_gpt4o_mini() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gpt-4o-mini").unwrap();
assert!((cost - 0.75).abs() < 0.001, "gpt-4o-mini cost: {cost}");
}
#[test]
fn test_estimate_cost_gpt41() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gpt-4.1").unwrap();
assert!((cost - 2.8).abs() < 0.001, "gpt-4.1 cost: {cost}");
}
#[test]
fn test_estimate_cost_gpt41_mini() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gpt-4.1-mini").unwrap();
assert!((cost - 2.0).abs() < 0.001, "gpt-4.1-mini cost: {cost}");
}
#[test]
fn test_estimate_cost_o3() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "o3").unwrap();
assert!((cost - 2.8).abs() < 0.001, "o3 cost: {cost}");
}
#[test]
fn test_estimate_cost_o4_mini() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "o4-mini").unwrap();
assert!((cost - 1.54).abs() < 0.001, "o4-mini cost: {cost}");
}
#[test]
fn test_estimate_cost_gemini_25_pro() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gemini-2.5-pro").unwrap();
assert!((cost - 2.25).abs() < 0.001, "gemini-2.5-pro cost: {cost}");
}
#[test]
fn test_estimate_cost_gemini_25_flash() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gemini-2.5-flash").unwrap();
assert!((cost - 0.75).abs() < 0.001, "gemini-2.5-flash cost: {cost}");
}
#[test]
fn test_estimate_cost_gemini_20_flash() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "gemini-2.0-flash").unwrap();
assert!((cost - 0.50).abs() < 0.001, "gemini-2.0-flash cost: {cost}");
}
#[test]
fn test_estimate_cost_deepseek_chat() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "deepseek-chat").unwrap();
assert!((cost - 1.37).abs() < 0.001, "deepseek-chat cost: {cost}");
}
#[test]
fn test_estimate_cost_deepseek_reasoner() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "deepseek-reasoner").unwrap();
assert!(
(cost - 2.74).abs() < 0.001,
"deepseek-reasoner cost: {cost}"
);
}
#[test]
fn test_estimate_cost_mistral_large() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "mistral-large-latest").unwrap();
assert!((cost - 2.6).abs() < 0.001, "mistral-large cost: {cost}");
}
#[test]
fn test_estimate_cost_mistral_small() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "mistral-small-latest").unwrap();
assert!((cost - 0.40).abs() < 0.001, "mistral-small cost: {cost}");
}
#[test]
fn test_estimate_cost_codestral() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "codestral-latest").unwrap();
assert!((cost - 1.20).abs() < 0.001, "codestral cost: {cost}");
}
#[test]
fn test_estimate_cost_grok3() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "grok-3").unwrap();
assert!((cost - 4.5).abs() < 0.001, "grok-3 cost: {cost}");
}
#[test]
fn test_estimate_cost_grok3_mini() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "grok-3-mini").unwrap();
assert!((cost - 0.80).abs() < 0.001, "grok-3-mini cost: {cost}");
}
#[test]
fn test_estimate_cost_groq_llama70b() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "llama-3.3-70b-versatile").unwrap();
assert!((cost - 1.38).abs() < 0.001, "llama-3.3-70b cost: {cost}");
}
#[test]
fn test_estimate_cost_groq_llama8b() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "llama-3.1-8b-instant").unwrap();
assert!((cost - 0.13).abs() < 0.001, "llama-3.1-8b cost: {cost}");
}
#[test]
fn test_estimate_cost_glm4_plus() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "glm-4-plus").unwrap();
assert!((cost - 1.40).abs() < 0.001, "glm-4-plus cost: {cost}");
}
#[test]
fn test_estimate_cost_glm4_air() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "glm-4-air").unwrap();
assert!((cost - 0.14).abs() < 0.001, "glm-4-air cost: {cost}");
}
#[test]
fn test_estimate_cost_glm4_flash() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "glm-4-flash").unwrap();
assert!((cost - 0.02).abs() < 0.001, "glm-4-flash cost: {cost}");
}
#[test]
fn test_estimate_cost_glm5() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "glm-5").unwrap();
assert!((cost - 1.40).abs() < 0.001, "glm-5 cost: {cost}");
}
#[test]
fn test_estimate_cost_openrouter_anthropic_prefix() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "anthropic/claude-sonnet-4-20250514").unwrap();
let direct_cost = estimate_cost(&usage, "claude-sonnet-4-20250514").unwrap();
assert!(
(cost - direct_cost).abs() < 0.001,
"OpenRouter prefix should resolve to same pricing"
);
}
#[test]
fn test_estimate_cost_openrouter_openai_prefix() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "openai/gpt-4o").unwrap();
let direct_cost = estimate_cost(&usage, "gpt-4o").unwrap();
assert!(
(cost - direct_cost).abs() < 0.001,
"OpenRouter openai/ prefix should resolve to same pricing"
);
}
#[test]
fn test_estimate_cost_openrouter_google_prefix() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 0,
cache_write: 0,
total_tokens: 0,
};
let cost = estimate_cost(&usage, "google/gemini-2.0-flash").unwrap();
let direct_cost = estimate_cost(&usage, "gemini-2.0-flash").unwrap();
assert!(
(cost - direct_cost).abs() < 0.001,
"OpenRouter google/ prefix should resolve to same pricing"
);
}
#[test]
fn test_non_anthropic_providers_zero_cache_costs() {
let usage = yoagent::Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
total_tokens: 0,
};
let (_, cw, cr, _) = cost_breakdown(&usage, "gpt-4o").unwrap();
assert!(
cw.abs() < 0.001 && cr.abs() < 0.001,
"Non-Anthropic models should have zero cache costs: cw={cw}, cr={cr}"
);
}
#[test]
fn test_format_tool_summary_bash() {
let args = serde_json::json!({"command": "echo hello"});
assert_eq!(format_tool_summary("bash", &args), "$ echo hello");
}
#[test]
fn test_format_tool_summary_bash_long_command() {
let long_cmd = "a".repeat(100);
let args = serde_json::json!({"command": long_cmd});
let result = format_tool_summary("bash", &args);
assert!(result.starts_with("$ "));
assert!(result.ends_with('…'));
assert!(result.len() < 100);
}
#[test]
fn test_format_tool_summary_read_file() {
let args = serde_json::json!({"path": "src/main.rs"});
assert_eq!(format_tool_summary("read_file", &args), "read src/main.rs");
}
#[test]
fn test_format_tool_summary_write_file() {
let args = serde_json::json!({"path": "out.txt"});
assert_eq!(format_tool_summary("write_file", &args), "write out.txt");
}
#[test]
fn test_format_tool_summary_edit_file() {
let args = serde_json::json!({"path": "foo.rs"});
assert_eq!(format_tool_summary("edit_file", &args), "edit foo.rs");
}
#[test]
fn test_format_tool_summary_list_files() {
let args = serde_json::json!({"path": "src/"});
assert_eq!(format_tool_summary("list_files", &args), "ls src/");
}
#[test]
fn test_format_tool_summary_list_files_no_path() {
let args = serde_json::json!({});
assert_eq!(format_tool_summary("list_files", &args), "ls .");
}
#[test]
fn test_format_tool_summary_search() {
let args = serde_json::json!({"pattern": "TODO"});
assert_eq!(format_tool_summary("search", &args), "search 'TODO'");
}
#[test]
fn test_format_tool_summary_unknown_tool() {
let args = serde_json::json!({});
assert_eq!(format_tool_summary("custom_tool", &args), "custom_tool");
}
#[test]
fn test_color_struct_display_outputs_ansi() {
let c = Color("\x1b[1m");
let formatted = format!("{c}");
assert!(formatted == "\x1b[1m" || formatted.is_empty());
}
#[test]
fn test_color_struct_display_consistency() {
let result = format!("{BOLD}{DIM}{GREEN}{YELLOW}{CYAN}{RED}{RESET}");
assert!(result.contains('\x1b') || result.is_empty());
}
fn render_full(input: &str) -> String {
let mut r = MarkdownRenderer::new();
let mut out = r.render_delta(input);
out.push_str(&r.flush());
out
}
#[test]
fn test_md_code_block_detection() {
let input = "before\n```\ncode line\n```\nafter\n";
let out = render_full(input);
assert!(out.contains(&format!("{DIM}code line{RESET}")));
assert!(out.contains("before"));
assert!(out.contains("after"));
}
#[test]
fn test_md_code_block_with_language() {
let input = "```rust\nlet x = 1;\n```\n";
let mut r = MarkdownRenderer::new();
let out = r.render_delta(input);
let flushed = r.flush();
let full = format!("{out}{flushed}");
assert!(full.contains(&format!("{DIM}```rust{RESET}")));
assert!(full.contains(&format!("{BOLD_CYAN}let{RESET}")));
assert!(full.contains(&format!("{YELLOW}1{RESET}")));
}
#[test]
fn test_md_inline_code() {
let out = render_full("use `Option<T>` here\n");
assert!(out.contains(&format!("{CYAN}Option<T>{RESET}")));
}
#[test]
fn test_md_bold_text() {
let out = render_full("this is **important** stuff\n");
assert!(out.contains(&format!("{BOLD}important{RESET}")));
}
#[test]
fn test_md_header_rendering() {
let out = render_full("# Hello World\n");
assert!(out.contains(&format!("{BOLD}{CYAN}# Hello World{RESET}")));
}
#[test]
fn test_md_header_h2() {
let out = render_full("## Section Two\n");
assert!(out.contains(&format!("{BOLD}{CYAN}## Section Two{RESET}")));
}
#[test]
fn test_md_partial_delta_fence() {
let mut r = MarkdownRenderer::new();
let out1 = r.render_delta("``");
assert_eq!(out1, "");
let out2 = r.render_delta("`\n");
assert!(out2.contains(&format!("{DIM}```{RESET}")));
let out3 = r.render_delta("code here\n");
assert!(out3.contains(&format!("{DIM}code here{RESET}")));
let out4 = r.render_delta("```\n");
assert!(out4.contains(&format!("{DIM}```{RESET}")));
let out5 = r.render_delta("normal\n");
assert!(out5.contains("normal"));
assert!(!out5.contains(&format!("{DIM}")));
}
#[test]
fn test_md_empty_delta() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("");
assert_eq!(out, "");
let flushed = r.flush();
assert_eq!(flushed, "");
}
#[test]
fn test_md_multiple_code_blocks() {
let input = "text\n```\nblock1\n```\nmiddle\n```python\nblock2\n```\nend\n";
let out = render_full(input);
assert!(out.contains(&format!("{DIM}block1{RESET}")));
assert!(out.contains("middle"));
assert!(out.contains("block2"));
assert!(out.contains("end"));
}
#[test]
fn test_md_inline_code_inside_bold() {
let out = render_full("**bold** and `code`\n");
assert!(out.contains(&format!("{BOLD}bold{RESET}")));
assert!(out.contains(&format!("{CYAN}code{RESET}")));
}
#[test]
fn test_md_unmatched_backtick() {
let out = render_full("it's a `partial\n");
assert!(out.contains('`'));
assert!(out.contains("partial"));
}
#[test]
fn test_md_unmatched_bold() {
let out = render_full("star **power\n");
assert!(out.contains("**"));
assert!(out.contains("power"));
}
#[test]
fn test_md_flush_partial_line() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("no");
assert!(
out.contains("no"),
"Short non-fence/non-header text resolves immediately"
);
let out2 = r.render_delta(" newline here");
assert!(out2.contains(" newline here"));
}
#[test]
fn test_md_flush_with_inline_formatting() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("hello **world**");
let flushed = r.flush();
let total = format!("{out}{flushed}");
assert!(total.contains(&format!("{BOLD}world{RESET}")));
}
#[test]
fn test_md_default_trait() {
let r = MarkdownRenderer::default();
assert!(!r.in_code_block);
assert!(r.code_lang.is_none());
assert!(r.line_buffer.is_empty());
assert!(r.line_start);
}
#[test]
fn test_md_streaming_mid_line_immediate_output() {
let mut r = MarkdownRenderer::new();
let out1 = r.render_delta("Hello ");
assert!(
out1.contains("Hello "),
"Expected immediate output for non-fence/non-header text, got: '{out1}'"
);
let out2 = r.render_delta("world");
assert!(
out2.contains("world"),
"Mid-line delta should produce immediate output, got: '{out2}'"
);
let out3 = r.render_delta(" how");
assert!(
out3.contains(" how"),
"Mid-line delta should produce immediate output, got: '{out3}'"
);
}
#[test]
fn test_md_streaming_newline_resets_to_line_start() {
let mut r = MarkdownRenderer::new();
let _ = r.render_delta("Hello world");
let _ = r.render_delta("\n");
let out = r.render_delta("``");
assert_eq!(
out, "",
"Short ambiguous text at line start should be buffered"
);
}
#[test]
fn test_md_streaming_code_fence_detected_at_line_start() {
let mut r = MarkdownRenderer::new();
let out1 = r.render_delta("```\n");
assert!(out1.contains(&format!("{DIM}```{RESET}")));
assert!(r.in_code_block);
let out2 = r.render_delta("some code\n");
assert!(out2.contains(&format!("{DIM}some code{RESET}")));
let out3 = r.render_delta("```\n");
assert!(out3.contains(&format!("{DIM}```{RESET}")));
assert!(!r.in_code_block);
}
#[test]
fn test_md_streaming_header_detected_at_line_start() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("# My Header\n");
assert!(out.contains(&format!("{BOLD}{CYAN}# My Header{RESET}")));
}
#[test]
fn test_md_streaming_bold_mid_line() {
let mut r = MarkdownRenderer::new();
let out1 = r.render_delta("This is ");
assert!(out1.contains("This is "));
let out2 = r.render_delta("**important**");
assert!(
out2.contains(&format!("{BOLD}important{RESET}")),
"Bold formatting should work in mid-line streaming, got: '{out2}'"
);
}
#[test]
fn test_md_streaming_inline_code_mid_line() {
let mut r = MarkdownRenderer::new();
let out1 = r.render_delta("Use the ");
assert!(out1.contains("Use the "));
let out2 = r.render_delta("`Option`");
assert!(
out2.contains(&format!("{CYAN}Option{RESET}")),
"Inline code should work in mid-line streaming, got: '{out2}'"
);
}
#[test]
fn test_md_streaming_word_by_word_paragraph() {
let mut r = MarkdownRenderer::new();
let words = ["The ", "quick ", "brown ", "fox ", "jumps"];
let mut got_output = false;
for word in &words[..] {
let out = r.render_delta(word);
if !out.is_empty() {
got_output = true;
}
}
assert!(
got_output,
"Word-by-word streaming should produce output before newline"
);
let _flushed = r.flush();
let mut total = String::new();
let mut r2 = MarkdownRenderer::new();
for word in &words[..] {
total.push_str(&r2.render_delta(word));
}
total.push_str(&r2.flush());
assert!(total.contains("The "));
assert!(total.contains("fox "));
}
#[test]
fn test_md_streaming_line_start_buffer_short_text() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("#");
assert_eq!(out, "", "Single '#' at line start should be buffered");
let out2 = r.render_delta(" Title\n");
assert!(out2.contains(&format!("{BOLD}{CYAN}# Title{RESET}")));
}
#[test]
fn test_md_streaming_line_start_resolves_normal() {
let mut r = MarkdownRenderer::new();
let out = r.render_delta("Normal text");
assert!(
out.contains("Normal text"),
"Non-fence/non-header text should be output once resolved, got: '{out}'"
);
}
#[test]
fn test_md_streaming_existing_tests_still_pass() {
let out = render_full("Hello **world** and `code`\n");
assert!(out.contains("Hello "));
assert!(out.contains(&format!("{BOLD}world{RESET}")));
assert!(out.contains(&format!("{CYAN}code{RESET}")));
}
#[test]
fn test_md_streaming_in_code_block_immediate() {
let mut r = MarkdownRenderer::new();
let _ = r.render_delta("```rust\n");
assert!(r.in_code_block);
let out = r.render_delta("let x");
let flushed = r.flush();
let total = format!("{out}{flushed}");
assert!(
total.contains("let") || total.contains("x"),
"Code block content should eventually render, got: '{total}'"
);
}
#[test]
fn test_md_plain_text_unchanged() {
let out = render_full("just plain text\n");
assert!(out.contains("just plain text"));
}
#[test]
fn test_md_multiple_inline_codes_one_line() {
let out = render_full("use `foo` and `bar` here\n");
assert!(out.contains(&format!("{CYAN}foo{RESET}")));
assert!(out.contains(&format!("{CYAN}bar{RESET}")));
}
#[test]
fn test_md_code_block_preserves_content() {
let input = "```\nfn main() {\n println!(\"hello\");\n}\n```\n";
let out = render_full(input);
assert!(out.contains("fn main()"));
assert!(out.contains("println!"));
}
#[test]
fn test_highlight_rust_keywords() {
let out = highlight_code_line("rust", " let mut x = 42;");
assert!(out.contains(&format!("{BOLD_CYAN}let{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}mut{RESET}")));
assert!(out.contains(&format!("{YELLOW}42{RESET}")));
}
#[test]
fn test_highlight_rust_fn() {
let out = highlight_code_line("rust", "fn main() {");
assert!(out.contains(&format!("{BOLD_CYAN}fn{RESET}")));
assert!(out.contains("main"));
}
#[test]
fn test_highlight_rust_string() {
let out = highlight_code_line("rs", r#"let s = "hello world";"#);
assert!(out.contains(&format!("{GREEN}\"hello world\"{RESET}")));
}
#[test]
fn test_highlight_rust_comment() {
let out = highlight_code_line("rust", " // this is a comment");
assert!(out.contains(&format!("{DIM}")));
assert!(out.contains("this is a comment"));
}
#[test]
fn test_highlight_rust_full_line_comment() {
let out = highlight_code_line("rust", "// full line comment");
assert_eq!(out, format!("{DIM}// full line comment{RESET}"));
}
#[test]
fn test_highlight_python_keywords() {
let out = highlight_code_line("python", "def hello(self):");
assert!(out.contains(&format!("{BOLD_CYAN}def{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}self{RESET}")));
}
#[test]
fn test_highlight_python_comment() {
let out = highlight_code_line("py", "# a comment");
assert_eq!(out, format!("{DIM}# a comment{RESET}"));
}
#[test]
fn test_highlight_js_keywords() {
let out = highlight_code_line("javascript", "const x = async () => {");
assert!(out.contains(&format!("{BOLD_CYAN}const{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}async{RESET}")));
}
#[test]
fn test_highlight_ts_alias() {
let out = highlight_code_line("ts", "let y = 10;");
assert!(out.contains(&format!("{BOLD_CYAN}let{RESET}")));
assert!(out.contains(&format!("{YELLOW}10{RESET}")));
}
#[test]
fn test_highlight_go_keywords() {
let out = highlight_code_line("go", "func main() {");
assert!(out.contains(&format!("{BOLD_CYAN}func{RESET}")));
}
#[test]
fn test_highlight_shell_keywords() {
let out = highlight_code_line("bash", "if [ -f file ]; then");
assert!(out.contains(&format!("{BOLD_CYAN}if{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}then{RESET}")));
}
#[test]
fn test_highlight_shell_comment() {
let out = highlight_code_line("sh", "# shell comment");
assert_eq!(out, format!("{DIM}# shell comment{RESET}"));
}
#[test]
fn test_highlight_unknown_lang_falls_back_to_dim() {
let out = highlight_code_line("haskell", "main = putStrLn");
assert_eq!(out, format!("{DIM}main = putStrLn{RESET}"));
}
#[test]
fn test_highlight_empty_line() {
let out = highlight_code_line("rust", "");
assert_eq!(out, "");
}
#[test]
fn test_highlight_no_false_keyword_in_identifier() {
let out = highlight_code_line("rust", "let letter = 1;");
assert!(out.contains(&format!("{BOLD_CYAN}let{RESET}")));
assert!(out.contains("letter"));
let letter_highlighted = format!("{BOLD_CYAN}letter{RESET}");
assert!(!out.contains(&letter_highlighted));
}
#[test]
fn test_highlight_string_with_escape() {
let out = highlight_code_line("rust", r#"let s = "he\"llo";"#);
assert!(out.contains(&format!("{GREEN}")));
assert!(out.contains(&format!("{BOLD_CYAN}let{RESET}")));
}
#[test]
fn test_highlight_inline_comment_after_code() {
let out = highlight_code_line("rust", "let x = 1; // comment");
assert!(out.contains(&format!("{BOLD_CYAN}let{RESET}")));
assert!(out.contains(&format!("{DIM}// comment{RESET}")));
}
#[test]
fn test_highlight_number_float() {
let out = highlight_code_line("rust", "let pi = 3.14;");
assert!(out.contains(&format!("{YELLOW}3.14{RESET}")));
}
#[test]
fn test_normalize_lang_aliases() {
assert_eq!(normalize_lang("rust"), Some("rust"));
assert_eq!(normalize_lang("rs"), Some("rust"));
assert_eq!(normalize_lang("Python"), Some("python"));
assert_eq!(normalize_lang("JS"), Some("js"));
assert_eq!(normalize_lang("typescript"), Some("js"));
assert_eq!(normalize_lang("tsx"), Some("js"));
assert_eq!(normalize_lang("golang"), Some("go"));
assert_eq!(normalize_lang("zsh"), Some("shell"));
assert_eq!(normalize_lang("haskell"), None);
}
#[test]
fn test_highlight_renders_through_markdown() {
let input = "```rust\nfn main() {\n return 42;\n}\n```\n";
let out = render_full(input);
assert!(out.contains(&format!("{BOLD_CYAN}fn{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}return{RESET}")));
assert!(out.contains(&format!("{YELLOW}42{RESET}")));
}
#[test]
fn test_highlight_rust_types() {
let out = highlight_code_line("rust", "let v: Vec<String> = Vec::new();");
assert!(out.contains(&format!("{MAGENTA}Vec{RESET}")));
assert!(out.contains(&format!("{MAGENTA}String{RESET}")));
}
#[test]
fn test_highlight_rust_option_result() {
let out = highlight_code_line("rust", "fn foo() -> Option<Result<u32, String>> {");
assert!(out.contains(&format!("{MAGENTA}Option{RESET}")));
assert!(out.contains(&format!("{MAGENTA}Result{RESET}")));
assert!(out.contains(&format!("{MAGENTA}u32{RESET}")));
}
#[test]
fn test_highlight_rust_primitive_types() {
let out = highlight_code_line("rust", "let x: i32 = 0;");
assert!(out.contains(&format!("{MAGENTA}i32{RESET}")));
assert!(out.contains(&format!("{YELLOW}0{RESET}")));
}
#[test]
fn test_highlight_rust_self_type() {
let out = highlight_code_line("rust", "impl Self {");
assert!(out.contains(&format!("{MAGENTA}Self{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}impl{RESET}")));
}
#[test]
fn test_highlight_python_string() {
let out = highlight_code_line("python", "name = \"hello world\"");
assert!(out.contains(&format!("{GREEN}\"hello world\"{RESET}")));
}
#[test]
fn test_highlight_python_single_quote_string() {
let out = highlight_code_line("python", "name = 'hello'");
assert!(out.contains(&format!("{GREEN}'hello'{RESET}")));
}
#[test]
fn test_highlight_python_inline_comment() {
let out = highlight_code_line("python", "x = 1 # set x");
assert!(out.contains(&format!("{YELLOW}1{RESET}")));
assert!(out.contains(&format!("{DIM}")));
assert!(out.contains("set x"));
}
#[test]
fn test_highlight_python_class_def() {
let out = highlight_code_line("python", "class MyClass(Base):");
assert!(out.contains(&format!("{BOLD_CYAN}class{RESET}")));
assert!(out.contains("MyClass"));
}
#[test]
fn test_highlight_python_boolean_none() {
let out = highlight_code_line("python", "if True and not None:");
assert!(out.contains(&format!("{BOLD_CYAN}True{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}None{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}not{RESET}")));
}
#[test]
fn test_highlight_python_import() {
let out = highlight_code_line("python", "from os import path");
assert!(out.contains(&format!("{BOLD_CYAN}from{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}import{RESET}")));
}
#[test]
fn test_highlight_js_function_declaration() {
let out = highlight_code_line("js", "function hello() {");
assert!(out.contains(&format!("{BOLD_CYAN}function{RESET}")));
}
#[test]
fn test_highlight_js_string_template() {
let out = highlight_code_line("javascript", "const msg = \"hello\";");
assert!(out.contains(&format!("{BOLD_CYAN}const{RESET}")));
assert!(out.contains(&format!("{GREEN}\"hello\"{RESET}")));
}
#[test]
fn test_highlight_js_null_undefined() {
let out = highlight_code_line("js", "if (x === null || y === undefined) {");
assert!(out.contains(&format!("{BOLD_CYAN}null{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}undefined{RESET}")));
}
#[test]
fn test_highlight_js_comment() {
let out = highlight_code_line("js", "// this is a JS comment");
assert_eq!(out, format!("{DIM}// this is a JS comment{RESET}"));
}
#[test]
fn test_highlight_tsx_recognized() {
let out = highlight_code_line("tsx", "const App = () => {");
assert!(out.contains(&format!("{BOLD_CYAN}const{RESET}")));
}
#[test]
fn test_highlight_shell_for_loop() {
let out = highlight_code_line("bash", "for f in *.txt; do");
assert!(out.contains(&format!("{BOLD_CYAN}for{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}in{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}do{RESET}")));
}
#[test]
fn test_highlight_shell_string() {
let out = highlight_code_line("shell", "echo \"hello world\"");
assert!(out.contains(&format!("{BOLD_CYAN}echo{RESET}")));
assert!(out.contains(&format!("{GREEN}\"hello world\"{RESET}")));
}
#[test]
fn test_highlight_shell_export() {
let out = highlight_code_line("bash", "export PATH=\"/usr/bin\"");
assert!(out.contains(&format!("{BOLD_CYAN}export{RESET}")));
}
#[test]
fn test_highlight_zsh_recognized() {
let out = highlight_code_line("zsh", "if [ -f file ]; then");
assert!(out.contains(&format!("{BOLD_CYAN}if{RESET}")));
}
#[test]
fn test_highlight_c_keywords() {
let out = highlight_code_line("c", "int main() {");
assert!(out.contains(&format!("{BOLD_CYAN}int{RESET}")));
assert!(out.contains("main"));
}
#[test]
fn test_highlight_cpp_keywords() {
let out = highlight_code_line("cpp", "class Foo : public Bar {");
assert!(out.contains(&format!("{BOLD_CYAN}class{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}public{RESET}")));
}
#[test]
fn test_highlight_c_comment() {
let out = highlight_code_line("c", "// C comment");
assert_eq!(out, format!("{DIM}// C comment{RESET}"));
}
#[test]
fn test_highlight_c_string() {
let out = highlight_code_line("c", "char *s = \"hello\";");
assert!(out.contains(&format!("{GREEN}\"hello\"{RESET}")));
}
#[test]
fn test_highlight_c_types() {
let out = highlight_code_line("c", "size_t len = strlen(s);");
assert!(out.contains(&format!("{MAGENTA}size_t{RESET}")));
}
#[test]
fn test_highlight_hpp_recognized() {
let out = highlight_code_line("hpp", "namespace foo {");
assert!(out.contains(&format!("{BOLD_CYAN}namespace{RESET}")));
}
#[test]
fn test_highlight_go_types() {
let out = highlight_code_line("go", "var x int = 42");
assert!(out.contains(&format!("{BOLD_CYAN}var{RESET}")));
assert!(out.contains(&format!("{MAGENTA}int{RESET}")));
assert!(out.contains(&format!("{YELLOW}42{RESET}")));
}
#[test]
fn test_highlight_go_string_type() {
let out = highlight_code_line("go", "func greet(name string) error {");
assert!(out.contains(&format!("{BOLD_CYAN}func{RESET}")));
assert!(out.contains(&format!("{MAGENTA}string{RESET}")));
assert!(out.contains(&format!("{MAGENTA}error{RESET}")));
}
#[test]
fn test_highlight_json_key_value() {
let out = highlight_code_line("json", r#" "name": "yoyo","#);
assert!(out.contains(&format!("{CYAN}\"name\"{RESET}")));
assert!(out.contains(&format!("{GREEN}\"yoyo\"{RESET}")));
}
#[test]
fn test_highlight_json_number() {
let out = highlight_code_line("json", r#" "count": 42,"#);
assert!(out.contains(&format!("{CYAN}\"count\"{RESET}")));
assert!(out.contains(&format!("{YELLOW}42{RESET}")));
}
#[test]
fn test_highlight_json_boolean() {
let out = highlight_code_line("json", r#" "active": true,"#);
assert!(out.contains(&format!("{BOLD_CYAN}true{RESET}")));
}
#[test]
fn test_highlight_json_null() {
let out = highlight_code_line("json", r#" "value": null"#);
assert!(out.contains(&format!("{BOLD_CYAN}null{RESET}")));
}
#[test]
fn test_highlight_json_braces() {
let out = highlight_code_line("json", " {");
assert!(out.contains('{'));
}
#[test]
fn test_highlight_jsonc_recognized() {
let out = highlight_code_line("jsonc", r#" "key": "value""#);
assert!(out.contains(&format!("{CYAN}\"key\"{RESET}")));
}
#[test]
fn test_highlight_yaml_key_value() {
let out = highlight_code_line("yaml", "name: yoyo");
assert!(out.contains(&format!("{BOLD_YELLOW}name{RESET}")));
}
#[test]
fn test_highlight_yaml_string_value() {
let out = highlight_code_line("yaml", "name: \"yoyo\"");
assert!(out.contains(&format!("{BOLD_YELLOW}name{RESET}")));
assert!(out.contains(&format!("{GREEN}\"yoyo\"{RESET}")));
}
#[test]
fn test_highlight_yaml_boolean() {
let out = highlight_code_line("yaml", "enabled: true");
assert!(out.contains(&format!("{BOLD_CYAN}true{RESET}")));
}
#[test]
fn test_highlight_yaml_number() {
let out = highlight_code_line("yaml", "port: 8080");
assert!(out.contains(&format!("{YELLOW}8080{RESET}")));
}
#[test]
fn test_highlight_yaml_comment() {
let out = highlight_code_line("yml", "# a yaml comment");
assert_eq!(out, format!("{DIM}# a yaml comment{RESET}"));
}
#[test]
fn test_highlight_yaml_document_separator() {
let out = highlight_code_line("yaml", "---");
assert!(out.contains(&format!("{DIM}---{RESET}")));
}
#[test]
fn test_highlight_yml_alias() {
assert_eq!(normalize_lang("yml"), Some("yaml"));
}
#[test]
fn test_highlight_toml_section() {
let out = highlight_code_line("toml", "[package]");
assert!(out.contains(&format!("{BOLD}{CYAN}[package]{RESET}")));
}
#[test]
fn test_highlight_toml_key_string() {
let out = highlight_code_line("toml", "name = \"yoyo\"");
assert!(out.contains(&format!("{BOLD_YELLOW}name{RESET}")));
assert!(out.contains(&format!("{GREEN}\"yoyo\"{RESET}")));
}
#[test]
fn test_highlight_toml_key_number() {
let out = highlight_code_line("toml", "version = 1");
assert!(out.contains(&format!("{BOLD_YELLOW}version{RESET}")));
assert!(out.contains(&format!("{YELLOW}1{RESET}")));
}
#[test]
fn test_highlight_toml_boolean() {
let out = highlight_code_line("toml", "enabled = true");
assert!(out.contains(&format!("{BOLD_CYAN}true{RESET}")));
}
#[test]
fn test_highlight_toml_comment() {
let out = highlight_code_line("toml", "# a toml comment");
assert_eq!(out, format!("{DIM}# a toml comment{RESET}"));
}
#[test]
fn test_highlight_toml_array_section() {
let out = highlight_code_line("toml", "[[bin]]");
assert!(out.contains(&format!("{BOLD}{CYAN}[[bin]]{RESET}")));
}
#[test]
fn test_normalize_lang_c_family() {
assert_eq!(normalize_lang("c"), Some("c"));
assert_eq!(normalize_lang("cpp"), Some("c"));
assert_eq!(normalize_lang("c++"), Some("c"));
assert_eq!(normalize_lang("cc"), Some("c"));
assert_eq!(normalize_lang("h"), Some("c"));
assert_eq!(normalize_lang("hpp"), Some("c"));
}
#[test]
fn test_normalize_lang_data_formats() {
assert_eq!(normalize_lang("json"), Some("json"));
assert_eq!(normalize_lang("jsonc"), Some("json"));
assert_eq!(normalize_lang("yaml"), Some("yaml"));
assert_eq!(normalize_lang("yml"), Some("yaml"));
assert_eq!(normalize_lang("toml"), Some("toml"));
}
#[test]
fn test_highlight_json_through_markdown() {
let input = "```json\n{\"name\": \"yoyo\"}\n```\n";
let out = render_full(input);
assert!(out.contains(&format!("{CYAN}\"name\"{RESET}")));
assert!(out.contains(&format!("{GREEN}\"yoyo\"{RESET}")));
}
#[test]
fn test_highlight_yaml_through_markdown() {
let input = "```yaml\nname: yoyo\n```\n";
let out = render_full(input);
assert!(out.contains(&format!("{BOLD_YELLOW}name{RESET}")));
}
#[test]
fn test_highlight_toml_through_markdown() {
let input = "```toml\n[package]\nname = \"yoyo\"\n```\n";
let out = render_full(input);
assert!(out.contains(&format!("{BOLD}{CYAN}[package]{RESET}")));
assert!(out.contains(&format!("{GREEN}\"yoyo\"{RESET}")));
}
#[test]
fn test_highlight_c_through_markdown() {
let input = "```c\nint main() {\n return 0;\n}\n```\n";
let out = render_full(input);
assert!(out.contains(&format!("{BOLD_CYAN}int{RESET}")));
assert!(out.contains(&format!("{BOLD_CYAN}return{RESET}")));
assert!(out.contains(&format!("{YELLOW}0{RESET}")));
}
#[test]
fn test_spinner_frames_not_empty() {
assert!(!SPINNER_FRAMES.is_empty());
}
#[test]
fn test_spinner_frames_are_braille() {
for &frame in SPINNER_FRAMES {
assert!(
('\u{2800}'..='\u{28FF}').contains(&frame),
"Expected braille character, got {:?}",
frame
);
}
}
#[test]
fn test_spinner_frame_cycling() {
for (i, &expected) in SPINNER_FRAMES.iter().enumerate() {
assert_eq!(spinner_frame(i), expected);
}
}
#[test]
fn test_spinner_frame_wraps_around() {
let len = SPINNER_FRAMES.len();
assert_eq!(spinner_frame(0), spinner_frame(len));
assert_eq!(spinner_frame(1), spinner_frame(len + 1));
assert_eq!(spinner_frame(2), spinner_frame(len + 2));
}
#[test]
fn test_spinner_frame_large_index() {
let frame = spinner_frame(999_999);
assert!(SPINNER_FRAMES.contains(&frame));
}
#[test]
fn test_spinner_frames_all_unique() {
let mut seen = std::collections::HashSet::new();
for &frame in SPINNER_FRAMES {
assert!(seen.insert(frame), "Duplicate spinner frame: {:?}", frame);
}
}
#[test]
fn test_format_edit_diff_single_line_change() {
let diff = format_edit_diff("old line", "new line");
assert!(diff.contains("- old line"));
assert!(diff.contains("+ new line"));
assert!(diff.contains(&format!("{RED}")));
assert!(diff.contains(&format!("{GREEN}")));
}
#[test]
fn test_format_edit_diff_multi_line_change() {
let old = "line 1\nline 2\nline 3";
let new = "line A\nline B";
let diff = format_edit_diff(old, new);
assert!(diff.contains("- line 1"));
assert!(diff.contains("- line 2"));
assert!(diff.contains("- line 3"));
assert!(diff.contains("+ line A"));
assert!(diff.contains("+ line B"));
}
#[test]
fn test_format_edit_diff_addition_only() {
let diff = format_edit_diff("", "new content\nmore content");
assert!(!diff.contains("- "));
assert!(diff.contains("+ new content"));
assert!(diff.contains("+ more content"));
}
#[test]
fn test_format_edit_diff_deletion_only() {
let diff = format_edit_diff("old content\nmore old", "");
assert!(diff.contains("- old content"));
assert!(diff.contains("- more old"));
assert!(!diff.contains("+ "));
}
#[test]
fn test_format_edit_diff_long_diff_truncation() {
let old_lines: Vec<&str> = (0..15).map(|_| "old").collect();
let new_lines: Vec<&str> = (0..15).map(|_| "new").collect();
let old = old_lines.join("\n");
let new = new_lines.join("\n");
let diff = format_edit_diff(&old, &new);
assert!(diff.contains("more lines)"));
}
#[test]
fn test_format_edit_diff_empty_both() {
let diff = format_edit_diff("", "");
assert!(diff.is_empty());
}
#[test]
fn test_format_edit_diff_empty_old_text_new_file_section() {
let diff = format_edit_diff("", "fn new_function() {\n println!(\"hello\");\n}");
assert!(!diff.contains("- "));
assert!(diff.contains("+ fn new_function()"));
assert!(diff.contains("+ }"));
}
#[test]
fn test_format_edit_diff_short_diff_not_truncated() {
let diff = format_edit_diff("a", "b");
assert!(!diff.contains("more lines"));
}
#[test]
fn test_format_tool_summary_write_file_with_content() {
let args = serde_json::json!({"path": "out.txt", "content": "line1\nline2\nline3"});
let result = format_tool_summary("write_file", &args);
assert_eq!(result, "write out.txt (3 lines)");
}
#[test]
fn test_format_tool_summary_write_file_single_line() {
let args = serde_json::json!({"path": "out.txt", "content": "hello"});
let result = format_tool_summary("write_file", &args);
assert_eq!(result, "write out.txt (1 line)");
}
#[test]
fn test_format_tool_summary_write_file_no_content() {
let args = serde_json::json!({"path": "out.txt"});
let result = format_tool_summary("write_file", &args);
assert_eq!(result, "write out.txt");
}
#[test]
fn test_format_tool_summary_read_file_with_offset_and_limit() {
let args = serde_json::json!({"path": "src/main.rs", "offset": 10, "limit": 50});
let result = format_tool_summary("read_file", &args);
assert_eq!(result, "read src/main.rs:10..60");
}
#[test]
fn test_format_tool_summary_read_file_with_offset_only() {
let args = serde_json::json!({"path": "src/main.rs", "offset": 100});
let result = format_tool_summary("read_file", &args);
assert_eq!(result, "read src/main.rs:100..");
}
#[test]
fn test_format_tool_summary_read_file_with_limit_only() {
let args = serde_json::json!({"path": "src/main.rs", "limit": 25});
let result = format_tool_summary("read_file", &args);
assert_eq!(result, "read src/main.rs (25 lines)");
}
#[test]
fn test_format_tool_summary_read_file_no_extras() {
let args = serde_json::json!({"path": "src/main.rs"});
let result = format_tool_summary("read_file", &args);
assert_eq!(result, "read src/main.rs");
}
#[test]
fn test_format_tool_summary_edit_file_with_text() {
let args = serde_json::json!({
"path": "foo.rs",
"old_text": "fn old() {\n}\n",
"new_text": "fn new() {\n // improved\n do_stuff();\n}\n"
});
let result = format_tool_summary("edit_file", &args);
assert_eq!(result, "edit foo.rs (2 → 4 lines)");
}
#[test]
fn test_format_tool_summary_edit_file_no_text() {
let args = serde_json::json!({"path": "foo.rs"});
let result = format_tool_summary("edit_file", &args);
assert_eq!(result, "edit foo.rs");
}
#[test]
fn test_format_tool_summary_edit_file_same_lines() {
let args = serde_json::json!({
"path": "foo.rs",
"old_text": "let x = 1;",
"new_text": "let x = 2;"
});
let result = format_tool_summary("edit_file", &args);
assert_eq!(result, "edit foo.rs (1 → 1 lines)");
}
#[test]
fn test_format_tool_summary_search_with_path() {
let args = serde_json::json!({"pattern": "TODO", "path": "src/"});
let result = format_tool_summary("search", &args);
assert_eq!(result, "search 'TODO' in src/");
}
#[test]
fn test_format_tool_summary_search_with_include() {
let args = serde_json::json!({"pattern": "fn main", "include": "*.rs"});
let result = format_tool_summary("search", &args);
assert_eq!(result, "search 'fn main' (*.rs)");
}
#[test]
fn test_format_tool_summary_search_with_path_and_include() {
let args = serde_json::json!({"pattern": "test", "path": "src/", "include": "*.rs"});
let result = format_tool_summary("search", &args);
assert_eq!(result, "search 'test' in src/ (*.rs)");
}
#[test]
fn test_format_tool_summary_search_pattern_only() {
let args = serde_json::json!({"pattern": "TODO"});
let result = format_tool_summary("search", &args);
assert_eq!(result, "search 'TODO'");
}
#[test]
fn test_format_tool_summary_list_files_with_pattern() {
let args = serde_json::json!({"path": "src/", "pattern": "*.rs"});
let result = format_tool_summary("list_files", &args);
assert_eq!(result, "ls src/ (*.rs)");
}
#[test]
fn test_format_tool_summary_list_files_pattern_no_path() {
let args = serde_json::json!({"pattern": "*.toml"});
let result = format_tool_summary("list_files", &args);
assert_eq!(result, "ls . (*.toml)");
}
#[test]
fn test_format_tool_summary_bash_multiline_shows_first_line() {
let args = serde_json::json!({"command": "cd src\ngrep -r 'test' ."});
let result = format_tool_summary("bash", &args);
assert!(
result.starts_with("$ cd src"),
"Should show first line: {result}"
);
assert!(
result.contains("(2 lines)"),
"Should indicate line count: {result}"
);
}
#[test]
fn test_pluralize_singular() {
assert_eq!(pluralize(1, "line", "lines"), "line");
assert_eq!(pluralize(1, "file", "files"), "file");
}
#[test]
fn test_pluralize_plural() {
assert_eq!(pluralize(0, "line", "lines"), "lines");
assert_eq!(pluralize(2, "line", "lines"), "lines");
assert_eq!(pluralize(100, "file", "files"), "files");
}
#[test]
fn test_truncate_tool_output_under_threshold_unchanged() {
let short = "hello world\nsecond line\nthird line";
let result = truncate_tool_output(short, 30_000);
assert_eq!(result, short);
}
#[test]
fn test_truncate_tool_output_empty_string() {
let result = truncate_tool_output("", 30_000);
assert_eq!(result, "");
}
#[test]
fn test_truncate_tool_output_exactly_at_threshold() {
let line = "x".repeat(100);
let lines: Vec<String> = (0..300).map(|_| line.clone()).collect();
let output = lines.join("\n");
let result = truncate_tool_output(&output, output.len());
assert_eq!(result, output);
}
#[test]
fn test_truncate_tool_output_over_threshold_has_marker() {
let line = "x".repeat(200);
let lines: Vec<String> = (0..200).map(|i| format!("line{i}: {line}")).collect();
let output = lines.join("\n");
assert!(output.len() > 30_000);
let result = truncate_tool_output(&output, 30_000);
assert!(result.contains("[... truncated"));
assert!(result.contains("lines ...]"));
assert!(result.contains("line0:"));
assert!(result.contains("line99:"));
assert!(result.contains("line199:"));
assert!(result.contains("line150:"));
assert!(!result.contains("line100:"));
assert!(!result.contains("line120:"));
}
#[test]
fn test_truncate_tool_output_preserves_head_and_tail_count() {
let lines: Vec<String> = (0..300).map(|i| format!("{:>200}", i)).collect();
let output = lines.join("\n");
let result = truncate_tool_output(&output, 30_000);
let _result_lines: Vec<&str> = result.lines().collect();
for i in 0..100 {
let expected = format!("{:>200}", i);
assert!(result.contains(&expected), "Missing head line {i}");
}
for i in 250..300 {
let expected = format!("{:>200}", i);
assert!(result.contains(&expected), "Missing tail line {i}");
}
assert!(!result.contains(&format!("{:>200}", 150)));
assert!(result.contains("[... truncated 150 lines ...]"));
assert!(result.len() < output.len());
}
#[test]
fn test_truncate_tool_output_few_long_lines_not_truncated() {
let line = "x".repeat(500);
let lines: Vec<String> = (0..140).map(|_| line.clone()).collect();
let output = lines.join("\n");
assert!(output.len() > 30_000);
let result = truncate_tool_output(&output, 30_000);
assert_eq!(
result, output,
"Too few lines to truncate, should be unchanged"
);
}
#[test]
fn test_truncate_tool_output_single_truncated_line_in_marker() {
let line = "x".repeat(300);
let lines: Vec<String> = (0..151).map(|_| line.clone()).collect();
let output = lines.join("\n");
assert!(output.len() > 30_000);
let result = truncate_tool_output(&output, 30_000);
assert!(result.contains("[... truncated 1 line ...]"));
}
#[test]
fn test_truncate_tool_output_default_threshold_constant() {
assert_eq!(TOOL_OUTPUT_MAX_CHARS, 30_000);
}
}