acp: Allow collapsing edit file tool calls (#36675)

Bennet Bo Fenner created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/acp/entry_view_state.rs | 18 ++++++++---
crates/agent_ui/src/acp/thread_view.rs      | 34 ++++++++++++----------
2 files changed, 31 insertions(+), 21 deletions(-)

Detailed changes

crates/agent_ui/src/acp/entry_view_state.rs 🔗

@@ -1,6 +1,7 @@
 use std::ops::Range;
 
 use acp_thread::{AcpThread, AgentThreadEntry};
+use agent_client_protocol::ToolCallId;
 use agent2::HistoryStore;
 use collections::HashMap;
 use editor::{Editor, EditorMode, MinimapVisibility};
@@ -106,6 +107,7 @@ impl EntryViewState {
                 }
             }
             AgentThreadEntry::ToolCall(tool_call) => {
+                let id = tool_call.id.clone();
                 let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
                 let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
 
@@ -131,16 +133,21 @@ impl EntryViewState {
                         .into_any();
                         cx.emit(EntryViewEvent {
                             entry_index: index,
-                            view_event: ViewEvent::NewTerminal(terminal.entity_id()),
+                            view_event: ViewEvent::NewTerminal(id.clone()),
                         });
                         element
                     });
                 }
 
                 for diff in diffs {
-                    views
-                        .entry(diff.entity_id())
-                        .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any());
+                    views.entry(diff.entity_id()).or_insert_with(|| {
+                        let element = create_editor_diff(diff.clone(), window, cx).into_any();
+                        cx.emit(EntryViewEvent {
+                            entry_index: index,
+                            view_event: ViewEvent::NewDiff(id.clone()),
+                        });
+                        element
+                    });
                 }
             }
             AgentThreadEntry::AssistantMessage(_) => {
@@ -192,7 +199,8 @@ pub struct EntryViewEvent {
 }
 
 pub enum ViewEvent {
-    NewTerminal(EntityId),
+    NewDiff(ToolCallId),
+    NewTerminal(ToolCallId),
     MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
 }
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -20,11 +20,11 @@ use file_icons::FileIcons;
 use fs::Fs;
 use gpui::{
     Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
-    EdgesRefinement, ElementId, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length,
-    ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
-    Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
-    Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
-    point, prelude::*, pulsating_between,
+    EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
+    ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
+    Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
+    WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
+    prelude::*, pulsating_between,
 };
 use language::Buffer;
 
@@ -256,7 +256,6 @@ pub struct AcpThreadView {
     auth_task: Option<Task<()>>,
     expanded_tool_calls: HashSet<acp::ToolCallId>,
     expanded_thinking_blocks: HashSet<(usize, usize)>,
-    expanded_terminals: HashSet<EntityId>,
     edits_expanded: bool,
     plan_expanded: bool,
     editor_expanded: bool,
@@ -354,7 +353,6 @@ impl AcpThreadView {
             auth_task: None,
             expanded_tool_calls: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
-            expanded_terminals: HashSet::default(),
             editing_message: None,
             edits_expanded: false,
             plan_expanded: false,
@@ -677,9 +675,14 @@ impl AcpThreadView {
         cx: &mut Context<Self>,
     ) {
         match &event.view_event {
-            ViewEvent::NewTerminal(terminal_id) => {
+            ViewEvent::NewDiff(tool_call_id) => {
+                if AgentSettings::get_global(cx).expand_edit_card {
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
+                }
+            }
+            ViewEvent::NewTerminal(tool_call_id) => {
                 if AgentSettings::get_global(cx).expand_terminal_card {
-                    self.expanded_terminals.insert(*terminal_id);
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
                 }
             }
             ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
@@ -1559,10 +1562,9 @@ impl AcpThreadView {
             matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
         let use_card_layout = needs_confirmation || is_edit;
 
-        let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
+        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 
-        let is_open =
-            needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id);
+        let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
 
         let gradient_overlay = |color: Hsla| {
             div()
@@ -2014,7 +2016,7 @@ impl AcpThreadView {
             .map(|path| format!("{}", path.display()))
             .unwrap_or_else(|| "current directory".to_string());
 
-        let is_expanded = self.expanded_terminals.contains(&terminal.entity_id());
+        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
 
         let header = h_flex()
             .id(SharedString::from(format!(
@@ -2154,12 +2156,12 @@ impl AcpThreadView {
                 .opened_icon(IconName::ChevronUp)
                 .closed_icon(IconName::ChevronDown)
                 .on_click(cx.listener({
-                    let terminal_id = terminal.entity_id();
+                    let id = tool_call.id.clone();
                     move |this, _event, _window, _cx| {
                         if is_expanded {
-                            this.expanded_terminals.remove(&terminal_id);
+                            this.expanded_tool_calls.remove(&id);
                         } else {
-                            this.expanded_terminals.insert(terminal_id);
+                            this.expanded_tool_calls.insert(id.clone());
                         }
                     }
                 })),