diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index ff98888ca650bd..8d0ff27db8abd8 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -42,7 +42,10 @@ use text::Bias; use ui::App; use util::markdown::MarkdownEscaped; use util::path_list::PathList; -use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle}; +use util::{ + ResultExt, get_default_system_shell_preferring_bash, + paths::{PathStyle, is_absolute}, +}; use uuid::Uuid; /// Returned when the model stops because it exhausted its output token budget. @@ -550,9 +553,16 @@ impl ToolCall { ) -> Option { let buffer = project .update(cx, |project, cx| { - project - .project_path_for_absolute_path(&location.path, cx) - .map(|path| project.open_buffer(path, cx)) + if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { + Some(project.open_buffer(path, cx)) + } else if is_absolute( + location.path.to_string_lossy().as_ref(), + project.path_style(cx), + ) { + Some(project.open_local_buffer(&location.path, cx)) + } else { + None + } }) .ok()??; let buffer = buffer.await.log_err()?; @@ -4198,6 +4208,56 @@ mod tests { }); } + #[gpui::test] + async fn test_tool_call_location_resolves_external_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/tmp/skills/test-skill"), + json!({ "SKILL.md": "skill body" }), + ) + .await; + let project = Project::test(fs, [], cx).await; + let connection = Rc::new(FakeAgentConnection::new()); + let thread = cx + .update(|cx| { + connection.new_session(project, PathList::new(&[Path::new(path!("/project"))]), cx) + }) + .await + .unwrap(); + + let skill_path = std::path::PathBuf::from(path!("/tmp/skills/test-skill/SKILL.md")); + thread + .update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall( + acp::ToolCall::new("write_file", "Write SKILL.md") + .kind(acp::ToolKind::Edit) + .status(acp::ToolCallStatus::Completed) + .locations(vec![acp::ToolCallLocation::new(skill_path.clone())]), + ), + cx, + ) + }) + .unwrap(); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, cx| { + let (tool_call_location, agent_location) = thread.entries[0] + .location(0) + .expect("external tool-call location should resolve"); + assert_eq!(tool_call_location.path, skill_path); + + let buffer = agent_location + .buffer + .upgrade() + .expect("resolved location should keep an open buffer"); + assert_eq!(buffer.read(cx).text(), "skill body"); + }); + } + #[gpui::test] async fn test_no_pending_edits_if_tool_calls_are_completed(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index e704c864b2c691..6eb05d01705fb4 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -28,8 +28,8 @@ use language_model::{ }; use settings::update_settings_file; use ui::{ButtonLike, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, Tab}; -use workspace::SERIALIZATION_THROTTLE_TIME; use workspace::notifications::NotificationId; +use workspace::{OpenOptions, SERIALIZATION_THROTTLE_TIME}; use super::*; @@ -8296,12 +8296,24 @@ impl ThreadView { .project .upgrade()? .read(cx) - .find_project_path(&tool_call_location.path, cx)?; + .find_project_path(&tool_call_location.path, cx); let open_task = self .workspace .update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) + if let Some(project_path) = project_path { + workspace.open_path(project_path, None, true, window, cx) + } else { + workspace.open_abs_path( + tool_call_location.path.clone(), + OpenOptions { + focus: Some(true), + ..Default::default() + }, + window, + cx, + ) + } }) .log_err()?; window