tool_summary.rs (5173B)
1 //! Formatting utilities for tool execution summaries shown in the UI. 2 //! 3 //! These functions convert raw tool inputs and outputs into human-readable 4 //! summary strings that are displayed to users after tool execution. 5 6 /// Extract string content from a tool response, handling various JSON structures 7 pub fn extract_response_content(response: &serde_json::Value) -> Option<String> { 8 // Try direct string first 9 if let Some(s) = response.as_str() { 10 return Some(s.to_string()); 11 } 12 // Try "content" field (common wrapper) 13 if let Some(s) = response.get("content").and_then(|v| v.as_str()) { 14 return Some(s.to_string()); 15 } 16 // Try file.content for Read tool responses 17 if let Some(s) = response 18 .get("file") 19 .and_then(|f| f.get("content")) 20 .and_then(|v| v.as_str()) 21 { 22 return Some(s.to_string()); 23 } 24 // Try "output" field 25 if let Some(s) = response.get("output").and_then(|v| v.as_str()) { 26 return Some(s.to_string()); 27 } 28 // Try "result" field 29 if let Some(s) = response.get("result").and_then(|v| v.as_str()) { 30 return Some(s.to_string()); 31 } 32 // Fallback: serialize the whole response if it's not null 33 if !response.is_null() { 34 return Some(response.to_string()); 35 } 36 None 37 } 38 39 /// Format a human-readable summary for tool execution results 40 pub fn format_tool_summary( 41 tool_name: &str, 42 input: &serde_json::Value, 43 response: &serde_json::Value, 44 ) -> String { 45 match tool_name { 46 "Read" => format_read_summary(input, response), 47 "Write" => format_write_summary(input), 48 "Bash" => format_bash_summary(input, response), 49 "Grep" => format_grep_summary(input), 50 "Glob" => format_glob_summary(input), 51 "Edit" => format_edit_summary(input), 52 "Task" => format_task_summary(input), 53 _ => String::new(), 54 } 55 } 56 57 fn format_read_summary(input: &serde_json::Value, response: &serde_json::Value) -> String { 58 let file = input 59 .get("file_path") 60 .and_then(|v| v.as_str()) 61 .unwrap_or("?"); 62 let filename = file.rsplit('/').next().unwrap_or(file); 63 // Try to get numLines directly from file metadata (most accurate) 64 let lines = response 65 .get("file") 66 .and_then(|f| f.get("numLines").or_else(|| f.get("totalLines"))) 67 .and_then(|v| v.as_u64()) 68 .map(|n| n as usize) 69 // Fallback to counting lines in content 70 .or_else(|| { 71 extract_response_content(response) 72 .as_ref() 73 .map(|s| s.lines().count()) 74 }) 75 .unwrap_or(0); 76 format!("{} ({} lines)", filename, lines) 77 } 78 79 fn format_write_summary(input: &serde_json::Value) -> String { 80 let file = input 81 .get("file_path") 82 .and_then(|v| v.as_str()) 83 .unwrap_or("?"); 84 let filename = file.rsplit('/').next().unwrap_or(file); 85 let bytes = input 86 .get("content") 87 .and_then(|v| v.as_str()) 88 .map(|s| s.len()) 89 .unwrap_or(0); 90 format!("{} ({} bytes)", filename, bytes) 91 } 92 93 fn format_bash_summary(input: &serde_json::Value, response: &serde_json::Value) -> String { 94 let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or(""); 95 // Truncate long commands 96 let cmd_display = if cmd.len() > 40 { 97 format!("{}...", &cmd[..37]) 98 } else { 99 cmd.to_string() 100 }; 101 let output_len = extract_response_content(response) 102 .as_ref() 103 .map(|s| s.len()) 104 .unwrap_or(0); 105 if output_len > 0 { 106 format!("`{}` ({} chars)", cmd_display, output_len) 107 } else { 108 format!("`{}`", cmd_display) 109 } 110 } 111 112 fn format_grep_summary(input: &serde_json::Value) -> String { 113 let pattern = input.get("pattern").and_then(|v| v.as_str()).unwrap_or("?"); 114 format!("'{}'", pattern) 115 } 116 117 fn format_glob_summary(input: &serde_json::Value) -> String { 118 let pattern = input.get("pattern").and_then(|v| v.as_str()).unwrap_or("?"); 119 format!("'{}'", pattern) 120 } 121 122 fn format_edit_summary(input: &serde_json::Value) -> String { 123 let file = input 124 .get("file_path") 125 .and_then(|v| v.as_str()) 126 .unwrap_or("?"); 127 let filename = file.rsplit('/').next().unwrap_or(file); 128 filename.to_string() 129 } 130 131 fn format_task_summary(input: &serde_json::Value) -> String { 132 let description = input 133 .get("description") 134 .and_then(|v| v.as_str()) 135 .unwrap_or("task"); 136 let subagent_type = input 137 .get("subagent_type") 138 .and_then(|v| v.as_str()) 139 .unwrap_or("unknown"); 140 format!("{} ({})", description, subagent_type) 141 } 142 143 /// Truncate output to a maximum size, keeping the end (most recent) content 144 pub fn truncate_output(output: &str, max_size: usize) -> String { 145 if output.len() <= max_size { 146 output.to_string() 147 } else { 148 let start = output.len() - max_size; 149 // Find a newline near the start to avoid cutting mid-line 150 let adjusted_start = output[start..] 151 .find('\n') 152 .map(|pos| start + pos + 1) 153 .unwrap_or(start); 154 format!("...\n{}", &output[adjusted_start..]) 155 } 156 }