Display and jump to tool locations (#34304)

Agus Zubiaga created

Release Notes:

- N/A

Change summary

crates/acp/src/acp.rs                  |   8 +
crates/agent_ui/src/acp/thread_view.rs | 120 ++++++++++++++++++++++++---
2 files changed, 115 insertions(+), 13 deletions(-)

Detailed changes

crates/acp/src/acp.rs 🔗

@@ -176,6 +176,14 @@ impl AgentThreadEntry {
             None
         }
     }
+
+    pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
+        if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
+            Some(locations)
+        } else {
+            None
+        }
+    }
 }
 
 #[derive(Debug)]

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

@@ -925,10 +925,43 @@ impl AcpThreadView {
                                     .size(IconSize::Small)
                                     .color(Color::Muted),
                             )
-                            .child(self.render_markdown(
-                                tool_call.label.clone(),
-                                default_markdown_style(needs_confirmation, window, cx),
-                            )),
+                            .child(if tool_call.locations.len() == 1 {
+                                let name = tool_call.locations[0]
+                                    .path
+                                    .file_name()
+                                    .unwrap_or_default()
+                                    .display()
+                                    .to_string();
+
+                                h_flex()
+                                    .id(("open-tool-call-location", entry_ix))
+                                    .child(name)
+                                    .w_full()
+                                    .max_w_full()
+                                    .pr_1()
+                                    .gap_0p5()
+                                    .cursor_pointer()
+                                    .rounded_sm()
+                                    .opacity(0.8)
+                                    .hover(|label| {
+                                        label.opacity(1.).bg(cx
+                                            .theme()
+                                            .colors()
+                                            .element_hover
+                                            .opacity(0.5))
+                                    })
+                                    .tooltip(Tooltip::text("Jump to File"))
+                                    .on_click(cx.listener(move |this, _, window, cx| {
+                                        this.open_tool_call_location(entry_ix, 0, window, cx);
+                                    }))
+                                    .into_any_element()
+                            } else {
+                                self.render_markdown(
+                                    tool_call.label.clone(),
+                                    default_markdown_style(needs_confirmation, window, cx),
+                                )
+                                .into_any()
+                            }),
                     )
                     .child(
                         h_flex()
@@ -988,15 +1021,19 @@ impl AcpThreadView {
         cx: &Context<Self>,
     ) -> AnyElement {
         match content {
-            ToolCallContent::Markdown { markdown } => self
-                .render_markdown(markdown.clone(), default_markdown_style(false, window, cx))
-                .into_any_element(),
+            ToolCallContent::Markdown { markdown } => {
+                div()
+                    .p_2()
+                    .child(self.render_markdown(
+                        markdown.clone(),
+                        default_markdown_style(false, window, cx),
+                    ))
+                    .into_any_element()
+            }
             ToolCallContent::Diff {
-                diff: Diff {
-                    path, multibuffer, ..
-                },
+                diff: Diff { multibuffer, .. },
                 ..
-            } => self.render_diff_editor(multibuffer, path),
+            } => self.render_diff_editor(multibuffer),
         }
     }
 
@@ -1416,10 +1453,9 @@ impl AcpThreadView {
         }
     }
 
-    fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>, path: &Path) -> AnyElement {
+    fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
         v_flex()
             .h_full()
-            .child(path.to_string_lossy().to_string())
             .child(
                 if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
                     editor.clone().into_any_element()
@@ -2076,6 +2112,64 @@ impl AcpThreadView {
         }
     }
 
+    fn open_tool_call_location(
+        &self,
+        entry_ix: usize,
+        location_ix: usize,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        let location = self
+            .thread()?
+            .read(cx)
+            .entries()
+            .get(entry_ix)?
+            .locations()?
+            .get(location_ix)?;
+
+        let project_path = self
+            .project
+            .read(cx)
+            .find_project_path(&location.path, cx)?;
+
+        let open_task = self
+            .workspace
+            .update(cx, |worskpace, cx| {
+                worskpace.open_path(project_path, None, true, window, cx)
+            })
+            .log_err()?;
+
+        window
+            .spawn(cx, async move |cx| {
+                let item = open_task.await?;
+
+                let Some(active_editor) = item.downcast::<Editor>() else {
+                    return anyhow::Ok(());
+                };
+
+                active_editor.update_in(cx, |editor, window, cx| {
+                    let snapshot = editor.buffer().read(cx).snapshot(cx);
+                    let first_hunk = editor
+                        .diff_hunks_in_ranges(
+                            &[editor::Anchor::min()..editor::Anchor::max()],
+                            &snapshot,
+                        )
+                        .next();
+                    if let Some(first_hunk) = first_hunk {
+                        let first_hunk_start = first_hunk.multi_buffer_range().start;
+                        editor.change_selections(Default::default(), window, cx, |selections| {
+                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
+                        })
+                    }
+                })?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+
+        None
+    }
+
     pub fn open_thread_as_markdown(
         &self,
         workspace: Entity<Workspace>,