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())))