mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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 <conrad.irwin@gmail.com>
This commit is contained in:
parent
384ffb883f
commit
52da72d80a
1 changed files with 142 additions and 50 deletions
|
|
@ -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,71 +68,159 @@ 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;
|
||||
};
|
||||
|
||||
status_tx.send("Checking for latest version…".into())?;
|
||||
let latest_version = match node_runtime.npm_package_latest_version(&package_name).await
|
||||
{
|
||||
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);
|
||||
}
|
||||
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())
|
||||
}
|
||||
};
|
||||
|
||||
let should_install = node_runtime
|
||||
.should_install_npm_package(
|
||||
&package_name,
|
||||
&local_executable_path,
|
||||
&dir,
|
||||
VersionStrategy::Latest(&latest_version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install {
|
||||
status_tx.send("Installing latest version…".into())?;
|
||||
node_runtime
|
||||
.npm_install_packages(&dir, &[(&package_name, &latest_version)])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(command)
|
||||
}).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
|
||||
versions.sort();
|
||||
let newest_version = if let Some((file_name, version)) = versions.last().cloned()
|
||||
&& minimum_version.is_none_or(|minimum_version| version > minimum_version)
|
||||
{
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
node_runtime
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
|
||||
.await?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
|
|
|
|||
Loading…
Reference in a new issue