use crate::command::chat::constants::{THINKING_PULSE_MIN_FACTOR, THINKING_PULSE_PERIOD_MS};
use crate::command::chat::tools::classification::ToolCategory;
use crate::theme::Theme;
use std::io;
use std::sync::{Arc, Mutex};
const TOOL_ARG_PREVIEW_MAX_CHARS: usize = 60;
pub(crate) fn extract_tool_desc(tool_name: &str, arguments: &str) -> Option<String> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
match tool_name {
"Bash" | "Shell" => parsed.get("description")?.as_str().map(|s| s.to_string()),
"Read" | "Write" | "Edit" | "Glob" | "Grep" => parsed
.get("path")
.or_else(|| parsed.get("file_path"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
"Agent" | "Teammate" => parsed
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
"Ask" => parsed
.get("header")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
_ => None,
}
}
pub(crate) fn extract_bash_command(arguments: &str) -> Option<String> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
parsed
.get("command")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub(crate) fn make_args_preview(arguments: &str) -> String {
let total_len = arguments.chars().count();
if total_len <= TOOL_ARG_PREVIEW_MAX_CHARS {
return arguments.to_string();
}
let closing_bracket = arguments.chars().next().and_then(|c| match c {
'{' => Some('}'),
'[' => Some(']'),
_ => None,
});
let preview_len = if closing_bracket.is_some() {
TOOL_ARG_PREVIEW_MAX_CHARS - 4
} else {
TOOL_ARG_PREVIEW_MAX_CHARS
};
let preview: String = arguments.chars().take(preview_len).collect();
if let Some(bracket) = closing_bracket {
format!("{}...{}", preview, bracket)
} else {
format!("{}…", preview)
}
}
pub(crate) fn term_width() -> usize {
crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80)
}
pub(crate) fn box_width() -> usize {
term_width().saturating_sub(4).clamp(20, 56)
}
pub(crate) fn thinking_pulse_color() -> ratatui::style::Color {
use std::time::{SystemTime, UNIX_EPOCH};
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let period = THINKING_PULSE_PERIOD_MS as f64;
let phase = (millis % period as u128) as f64 / period;
let t = (phase * std::f64::consts::TAU).sin() * 0.5 + 0.5;
let theme = Theme::terminal();
let (r, g, b) = if let ratatui::style::Color::Rgb(r, g, b) = theme.label_ai {
let min_factor = THINKING_PULSE_MIN_FACTOR;
let factor = min_factor + (1.0 - min_factor) * t;
(
(r as f64 * factor).round().min(255.0) as u8,
(g as f64 * factor).round().min(255.0) as u8,
(b as f64 * factor).round().min(255.0) as u8,
)
} else {
(120, 220, 160)
};
ratatui::style::Color::Rgb(r, g, b)
}
pub(crate) fn print_tool_call_line(tool_name: &str, arguments: &str) {
use colored::Colorize;
let category = ToolCategory::from_name(tool_name);
let icon = category.icon();
let theme = Theme::terminal();
let tool_color = category.color(&theme);
let desc = if let Some(d) = extract_tool_desc(tool_name, arguments) {
d
} else if !arguments.is_empty() {
make_args_preview(arguments)
} else {
String::new()
};
let desc_colored = if desc.is_empty() {
String::new()
} else {
format!(" {}", desc.dimmed())
};
eprintln!(
" {} {} {}",
icon,
crate::util::color_adapt::apply_fg(tool_name, tool_color).bold(),
desc_colored
);
}
pub(crate) fn print_tool_result_line(
tool_name: &str,
is_error: bool,
summary: &str,
elapsed: &str,
) {
use colored::Colorize;
let category = ToolCategory::from_name(tool_name);
let theme = Theme::terminal();
let tool_color = category.color(&theme);
let status_icon = if is_error { "✗" } else { "✓" };
let status_style = if is_error { "red" } else { "green" };
eprintln!(
" 🔧 {} {}{} {}",
crate::util::color_adapt::apply_fg(tool_name, tool_color).bold(),
status_icon.color(status_style),
summary.dimmed(),
elapsed.dimmed(),
);
}
pub(crate) fn redraw_markdown(raw_lines: usize, cur_col: usize, text: &str) {
use crossterm::{cursor, execute, terminal};
let total_raw_lines = if cur_col > 0 {
raw_lines + 1
} else {
raw_lines
};
let mut stdout = io::stdout();
if total_raw_lines > 0 {
let _ = execute!(stdout, cursor::MoveToColumn(0));
if total_raw_lines > 1 {
let _ = execute!(stdout, cursor::MoveUp((total_raw_lines - 1) as u16));
}
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
}
crate::util::md_render::render_md(text);
}
pub(crate) fn redraw_streaming_as_markdown(
streaming_content: &Arc<Mutex<String>>,
raw_lines: &mut usize,
cur_col: &mut usize,
) {
let content = streaming_content.lock().unwrap();
if content.is_empty() {
return;
}
let tw = term_width();
let mut rl: usize = 0;
let mut cc: usize = 0;
for ch in content.chars() {
if ch == '\n' {
rl += 1;
cc = 0;
} else {
cc += 1;
if cc >= tw {
rl += 1;
cc = 0;
}
}
}
*raw_lines = rl;
*cur_col = cc;
redraw_markdown(*raw_lines, *cur_col, &content);
}