mod json_scan;
mod metadata;
mod sanitize;
mod subagents;
mod summary;
use sanitize::sanitize_read_target;
use sanitize::sanitize_tool_label;
use sanitize::sanitize_tool_params;
use sanitize::sanitize_read_metadata;
use sanitize::sanitize_detail_json;
use sanitize::sanitize_detail_output;
use json_scan::json_string_boundary_map;
use json_scan::parse_json_string_at;
use json_scan::skip_diff_prefixed_value_start;
use json_scan::skip_json_whitespace;
use sanitize::sanitize_read_block_output;
use json_scan::encode_json_string_fragment;
use json_scan::json_value_end;
use subagents::subagents_safe_output;
use metadata::append_metadata_lines;
use subagents::subagent_identity_list;
use metadata::grep_pattern_label;
use summary::append_bash_protection_detail;
pub(crate) use summary::bash_file_tracking_summary;
pub(crate) use summary::bash_protection_summary;
pub(crate) use summary::humanize_protection_summary;
pub(crate) use summary::pending_tool_summary_metadata;
pub(crate) use summary::prompt_injection_protection_summary;
pub(crate) use summary::tool_summary_metadata;
use summary::file_count_label;
use summary::string_arg;
use summary::task_count_label;
use crate::{
output::{
ActivityStatus, ToolActivityDetail, ToolStatus, is_credential_like_key,
redact_sensitive_text, sanitize_display_controls, sanitize_display_text,
},
providers::ToolCall,
subagents::SubagentsOutput,
tools::{
ToolCapability, ToolResult,
contract::{
REDACTED_METADATA_KEYS, arg_key, display_label as label, metadata_key as meta,
summary_metadata_label as summary_label,
},
},
};
use serde_json::Value;
use std::sync::Arc;
pub(crate) fn tool_activity_detail_pending(
call: &ToolCall,
label: impl Into<String>,
status: ActivityStatus,
) -> ToolActivityDetail {
let metadata = if is_tool(&call.name, ToolCapability::HashEdit) {
call.arguments
.get(arg_key::INPUT)
.and_then(Value::as_str)
.map(|input| serde_json::json!({"input_bytes": input.len()}))
.unwrap_or_else(|| serde_json::json!({}))
} else {
serde_json::json!({})
};
ToolActivityDetail {
tool_name: Arc::from(sanitize_display_text(&call.name)),
label: Arc::from(sanitize_tool_label(call, &label.into())),
params: sanitize_tool_params(call),
metadata,
status,
output: Arc::from(""),
applied_diff: None,
}
}
pub(crate) fn tool_activity_detail(
call: &ToolCall,
result: &ToolResult,
label: impl Into<String>,
status: ActivityStatus,
) -> ToolActivityDetail {
let read = is_tool(&call.name, ToolCapability::Read);
let mut metadata = if read {
sanitize_read_metadata(&result.metadata)
} else {
sanitize_detail_json(&result.metadata)
};
if call.name == crate::code_mode::TOOL_NAME {
let mut response = crate::protection::prompt_injection::replay_projection(
&result.content,
&result.metadata["prompt_injection_protection"],
)
.unwrap_or_else(|| result.content.clone());
crate::code_mode::preserve_execution_evidence(result, &mut response);
if !metadata.is_object() {
metadata = serde_json::json!({});
}
metadata["code_mode_response_token_estimate"] =
serde_json::json!(crate::context::estimate_text_tokens(&response));
}
ToolActivityDetail {
tool_name: Arc::from(sanitize_display_text(&call.name)),
label: Arc::from(sanitize_tool_label(call, &label.into())),
params: sanitize_tool_params(call),
metadata,
status,
output: Arc::from(if read && !result.success {
sanitize_read_target(&result.content)
} else {
sanitize_detail_output(&result.content)
}),
applied_diff: result
.display
.edit_diff
.as_deref()
.map(sanitize_detail_output)
.map(Arc::from),
}
}
pub(crate) fn format_tool_block(call: &ToolCall, result: &ToolResult) -> String {
format_tool_block_with_options(call, result)
}
fn format_tool_block_with_options(call: &ToolCall, result: &ToolResult) -> String {
let tool_name = if result.tool_name.is_empty() {
call.name.as_str()
} else {
result.tool_name.as_str()
};
let use_subagents_safe_output = is_tool(tool_name, ToolCapability::Subagents);
let mut lines = Vec::new();
lines.push(format!(
"STATUS: {}",
if result.success { "success" } else { "failure" }
));
append_metadata_lines(&mut lines, call, result);
if should_display_output(&call.name, result) {
lines.push("OUTPUT:".to_string());
}
let mut block = lines.join("\n");
block.push('\n');
if should_display_output(&call.name, result) {
let display_content = if use_subagents_safe_output {
subagents_safe_output(result)
} else if is_tool(tool_name, ToolCapability::Read) {
sanitize_read_block_output(result)
} else {
redact_sensitive_text(&result.content)
};
block.push_str(&display_content);
if !display_content.ends_with('\n') {
block.push('\n');
}
}
block
}
fn should_display_output(_call_name: &str, result: &ToolResult) -> bool {
!result.content.is_empty()
}
fn is_tool(name: &str, capability: ToolCapability) -> bool {
ToolCapability::from_dispatch_name(name) == Some(capability)
}
pub(crate) fn tool_display_label(call: &ToolCall) -> String {
let label = tool_label(call);
if ToolCapability::from_dispatch_name(&call.name) == Some(ToolCapability::Read) {
sanitize_read_target(&label)
} else {
redact_sensitive_text(&label)
}
}
fn tool_label(call: &ToolCall) -> String {
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Bash) => string_arg(&call.arguments, arg_key::COMMAND)
.or_else(|| string_arg(&call.arguments, arg_key::CMD))
.map(|command| format!("{} {command}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::HashEdit | ToolCapability::Write) => {
string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Read | ToolCapability::ViewImage) => {
string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.or_else(|| {
call.arguments
.get(arg_key::PATHS)
.and_then(Value::as_array)
.map(|paths| format!("{} {}", call.name, file_count_label(paths.len())))
})
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Grep) => grep_pattern_label(call),
Some(ToolCapability::AstGrep) => string_arg(&call.arguments, arg_key::PATTERN)
.map(|pattern| format!("{} {pattern}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::Find) => string_arg(&call.arguments, arg_key::QUERY)
.map(|query| format!("{} {query}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::ListFiles) => string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::Subagents) => call
.arguments
.get(arg_key::TASKS)
.and_then(Value::as_array)
.map(|tasks| format!("subagents ยท {}", task_count_label(tasks.len())))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::Web) => string_arg(&call.arguments, arg_key::QUERY)
.map(|query| format!("{} {query}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::MagiControl) => string_arg(&call.arguments, arg_key::ACTION)
.map(|action| format!("{} {action}", call.name))
.unwrap_or_else(|| call.name.clone()),
None => call.name.clone(),
}
}
pub(crate) fn should_show_pending_tool(call: &ToolCall) -> bool {
matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(
ToolCapability::Write
| ToolCapability::HashEdit
| ToolCapability::ViewImage
| ToolCapability::Find
| ToolCapability::Web,
)
)
}
pub(crate) fn pending_tool_status(call: &ToolCall) -> ToolStatus {
if matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(ToolCapability::Write)
) {
ToolStatus::Writing
} else {
ToolStatus::Running
}
}