Preserve focused tool calls in agent panel when scrolling them out of view (#54115) (cherry-pick to preview) (#54116)

zed-zippy[bot] , Max Brunsfeld , and Lukas created

Cherry-pick of #54115 to preview

----
Release Notes:

- Fixed a bug where the agent panel would sometimes close if you
scrolled up or down while it was zoomed.

Co-authored-by: Lukas <lukas@zed.dev>

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Lukas <lukas@zed.dev>

Change summary

crates/agent_ui/src/conversation_view/thread_view.rs | 19 +++++++--
crates/agent_ui/src/entry_view_state.rs              | 28 ++++++++++++--
2 files changed, 39 insertions(+), 8 deletions(-)

Detailed changes

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

@@ -4760,8 +4760,8 @@ impl ThreadView {
                         .into_any()
                 }
             }
-            AgentThreadEntry::ToolCall(tool_call) => self
-                .render_any_tool_call(
+            AgentThreadEntry::ToolCall(tool_call) => {
+                let tool_call = self.render_any_tool_call(
                     self.thread.read(cx).session_id(),
                     entry_ix,
                     tool_call,
@@ -4769,8 +4769,19 @@ impl ThreadView {
                     false,
                     window,
                     cx,
-                )
-                .into_any(),
+                );
+
+                if let Some(handle) = self
+                    .entry_view_state
+                    .read(cx)
+                    .entry(entry_ix)
+                    .and_then(|entry| entry.focus_handle(cx))
+                {
+                    tool_call.track_focus(&handle).into_any()
+                } else {
+                    tool_call.into_any()
+                }
+            }
             AgentThreadEntry::CompletedPlan(entries) => {
                 self.render_completed_plan(entries, window, cx)
             }

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -130,6 +130,7 @@ impl EntryViewState {
                         index,
                         Entry::ToolCall(ToolCallEntry {
                             content: HashMap::default(),
+                            focus_handle: cx.focus_handle(),
                         }),
                     );
                     let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else {
@@ -262,7 +263,7 @@ impl EntryViewState {
                 Entry::UserMessage { .. }
                 | Entry::AssistantMessage { .. }
                 | Entry::CompletedPlan => {}
-                Entry::ToolCall(ToolCallEntry { content }) => {
+                Entry::ToolCall(ToolCallEntry { content, .. }) => {
                     for view in content.values() {
                         if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
                             diff_editor.update(cx, |diff_editor, cx| {
@@ -321,6 +322,7 @@ impl AssistantMessageEntry {
 #[derive(Debug)]
 pub struct ToolCallEntry {
     content: HashMap<EntityId, AnyEntity>,
+    focus_handle: FocusHandle,
 }
 
 #[derive(Debug)]
@@ -336,7 +338,8 @@ impl Entry {
         match self {
             Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
             Self::AssistantMessage(message) => Some(message.focus_handle.clone()),
-            Self::ToolCall(_) | Self::CompletedPlan => None,
+            Self::ToolCall(tool_call) => Some(tool_call.focus_handle.clone()),
+            Self::CompletedPlan => None,
         }
     }
 
@@ -376,7 +379,7 @@ impl Entry {
 
     fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
         match self {
-            Self::ToolCall(ToolCallEntry { content }) => Some(content),
+            Self::ToolCall(ToolCallEntry { content, .. }) => Some(content),
             _ => None,
         }
     }
@@ -384,12 +387,29 @@ impl Entry {
     #[cfg(test)]
     pub fn has_content(&self) -> bool {
         match self {
-            Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(),
+            Self::ToolCall(ToolCallEntry { content, .. }) => !content.is_empty(),
             Self::UserMessage(_) | Self::AssistantMessage(_) | Self::CompletedPlan => false,
         }
     }
 }
 
+impl Focusable for ToolCallEntry {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Focusable for Entry {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        match self {
+            Self::UserMessage(editor) => editor.read(cx).focus_handle(cx),
+            Self::AssistantMessage(message) => message.focus_handle.clone(),
+            Self::ToolCall(tool_call) => tool_call.focus_handle.clone(),
+            Self::CompletedPlan => cx.focus_handle(),
+        }
+    }
+}
+
 fn create_terminal(
     workspace: WeakEntity<Workspace>,
     project: WeakEntity<Project>,