use crate::command::chat::constants::{AGENT_CALL_PROMPT_MAX_LINES, TOOL_ARG_PREVIEW_MAX_CHARS};
use crate::command::chat::storage::ToolCallItem;
use crate::command::chat::tools::classification::{ToolCategory, format_json_value};
use crate::command::chat::tools::tool_names;
use crate::util::text::wrap_text;
use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
use super::RenderContext;
use super::bubble::bordered_line;
use super::msg_render::agent_name_color;
use crate::command::chat::render::theme::Theme;
pub fn render_tool_call_request_msg(
sender_name: Option<&str>,
tool_calls: &[ToolCallItem],
ctx: &mut RenderContext<'_>,
) {
let lines = &mut *ctx.lines;
let theme = ctx.theme;
let bubble_max_width = ctx.bubble_max_width;
let expand = ctx.expand;
let content_w = bubble_max_width.saturating_sub(6);
lines.push(Line::from(""));
for (i, tc) in tool_calls.iter().enumerate() {
if i > 0 {
lines.push(Line::from(""));
}
let category = ToolCategory::from_name(&tc.name);
let icon = category.icon();
let tool_color = category.color(theme);
let sender_prefix_spans: Vec<Span<'static>> = if let Some(name) = sender_name {
let label_color = agent_name_color(name);
vec![
Span::styled(" ", Style::default()),
Span::styled(
name.to_string(),
Style::default()
.fg(label_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(" · ", Style::default().fg(theme.text_dim)),
]
} else {
vec![Span::styled(" ", Style::default())]
};
if expand {
let tool_desc = extract_tool_description_from_args(&tc.name, &tc.arguments);
let display_name = if let Some(ref desc) = tool_desc {
format!("{} - {}", tc.name, desc)
} else {
tc.name.clone()
};
let mut spans = sender_prefix_spans.clone();
spans.push(Span::styled(icon, Style::default().fg(tool_color)));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
display_name,
Style::default().fg(tool_color).add_modifier(Modifier::BOLD),
));
lines.push(Line::from(spans));
if !tc.arguments.is_empty() {
if !render_specialized_tool_call(
&tc.name,
&tc.arguments,
bubble_max_width,
content_w,
lines,
theme,
) {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&tc.arguments)
{
render_json_params_enhanced(&json_value, content_w, lines, theme);
} else {
for line in wrap_text(&tc.arguments, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(line, Style::default().fg(theme.text_dim)),
]));
}
}
}
}
} else {
if tc.name.as_str() == tool_names::AGENT
&& let Some(agent_args) = extract_agent_args(&tc.arguments)
{
let mut desc_parts: Vec<String> = Vec::new();
if agent_args.run_in_background {
desc_parts.push("[background]".to_string());
}
if let Some(ref desc) = agent_args.description {
desc_parts.push(desc.clone());
}
if desc_parts.is_empty() {
let first_line = agent_args.prompt.lines().next().unwrap_or("");
let cw: String = first_line
.chars()
.take(TOOL_ARG_PREVIEW_MAX_CHARS)
.collect();
let preview = if first_line.chars().count() > TOOL_ARG_PREVIEW_MAX_CHARS {
format!("{}...", cw)
} else {
cw
};
desc_parts.push(preview);
}
let desc_text = desc_parts.join(" ");
let mut spans = sender_prefix_spans.clone();
spans.push(Span::styled(icon, Style::default().fg(tool_color)));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
tc.name.clone(),
Style::default().fg(tool_color).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" {}", desc_text),
Style::default().fg(theme.text_dim),
));
lines.push(Line::from(spans));
continue;
}
if tc.name.as_str() == tool_names::TEAMMATE
&& let Some(tm_args) = extract_teammate_args(&tc.arguments)
{
let mut desc_parts: Vec<String> = Vec::new();
if tm_args.worktree {
desc_parts.push("[worktree]".to_string());
}
desc_parts.push(format!("{}({})", tm_args.name, tm_args.role));
let first_line = tm_args.prompt.lines().next().unwrap_or("");
let cw: String = first_line
.chars()
.take(TOOL_ARG_PREVIEW_MAX_CHARS)
.collect();
let preview = if first_line.chars().count() > TOOL_ARG_PREVIEW_MAX_CHARS {
format!("{}...", cw)
} else {
cw
};
desc_parts.push(preview);
let desc_text = desc_parts.join(" ");
let mut spans = sender_prefix_spans.clone();
spans.push(Span::styled(icon, Style::default().fg(tool_color)));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
tc.name.clone(),
Style::default().fg(tool_color).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" {}", desc_text),
Style::default().fg(theme.text_dim),
));
lines.push(Line::from(spans));
continue;
}
let tool_desc = extract_tool_description_from_args(&tc.name, &tc.arguments);
if let Some(desc) = tool_desc {
let mut spans = sender_prefix_spans.clone();
spans.push(Span::styled(icon, Style::default().fg(tool_color)));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
tc.name.clone(),
Style::default().fg(tool_color).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" {}", desc),
Style::default().fg(theme.text_dim),
));
lines.push(Line::from(spans));
} else {
let total_len = tc.arguments.chars().count();
let truncated = total_len > TOOL_ARG_PREVIEW_MAX_CHARS;
let closing_bracket = if truncated {
tc.arguments.chars().next().and_then(|c| match c {
'{' => Some('}'),
'[' => Some(']'),
_ => None,
})
} else {
None
};
let max_preview = TOOL_ARG_PREVIEW_MAX_CHARS;
let preview_len = if closing_bracket.is_some() {
max_preview - 4
} else {
max_preview
};
let args_preview: String = tc.arguments.chars().take(preview_len).collect();
let suffix = if truncated {
if let Some(bracket) = closing_bracket {
format!("...{}", bracket)
} else {
"…".to_string()
}
} else {
"".to_string()
};
let mut spans = sender_prefix_spans.clone();
spans.push(Span::styled(icon, Style::default().fg(tool_color)));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
tc.name.clone(),
Style::default().fg(tool_color).add_modifier(Modifier::BOLD),
));
if !args_preview.is_empty() {
spans.push(Span::styled(
format!(" {}{}", args_preview, suffix),
Style::default().fg(theme.text_dim),
));
}
lines.push(Line::from(spans));
}
}
}
}
pub(crate) fn render_json_params_enhanced(
json: &serde_json::Value,
max_width: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
if let Some(obj) = json.as_object() {
for (key, value) in obj {
let value_str = format_json_value(value);
let max_val_chars = max_width.saturating_sub(key.chars().count() + 7);
let value_display = if value_str.chars().count() > max_val_chars {
let truncated: String = value_str.chars().take(max_val_chars).collect();
format!("{}…", truncated)
} else {
value_str
};
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(format!("{}:", key), Style::default().fg(theme.text_dim)),
Span::styled(" ", Style::default()),
Span::styled(value_display, Style::default().fg(theme.text_normal)),
]));
}
} else {
let value_str = format_json_value(json);
for line in wrap_text(&value_str, max_width) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(line, Style::default().fg(theme.text_normal)),
]));
}
}
}
pub(crate) fn extract_tool_description_from_args(
tool_name: &str,
arguments: &str,
) -> Option<String> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
match tool_name {
tool_names::BASH => parsed.get("description")?.as_str().map(|s| s.to_string()),
tool_names::READ
| tool_names::WRITE
| tool_names::EDIT
| tool_names::GLOB
| tool_names::GREP => parsed
.get("path")
.or_else(|| parsed.get("file_path"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
tool_names::AGENT => parsed
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
tool_names::TEAMMATE => {
let name = parsed.get("name").and_then(|v| v.as_str()).unwrap_or("");
let role = parsed.get("role").and_then(|v| v.as_str()).unwrap_or(name);
Some(role.to_string())
}
tool_names::TASK => {
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("task");
let title = parsed.get("title").and_then(|v| v.as_str());
match title {
Some(t) => Some(format!("{}: {}", action, t)),
None => Some(action.to_string()),
}
}
tool_names::TASK_OUTPUT => {
let task_id = parsed
.get("task_id")
.and_then(|v| v.as_str())
.unwrap_or("?");
Some(format!("获取任务 {} 输出", task_id))
}
tool_names::WEB_SEARCH => parsed
.get("query")
.and_then(|v| v.as_str())
.map(|s| format!("搜索: {}", s)),
tool_names::WEB_FETCH => parsed
.get("url")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
tool_names::BROWSER => parsed
.get("url")
.or_else(|| parsed.get("action"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
tool_names::ASK => {
if let Some(questions) = parsed.get("questions").and_then(|v| v.as_array())
&& let Some(first) = questions.first()
{
return first
.get("question")
.and_then(|v| v.as_str())
.map(|s| truncate_str(s, TOOL_ARG_PREVIEW_MAX_CHARS));
}
None
}
tool_names::TODO_WRITE => {
if let Some(todos) = parsed.get("todos").and_then(|v| v.as_array()) {
let count = todos.len();
Some(format!("更新 {} 项待办", count))
} else {
Some("更新待办".to_string())
}
}
tool_names::TODO_READ => Some("读取待办列表".to_string()),
tool_names::COMPACT => {
let focus = parsed.get("focus").and_then(|v| v.as_str());
match focus {
Some(f) => Some(format!("压缩对话 (focus: {})", f)),
None => Some("压缩对话".to_string()),
}
}
tool_names::ENTER_PLAN_MODE => parsed
.get("description")
.and_then(|v| v.as_str())
.map(|s| format!("进入计划模式: {}", s))
.or_else(|| Some("进入计划模式".to_string())),
tool_names::EXIT_PLAN_MODE => Some("提交计划审批".to_string()),
tool_names::LOAD_SKILL => parsed
.get("name")
.and_then(|v| v.as_str())
.map(|s| format!("加载技能: {}", s)),
tool_names::REGISTER_HOOK => {
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("register");
let event = parsed.get("event").and_then(|v| v.as_str());
match event {
Some(e) => Some(format!("{} 钩子: {}", action, e)),
None => Some(format!("{} 钩子", action)),
}
}
tool_names::SEND_MESSAGE => {
let to = parsed.get("to").and_then(|v| v.as_str());
let msg = parsed
.get("message")
.and_then(|v| v.as_str())
.map(|s| truncate_str(s, 40));
match (to, msg) {
(Some(t), Some(m)) => Some(format!("→ {} {}", t, m)),
(Some(t), None) => Some(format!("→ {}", t)),
(None, Some(m)) => Some(format!("广播: {}", m)),
(None, None) => Some("发送消息".to_string()),
}
}
tool_names::ENTER_WORKTREE => parsed
.get("name")
.and_then(|v| v.as_str())
.map(|s| format!("进入工作树: {}", s))
.or_else(|| Some("进入工作树".to_string())),
tool_names::EXIT_WORKTREE => Some("退出工作树".to_string()),
tool_names::WORK_DONE => parsed
.get("summary")
.and_then(|v| v.as_str())
.map(|s| truncate_str(s, TOOL_ARG_PREVIEW_MAX_CHARS))
.or_else(|| Some("工作完成".to_string())),
tool_names::IGNORE_MESSAGE => Some("忽略消息".to_string()),
tool_names::COMPUTER_USE => parsed
.get("action")
.and_then(|v| v.as_str())
.map(|s| format!("计算机操作: {}", s)),
_ => None,
}
}
fn truncate_str(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars).collect();
format!("{}…", truncated)
}
}
pub(crate) struct TeammateCallArgs {
pub name: String,
pub role: String,
pub prompt: String,
pub worktree: bool,
}
pub(crate) fn extract_teammate_args(arguments: &str) -> Option<TeammateCallArgs> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
Some(TeammateCallArgs {
name: parsed.get("name")?.as_str()?.to_string(),
role: parsed
.get("role")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
prompt: parsed.get("prompt")?.as_str()?.to_string(),
worktree: parsed
.get("worktree")
.and_then(|v| v.as_bool())
.unwrap_or(false),
})
}
pub(crate) fn render_teammate_call_request_expanded(
args: &TeammateCallArgs,
bubble_max_width: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
let border_color = theme.text_dim;
let result_bg = theme.bg_primary;
let content_w = bubble_max_width.saturating_sub(6);
let mut meta_parts = vec![format!(
"{}({})",
args.name,
if args.role.is_empty() {
&args.name
} else {
&args.role
}
)];
if args.worktree {
meta_parts.push("[worktree]".to_string());
}
let meta_line = meta_parts.join(" ");
for wrapped in wrap_text(&meta_line, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default().bg(result_bg)),
Span::styled(wrapped, Style::default().fg(theme.text_dim).bg(result_bg)),
]));
}
let top_border = format!(" ┌{}┐", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
top_border,
Style::default().fg(border_color).bg(result_bg),
)));
let prompt_lines: Vec<&str> = args.prompt.lines().collect();
let total = prompt_lines.len();
let max_display = AGENT_CALL_PROMPT_MAX_LINES;
let display_lines = &prompt_lines[..total.min(max_display)];
for line in display_lines {
for wrapped in wrap_text(line, content_w) {
lines.push(bordered_line(
vec![Span::styled(
wrapped,
Style::default().fg(theme.text_dim).bg(result_bg),
)],
bubble_max_width,
border_color,
result_bg,
));
}
}
if total > max_display {
lines.push(bordered_line(
vec![Span::styled(
format!("... (共 {} 行)", total),
Style::default().fg(theme.text_dim).bg(result_bg),
)],
bubble_max_width,
border_color,
result_bg,
));
}
let bottom_border = format!(" └{}┘", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
bottom_border,
Style::default().fg(border_color).bg(result_bg),
)));
}
pub(crate) struct BashArgs {
pub command: Option<String>,
pub timeout: Option<u64>,
pub run_in_background: bool,
pub cwd: Option<String>,
}
pub(crate) fn extract_bash_args(arguments: &str) -> Option<BashArgs> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
Some(BashArgs {
command: parsed
.get("command")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
timeout: parsed.get("timeout").and_then(|v| v.as_u64()),
run_in_background: parsed
.get("run_in_background")
.and_then(|v| v.as_bool())
.unwrap_or(false),
cwd: parsed
.get("cwd")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
})
}
pub(crate) fn render_bash_call_request_expanded(
args: &BashArgs,
bubble_max_width: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
let content_w = bubble_max_width.saturating_sub(6);
if let Some(ref cmd) = args.command {
let cmd_with_prefix = format!("$ {}", cmd);
for line in crate::util::text::wrap_text(&cmd_with_prefix, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(line, Style::default().fg(theme.text_normal)),
]));
}
}
let mut meta_parts: Vec<String> = Vec::new();
if args.run_in_background {
meta_parts.push("[background]".to_string());
}
if let Some(timeout) = args.timeout {
meta_parts.push(format!("timeout: {}s", timeout));
}
if let Some(ref cwd) = args.cwd {
meta_parts.push(format!("cwd: {}", cwd));
}
if !meta_parts.is_empty() {
let meta_line = meta_parts.join(" ");
for line in crate::util::text::wrap_text(&meta_line, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(line, Style::default().fg(theme.text_dim)),
]));
}
}
}
pub(crate) struct AgentCallArgs {
pub prompt: String,
pub description: Option<String>,
pub run_in_background: bool,
}
pub(crate) fn extract_agent_args(arguments: &str) -> Option<AgentCallArgs> {
let parsed = serde_json::from_str::<serde_json::Value>(arguments).ok()?;
Some(AgentCallArgs {
prompt: parsed.get("prompt")?.as_str()?.to_string(),
description: parsed
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
run_in_background: parsed
.get("run_in_background")
.and_then(|v| v.as_bool())
.unwrap_or(false),
})
}
pub(crate) fn render_agent_call_request_expanded(
args: &AgentCallArgs,
bubble_max_width: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
let border_color = theme.text_dim;
let result_bg = theme.bg_primary;
let content_w = bubble_max_width.saturating_sub(6);
if args.run_in_background {
for wrapped in wrap_text("[background]", content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default().bg(result_bg)),
Span::styled(wrapped, Style::default().fg(theme.text_dim).bg(result_bg)),
]));
}
}
let top_border = format!(" ┌{}┐", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
top_border,
Style::default().fg(border_color).bg(result_bg),
)));
let prompt_lines: Vec<&str> = args.prompt.lines().collect();
let total = prompt_lines.len();
let max_display = AGENT_CALL_PROMPT_MAX_LINES;
let display_lines = &prompt_lines[..total.min(max_display)];
for line in display_lines {
for wrapped in wrap_text(line, content_w) {
lines.push(bordered_line(
vec![Span::styled(
wrapped,
Style::default().fg(theme.text_dim).bg(result_bg),
)],
bubble_max_width,
border_color,
result_bg,
));
}
}
if total > max_display {
lines.push(bordered_line(
vec![Span::styled(
format!("... (共 {} 行)", total),
Style::default().fg(theme.text_dim).bg(result_bg),
)],
bubble_max_width,
border_color,
result_bg,
));
}
let bottom_border = format!(" └{}┘", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
bottom_border,
Style::default().fg(border_color).bg(result_bg),
)));
}
pub(crate) fn render_exit_plan_mode_request(
bubble_max_width: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
let border_color = theme.text_dim;
let result_bg = theme.bg_primary;
let content_w = bubble_max_width.saturating_sub(6);
let top_border = format!(" ┌{}┐", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
top_border,
Style::default().fg(border_color).bg(result_bg),
)));
let hint = "提交计划审批,等待用户批准后退出计划模式";
for wrapped in wrap_text(hint, content_w) {
lines.push(bordered_line(
vec![Span::styled(
wrapped,
Style::default().fg(theme.text_dim).bg(result_bg),
)],
bubble_max_width,
border_color,
result_bg,
));
}
let bottom_border = format!(" └{}┘", "─".repeat(bubble_max_width.saturating_sub(4)));
lines.push(Line::from(Span::styled(
bottom_border,
Style::default().fg(border_color).bg(result_bg),
)));
}
fn render_specialized_tool_call(
tool_name: &str,
arguments: &str,
bubble_max_width: usize,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
match tool_name {
tool_names::BASH => {
if let Some(bash_args) = extract_bash_args(arguments) {
render_bash_call_request_expanded(&bash_args, bubble_max_width, lines, theme);
return true;
}
false
}
tool_names::AGENT => {
if let Some(agent_args) = extract_agent_args(arguments) {
render_agent_call_request_expanded(&agent_args, bubble_max_width, lines, theme);
return true;
}
false
}
tool_names::TEAMMATE => {
if let Some(tm_args) = extract_teammate_args(arguments) {
render_teammate_call_request_expanded(&tm_args, bubble_max_width, lines, theme);
return true;
}
false
}
tool_names::EXIT_PLAN_MODE => {
render_exit_plan_mode_request(bubble_max_width, lines, theme);
true
}
tool_names::TASK => render_task_call_request_expanded(arguments, content_w, lines, theme),
tool_names::TASK_OUTPUT => {
render_task_output_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::WEB_SEARCH => {
render_web_search_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::WEB_FETCH => {
render_web_fetch_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::BROWSER => {
render_browser_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::ASK => render_ask_call_request_expanded(arguments, content_w, lines, theme),
tool_names::TODO_WRITE => {
render_todo_write_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::TODO_READ => render_todo_read_call_request_expanded(content_w, lines, theme),
tool_names::COMPACT => {
render_compact_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::ENTER_PLAN_MODE => {
render_enter_plan_mode_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::LOAD_SKILL => {
render_load_skill_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::REGISTER_HOOK => {
render_register_hook_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::SEND_MESSAGE => {
render_send_message_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::ENTER_WORKTREE | tool_names::EXIT_WORKTREE => {
render_worktree_call_request_expanded(tool_name, arguments, content_w, lines, theme)
}
tool_names::WORK_DONE => {
render_work_done_call_request_expanded(arguments, content_w, lines, theme)
}
tool_names::IGNORE_MESSAGE => {
render_ignore_message_call_request_expanded(content_w, lines, theme)
}
tool_names::COMPUTER_USE => {
render_computer_use_call_request_expanded(arguments, content_w, lines, theme)
}
_ => false,
}
}
fn render_kv_line(
key: &str,
value: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) {
let max_val_chars = content_w.saturating_sub(key.chars().count() + 7);
let display = if value.chars().count() > max_val_chars {
format!("{}…", truncate_str(value, max_val_chars))
} else {
value.to_string()
};
for wrapped in wrap_text(&display, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(format!("{}:", key), Style::default().fg(theme.text_dim)),
Span::styled(" ", Style::default()),
Span::styled(wrapped, Style::default().fg(theme.text_normal)),
]));
}
}
fn render_tag_line(tag: &str, content_w: usize, lines: &mut Vec<Line<'static>>, theme: &Theme) {
for wrapped in wrap_text(tag, content_w) {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(wrapped, Style::default().fg(theme.text_dim)),
]));
}
}
fn render_task_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("task");
render_tag_line(&format!("[{}]", action), content_w, lines, theme);
if let Some(title) = parsed.get("title").and_then(|v| v.as_str()) {
render_kv_line("title", title, content_w, lines, theme);
}
if let Some(desc) = parsed.get("description").and_then(|v| v.as_str()) {
render_kv_line("description", desc, content_w, lines, theme);
}
if let Some(task_id) = parsed.get("taskId").and_then(|v| v.as_str()) {
render_kv_line("taskId", task_id, content_w, lines, theme);
}
if let Some(status) = parsed.get("status").and_then(|v| v.as_str()) {
render_kv_line("status", status, content_w, lines, theme);
}
true
}
fn render_task_output_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(task_id) = parsed.get("task_id").and_then(|v| v.as_str()) {
render_kv_line("task_id", task_id, content_w, lines, theme);
}
if let Some(block) = parsed.get("block").and_then(|v| v.as_bool()) {
render_kv_line(
"block",
if block {
"true (等待完成)"
} else {
"false (非阻塞)"
},
content_w,
lines,
theme,
);
}
if let Some(timeout) = parsed.get("timeout").and_then(|v| v.as_u64()) {
render_kv_line(
"timeout",
&format!("{}ms", timeout),
content_w,
lines,
theme,
);
}
true
}
fn render_web_search_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(query) = parsed.get("query").and_then(|v| v.as_str()) {
render_kv_line("query", query, content_w, lines, theme);
}
if let Some(count) = parsed.get("count").and_then(|v| v.as_u64()) {
render_kv_line("count", &count.to_string(), content_w, lines, theme);
}
if let Some(search_type) = parsed.get("type").and_then(|v| v.as_str()) {
render_kv_line("type", search_type, content_w, lines, theme);
}
true
}
fn render_web_fetch_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(url) = parsed.get("url").and_then(|v| v.as_str()) {
render_kv_line("url", url, content_w, lines, theme);
}
if let Some(mode) = parsed.get("extract_mode").and_then(|v| v.as_str()) {
render_kv_line("mode", mode, content_w, lines, theme);
}
true
}
fn render_browser_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(action) = parsed.get("action").and_then(|v| v.as_str()) {
render_kv_line("action", action, content_w, lines, theme);
}
if let Some(url) = parsed.get("url").and_then(|v| v.as_str()) {
render_kv_line("url", url, content_w, lines, theme);
}
true
}
fn render_ask_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(questions) = parsed.get("questions").and_then(|v| v.as_array()) {
for (i, q) in questions.iter().enumerate() {
let question_text = q.get("question").and_then(|v| v.as_str()).unwrap_or("?");
let header = q
.get("header")
.and_then(|v| v.as_str())
.unwrap_or("question");
let label = if questions.len() > 1 {
format!("Q{} [{}]", i + 1, header)
} else {
header.to_string()
};
render_kv_line(&label, question_text, content_w, lines, theme);
if let Some(options) = q.get("options").and_then(|v| v.as_array()) {
let opts_preview: Vec<String> = options
.iter()
.filter_map(|o| o.get("label").and_then(|l| l.as_str()).map(String::from))
.collect();
if !opts_preview.is_empty() {
render_kv_line(
"options",
&opts_preview.join(" / "),
content_w,
lines,
theme,
);
}
}
}
}
true
}
fn render_todo_write_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(todos) = parsed.get("todos").and_then(|v| v.as_array()) {
render_tag_line(
&format!("待办列表 ({} 项)", todos.len()),
content_w,
lines,
theme,
);
for todo in todos {
let content = todo.get("content").and_then(|v| v.as_str()).unwrap_or("?");
let status = todo
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("pending");
let bullet = match status {
"completed" => "[x]",
"in_progress" => "[~]",
"cancelled" => "[-]",
_ => "[ ]",
};
let line_text = format!("{} {}", bullet, content);
let display = truncate_str(&line_text, content_w);
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(display, Style::default().fg(theme.text_dim)),
]));
}
}
true
}
fn render_todo_read_call_request_expanded(
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
render_tag_line("读取待办列表", content_w, lines, theme);
true
}
fn render_compact_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
render_tag_line("压缩对话上下文", content_w, lines, theme);
if let Some(focus) = parsed.get("focus").and_then(|v| v.as_str()) {
render_kv_line("focus", focus, content_w, lines, theme);
}
true
}
fn render_enter_plan_mode_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
render_tag_line("进入计划模式(只读模式)", content_w, lines, theme);
if let Some(desc) = parsed.get("description").and_then(|v| v.as_str()) {
render_kv_line("plan", desc, content_w, lines, theme);
}
true
}
fn render_load_skill_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
let name = parsed
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
render_tag_line(&format!("加载技能: {}", name), content_w, lines, theme);
if let Some(args) = parsed.get("arguments").and_then(|v| v.as_str()) {
render_kv_line("arguments", args, content_w, lines, theme);
}
true
}
fn render_register_hook_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("register");
render_tag_line(&format!("[{}]", action), content_w, lines, theme);
if let Some(event) = parsed.get("event").and_then(|v| v.as_str()) {
render_kv_line("event", event, content_w, lines, theme);
}
if let Some(hook_type) = parsed.get("type").and_then(|v| v.as_str()) {
render_kv_line("type", hook_type, content_w, lines, theme);
}
if let Some(command) = parsed.get("command").and_then(|v| v.as_str()) {
render_kv_line("command", command, content_w, lines, theme);
}
if let Some(prompt) = parsed.get("prompt").and_then(|v| v.as_str()) {
render_kv_line(
"prompt",
&truncate_str(prompt, 100),
content_w,
lines,
theme,
);
}
true
}
fn render_send_message_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
let to = parsed.get("to").and_then(|v| v.as_str());
if let Some(target) = to {
render_kv_line("to", &format!("@{}", target), content_w, lines, theme);
} else {
render_tag_line("广播消息", content_w, lines, theme);
}
if let Some(message) = parsed.get("message").and_then(|v| v.as_str()) {
render_kv_line(
"message",
&truncate_str(message, 100),
content_w,
lines,
theme,
);
}
true
}
fn render_worktree_call_request_expanded(
tool_name: &str,
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if tool_name == tool_names::ENTER_WORKTREE {
render_tag_line("进入隔离工作树", content_w, lines, theme);
if let Some(name) = parsed.get("name").and_then(|v| v.as_str()) {
render_kv_line("name", name, content_w, lines, theme);
}
} else {
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("keep");
render_tag_line("退出工作树", content_w, lines, theme);
render_kv_line("action", action, content_w, lines, theme);
}
true
}
fn render_work_done_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
render_tag_line("工作完成声明", content_w, lines, theme);
if let Some(summary) = parsed.get("summary").and_then(|v| v.as_str()) {
render_kv_line("summary", summary, content_w, lines, theme);
}
true
}
fn render_ignore_message_call_request_expanded(
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
render_tag_line("忽略消息", content_w, lines, theme);
true
}
fn render_computer_use_call_request_expanded(
arguments: &str,
content_w: usize,
lines: &mut Vec<Line<'static>>,
theme: &Theme,
) -> bool {
let parsed = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(v) => v,
Err(_) => return false,
};
if let Some(action) = parsed.get("action").and_then(|v| v.as_str()) {
render_kv_line("action", action, content_w, lines, theme);
}
if let Some(display_num) = parsed.get("display_number").and_then(|v| v.as_u64()) {
render_kv_line("display", &display_num.to_string(), content_w, lines, theme);
}
true
}