agent: Rejecting agent changes shouldn't discard user edits (#31617)
The fix prevents data loss, but it also results in a somewhat confusing UX. Specifically, after the user has made changes to an AI-created file, selecting "Reject" will leave AI changes in place. This is because there's no trivial way to disentangle user edits from the edits made by the AI. A better solution might exist. In the meantime, this change should do. Closes * #30527 Release Notes: - Prevent data loss when reverting changes in an agent-created file
This commit is contained in:
parent
361ceee72b
commit
a5a116439e
1 changed files with 98 additions and 9 deletions
|
@ -415,14 +415,38 @@ impl ActionLog {
|
||||||
self.project
|
self.project
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
} else {
|
} else {
|
||||||
buffer
|
// For a file created by AI with no pre-existing content,
|
||||||
.read(cx)
|
// only delete the file if we're certain it contains only AI content
|
||||||
.entry_id(cx)
|
// with no edits from the user.
|
||||||
.and_then(|entry_id| {
|
|
||||||
self.project
|
let initial_version = tracked_buffer.version.clone();
|
||||||
.update(cx, |project, cx| project.delete_entry(entry_id, false, cx))
|
let current_version = buffer.read(cx).version();
|
||||||
})
|
|
||||||
.unwrap_or(Task::ready(Ok(())))
|
let current_content = buffer.read(cx).text();
|
||||||
|
let tracked_content = tracked_buffer.snapshot.text();
|
||||||
|
|
||||||
|
let is_ai_only_content =
|
||||||
|
initial_version == current_version && current_content == tracked_content;
|
||||||
|
|
||||||
|
if is_ai_only_content {
|
||||||
|
buffer
|
||||||
|
.read(cx)
|
||||||
|
.entry_id(cx)
|
||||||
|
.and_then(|entry_id| {
|
||||||
|
self.project.update(cx, |project, cx| {
|
||||||
|
project.delete_entry(entry_id, false, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(Task::ready(Ok(())))
|
||||||
|
} else {
|
||||||
|
// Not sure how to disentangle edits made by the user
|
||||||
|
// from edits made by the AI at this point.
|
||||||
|
// For now, preserve both to avoid data loss.
|
||||||
|
//
|
||||||
|
// TODO: Better solution (disable "Reject" after user makes some
|
||||||
|
// edit or find a way to differentiate between AI and user edits)
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.tracked_buffers.remove(&buffer);
|
self.tracked_buffers.remove(&buffer);
|
||||||
|
@ -1576,7 +1600,6 @@ mod tests {
|
||||||
project.find_project_path("dir/new_file", cx)
|
project.find_project_path("dir/new_file", cx)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||||
.await
|
.await
|
||||||
|
@ -1619,6 +1642,72 @@ mod tests {
|
||||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_reject_created_file_with_user_edits(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
|
||||||
|
let file_path = project
|
||||||
|
.read_with(cx, |project, cx| {
|
||||||
|
project.find_project_path("dir/new_file", cx)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// AI creates file with initial content
|
||||||
|
cx.update(|cx| {
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text("ai content", cx));
|
||||||
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
|
});
|
||||||
|
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// User makes additional edits
|
||||||
|
cx.update(|cx| {
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(10..10, "\nuser added this line")], None, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||||
|
|
||||||
|
// Reject all
|
||||||
|
action_log
|
||||||
|
.update(cx, |log, cx| {
|
||||||
|
log.reject_edits_in_ranges(
|
||||||
|
buffer.clone(),
|
||||||
|
vec![Point::new(0, 0)..Point::new(100, 0)],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// File should still contain all the content
|
||||||
|
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||||
|
|
||||||
|
let content = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||||
|
assert_eq!(content, "ai content\nuser added this line");
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue