From fd829cd2d09341235c29cf75645f60783a99e203 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Tue, 13 Jan 2026 13:41:09 -0800 Subject: [PATCH] agent_ui: Discard button for interrupted changes (#46735) Release Notes: - N/A --- crates/acp_thread/src/diff.rs | 16 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 53 +++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 35b16c4e4b2660fb7d9200a6960bb6dd3dae5c7a..12d06a563737421f046ac6a793bb3751072d9364 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -131,6 +131,22 @@ impl Diff { } } + /// Returns the original text before any edits were applied. + pub fn base_text(&self) -> &Arc { + match self { + Self::Pending(PendingDiff { base_text, .. }) => base_text, + Self::Finalized(FinalizedDiff { base_text, .. }) => base_text, + } + } + + /// Returns the buffer being edited (for pending diffs) or the snapshot buffer (for finalized diffs). + pub fn buffer(&self) -> &Entity { + match self { + Self::Pending(PendingDiff { new_buffer, .. }) => new_buffer, + Self::Finalized(FinalizedDiff { new_buffer, .. }) => new_buffer, + } + } + pub fn multibuffer(&self) -> &Entity { match self { Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 74a59cf43a89a30ffcab0f48dcc0edbd4eb08a30..b27b454bd1b49ca49c4799b32df5bb09d66d740d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3149,10 +3149,38 @@ impl AcpThreadView { cx, )) .when(is_collapsible || failed_or_canceled, |this| { + let is_cancelled_edit = is_edit + && matches!(tool_call.status, ToolCallStatus::Canceled); + let diff_for_discard = + if is_cancelled_edit && cx.has_flag::() { + tool_call.diffs().next().cloned() + } else { + None + }; this.child( h_flex() .px_1() - .gap_px() + .gap_1() + .when_some(diff_for_discard, |this, diff| { + this.child( + Button::new( + ("discard-partial-edit", entry_ix), + "Discard", + ) + .label_size(LabelSize::Small) + .tooltip(Tooltip::text( + "Discard partial edits and restore the original file content", + )) + .on_click(cx.listener(move |_this, _, _window, cx| { + let diff_data = diff.read(cx); + let base_text = diff_data.base_text().clone(); + let buffer = diff_data.buffer().clone(); + buffer.update(cx, |buffer, cx| { + buffer.set_text(base_text.as_ref(), cx); + }); + })), + ) + }) .when(is_collapsible, |this| { this.child( Disclosure::new(("expand-output", entry_ix), is_open) @@ -3173,11 +3201,24 @@ impl AcpThreadView { ) }) .when(failed_or_canceled, |this| { - this.child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) + if is_cancelled_edit { + this.child( + div() + .id(("tool-call-status-icon", entry_ix)) + .child( + Icon::new(IconName::Warning) + .color(Color::Error) + .size(IconSize::Small), + ) + .tooltip(Tooltip::text("Edit Interrupted")), + ) + } else { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + } }), ) }),