use serde_json::Value;
pub fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
s.into()
} else {
s.chars().take(n.saturating_sub(3)).collect::<String>() + "..."
}
}
pub fn word_wrap(text: &str, width: usize) -> Vec<String> {
if width <= 2 {
return text.lines().map(|s| s.to_string()).collect();
}
let mut result: Vec<String> = Vec::new();
for line in text.lines() {
if line.is_empty() {
result.push(String::new());
continue;
}
if visual_width(line) <= width {
result.push(line.to_string());
continue;
}
let mut current = String::new();
let mut visual_width = 0;
for ch in line.chars() {
let ch_width = if ch > '\u{7F}' { 2 } else { 1 };
if visual_width + ch_width > width && !current.is_empty() {
result.push(current.clone());
current.clear();
visual_width = 0;
}
current.push(ch);
visual_width += ch_width;
}
if !current.is_empty() {
result.push(current);
}
}
result
}
pub fn fmt_tokens(n: u64) -> String {
if n < 1_000 {
format!("{}", n)
} else if n < 10_000 {
format!("{:.1}k", n as f64 / 1_000.0)
} else if n < 1_000_000 {
format!("{:.0}k", n as f64 / 1_000.0)
} else {
format!("{:.1}m", n as f64 / 1_000_000.0)
}
}
pub fn progress_bar(pct: f64, width: usize) -> String {
let filled = ((pct / 100.0) * width as f64).round() as usize;
let filled = filled.min(width);
let mut s = String::with_capacity(width);
for i in 0..width {
s.push(if i < filled { '█' } else { '░' });
}
s
}
pub fn visual_width(s: &str) -> usize {
s.chars().map(|ch| if ch > '\u{7F}' { 2 } else { 1 }).sum()
}
pub fn truncate_visual(s: &str, max_width: usize) -> String {
let mut result = String::new();
let mut width = 0;
for ch in s.chars() {
let ch_w = if ch > '\u{7F}' { 2 } else { 1 };
if width + ch_w > max_width {
break;
}
result.push(ch);
width += ch_w;
}
result
}
pub fn truncate_visual_end(s: &str, max_width: usize) -> String {
let chars: Vec<char> = s.chars().collect();
let mut result = String::new();
let mut width = 0;
for &ch in chars.iter().rev() {
let ch_w = if ch > '\u{7F}' { 2 } else { 1 };
if width + ch_w > max_width {
break;
}
result.insert(0, ch);
width += ch_w;
}
result
}
pub fn extract_tool_detail(tool_name: &str, input: Option<&Value>) -> String {
let Some(input) = input else {
return String::new();
};
match tool_name.to_lowercase().as_str() {
"read" => input
.get("file_path")
.and_then(|v| v.as_str())
.or_else(|| input.get("path").and_then(|v| v.as_str()))
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"write" => input
.get("file_path")
.and_then(|v| v.as_str())
.or_else(|| input.get("path").and_then(|v| v.as_str()))
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"edit" | "multi_edit" => input
.get("file_path")
.and_then(|v| v.as_str())
.or_else(|| input.get("path").and_then(|v| v.as_str()))
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"search" | "grep" | "glob" => input
.get("pattern")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 30))
.unwrap_or_default(),
"ls" => input
.get("path")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"bash" => input
.get("command")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"websearch" => input
.get("query")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 30))
.unwrap_or_default(),
"webfetch" => input
.get("url")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 40))
.unwrap_or_default(),
"task" => input
.get("description")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 30))
.unwrap_or_default(),
"task_create" => input
.get("description")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 30))
.unwrap_or_default(),
"task_get" | "task_stop" => input
.get("task_id")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 20))
.unwrap_or_default(),
"monitor" => input
.get("target")
.and_then(|v| v.as_str())
.map(|s| truncate(s, 30))
.unwrap_or_default(),
_ => String::new(),
}
}