@@ -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<dyn Fs>,
+ dir: PathBuf,
+ node_runtime: NodeRuntime,
+ package_name: SharedString,
+ ) -> Result<String> {
+ 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)
}
}