Display edit diffs (#469)

This commit is contained in:
Solomon
2025-08-15 11:18:24 +01:00
committed by GitHub
parent 6d65ea18af
commit ca9504f84b
16 changed files with 842 additions and 143 deletions

View File

@@ -7,13 +7,16 @@ use json_patch::Patch;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{msg_store::MsgStore, path::make_path_relative, shell::get_shell_command};
use utils::{
diff::create_unified_diff, msg_store::MsgStore, path::make_path_relative,
shell::get_shell_command,
};
use crate::{
command::CommandBuilder,
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem as LogsTodoItem,
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem as LogsTodoItem,
stderr_processor::normalize_stderr_logs,
utils::{EntryIndexProvider, patch::ConversationPatch},
},
@@ -450,17 +453,16 @@ impl AmpContentItem {
path: make_path_relative(path, worktree_path),
},
AmpToolData::CreateFile { path, content, .. } => {
let diffs = content
let changes = content
.as_ref()
.map(|content| EditDiff::Replace {
old: String::new(),
new: content.clone(),
.map(|content| FileChange::Write {
content: content.clone(),
})
.into_iter()
.collect();
ActionType::FileEdit {
path: make_path_relative(path, worktree_path),
diffs,
changes,
}
}
AmpToolData::EditFile {
@@ -469,17 +471,21 @@ impl AmpContentItem {
new_str,
..
} => {
let diffs = if old_str.is_some() || new_str.is_some() {
vec![EditDiff::Replace {
old: old_str.clone().unwrap_or_default(),
new: new_str.clone().unwrap_or_default(),
let changes = if old_str.is_some() || new_str.is_some() {
vec![FileChange::Edit {
unified_diff: create_unified_diff(
path,
old_str.as_deref().unwrap_or(""),
new_str.as_deref().unwrap_or(""),
),
has_line_numbers: false,
}]
} else {
vec![]
};
ActionType::FileEdit {
path: make_path_relative(path, worktree_path),
diffs,
changes,
}
}
AmpToolData::Bash { command, .. } => ActionType::CommandRun {

View File

@@ -7,14 +7,18 @@ use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{
log_msg::LogMsg, msg_store::MsgStore, path::make_path_relative, shell::get_shell_command,
diff::{concatenate_diff_hunks, create_unified_diff, create_unified_diff_hunk},
log_msg::LogMsg,
msg_store::MsgStore,
path::make_path_relative,
shell::get_shell_command,
};
use crate::{
command::CommandBuilder,
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
stderr_processor::normalize_stderr_logs,
utils::{EntryIndexProvider, patch::ConversationPatch},
},
@@ -434,28 +438,32 @@ impl ClaudeLogProcessor {
old_string,
new_string,
} => {
let diffs = if old_string.is_some() || new_string.is_some() {
vec![EditDiff::Replace {
old: old_string.clone().unwrap_or_default(),
new: new_string.clone().unwrap_or_default(),
let changes = if old_string.is_some() || new_string.is_some() {
vec![FileChange::Edit {
unified_diff: create_unified_diff(
file_path,
&old_string.clone().unwrap_or_default(),
&new_string.clone().unwrap_or_default(),
),
has_line_numbers: false,
}]
} else {
vec![]
};
ActionType::FileEdit {
path: make_path_relative(file_path, worktree_path),
diffs,
changes,
}
}
ClaudeToolData::MultiEdit { file_path, edits } => {
let diffs = edits
let hunks: Vec<String> = edits
.iter()
.filter_map(|edit| {
if edit.old_string.is_some() || edit.new_string.is_some() {
Some(EditDiff::Replace {
old: edit.old_string.clone().unwrap_or_default(),
new: edit.new_string.clone().unwrap_or_default(),
})
Some(create_unified_diff_hunk(
&edit.old_string.clone().unwrap_or_default(),
&edit.new_string.clone().unwrap_or_default(),
))
} else {
None
}
@@ -463,17 +471,19 @@ impl ClaudeLogProcessor {
.collect();
ActionType::FileEdit {
path: make_path_relative(file_path, worktree_path),
diffs,
changes: vec![FileChange::Edit {
unified_diff: concatenate_diff_hunks(file_path, &hunks),
has_line_numbers: false,
}],
}
}
ClaudeToolData::Write { file_path, content } => {
let diffs = vec![EditDiff::Replace {
old: String::new(),
new: content.clone(),
let diffs = vec![FileChange::Write {
content: content.clone(),
}];
ActionType::FileEdit {
path: make_path_relative(file_path, worktree_path),
diffs,
changes: diffs,
}
}
ClaudeToolData::Bash { command, .. } => ActionType::CommandRun {
@@ -503,7 +513,7 @@ impl ClaudeLogProcessor {
}
ClaudeToolData::NotebookEdit { notebook_path, .. } => ActionType::FileEdit {
path: make_path_relative(notebook_path, worktree_path),
diffs: vec![],
changes: vec![],
},
ClaudeToolData::TodoWrite { todos } => ActionType::TodoManagement {
todos: todos

View File

@@ -7,13 +7,18 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{msg_store::MsgStore, path::make_path_relative, shell::get_shell_command};
use utils::{
diff::{concatenate_diff_hunks, extract_unified_diff_hunks},
msg_store::MsgStore,
path::make_path_relative,
shell::get_shell_command,
};
use crate::{
command::CommandBuilder,
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType,
ActionType, FileChange, NormalizedEntry, NormalizedEntryType,
utils::{EntryIndexProvider, patch::ConversationPatch},
},
};
@@ -342,7 +347,7 @@ pub enum CodexMsgContent {
PatchApplyBegin {
call_id: Option<String>,
auto_approved: Option<bool>,
changes: std::collections::HashMap<String, FileChange>,
changes: std::collections::HashMap<String, CodexFileChange>,
},
#[serde(rename = "patch_apply_end")]
@@ -389,7 +394,7 @@ pub enum CodexMsgContent {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FileChange {
pub enum CodexFileChange {
Add {
content: String,
},
@@ -471,25 +476,39 @@ impl CodexJson {
make_path_relative(file_path, &current_dir.to_string_lossy());
// Try to extract unified diff from change data
let mut diffs = vec![];
let mut changes = vec![];
match change_data {
FileChange::Update { unified_diff, .. } => {
CodexFileChange::Update {
unified_diff,
move_path,
} => {
let mut new_path = relative_path.clone();
if let Some(move_path) = move_path {
new_path = make_path_relative(
&move_path.to_string_lossy(),
&current_dir.to_string_lossy(),
);
changes.push(FileChange::Rename {
new_path: new_path.clone(),
});
}
if !unified_diff.is_empty() {
diffs.push(EditDiff::Unified {
unified_diff: unified_diff.clone(),
let hunks = extract_unified_diff_hunks(unified_diff);
changes.push(FileChange::Edit {
unified_diff: concatenate_diff_hunks(&new_path, &hunks),
has_line_numbers: true,
});
}
}
FileChange::Add { content } => {
// For new files, we could show the content as a diff
diffs.push(EditDiff::Replace {
old: String::new(),
new: content.clone(),
CodexFileChange::Add { content } => {
changes.push(FileChange::Write {
content: content.clone(),
});
}
FileChange::Delete => {
// For deletions, we don't have old content to show
CodexFileChange::Delete => {
changes.push(FileChange::Delete);
}
};
@@ -499,7 +518,7 @@ impl CodexJson {
tool_name: "edit".to_string(),
action_type: ActionType::FileEdit {
path: relative_path.clone(),
diffs,
changes,
},
},
content: relative_path,

View File

@@ -7,13 +7,21 @@ use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{msg_store::MsgStore, path::make_path_relative, shell::get_shell_command};
use utils::{
diff::{
concatenate_diff_hunks, create_unified_diff, create_unified_diff_hunk,
extract_unified_diff_hunks,
},
msg_store::MsgStore,
path::make_path_relative,
shell::get_shell_command,
};
use crate::{
command::CommandBuilder,
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
plain_text_processor::PlainTextLogProcessor,
utils::{ConversationPatch, EntryIndexProvider},
},
@@ -480,39 +488,50 @@ impl CursorToolCall {
(
ActionType::FileEdit {
path: path.clone(),
diffs: vec![],
changes: vec![],
},
format!("`{path}`"),
)
}
CursorToolCall::Edit { args, .. } => {
let path = make_path_relative(&args.path, worktree_path);
let mut diffs = vec![];
let mut changes = vec![];
if let Some(_apply_patch) = &args.apply_patch {
// todo: handle v4a
if let Some(apply_patch) = &args.apply_patch {
let hunks = extract_unified_diff_hunks(&apply_patch.patch_content);
changes.push(FileChange::Edit {
unified_diff: concatenate_diff_hunks(&path, &hunks),
has_line_numbers: false,
});
}
if let Some(str_replace) = &args.str_replace {
diffs.push(EditDiff::Replace {
old: str_replace.old_text.clone(),
new: str_replace.new_text.clone(),
changes.push(FileChange::Edit {
unified_diff: create_unified_diff(
&path,
&str_replace.old_text,
&str_replace.new_text,
),
has_line_numbers: false,
});
}
if let Some(multi_str_replace) = &args.multi_str_replace {
for edit in multi_str_replace.edits.iter() {
diffs.push(EditDiff::Replace {
old: edit.old_text.clone(),
new: edit.new_text.clone(),
});
}
let hunks: Vec<String> = multi_str_replace
.edits
.iter()
.map(|edit| create_unified_diff_hunk(&edit.old_text, &edit.new_text))
.collect();
changes.push(FileChange::Edit {
unified_diff: concatenate_diff_hunks(&path, &hunks),
has_line_numbers: false,
});
}
(
ActionType::FileEdit {
path: path.clone(),
diffs,
changes,
},
format!("`{path}`"),
)
@@ -522,7 +541,7 @@ impl CursorToolCall {
(
ActionType::FileEdit {
path: path.clone(),
diffs: vec![],
changes: vec![],
},
format!("`{path}`"),
)

View File

@@ -9,13 +9,16 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{msg_store::MsgStore, path::make_path_relative, shell::get_shell_command};
use utils::{
diff::create_unified_diff, msg_store::MsgStore, path::make_path_relative,
shell::get_shell_command,
};
use crate::{
command::CommandBuilder,
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
plain_text_processor::{MessageBoundary, PlainTextLogProcessor},
utils::EntryIndexProvider,
},
@@ -679,16 +682,14 @@ impl ToolUtils {
Tool::Write {
file_path, content, ..
} => {
let diffs = match content {
Some(content) => vec![EditDiff::Replace {
old: String::new(),
new: content.clone(),
}],
None => Vec::new(),
let changes = if let Some(content) = content.clone() {
vec![FileChange::Write { content }]
} else {
vec![]
};
ActionType::FileEdit {
path: make_path_relative(file_path, worktree_path),
diffs,
changes,
}
}
Tool::Edit {
@@ -697,16 +698,16 @@ impl ToolUtils {
new_string,
..
} => {
let diffs = match (old_string, new_string) {
(Some(old), Some(new)) => vec![EditDiff::Replace {
old: old.clone(),
new: new.clone(),
let changes = match (old_string, new_string) {
(Some(old), Some(new)) => vec![FileChange::Edit {
unified_diff: create_unified_diff(file_path, old, new),
has_line_numbers: false,
}],
_ => Vec::new(),
};
ActionType::FileEdit {
path: make_path_relative(file_path, worktree_path),
diffs,
changes,
}
}
Tool::Bash { command, .. } => ActionType::CommandRun {

View File

@@ -5,14 +5,6 @@ pub mod plain_text_processor;
pub mod stderr_processor;
pub mod utils;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(tag = "format", rename_all = "snake_case")]
pub enum EditDiff {
Unified { unified_diff: String },
Replace { old: String, new: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct NormalizedConversation {
pub entries: Vec<NormalizedEntry>,
@@ -64,7 +56,7 @@ pub enum ActionType {
},
FileEdit {
path: String,
diffs: Vec<EditDiff>,
changes: Vec<FileChange>,
},
CommandRun {
command: String,
@@ -89,3 +81,21 @@ pub enum ActionType {
description: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum FileChange {
/// Create a file if it doesn't exist, and overwrite its content.
Write { content: String },
/// Delete a file.
Delete,
/// Rename a file.
Rename { new_path: String },
/// Edit a file with a unified diff.
Edit {
/// Unified diff containing file header and hunks.
unified_diff: String,
/// Whether line number in the hunks are reliable.
has_line_numbers: bool,
},
}