diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs index 0aa57513a7..f89eab2032 100644 --- a/crates/acp/src/acp.rs +++ b/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)] diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 353d712afd..7ab395815f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/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, ) -> 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, path: &Path) -> AnyElement { + fn render_diff_editor(&self, multibuffer: &Entity) -> 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, + ) -> 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::() 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,