use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use regex::Regex;
use std::sync::LazyLock;
use crate::chat::message::{ChatMessage, Role};
use crate::tui::sidebar::format_tool_detail;
use super::component::Chat;
use super::rendering::{display_tool_name, tool_color};
static TOOL_CALL_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?s)<tool_call>.*?</tool_call>").expect("valid regex"));
pub(super) fn strip_tool_call_xml(text: &str) -> String {
let cleaned = TOOL_CALL_RE.replace_all(text, "");
let cleaned = if let Some(idx) = cleaned.rfind("<tool_call>") {
if cleaned[idx..].contains("</tool_call>") {
&cleaned
} else {
&cleaned[..idx]
}
} else {
&cleaned
};
let mut result = String::with_capacity(cleaned.len());
let mut prev_blank = false;
for line in cleaned.lines() {
let blank = line.trim().is_empty();
if blank && prev_blank {
continue;
}
if !result.is_empty() {
result.push('\n');
}
result.push_str(line);
prev_blank = blank;
}
result.trim().to_string()
}
impl Chat {
pub(super) fn handle_tool_call_start(&mut self, name: &str, args_json: &str) {
if !self.streaming.buffer.is_empty() {
let text = strip_tool_call_xml(&std::mem::take(&mut self.streaming.buffer));
if !text.is_empty() {
let rendered = crate::chat::markdown::render_markdown(&text);
let msg = ChatMessage::new(Role::Assistant, &text);
self.rig_history.push(msg.to_rig_message());
self.messages.push(msg);
self.rendered_messages.push(rendered);
}
}
let detail = format_tool_detail(name, args_json);
let display_name = display_tool_name(name);
let color = tool_color(name);
let content = format!("\u{25cf} {display_name} {detail}");
let mut spans = vec![
Span::styled(" \u{25cf} ", Style::default().fg(color)),
Span::styled(
display_name,
Style::default().fg(color).add_modifier(Modifier::BOLD),
),
];
if !detail.is_empty() {
spans.push(Span::styled(
format!(" {detail}"),
Style::default().fg(Color::DarkGray),
));
}
let rendered = vec![Line::from(spans)];
let msg = ChatMessage::new(Role::System, content);
self.messages.push(msg);
self.rendered_messages.push(rendered);
}
pub(super) fn handle_tool_result(&mut self, name: &str, _result: &str, duration_ms: u64) {
let display_name = display_tool_name(name);
let content = format!("\u{2714} {display_name} ({duration_ms}ms)");
let rendered = vec![Line::from(vec![
Span::styled(" \u{2714} ", Style::default().fg(Color::Green)),
Span::styled(display_name, Style::default().fg(Color::Green)),
Span::styled(
format!(" ({duration_ms}ms)"),
Style::default().fg(Color::DarkGray),
),
])];
let msg = ChatMessage::new(Role::System, content);
self.messages.push(msg);
self.rendered_messages.push(rendered);
}
pub(super) fn handle_tool_error(&mut self, name: &str, error: &str) {
let display_name = display_tool_name(name);
let brief = error.lines().next().unwrap_or(error);
let brief = if brief.len() > 60 {
let mut end = 57;
while !brief.is_char_boundary(end) && end > 0 {
end -= 1;
}
format!("{}...", &brief[..end])
} else {
brief.to_string()
};
let content = format!("\u{2718} {display_name}: {brief}");
let rendered = vec![Line::from(vec![
Span::styled(" \u{2718} ", Style::default().fg(Color::Red)),
Span::styled(
format!("{display_name}: "),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
Span::styled(brief, Style::default().fg(Color::Red)),
])];
let msg = ChatMessage::new(Role::System, content);
self.messages.push(msg);
self.rendered_messages.push(rendered);
}
}