Skip to content

Commit d83f9e6

Browse files
committed
add visual diff display
1 parent b557d9d commit d83f9e6

9 files changed

Lines changed: 160 additions & 3 deletions

File tree

.sofosrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ src/
5555
│ ├── types.rs # Tool definitions for API
5656
│ └── mod.rs
5757
├── conversation.rs # Message history management
58+
├── diff.rs # Contextual diff generation and display
5859
├── history.rs # Session persistence + custom instructions
5960
├── repl.rs # Main REPL loop and display logic
6061
├── syntax.rs # Markdown/code syntax highlighting
@@ -71,6 +72,13 @@ src/
7172
- Handles serialization/deserialization for Anthropic API
7273
- Supports both regular and server-side tools (like web_search)
7374

75+
**src/diff.rs**
76+
- Generate contextual diffs showing only changed code blocks
77+
- Uses `similar` crate for accurate line-by-line diffing
78+
- Formats output with colored backgrounds (red for deletions, blue for additions)
79+
- Context lines (default: 2) show unchanged code around changes
80+
- Used by morph_edit_file tool to display what changed
81+
7482
**src/history.rs**
7583
- SessionMetadata: Preview and timestamps for session list
7684
- Session: Dual storage (api_messages + display_messages)
@@ -231,6 +239,7 @@ When waiting for Claude's response after tool execution:
231239
- `colored`: Terminal colors and formatting
232240
- `rustyline`: REPL with readline support
233241
- `clap`: Command-line argument parsing
242+
- `similar`: Text diffing for visual change display
234243

235244
### Optional Dependencies
236245
- `ripgrep`: Code search functionality (runtime check)

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ thiserror = "2.0"
2323
rustyline = "17.0"
2424
colored = "3.0"
2525
syntect = "5.2"
26+
similar = "2.7"
2627

2728
# Utilities
2829
futures = "0.3"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A blazingly fast, interactive AI coding assistant powered by Claude, implemented
1616
- **Code Search** - Fast regex-based code search using `ripgrep` (optional)
1717
- **Web Search** - Real-time web information via Claude's native search tool
1818
- **Bash Execution** - Run tests and build commands safely (read-only, sandboxed)
19+
- **Visual diff display** - See exactly what changed with colored diffs (red for deletions, blue for additions)
1920
- **Secure** - All operations restricted to workspace, prevents directory traversal
2021

2122
## Benchmark
@@ -26,7 +27,7 @@ A blazingly fast, interactive AI coding assistant powered by Claude, implemented
2627
- **Sofos Code:** 1m 40s ⚡
2728
- **Claude Code CLI:** 4m 35s
2829

29-
**~2.8x faster** (both with Morph Apply and `ripgrep` enabled).
30+
**~2.8x faster** (both tests with Morph Apply and `ripgrep` enabled).
3031

3132
## Installation
3233

docs/DIFF_DISPLAY.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Morph Edit with Visual Diff Example
2+
3+
When using `morph_edit_file`, Sofos now displays a contextual diff showing exactly what changed.
4+
5+
## Example Output
6+
7+
```
8+
Using tool: morph_edit_file
9+
10+
Successfully applied Morph edit to 'example.js'
11+
12+
Changes:
13+
function example() {
14+
- var x = 1;
15+
+ const x = 1;
16+
- var y = 2;
17+
+ let y = 2;
18+
- var z = 3;
19+
+ const z = 3;
20+
21+
console.log(x + y + z);
22+
```
23+
24+
## Features
25+
26+
- **Contextual Display**: Shows only changed blocks with 2 lines of surrounding context
27+
- **Color Coding**:
28+
- Lines starting with `-` have RED background with BLACK text (deletions)
29+
- Lines starting with `+` have BLUE background with BLACK text (additions)
30+
- Context lines show with normal formatting (starting with two spaces)
31+
- **Multiple Hunks**: If changes are separated, they're shown with `...` separator
32+
- **Compact**: No need to see the entire file, just what changed
33+
34+
## Technical Details
35+
36+
- Uses the `similar` crate for accurate line-by-line diffing
37+
- Default context: 2 lines before and after changes
38+
- Groups nearby changes into single hunks for readability
39+
- Automatically handles edge cases (beginning/end of file)
40+
41+
## Comparison with Other Tools
42+
43+
This is similar to `git diff` output but optimized for terminal viewing with colored backgrounds instead of just prefixes.

