From 8f0826f5435eb1d408ceffce4d5e56a465b37e9e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 20 Mar 2026 15:20:15 +0100 Subject: [PATCH] acp: Set agent server cwd from project paths (#52005) ## Context Add Project::default_path_list and reuse it for agent session work directories and ACP server startup so agent processes start in the project context by default We previously removed all cwd from this hoping to have a global process shared, but it doesn't work for remote projects. Since we're at the project boundary anyway, we might as well start it up in a similar spot as a new thread. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - acp: Make sure the agent server is started in a project directory --- crates/agent/src/native_agent_server.rs | 3 +- crates/agent_servers/src/acp.rs | 12 ++++ crates/agent_servers/src/agent_servers.rs | 3 +- crates/agent_servers/src/custom.rs | 9 ++- crates/agent_servers/src/e2e_tests.rs | 5 +- crates/agent_ui/src/agent_connection_store.rs | 2 +- crates/agent_ui/src/conversation_view.rs | 31 ++-------- crates/agent_ui/src/mention_set.rs | 2 +- crates/agent_ui/src/test_support.rs | 2 + crates/project/src/project.rs | 29 +++++++++- .../tests/integration/project_tests.rs | 57 +++++++++++++++++++ crates/zed/src/visual_test_runner.rs | 7 ++- 12 files changed, 124 insertions(+), 38 deletions(-) diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index b2c3c913f19a877dcd001bd771809ce7f9a4afa5..7f19f9005e3ff54e361f57075b7af06508476564 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/crates/agent/src/native_agent_server.rs @@ -7,7 +7,7 @@ use anyhow::Result; use collections::HashSet; use fs::Fs; use gpui::{App, Entity, Task}; -use project::AgentId; +use project::{AgentId, Project}; use prompt_store::PromptStore; use settings::{LanguageModelSelection, Settings as _, update_settings_file}; @@ -37,6 +37,7 @@ impl AgentServer for NativeAgentServer { fn connect( &self, _delegate: AgentServerDelegate, + _project: Entity, cx: &mut App, ) -> Task>> { log::debug!("NativeAgentServer::connect"); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index f9be24f21f7beebffaf8bcfcbcd3392445784012..beb313445e7d4223fea96f2b68dea2e7dbf4e047 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -166,6 +166,7 @@ impl AgentSessionList for AcpSessionList { pub async fn connect( agent_id: AgentId, + project: Entity, display_name: SharedString, command: AgentServerCommand, default_mode: Option, @@ -175,6 +176,7 @@ pub async fn connect( ) -> Result> { let conn = AcpConnection::stdio( agent_id, + project, display_name, command.clone(), default_mode, @@ -191,6 +193,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1 impl AcpConnection { pub async fn stdio( agent_id: AgentId, + project: Entity, display_name: SharedString, command: AgentServerCommand, default_mode: Option, @@ -203,6 +206,15 @@ impl AcpConnection { let mut child = builder.build_std_command(Some(command.path.display().to_string()), &command.args); child.envs(command.env.iter().flatten()); + if let Some(cwd) = project.update(cx, |project, cx| { + project + .default_path_list(cx) + .ordered_paths() + .next() + .cloned() + }) { + child.current_dir(cwd); + } let mut child = Child::spawn(child, Stdio::piped(), Stdio::piped(), Stdio::piped())?; let stdout = child.stdout.take().context("Failed to take stdout")?; diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 983d6b5088ccec74c7076b7956dd4ff5f68f7da0..2016e5aaaa27b62c956c5eee49c989172980de49 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -9,7 +9,7 @@ use collections::{HashMap, HashSet}; pub use custom::*; use fs::Fs; use http_client::read_no_proxy_from_env; -use project::{AgentId, agent_server_store::AgentServerStore}; +use project::{AgentId, Project, agent_server_store::AgentServerStore}; use acp_thread::AgentConnection; use anyhow::Result; @@ -42,6 +42,7 @@ pub trait AgentServer: Send { fn connect( &self, delegate: AgentServerDelegate, + project: Entity, cx: &mut App, ) -> Task>>; diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index ed76d022f5388db0c5346fd2fc664c2ef26ae761..ecf89a0671c687ad4f3b359a2cdae906a1c67cb5 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -5,9 +5,12 @@ use anyhow::{Context as _, Result}; use collections::HashSet; use credentials_provider::CredentialsProvider; use fs::Fs; -use gpui::{App, AppContext as _, Task}; +use gpui::{App, AppContext as _, Entity, Task}; use language_model::{ApiKey, EnvVar}; -use project::agent_server_store::{AgentId, AllAgentServersSettings}; +use project::{ + Project, + agent_server_store::{AgentId, AllAgentServersSettings}, +}; use settings::{SettingsStore, update_settings_file}; use std::{rc::Rc, sync::Arc}; use ui::IconName; @@ -289,6 +292,7 @@ impl AgentServer for CustomAgentServer { fn connect( &self, delegate: AgentServerDelegate, + project: Entity, cx: &mut App, ) -> Task>> { let agent_id = self.agent_id(); @@ -371,6 +375,7 @@ impl AgentServer for CustomAgentServer { .await?; let connection = crate::acp::connect( agent_id, + project, display_name, command, default_mode, diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index b9365296c3fdb9ed7dc45c1c146d0abd7a831fce..c7f90ac6bc431c5bb0ca5b18bdacf2e31465bfba 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -434,7 +434,10 @@ pub async fn new_test_thread( let store = project.read_with(cx, |project, _| project.agent_server_store().clone()); let delegate = AgentServerDelegate::new(store, None); - let connection = cx.update(|cx| server.connect(delegate, cx)).await.unwrap(); + let connection = cx + .update(|cx| server.connect(delegate, project.clone(), cx)) + .await + .unwrap(); cx.update(|cx| { connection.new_session(project.clone(), PathList::new(&[current_dir.as_ref()]), cx) diff --git a/crates/agent_ui/src/agent_connection_store.rs b/crates/agent_ui/src/agent_connection_store.rs index 545fedae278c7bb6747984833a83abf5fdb01602..89b3b0ef16f46753a747b1e06a9b9e4a76e839e8 100644 --- a/crates/agent_ui/src/agent_connection_store.rs +++ b/crates/agent_ui/src/agent_connection_store.rs @@ -160,7 +160,7 @@ impl AgentConnectionStore { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let delegate = AgentServerDelegate::new(agent_server_store, Some(new_version_tx)); - let connect_task = server.connect(delegate, cx); + let connect_task = server.connect(delegate, self.project.clone(), cx); let connect_task = cx.spawn(async move |_this, cx| match connect_task.await { Ok(connection) => cx.update(|cx| { let history = connection diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 46453c2143841039aa441a6ed41d7d40acee5532..3c6eff594c04e3af346dc9c233cfd9a901322923 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -619,32 +619,7 @@ impl ConversationView { session_id: resume_session_id.clone(), }; } - let mut worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - // Pick the first non-single-file worktree for the root directory if there are any, - // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees. - worktrees.sort_by(|l, r| { - l.read(cx) - .is_single_file() - .cmp(&r.read(cx).is_single_file()) - }); - let worktree_roots: Vec> = worktrees - .iter() - .filter_map(|worktree| { - let worktree = worktree.read(cx); - if worktree.is_single_file() { - Some(worktree.abs_path().parent()?.into()) - } else { - Some(worktree.abs_path()) - } - }) - .collect(); - let session_work_dirs = work_dirs.unwrap_or_else(|| { - if worktree_roots.is_empty() { - PathList::new(&[paths::home_dir().as_path()]) - } else { - PathList::new(&worktree_roots) - } - }); + let session_work_dirs = work_dirs.unwrap_or_else(|| project.read(cx).default_path_list(cx)); let connection_entry = connection_store.update(cx, |store, cx| { store.request_connection(connection_key, agent.clone(), cx) @@ -1624,7 +1599,7 @@ impl ConversationView { .read(cx) .work_dirs() .cloned() - .unwrap_or_else(|| PathList::new(&[paths::home_dir().as_path()])); + .unwrap_or_else(|| self.project.read(cx).default_path_list(cx)); let subagent_thread_task = connected.connection.clone().load_session( subagent_id.clone(), @@ -3651,6 +3626,7 @@ pub(crate) mod tests { fn connect( &self, _delegate: AgentServerDelegate, + _project: Entity, _cx: &mut App, ) -> Task>> { Task::ready(Ok(Rc::new(self.connection.clone()))) @@ -3675,6 +3651,7 @@ pub(crate) mod tests { fn connect( &self, _delegate: AgentServerDelegate, + _project: Entity, _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!( diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index d1af49320695424e53cce223e63a57cf8bdbeec5..877fb1eb6d9b5dea47393af63776e3eeca0668e5 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -562,7 +562,7 @@ impl MentionSet { )); let delegate = AgentServerDelegate::new(project.read(cx).agent_server_store().clone(), None); - let connection = server.connect(delegate, cx); + let connection = server.connect(delegate, project.clone(), cx); cx.spawn(async move |_, cx| { let agent = connection.await?; let agent = agent.downcast::().unwrap(); diff --git a/crates/agent_ui/src/test_support.rs b/crates/agent_ui/src/test_support.rs index 66c8c447a827e7f36c3098b4835026836ef8ccd8..49639136854c8228d027573ee22f423ce687c2d5 100644 --- a/crates/agent_ui/src/test_support.rs +++ b/crates/agent_ui/src/test_support.rs @@ -3,6 +3,7 @@ use agent_client_protocol as acp; use agent_servers::{AgentServer, AgentServerDelegate}; use gpui::{Entity, Task, TestAppContext, VisualTestContext}; use project::AgentId; +use project::Project; use settings::SettingsStore; use std::any::Any; use std::rc::Rc; @@ -45,6 +46,7 @@ where fn connect( &self, _delegate: AgentServerDelegate, + _project: Entity, _cx: &mut gpui::App, ) -> Task>> { Task::ready(Ok(Rc::new(self.connection.clone()))) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9ba5e9accd670a5aa4e1d449c445e83b84679676..aeb526db341559f8e1829c6e55d9d48c7b34dd07 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -33,7 +33,7 @@ pub mod search_history; pub mod yarn; use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope}; -use itertools::Either; +use itertools::{Either, Itertools}; use crate::{ git_store::GitStore, @@ -134,6 +134,7 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, maybe, + path_list::PathList, paths::{PathStyle, SanitizedPath, is_absolute}, rel_path::RelPath, }; @@ -2286,6 +2287,32 @@ impl Project { self.worktree_store.read(cx).visible_worktrees(cx) } + pub fn default_path_list(&self, cx: &App) -> PathList { + let worktree_roots = self + .visible_worktrees(cx) + .sorted_by(|left, right| { + left.read(cx) + .is_single_file() + .cmp(&right.read(cx).is_single_file()) + }) + .filter_map(|worktree| { + let worktree = worktree.read(cx); + let path = worktree.abs_path(); + if worktree.is_single_file() { + Some(path.parent()?.to_path_buf()) + } else { + Some(path.to_path_buf()) + } + }) + .collect::>(); + + if worktree_roots.is_empty() { + PathList::new(&[paths::home_dir().as_path()]) + } else { + PathList::new(&worktree_roots) + } + } + #[inline] pub fn worktree_for_root_name(&self, root_name: &str, cx: &App) -> Option> { self.visible_worktrees(cx) diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 087ecc8060543e5bd1c05eaac9848b96681d6473..e8cf950dd34af09fa432a6c96553db389ba2ff1c 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -126,6 +126,63 @@ async fn test_block_via_smol(cx: &mut gpui::TestAppContext) { task.await; } +#[gpui::test] +async fn test_default_session_work_dirs_prefers_directory_worktrees_over_single_file_parents( + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "dir-project": { + "src": { + "main.rs": "fn main() {}" + } + }, + "single-file.rs": "fn helper() {}" + }), + ) + .await; + + let project = Project::test( + fs, + [ + Path::new(path!("/root/single-file.rs")), + Path::new(path!("/root/dir-project")), + ], + cx, + ) + .await; + + let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); + let ordered_paths = work_dirs.ordered_paths().cloned().collect::>(); + + assert_eq!( + ordered_paths, + vec![ + PathBuf::from(path!("/root/dir-project")), + PathBuf::from(path!("/root")), + ] + ); +} + +#[gpui::test] +async fn test_default_session_work_dirs_falls_back_to_home_for_empty_project( + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); + let ordered_paths = work_dirs.ordered_paths().cloned().collect::>(); + + assert_eq!(ordered_paths, vec![paths::home_dir().to_path_buf()]); +} + // NOTE: // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus // we assume that they are not supported out of the box. diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index af95c8dae2a3cdd4391603a0b30e17669a337a43..b2e88c1d0f9fb861522bce869478c7303aae54eb 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -103,11 +103,11 @@ use { feature_flags::FeatureFlagAppExt as _, git_ui::project_diff::ProjectDiff, gpui::{ - App, AppContext as _, Bounds, KeyBinding, Modifiers, VisualTestAppContext, WindowBounds, - WindowHandle, WindowOptions, point, px, size, + App, AppContext as _, Bounds, Entity, KeyBinding, Modifiers, VisualTestAppContext, + WindowBounds, WindowHandle, WindowOptions, point, px, size, }, image::RgbaImage, - project::AgentId, + project::{AgentId, Project}, project_panel::ProjectPanel, settings::{NotifyWhenAgentWaiting, Settings as _}, settings_ui::SettingsWindow, @@ -1966,6 +1966,7 @@ impl AgentServer for StubAgentServer { fn connect( &self, _delegate: AgentServerDelegate, + _project: Entity, _cx: &mut App, ) -> gpui::Task>> { gpui::Task::ready(Ok(Rc::new(self.connection.clone())))