From 52da72d80af8a985db74e04c081fef0453e55e00 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 29 Aug 2025 00:16:49 -0400 Subject: [PATCH] acp: Install new versions of agent binaries in the background (#37141) Release Notes: - acp: New releases of external agents are now installed in the background. Co-authored-by: Conrad Irwin --- crates/agent_servers/src/agent_servers.rs | 186 ++++++++++++++++------ 1 file changed, 139 insertions(+), 47 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index e1b4057b71b0b4aee84548df74935d9b0598f598..83b3be76ce709c9b8c4d9f13ca55632a79e7b677 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -7,20 +7,24 @@ mod settings; #[cfg(any(test, feature = "test-support"))] pub mod e2e_tests; +use anyhow::Context as _; pub use claude::*; pub use custom::*; +use fs::Fs; +use fs::RemoveOptions; +use fs::RenameOptions; +use futures::StreamExt as _; pub use gemini::*; +use gpui::AppContext; +use node_runtime::NodeRuntime; pub use settings::*; use acp_thread::AgentConnection; use acp_thread::LoadError; use anyhow::Result; use anyhow::anyhow; -use anyhow::bail; use collections::HashMap; -use gpui::AppContext as _; use gpui::{App, AsyncApp, Entity, SharedString, Task}; -use node_runtime::VersionStrategy; use project::Project; use schemars::JsonSchema; use semver::Version; @@ -64,70 +68,158 @@ impl AgentServerDelegate { let project = self.project; let fs = project.read(cx).fs().clone(); let Some(node_runtime) = project.read(cx).node_runtime().cloned() else { - return Task::ready(Err(anyhow!("Missing node runtime"))); + return Task::ready(Err(anyhow!( + "External agents are not yet available in remote projects." + ))); }; let mut status_tx = self.status_tx; cx.spawn(async move |cx| { if !ignore_system_version { if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await { - return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() }) + return Ok(AgentServerCommand { + path: bin, + args: Vec::new(), + env: Default::default(), + }); } } - cx.background_spawn(async move { + cx.spawn(async move |cx| { let node_path = node_runtime.binary_path().await?; - let dir = paths::data_dir().join("external_agents").join(binary_name.as_str()); + let dir = paths::data_dir() + .join("external_agents") + .join(binary_name.as_str()); fs.create_dir(&dir).await?; - let local_executable_path = dir.join(entrypoint_path); - let command = AgentServerCommand { - path: node_path, - args: vec![local_executable_path.to_string_lossy().to_string()], - env: Default::default(), - }; - let installed_version = node_runtime - .npm_package_installed_version(&dir, &package_name) - .await? - .filter(|version| { - Version::from_str(&version) - .is_ok_and(|version| Some(version) >= minimum_version) - }); + let mut stream = fs.read_dir(&dir).await?; + let mut versions = Vec::new(); + let mut to_delete = Vec::new(); + while let Some(entry) = stream.next().await { + let Ok(entry) = entry else { continue }; + let Some(file_name) = entry.file_name() else { + continue; + }; + + if let Some(version) = file_name + .to_str() + .and_then(|name| semver::Version::from_str(&name).ok()) + { + versions.push((file_name.to_owned(), version)); + } else { + to_delete.push(file_name.to_owned()) + } + } - status_tx.send("Checking for latest version…".into())?; - let latest_version = match node_runtime.npm_package_latest_version(&package_name).await + versions.sort(); + let newest_version = if let Some((file_name, version)) = versions.last().cloned() + && minimum_version.is_none_or(|minimum_version| version > minimum_version) { - Ok(latest_version) => latest_version, - Err(e) => { - if let Some(installed_version) = installed_version { - log::error!("{e}"); - log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}"); - return Ok(command); - } else { - bail!(e); + versions.pop(); + Some(file_name) + } else { + None + }; + to_delete.extend(versions.into_iter().map(|(file_name, _)| file_name)); + + cx.background_spawn({ + let fs = fs.clone(); + let dir = dir.clone(); + async move { + for file_name in to_delete { + fs.remove_dir( + &dir.join(file_name), + RemoveOptions { + recursive: true, + ignore_if_not_exists: false, + }, + ) + .await + .ok(); } } + }) + .detach(); + + let version = if let Some(file_name) = newest_version { + cx.background_spawn({ + let file_name = file_name.clone(); + let dir = dir.clone(); + async move { + let latest_version = + node_runtime.npm_package_latest_version(&package_name).await; + if let Ok(latest_version) = latest_version + && &latest_version != &file_name.to_string_lossy() + { + Self::download_latest_version( + fs, + dir.clone(), + node_runtime, + package_name, + ) + .await + .log_err(); + } + } + }) + .detach(); + file_name + } else { + status_tx.send("Installing…".into()).ok(); + let dir = dir.clone(); + cx.background_spawn(Self::download_latest_version( + fs, + dir.clone(), + node_runtime, + package_name, + )) + .await? + .into() }; + anyhow::Ok(AgentServerCommand { + path: node_path, + args: vec![ + dir.join(version) + .join(entrypoint_path) + .to_string_lossy() + .to_string(), + ], + env: Default::default(), + }) + }) + .await + .map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into()) + }) + } - let should_install = node_runtime - .should_install_npm_package( - &package_name, - &local_executable_path, - &dir, - VersionStrategy::Latest(&latest_version), - ) - .await; + async fn download_latest_version( + fs: Arc, + dir: PathBuf, + node_runtime: NodeRuntime, + package_name: SharedString, + ) -> Result { + let tmp_dir = tempfile::tempdir_in(&dir)?; - if should_install { - status_tx.send("Installing latest version…".into())?; - node_runtime - .npm_install_packages(&dir, &[(&package_name, &latest_version)]) - .await?; - } + node_runtime + .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")]) + .await?; - Ok(command) - }).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into()) - }) + let version = node_runtime + .npm_package_installed_version(tmp_dir.path(), &package_name) + .await? + .context("expected package to be installed")?; + + fs.rename( + &tmp_dir.keep(), + &dir.join(&version), + RenameOptions { + ignore_if_exists: true, + overwrite: false, + }, + ) + .await?; + + anyhow::Ok(version) } }