From 07a0ce91a1430abe5eb35f07f1be95d0f2e0c895 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 12 Feb 2026 12:50:24 +0100 Subject: [PATCH] Add support for ACP registry in remote projects (#48935) Closes #47910 https://github.com/user-attachments/assets/de2d18ef-46fd-4201-88e4-6214ddf0fd06 - [x] Tests or screenshots needed? - [x] Code Reviewed - [x] Manual QA Release Notes: - Added support for installing ACP agents via ACP registry in remote projects --------- Co-authored-by: Ben Brandt Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com> --- crates/project/src/agent_registry_store.rs | 23 ++++++------ crates/project/src/agent_server_store.rs | 37 +++++++++++++++++--- crates/remote_server/src/headless_project.rs | 6 ++-- crates/zed/src/main.rs | 6 +++- crates/zed/src/visual_test_runner.rs | 6 +++- crates/zed/src/zed.rs | 6 +++- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/crates/project/src/agent_registry_store.rs b/crates/project/src/agent_registry_store.rs index e1f97220bffb422aebbbb2d01e65cb63662187b5..5b047a815096d778b4d120132f0e024eaf128942 100644 --- a/crates/project/src/agent_registry_store.rs +++ b/crates/project/src/agent_registry_store.rs @@ -11,7 +11,7 @@ use http_client::{AsyncBody, HttpClient}; use serde::Deserialize; use settings::Settings; -use crate::agent_server_store::{AllAgentServersSettings, CustomAgentServerSettings}; +use crate::agent_server_store::AllAgentServersSettings; const REGISTRY_URL: &str = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json"; const REFRESH_THROTTLE_DURATION: Duration = Duration::from_secs(60 * 60); @@ -117,23 +117,19 @@ impl AgentRegistryStore { /// are registry agents configured in settings, it will trigger a network fetch. /// Otherwise, call `refresh()` explicitly when you need fresh data /// (e.g., when opening the Agent Registry page). - pub fn init_global(cx: &mut App) -> Entity { + pub fn init_global( + cx: &mut App, + fs: Arc, + http_client: Arc, + ) -> Entity { if let Some(store) = Self::try_global(cx) { return store; } - let fs = ::global(cx); - let http_client: Arc = cx.http_client(); - let store = cx.new(|cx| Self::new(fs, http_client, cx)); cx.set_global(GlobalAgentRegistryStore(store.clone())); - let has_registry_agents_in_settings = AllAgentServersSettings::get_global(cx) - .custom - .values() - .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. })); - - if has_registry_agents_in_settings { + if AllAgentServersSettings::get_global(cx).has_registry_agents() { store.update(cx, |store, cx| { if store.agents.is_empty() { store.refresh(cx); @@ -191,7 +187,10 @@ impl AgentRegistryStore { build_registry_agents(fs.clone(), http_client, data.index, data.raw_body, true) .await } - Err(error) => Err(error), + Err(error) => { + log::error!("AgentRegistryStore::refresh: fetch failed: {error:#}"); + Err(error) + } }; this.update(cx, |this, cx| { diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 3ebb9f1143483d7914b35fbe88ce171e741fe86a..cd601419e6074ccc882045f47bac9271163a462a 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -408,6 +408,14 @@ impl AgentServerStore { .get::(None) .clone(); + // If we don't have agents from the registry loaded yet, trigger a + // refresh, which will cause this function to be called again + if new_settings.has_registry_agents() + && let Some(registry) = AgentRegistryStore::try_global(cx) + { + registry.update(cx, |registry, cx| registry.refresh_if_stale(cx)); + } + self.external_agents.clear(); self.external_agents.insert( GEMINI_NAME.into(), @@ -554,7 +562,7 @@ impl AgentServerStore { CustomAgentServerSettings::Registry { env, .. } => { let Some(agent) = registry_agents_by_id.get(name) else { if registry_store.is_some() { - log::warn!("Registry agent '{}' not found in ACP registry", name); + log::debug!("Registry agent '{}' not found in ACP registry", name); } continue; }; @@ -914,10 +922,20 @@ impl AgentServerStore { } else { ExternalAgentSource::Custom }; - let (icon, display_name, source) = - metadata - .remove(&agent_name) - .unwrap_or((None, None, fallback_source)); + let (icon, display_name, source) = metadata + .remove(&agent_name) + .or_else(|| { + AgentRegistryStore::try_global(cx) + .and_then(|store| store.read(cx).agent(&agent_name.0)) + .map(|s| { + ( + s.icon_path().cloned(), + Some(s.name().clone()), + ExternalAgentSource::Registry, + ) + }) + }) + .unwrap_or((None, None, fallback_source)); let source = if fallback_source == ExternalAgentSource::Builtin { ExternalAgentSource::Builtin } else { @@ -2239,6 +2257,15 @@ pub struct AllAgentServersSettings { pub codex: Option, pub custom: HashMap, } + +impl AllAgentServersSettings { + pub fn has_registry_agents(&self) -> bool { + self.custom + .values() + .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. })) + } +} + #[derive(Default, Clone, JsonSchema, Debug, PartialEq)] pub struct BuiltinAgentServerSettings { pub path: Option, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 2b0b2be0c00ca5fb22a7ad10001ca95fce6c7ad7..65e060be9dac7b1232018e6774d5fd8eeb6ad34a 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -12,8 +12,8 @@ use http_client::HttpClient; use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation}; use node_runtime::NodeRuntime; use project::{ - LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, ProjectPath, - ToolchainStore, WorktreeId, + AgentRegistryStore, LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, + ProjectPath, ToolchainStore, WorktreeId, agent_server_store::AgentServerStore, buffer_store::{BufferStore, BufferStoreEvent}, context_server_store::ContextServerStore, @@ -223,6 +223,8 @@ impl HeadlessProject { lsp_store }); + AgentRegistryStore::init_global(cx, fs.clone(), http_client.clone()); + let agent_server_store = cx.new(|cx| { let mut agent_server_store = AgentServerStore::local( node_runtime.clone(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6dc23fa4c585fa58502c794d8d2480ce6a1b278d..c88a83b180d4107abf4573ab46619f4687937418 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -621,7 +621,11 @@ fn main() { snippet_provider::init(cx); edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); - project::AgentRegistryStore::init_global(cx); + project::AgentRegistryStore::init_global( + cx, + app_state.fs.clone(), + app_state.client.http_client(), + ); agent_ui::init( app_state.fs.clone(), app_state.client.clone(), diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 5de8cac8e14bfa31d32361ab77765b721fab26e6..716710f0976b55c1ecae862f101e710349aa9c36 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -201,7 +201,11 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> language_model::init(app_state.client.clone(), cx); language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); git_ui::init(cx); - project::AgentRegistryStore::init_global(cx); + project::AgentRegistryStore::init_global( + cx, + app_state.fs.clone(), + app_state.client.http_client(), + ); agent_ui::init( app_state.fs.clone(), app_state.client.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 49229ddeb1181f1b52655319f4dcfef570186898..5327d5a8818bbcbe2290f5add9612d215cfc6890 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5058,7 +5058,11 @@ mod tests { git_graph::init(cx); web_search_providers::init(app_state.client.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); - project::AgentRegistryStore::init_global(cx); + project::AgentRegistryStore::init_global( + cx, + app_state.fs.clone(), + app_state.client.http_client(), + ); agent_ui::init( app_state.fs.clone(), app_state.client.clone(),