pub(crate) fn slugify(name: &str) -> String {
let mut slug = String::new();
let mut prev_dash = false;
for ch in name.trim().to_lowercase().chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch);
prev_dash = false;
} else if !prev_dash {
slug.push('-');
prev_dash = true;
}
}
let slug = slug.trim_matches('-').to_string();
if slug.is_empty() {
"rec".to_string()
} else {
slug
}
}
pub(crate) fn truncate(text: &str, max: usize) -> String {
let one_line: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
if one_line.chars().count() <= max {
one_line
} else {
let head: String = one_line.chars().take(max).collect();
format!("{head}…")
}
}
fn strip_leading_cd(command: &str) -> String {
let mut cmd = command.trim();
while let Some(after_cd) = cmd.strip_prefix("cd ") {
let nl = after_cd.find('\n');
let semi = after_cd.find(';');
let amp = after_cd.find("&&");
let Some(pos) = [nl, semi, amp].into_iter().flatten().min() else {
break; };
let sep_len = if after_cd[pos..].starts_with("&&") {
2
} else {
1
};
let next = after_cd[pos + sep_len..].trim_start();
if next.is_empty() {
break;
}
cmd = next;
}
cmd.to_string()
}
pub(crate) fn summarize_input(tool_name: &str, input: &serde_json::Value) -> String {
let field = |key: &str| input.get(key).and_then(|v| v.as_str()).map(str::to_string);
let raw = match tool_name {
"Bash" => field("command").map(|c| strip_leading_cd(&c)),
"Read" | "Write" | "Edit" | "MultiEdit" | "NotebookEdit" => field("file_path"),
"Glob" => field("pattern"),
"Grep" => field("pattern").map(|p| {
field("path")
.map(|path| format!("{p} in {path}"))
.unwrap_or(p)
}),
"WebFetch" | "WebSearch" => field("url").or_else(|| field("query")),
name if is_computer_use(name) => Some(describe_computer_use(name, input)),
_ => None,
};
let raw = raw.unwrap_or_else(|| describe_unknown(input));
truncate(&raw, 160)
}
pub(crate) fn is_computer_use(tool_name: &str) -> bool {
let t = tool_name.to_ascii_lowercase();
t == "computer" || t.contains("computer_use") || t.contains("computer-use")
}
fn describe_computer_use(tool_name: &str, input: &serde_json::Value) -> String {
let serde_json::Value::Object(map) = input else {
return action_verb_from_name(tool_name).to_string();
};
let verb = map
.get("action")
.and_then(|v| v.as_str())
.unwrap_or_else(|| action_verb_from_name(tool_name));
if let Some(actions) = map.get("actions").and_then(|v| v.as_array()) {
let parts: Vec<String> = actions
.iter()
.filter_map(|a| {
let m = a.as_object()?;
let sub = m.get("action").and_then(|v| v.as_str()).unwrap_or("action");
Some(render_action(sub, m))
})
.collect();
if !parts.is_empty() {
return format!("{verb} ×{}: {}", parts.len(), parts.join(", "));
}
}
render_action(verb, map)
}
fn action_verb_from_name(tool_name: &str) -> &str {
tool_name.rsplit("__").next().unwrap_or(tool_name)
}
fn render_action(verb: &str, map: &serde_json::Map<String, serde_json::Value>) -> String {
let str_of = |keys: &[&str]| {
keys.iter()
.find_map(|k| map.get(*k).and_then(|v| v.as_str()))
};
if let Some(coord) = coordinate_str(map) {
if let Some(dir) = str_of(&["scroll_direction"]) {
return format!("{verb} {dir} {coord}");
}
return format!("{verb} {coord}");
}
if let Some(text) = str_of(&["text", "key"]) {
return format!("{verb} \"{text}\"");
}
if let Some(app) = str_of(&["app", "application", "bundleId", "name"]) {
return format!("{verb} \"{app}\"");
}
if let Some(apps) = map.get("apps").and_then(|v| v.as_array()) {
let names: Vec<&str> = apps.iter().filter_map(|v| v.as_str()).collect();
if !names.is_empty() {
return format!("{verb} \"{}\"", names.join(", "));
}
}
verb.to_string()
}
fn coordinate_str(map: &serde_json::Map<String, serde_json::Value>) -> Option<String> {
match map.get("coordinate")? {
serde_json::Value::Array(a) if a.len() == 2 => Some(format!("({},{})", a[0], a[1])),
serde_json::Value::Object(o) => match (o.get("x"), o.get("y")) {
(Some(x), Some(y)) => Some(format!("({x},{y})")),
_ => None,
},
_ => None,
}
}
fn describe_unknown(input: &serde_json::Value) -> String {
let serde_json::Value::Object(map) = input else {
return match input {
serde_json::Value::Null => "(no input)".to_string(),
other => other.to_string(),
};
};
const INFORMATIVE: &[&str] = &[
"url",
"selector",
"text",
"query",
"path",
"file_path",
"command",
"name",
"message",
"body",
"content",
"pattern",
"value",
"key",
];
let mut shown: Vec<String> = Vec::new();
for key in INFORMATIVE {
if let Some(value) = map.get(*key).and_then(|v| v.as_str())
&& !value.trim().is_empty()
{
shown.push(format!("{key}={value}"));
if shown.len() == 2 {
break;
}
}
}
if shown.is_empty() {
let keys: Vec<&str> = map.keys().map(String::as_str).collect();
format!("fields: {}", keys.join(", "))
} else {
shown.join(" · ")
}
}
#[cfg(test)]
mod tests {
use super::{is_computer_use, slugify, summarize_input, truncate};
#[test]
fn slugify_normalizes_names() {
assert_eq!(slugify("Git Change Summary"), "git-change-summary");
assert_eq!(slugify(" weird__name!! "), "weird-name");
assert_eq!(slugify("!!!"), "rec");
}
#[test]
fn truncate_collapses_and_caps() {
assert_eq!(truncate("a b c", 80), "a b c");
assert!(truncate(&"x".repeat(200), 10).ends_with('…'));
}
#[test]
fn summarize_strips_leading_cd_boilerplate() {
assert_eq!(
summarize_input(
"Bash",
&serde_json::json!({ "command": "cd /a/b/c\ngit log --oneline" })
),
"git log --oneline"
);
assert_eq!(
summarize_input(
"Bash",
&serde_json::json!({ "command": "cd /x && cd /y && cargo test" })
),
"cargo test"
);
assert_eq!(
summarize_input("Bash", &serde_json::json!({ "command": "cd /only" })),
"cd /only"
);
}
#[test]
fn summarize_reads_tool_specific_fields() {
assert_eq!(
summarize_input("Bash", &serde_json::json!({ "command": "git status" })),
"git status"
);
assert_eq!(
summarize_input("Write", &serde_json::json!({ "file_path": "/tmp/x.md" })),
"/tmp/x.md"
);
assert_eq!(
summarize_input("Unknown", &serde_json::json!({ "a": 1, "b": 2 })),
"fields: a, b"
);
}
#[test]
fn summarize_renders_computer_use_actions() {
assert!(is_computer_use("mcp__computer-use__computer"));
assert!(is_computer_use("computer"));
assert!(!is_computer_use("Bash"));
assert_eq!(
summarize_input(
"mcp__computer-use__computer",
&serde_json::json!({ "action": "left_click", "coordinate": [812, 344] })
),
"left_click (812,344)"
);
assert_eq!(
summarize_input(
"mcp__computer-use__computer",
&serde_json::json!({ "action": "type", "text": "42.50" })
),
"type \"42.50\""
);
assert_eq!(
summarize_input("computer", &serde_json::json!({ "action": "screenshot" })),
"screenshot"
);
}
#[test]
fn summarize_renders_per_action_computer_use_server() {
assert_eq!(
summarize_input("mcp__computer-use__screenshot", &serde_json::json!({})),
"screenshot"
);
assert_eq!(
summarize_input(
"mcp__computer-use__left_click",
&serde_json::json!({ "coordinate": [398, 339] })
),
"left_click (398,339)"
);
assert_eq!(
summarize_input(
"mcp__computer-use__open_application",
&serde_json::json!({ "app": "Calculadora" })
),
"open_application \"Calculadora\""
);
assert_eq!(
summarize_input(
"mcp__computer-use__request_access",
&serde_json::json!({ "apps": ["Calculadora"], "reason": "demo" })
),
"request_access \"Calculadora\""
);
assert_eq!(
summarize_input(
"mcp__computer-use__type",
&serde_json::json!({ "text": "42" })
),
"type \"42\""
);
}
#[test]
fn summarize_renders_a_computer_batch_as_its_sequence() {
let summary = summarize_input(
"mcp__computer-use__computer_batch",
&serde_json::json!({ "actions": [
{ "action": "left_click", "coordinate": [398, 339] },
{ "action": "left_click", "coordinate": [372, 388] },
] }),
);
assert!(summary.starts_with("computer_batch ×2:"), "{summary}");
assert!(summary.contains("left_click (398,339)"), "{summary}");
assert!(summary.contains("left_click (372,388)"), "{summary}");
}
#[test]
fn summarize_renders_browser_and_mcp_tool_values() {
assert_eq!(
summarize_input(
"mcp__playwright__browser_navigate",
&serde_json::json!({ "url": "https://app.example.com/expenses" })
),
"url=https://app.example.com/expenses"
);
assert_eq!(
summarize_input(
"mcp__playwright__browser_type",
&serde_json::json!({ "selector": "#amount", "text": "42.50" })
),
"selector=#amount · text=42.50"
);
}
}