notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }