Skip to main content

mcp_rtk/
diff.rs

1//! Side-by-side diff display for raw vs filtered tool responses.
2
3use crate::display::*;
4
5/// Display a colored diff between raw and filtered JSON strings.
6///
7/// Shows lines present only in raw (removed by filtering) in red,
8/// lines present only in filtered in green, and common lines dimmed.
9pub fn print_diff(raw: &str, filtered: &str, tool: &str, preset: Option<&str>) {
10    let raw_pretty = prettify(raw);
11    let filtered_pretty = prettify(filtered);
12
13    let raw_lines: Vec<&str> = raw_pretty.lines().collect();
14    let filtered_lines: Vec<&str> = filtered_pretty.lines().collect();
15
16    // Header
17    eprintln!();
18    eprintln!("  {BOLD}{GREEN}mcp-rtk{RESET}{DIM} — diff{RESET}");
19    eprintln!("  {DIM}{}{RESET}", "─".repeat(56));
20    eprintln!();
21    eprintln!("  {DIM}Tool:{RESET}    {BOLD}{tool}{RESET}");
22    if let Some(p) = preset {
23        eprintln!("  {DIM}Preset:{RESET}  {BOLD}{p}{RESET}");
24    }
25
26    let input_bytes = raw.len();
27    let output_bytes = filtered.len();
28    let saved = input_bytes.saturating_sub(output_bytes);
29    let pct = if input_bytes > 0 {
30        (saved as f64 / input_bytes as f64) * 100.0
31    } else {
32        0.0
33    };
34    let pct_color = pct_to_color(pct);
35
36    eprintln!(
37        "  {DIM}Input:{RESET}   {} bytes (~{} tokens)",
38        input_bytes,
39        input_bytes / 4
40    );
41    eprintln!(
42        "  {DIM}Output:{RESET}  {} bytes (~{} tokens)",
43        output_bytes,
44        output_bytes / 4
45    );
46    eprintln!("  {DIM}Saved:{RESET}   {pct_color}{BOLD}{saved} bytes ({pct:.1}%){RESET}");
47    eprintln!();
48    eprintln!("  {RED}--- raw{RESET}    {GREEN}+++ filtered{RESET}");
49    eprintln!("  {DIM}{}{RESET}", "─".repeat(56));
50
51    // Simple line-based diff using longest common subsequence
52    let ops = compute_diff(&raw_lines, &filtered_lines);
53    for op in &ops {
54        match op {
55            DiffOp::Equal(line) => {
56                println!("  {DIM} {line}{RESET}");
57            }
58            DiffOp::Remove(line) => {
59                println!("  {RED}-{line}{RESET}");
60            }
61            DiffOp::Add(line) => {
62                println!("  {GREEN}+{line}{RESET}");
63            }
64        }
65    }
66    eprintln!();
67}
68
69fn prettify(s: &str) -> String {
70    serde_json::from_str::<serde_json::Value>(s)
71        .ok()
72        .and_then(|v| serde_json::to_string_pretty(&v).ok())
73        .unwrap_or_else(|| s.to_string())
74}
75
76enum DiffOp<'a> {
77    Equal(&'a str),
78    Remove(&'a str),
79    Add(&'a str),
80}
81
82const MAX_DIFF_LINES: usize = 10_000;
83
84fn compute_diff<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<DiffOp<'a>> {
85    let m = old.len().min(MAX_DIFF_LINES);
86    let n = new.len().min(MAX_DIFF_LINES);
87    let old = &old[..m];
88    let new = &new[..n];
89
90    // Build LCS table
91    let mut table = vec![vec![0u32; n + 1]; m + 1];
92    for i in 1..=m {
93        for j in 1..=n {
94            if old[i - 1] == new[j - 1] {
95                table[i][j] = table[i - 1][j - 1] + 1;
96            } else {
97                table[i][j] = table[i - 1][j].max(table[i][j - 1]);
98            }
99        }
100    }
101
102    // Backtrack to produce diff
103    let mut ops = Vec::new();
104    let mut i = m;
105    let mut j = n;
106    while i > 0 || j > 0 {
107        if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
108            ops.push(DiffOp::Equal(old[i - 1]));
109            i -= 1;
110            j -= 1;
111        } else if j > 0 && (i == 0 || table[i][j - 1] >= table[i - 1][j]) {
112            ops.push(DiffOp::Add(new[j - 1]));
113            j -= 1;
114        } else {
115            ops.push(DiffOp::Remove(old[i - 1]));
116            i -= 1;
117        }
118    }
119
120    ops.reverse();
121    ops
122}