Add support for ACP registry in remote projects (#48935)

Bennet Bo Fenner , Ben Brandt , and Zed Zippy created

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 <benjamin.j.brandt@gmail.com>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>

Change summary

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

Detailed changes

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<Self> {
+    pub fn init_global(
+        cx: &mut App,
+        fs: Arc<dyn Fs>,
+        http_client: Arc<dyn HttpClient>,
+    ) -> Entity<Self> {
         if let Some(store) = Self::try_global(cx) {
             return store;
         }
 
-        let fs = <dyn Fs>::global(cx);
-        let http_client: Arc<dyn HttpClient> = 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| {

crates/project/src/agent_server_store.rs 🔗

@@ -408,6 +408,14 @@ impl AgentServerStore {
             .get::<AllAgentServersSettings>(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<BuiltinAgentServerSettings>,
     pub custom: HashMap<String, CustomAgentServerSettings>,
 }
+
+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<PathBuf>,

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

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

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

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