use crate::view::diff::render_diff;
use crate::view::syntax::SyntaxHighlighter;
use crate::theme::Theme;
use crate::view::wrap::{as_u16, truncate_to_width, wrap_line};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::item_view::indent_lines;
use super::progress_indicator::spinner_frame;
use super::tool_calls::{SUB_AGENT_VISIBLE_TOOL_LIMIT, SubAgentState, ToolCall, ToolStatus};
const MAX_TOOL_ARG_WIDTH: usize = 200;
pub(crate) fn tool_lines(
tool: &ToolCall,
content_width: u16,
padding: usize,
spinner_tick: usize,
theme: &Theme,
highlighter: &mut SyntaxHighlighter,
) -> Vec<Line<'static>> {
let parsed_command = tool.bash_command();
let bash_command = visible_bash_command(parsed_command.as_deref(), tool.display_value.as_deref(), &tool.status);
let detail = bash_command.map_or_else(
|| tool_detail(tool.display_value.as_deref(), &tool.raw_input, &tool.status),
|command| bash_tool_detail(command, tool.display_value.as_deref(), &tool.status),
);
let prefix = Line::from(vec![
Span::raw(" ".repeat(padding)),
status_glyph(&tool.status, spinner_tick, theme),
Span::raw(" "),
Span::styled(tool.title.clone(), Style::new().fg(theme.text_primary)),
]);
let suffix = tool_suffix(detail, &tool.status, theme);
let mut lines = tool_line(prefix, suffix, bash_command, content_width, padding + 2, theme, highlighter);
if matches!(tool.status, ToolStatus::Success)
&& let Some(preview) = &tool.diff
{
lines.extend(indent_lines(render_diff(preview, content_width, theme, highlighter), padding));
}
if !tool.sub_agents.is_empty() {
lines.push(Line::default());
lines.extend(sub_agent_tree_lines(&tool.sub_agents, content_width, spinner_tick, padding, theme, highlighter));
}
lines
}
fn sub_agent_tree_lines(
sub_agents: &[SubAgentState],
content_width: u16,
spinner_tick: usize,
padding: usize,
theme: &Theme,
highlighter: &mut SyntaxHighlighter,
) -> Vec<Line<'static>> {
let pad = " ".repeat(padding);
let muted = Style::new().fg(theme.muted);
let mut lines: Vec<Line<'static>> = Vec::new();
for (index, agent) in sub_agents.iter().enumerate() {
if index > 0 {
lines.push(Line::raw(format!("{pad} ")));
}
let done = if agent.done { ToolStatus::Success } else { ToolStatus::Running };
lines.push(Line::from(vec![
Span::raw(format!("{pad} ")),
status_glyph(&done, spinner_tick, theme),
Span::raw(format!(" {}", agent.agent_name)),
]));
let hidden = agent.tool_calls.len().saturating_sub(SUB_AGENT_VISIBLE_TOOL_LIMIT);
if hidden > 0 {
lines.push(Line::styled(format!("{pad} … {hidden} earlier tool calls"), muted));
}
let visible: Vec<_> = agent.tool_calls.iter().skip(hidden).collect();
for (index, tool) in visible.iter().enumerate() {
let branch = if index + 1 == visible.len() { " └─ " } else { " ├─ " };
let parsed_command = tool.bash_command();
let bash_command =
visible_bash_command(parsed_command.as_deref(), tool.display_value.as_deref(), &tool.status);
let detail = bash_command.map_or_else(
|| tool_detail(tool.display_value.as_deref(), &tool.arguments, &tool.status),
|command| bash_tool_detail(command, tool.display_value.as_deref(), &tool.status),
);
let prefix = Line::from(vec![
Span::raw(format!("{pad}{branch}")),
status_glyph(&tool.status, spinner_tick, theme),
Span::raw(format!(" {}", tool.name)),
]);
let suffix = tool_suffix(detail, &tool.status, theme);
lines.extend(tool_line(prefix, suffix, bash_command, content_width, padding + 6, theme, highlighter));
}
}
lines
}
fn status_glyph(status: &ToolStatus, spinner_tick: usize, theme: &Theme) -> Span<'static> {
let (glyph, color) = match status {
ToolStatus::Running => (spinner_frame(spinner_tick), theme.info),
ToolStatus::Success => ("✓", theme.success),
ToolStatus::Error(_) => ("✗", theme.error),
};
Span::styled(glyph, Style::new().fg(color))
}
fn tool_detail(display_value: Option<&str>, raw_input: &str, status: &ToolStatus) -> String {
match display_value.filter(|value| !value.is_empty()) {
Some(value) => format!(" ({value})"),
None if matches!(status, ToolStatus::Running) => String::new(),
None => format!(" {}", truncate_to_width(raw_input, MAX_TOOL_ARG_WIDTH)),
}
}
fn visible_bash_command<'a>(
command: Option<&'a str>,
display_value: Option<&str>,
status: &ToolStatus,
) -> Option<&'a str> {
command.filter(|_| !matches!(status, ToolStatus::Running) || display_value.is_some_and(|value| !value.is_empty()))
}
fn bash_tool_detail(command: &str, display_value: Option<&str>, status: &ToolStatus) -> String {
if matches!(status, ToolStatus::Running) {
return String::new();
}
let Some(value) = display_value.filter(|value| !value.is_empty() && *value != command) else {
return String::new();
};
value.rfind(" (exit ").map_or_else(|| format!(" ({value})"), |index| format!(" {}", &value[index + 1..]))
}
fn tool_suffix(detail: String, status: &ToolStatus, theme: &Theme) -> Vec<Span<'static>> {
let mut suffix = vec![Span::styled(detail, Style::new().fg(theme.muted))];
if let ToolStatus::Error(cause) = status {
suffix.push(Span::styled(format!(" {cause}"), Style::new().fg(theme.error)));
}
suffix
}
fn tool_line(
mut line: Line<'static>,
suffix: Vec<Span<'static>>,
command: Option<&str>,
width: u16,
continuation_padding: usize,
theme: &Theme,
highlighter: &mut SyntaxHighlighter,
) -> Vec<Line<'static>> {
let command_lines = command.map(|command| highlighter.highlight(command, "bash", theme));
if let Some(command_lines) = &command_lines
&& let Some(first) = command_lines.first()
{
line.push_span(Span::raw(" "));
line.spans.extend(styled_code_line(first.clone(), theme).spans);
}
line.spans.extend(suffix);
let mut lines = wrap_line(line, width);
if let Some(command_lines) = command_lines {
let continuation_width = width.saturating_sub(as_u16(continuation_padding));
for command_line in command_lines.iter().skip(1) {
lines.extend(wrap_line(styled_code_line(command_line.clone(), theme), continuation_width));
}
}
if continuation_padding > 0 {
let prefix = " ".repeat(continuation_padding);
for line in lines.iter_mut().skip(1) {
line.spans.insert(0, Span::raw(prefix.clone()));
}
}
lines
}
fn styled_code_line(mut line: Line<'static>, theme: &Theme) -> Line<'static> {
line.style = line.style.patch(Style::new().fg(theme.code_fg));
line
}