src/diff.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use colored::Colorize;
2+
use similar::{ChangeTag, TextDiff};
3+
4+
/// Generate a contextual diff display showing only changed blocks
5+
/// Returns a formatted string with colored additions (blue +) and deletions (red -)
6+
pub fn generate_contextual_diff(original: &str, modified: &str, context_lines: usize) -> String {
7+
let diff = TextDiff::from_lines(original, modified);
8+
let mut output = Vec::new();
9+
10+
for (idx, group) in diff.grouped_ops(context_lines).iter().enumerate() {
11+
if idx > 0 {
12+
// Add separator between hunks
13+
output.push("".to_string());
14+
output.push("...".dimmed().to_string());
15+
output.push("".to_string());
16+
}
17+
18+
for op in group {
19+
for change in diff.iter_changes(op) {
20+
let s: String = match change.tag() {
21+
ChangeTag::Delete => {
22+
let line = format!("- {}", change.value().trim_end());
23+
line.on_red().black().to_string()
24+
}
25+
ChangeTag::Insert => {
26+
let line = format!("+ {}", change.value().trim_end());
27+
line.on_blue().black().to_string()
28+
}
29+
ChangeTag::Equal => {
30+
let line = format!(" {}", change.value().trim_end());
31+
line.normal().to_string()
32+
}
33+
};
34+
35+
output.push(s);
36+
}
37+
}
38+
}
39+
40+
output.join("\n")
41+
}
42+
43+
/// Generate a compact diff showing only changed lines with minimal context
44+
pub fn generate_compact_diff(original: &str, modified: &str) -> String {
45+
generate_contextual_diff(original, modified, 2)
46+
}
47+
48+
#[cfg(test)]
49+
mod tests {
50+
use super::*;
51+
52+
#[test]
53+
fn test_simple_diff() {
54+
let original = "line 1\nline 2\nline 3\n";
55+
let modified = "line 1\nline 2 modified\nline 3\n";
56+
57+
let diff = generate_compact_diff(original, modified);
58+
assert!(diff.contains("line 2"));
59+
}
60+
61+
#[test]
62+
fn test_multiple_changes() {
63+
let original = "var x = 1;\nvar y = 2;\nvar z = 3;\n";
64+
let modified = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
65+
66+
let diff = generate_compact_diff(original, modified);
67+
assert!(diff.contains("-"));
68+
assert!(diff.contains("+"));
69+
}
70+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod api;
22
mod cli;
33
mod conversation;
4+
mod diff;
45
mod error;
56
mod history;
67
mod repl;

src/repl.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,10 @@ impl Repl {
744744
format!("Found {} items in {}", item_count, path.bright_cyan())
745745
}
746746
}
747+
"morph_edit_file" => {
748+
// For morph edits, show the full output including the diff
749+
output.to_string()
750+
}
747751
_ => {
748752
// For all other tools, return the full output
749753
output.to_string()

src/tools/mod.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod filesystem;
44
pub mod types;
55

66
use crate::api::MorphClient;
7+
use crate::diff;
78
use crate::error::{Result, SofosError};
89
use bashexec::BashExecutor;
910
use codesearch::CodeSearchTool;
@@ -91,8 +92,21 @@ impl ToolExecutor {
9192
SofosError::ToolExecution("Missing 'content' parameter".to_string())
9293
})?;
9394

95+
// Check if file exists and read original content for diff
96+
let original_content = self.fs_tool.read_file(path).ok();
97+
9498
self.fs_tool.write_file(path, content)?;
95-
Ok(format!("Successfully wrote to file '{}'", path))
99+
100+
// If file existed before, show diff
101+
if let Some(original) = original_content {
102+
let diff_output = diff::generate_compact_diff(&original, content);
103+
Ok(format!(
104+
"Successfully wrote to file '{}'\n\nChanges:\n{}",
105+
path, diff_output
106+
))
107+
} else {
108+
Ok(format!("Successfully created file '{}'", path))
109+
}
96110
}
97111
"list_directory" => {
98112
let path = input["path"].as_str().ok_or_else(|| {
@@ -151,7 +165,14 @@ impl ToolExecutor {
151165
.await?;
152166

153167
self.fs_tool.write_file(path, &merged_code)?;
154-
Ok(format!("Successfully applied Morph edit to '{}'", path))
168+
169+
// Generate diff for display
170+
let diff_output = diff::generate_compact_diff(&original_code, &merged_code);
171+
172+
Ok(format!(
173+
"Successfully applied Morph edit to '{}'\n\nChanges:\n{}",
174+
path, diff_output
175+
))
155176
}
156177
"delete_file" => {
157178
let path = input["path"].as_str().ok_or_else(|| {

0 commit comments

Comments
 (0)