Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions crates/chat-cli/src/cli/chat/cli/diff_tool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::path::Path;
use uuid::Uuid;

use crate::cli::chat::ChatError;
use crate::util::env_var::try_get_diff_tool;

/// Check if custom diff tool is configured
pub fn has_diff_tool() -> bool {
try_get_diff_tool().is_ok()
}

/// Launch custom diff tool with two file paths
pub fn launch_diff_tool(before_path: &Path, after_path: &Path) -> Result<(), ChatError> {
let diff_tool_cmd = try_get_diff_tool()
.map_err(|_| ChatError::Custom("Q_DIFF_TOOL not configured".into()))?;

let mut parts = shlex::split(&diff_tool_cmd)
.ok_or_else(|| ChatError::Custom("Failed to parse Q_DIFF_TOOL command".into()))?;

if parts.is_empty() {
return Err(ChatError::Custom("Q_DIFF_TOOL is empty".into()));
}

let tool_bin = parts.remove(0);
let mut cmd = std::process::Command::new(tool_bin);

for arg in parts {
cmd.arg(arg);
}

cmd
.arg(before_path)
.arg(after_path)
.status()
.map_err(|e| ChatError::Custom(format!("Failed to launch diff tool: {}", e).into()))?;

Ok(())
}

/// Create temporary files and launch diff tool with content
pub fn diff_with_tool(before_content: &str, after_content: &str, label: &str) -> Result<(), ChatError> {
let temp_dir = std::env::temp_dir();
let uuid = Uuid::new_v4();

// Sanitize label to create valid filename
let safe_label = label.replace(['/', '\\', ':'], "_");

let before_file = temp_dir.join(format!("q_diff_before_{}_{}.txt", safe_label, uuid));
let after_file = temp_dir.join(format!("q_diff_after_{}_{}.txt", safe_label, uuid));

std::fs::write(&before_file, before_content)
.map_err(|e| ChatError::Custom(format!("Failed to create before file: {}", e).into()))?;

std::fs::write(&after_file, after_content)
.map_err(|e| ChatError::Custom(format!("Failed to create after file: {}", e).into()))?;

// Don't cleanup - let OS temp directory cleanup handle it
// Files need to persist for async tools and approval flow
launch_diff_tool(&before_file, &after_file)
}
1 change: 1 addition & 0 deletions crates/chat-cli/src/cli/chat/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod checkpoint;
pub mod clear;
pub mod compact;
pub mod context;
pub mod diff_tool;
pub mod editor;
pub mod experiment;
pub mod hooks;
Expand Down
39 changes: 35 additions & 4 deletions crates/chat-cli/src/cli/chat/tools/fs_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ impl FsWrite {
Default::default()
};
let new = stylize_output_if_able(&relative_path, &file_text);
print_diff(output, &prev, &new, 1)?;
print_diff_or_launch_tool(output, &prev, &new, 1, &relative_path.to_string_lossy())?;

// Display summary as purpose if available after the diff
super::display_purpose(self.get_summary(), output)?;
Expand Down Expand Up @@ -345,7 +345,7 @@ impl FsWrite {

let old = stylize_output_if_able(&relative_path, &old);
let new = stylize_output_if_able(&relative_path, &new);
print_diff(output, &old, &new, start_line)?;
print_diff_or_launch_tool(output, &old, &new, start_line, &relative_path.to_string_lossy())?;

// Display summary as purpose if available after the diff
super::display_purpose(self.get_summary(), output)?;
Expand All @@ -364,7 +364,7 @@ impl FsWrite {
};
let old_str = stylize_output_if_able(&relative_path, old_str);
let new_str = stylize_output_if_able(&relative_path, new_str);
print_diff(output, &old_str, &new_str, start_line)?;
print_diff_or_launch_tool(output, &old_str, &new_str, start_line, &relative_path.to_string_lossy())?;

// Display summary as purpose if available after the diff
super::display_purpose(self.get_summary(), output)?;
Expand All @@ -376,7 +376,7 @@ impl FsWrite {
let relative_path = format_path(cwd, &path);
let start_line = os.fs.read_to_string_sync(&path)?.lines().count() + 1;
let file = stylize_output_if_able(&relative_path, new_str);
print_diff(output, &Default::default(), &file, start_line)?;
print_diff_or_launch_tool(output, &Default::default(), &file, start_line, &relative_path.to_string_lossy())?;

// Display summary as purpose if available after the diff
super::display_purpose(self.get_summary(), output)?;
Expand Down Expand Up @@ -633,6 +633,37 @@ fn get_lines_with_context(
)
}

/// Prints diff using custom tool if configured, otherwise falls back to inline diff
fn print_diff_or_launch_tool(
output: &mut impl Write,
old_str: &StylizedFile,
new_str: &StylizedFile,
start_line: usize,
label: &str,
) -> Result<()> {
use crate::cli::chat::cli::diff_tool;

if diff_tool::has_diff_tool() {
// Strip all ANSI codes for external tools
let old_content = String::from_utf8_lossy(&strip_ansi_escapes::strip(&old_str.content)).to_string();
let new_content = String::from_utf8_lossy(&strip_ansi_escapes::strip(&new_str.content)).to_string();

match diff_tool::diff_with_tool(&old_content, &new_content, label) {
Ok(()) => {
writeln!(output, "Diff displayed in external tool.")?;
Ok(())
},
Err(e) => {
writeln!(output, "Failed to launch diff tool: {}", e)?;
writeln!(output, "Falling back to inline diff...\n")?;
print_diff(output, old_str, new_str, start_line)
},
}
} else {
print_diff(output, old_str, new_str, start_line)
}
}

/// Prints a git-diff style comparison between `old_str` and `new_str`.
/// - `start_line` - 1-indexed line number that `old_str` and `new_str` start at.
fn print_diff(
Expand Down
3 changes: 3 additions & 0 deletions crates/chat-cli/src/util/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ pub mod env_var {
/// Editor environment variable
EDITOR = "EDITOR",

/// Custom diff tool command
Q_DIFF_TOOL = "Q_DIFF_TOOL",

/// Terminal type
TERM = "TERM",

Expand Down
5 changes: 5 additions & 0 deletions crates/chat-cli/src/util/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ pub fn try_get_editor() -> Result<String, std::env::VarError> {
Env::new().get(EDITOR)
}

/// Try to get diff tool without fallback
pub fn try_get_diff_tool() -> Result<String, std::env::VarError> {
Env::new().get(Q_DIFF_TOOL)
}

/// Get terminal type
pub fn get_term() -> Option<String> {
Env::new().get(TERM).ok()
Expand Down