agent_servers: Migrate all built-in agents to go via registry (#50094)

Ben Brandt , Anthony Eid , and cameron created

This has lots of benefits, but mainly allows users to uninstall agents.

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: cameron <cameron.studdstreet@gmail.com>

Change summary

crates/agent/src/native_agent_server.rs                   |  12 
crates/agent_servers/src/acp.rs                           |  23 
crates/agent_servers/src/agent_servers.rs                 |   8 
crates/agent_servers/src/claude.rs                        | 258 --
crates/agent_servers/src/codex.rs                         | 270 ---
crates/agent_servers/src/custom.rs                        |  74 
crates/agent_servers/src/e2e_tests.rs                     |  17 
crates/agent_servers/src/gemini.rs                        | 125 -
crates/agent_ui/src/acp/thread_view.rs                    | 128 -
crates/agent_ui/src/acp/thread_view/active_thread.rs      |  21 
crates/agent_ui/src/agent_configuration.rs                | 130 -
crates/agent_ui/src/agent_panel.rs                        | 311 +-
crates/agent_ui/src/agent_registry_ui.rs                  |   8 
crates/agent_ui/src/agent_ui.rs                           |   6 
crates/agent_ui/src/mention_set.rs                        |   2 
crates/agent_ui/src/ui/acp_onboarding_modal.rs            |  18 
crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs   |  14 
crates/client/src/zed_urls.rs                             |  16 
crates/migrator/src/migrations.rs                         |   6 
crates/migrator/src/migrations/m_2026_02_25/settings.rs   | 161 +
crates/migrator/src/migrator.rs                           | 412 ++++
crates/project/src/agent_registry_store.rs                |   2 
crates/project/src/agent_server_store.rs                  | 787 --------
crates/project/tests/integration/ext_agent_tests.rs       |  15 
crates/project/tests/integration/extension_agent_tests.rs |  35 
crates/remote_server/src/remote_editing_tests.rs          |   8 
crates/settings_content/src/agent.rs                      |  78 
crates/zed/src/visual_test_runner.rs                      |   4 
docs/src/ai/external-agents.md                            |  44 
29 files changed, 1,009 insertions(+), 1,984 deletions(-)

Detailed changes

crates/agent/src/native_agent_server.rs πŸ”—

@@ -37,12 +37,7 @@ impl AgentServer for NativeAgentServer {
         &self,
         delegate: AgentServerDelegate,
         cx: &mut App,
-    ) -> Task<
-        Result<(
-            Rc<dyn acp_thread::AgentConnection>,
-            Option<task::SpawnInTerminal>,
-        )>,
-    > {
+    ) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
         log::debug!("NativeAgentServer::connect");
         let project = delegate.project().clone();
         let fs = self.fs.clone();
@@ -62,10 +57,7 @@ impl AgentServer for NativeAgentServer {
             let connection = NativeAgentConnection(agent);
             log::debug!("NativeAgentServer connection established successfully");
 
-            Ok((
-                Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
-                None,
-            ))
+            Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
         })
     }
 

crates/agent_servers/src/acp.rs πŸ”—

@@ -10,7 +10,7 @@ use collections::HashMap;
 use futures::AsyncBufReadExt as _;
 use futures::io::BufReader;
 use project::Project;
-use project::agent_server_store::AgentServerCommand;
+use project::agent_server_store::{AgentServerCommand, GEMINI_NAME};
 use serde::Deserialize;
 use settings::Settings as _;
 use task::ShellBuilder;
@@ -319,8 +319,27 @@ impl AcpConnection {
             None
         };
 
+        // TODO: Remove this override once Google team releases their official auth methods
+        let auth_methods = if server_name == GEMINI_NAME {
+            let mut args = command.args.clone();
+            args.retain(|a| a != "--experimental-acp");
+            let value = serde_json::json!({
+                "label": "gemini /auth",
+                "command": command.path.to_string_lossy().into_owned(),
+                "args": args,
+                "env": command.env.clone().unwrap_or_default(),
+            });
+            let meta = acp::Meta::from_iter([("terminal-auth".to_string(), value)]);
+            vec![
+                acp::AuthMethod::new("spawn-gemini-cli", "Login")
+                    .description("Login with your Google or Vertex AI account")
+                    .meta(meta),
+            ]
+        } else {
+            response.auth_methods
+        };
         Ok(Self {
-            auth_methods: response.auth_methods,
+            auth_methods,
             connection,
             server_name,
             display_name,

crates/agent_servers/src/agent_servers.rs πŸ”—

@@ -1,19 +1,13 @@
 mod acp;
-mod claude;
-mod codex;
 mod custom;
-mod gemini;
 
 #[cfg(any(test, feature = "test-support"))]
 pub mod e2e_tests;
 
-pub use claude::*;
 use client::ProxySettings;
-pub use codex::*;
 use collections::{HashMap, HashSet};
 pub use custom::*;
 use fs::Fs;
-pub use gemini::*;
 use http_client::read_no_proxy_from_env;
 use project::agent_server_store::AgentServerStore;
 
@@ -60,7 +54,7 @@ pub trait AgentServer: Send {
         &self,
         delegate: AgentServerDelegate,
         cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
+    ) -> Task<Result<Rc<dyn AgentConnection>>>;
 
     fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
 

crates/agent_servers/src/claude.rs πŸ”—

@@ -1,258 +0,0 @@
-use agent_client_protocol as acp;
-use collections::HashSet;
-use fs::Fs;
-use settings::{SettingsStore, update_settings_file};
-use std::rc::Rc;
-use std::sync::Arc;
-use std::{any::Any, path::PathBuf};
-
-use anyhow::{Context as _, Result};
-use gpui::{App, AppContext as _, SharedString, Task};
-use project::agent_server_store::{AllAgentServersSettings, CLAUDE_AGENT_NAME};
-
-use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
-use acp_thread::AgentConnection;
-
-#[derive(Clone)]
-pub struct ClaudeCode;
-
-pub struct AgentServerLoginCommand {
-    pub path: PathBuf,
-    pub arguments: Vec<String>,
-}
-
-impl AgentServer for ClaudeCode {
-    fn name(&self) -> SharedString {
-        "Claude Agent".into()
-    }
-
-    fn logo(&self) -> ui::IconName {
-        ui::IconName::AiClaude
-    }
-
-    fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).claude.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
-    }
-
-    fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        update_settings_file(fs, cx, |settings, _| {
-            settings
-                .agent_servers
-                .get_or_insert_default()
-                .claude
-                .get_or_insert_default()
-                .default_mode = mode_id.map(|m| m.to_string())
-        });
-    }
-
-    fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).claude.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
-    }
-
-    fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        update_settings_file(fs, cx, |settings, _| {
-            settings
-                .agent_servers
-                .get_or_insert_default()
-                .claude
-                .get_or_insert_default()
-                .default_model = model_id.map(|m| m.to_string())
-        });
-    }
-
-    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).claude.clone()
-        });
-
-        settings
-            .as_ref()
-            .map(|s| {
-                s.favorite_models
-                    .iter()
-                    .map(|id| acp::ModelId::new(id.clone()))
-                    .collect()
-            })
-            .unwrap_or_default()
-    }
-
-    fn toggle_favorite_model(
-        &self,
-        model_id: acp::ModelId,
-        should_be_favorite: bool,
-        fs: Arc<dyn Fs>,
-        cx: &App,
-    ) {
-        update_settings_file(fs, cx, move |settings, _| {
-            let favorite_models = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .claude
-                .get_or_insert_default()
-                .favorite_models;
-
-            let model_id_str = model_id.to_string();
-            if should_be_favorite {
-                if !favorite_models.contains(&model_id_str) {
-                    favorite_models.push(model_id_str);
-                }
-            } else {
-                favorite_models.retain(|id| id != &model_id_str);
-            }
-        });
-    }
-
-    fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).claude.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_config_options.get(config_id).cloned())
-    }
-
-    fn set_default_config_option(
-        &self,
-        config_id: &str,
-        value_id: Option<&str>,
-        fs: Arc<dyn Fs>,
-        cx: &mut App,
-    ) {
-        let config_id = config_id.to_string();
-        let value_id = value_id.map(|s| s.to_string());
-        update_settings_file(fs, cx, move |settings, _| {
-            let config_options = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .claude
-                .get_or_insert_default()
-                .default_config_options;
-
-            if let Some(value) = value_id.clone() {
-                config_options.insert(config_id.clone(), value);
-            } else {
-                config_options.remove(&config_id);
-            }
-        });
-    }
-
-    fn favorite_config_option_value_ids(
-        &self,
-        config_id: &acp::SessionConfigId,
-        cx: &mut App,
-    ) -> HashSet<acp::SessionConfigValueId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).claude.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
-            .map(|values| {
-                values
-                    .iter()
-                    .cloned()
-                    .map(acp::SessionConfigValueId::new)
-                    .collect()
-            })
-            .unwrap_or_default()
-    }
-
-    fn toggle_favorite_config_option_value(
-        &self,
-        config_id: acp::SessionConfigId,
-        value_id: acp::SessionConfigValueId,
-        should_be_favorite: bool,
-        fs: Arc<dyn Fs>,
-        cx: &App,
-    ) {
-        let config_id = config_id.to_string();
-        let value_id = value_id.to_string();
-
-        update_settings_file(fs, cx, move |settings, _| {
-            let favorites = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .claude
-                .get_or_insert_default()
-                .favorite_config_option_values;
-
-            let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
-
-            if should_be_favorite {
-                if !entry.iter().any(|v| v == &value_id) {
-                    entry.push(value_id.clone());
-                }
-            } else {
-                entry.retain(|v| v != &value_id);
-                if entry.is_empty() {
-                    favorites.remove(&config_id);
-                }
-            }
-        });
-    }
-
-    fn connect(
-        &self,
-        delegate: AgentServerDelegate,
-        cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
-        let name = self.name();
-        let store = delegate.store.downgrade();
-        let extra_env = load_proxy_env(cx);
-        let default_mode = self.default_mode(cx);
-        let default_model = self.default_model(cx);
-        let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
-            settings
-                .get::<AllAgentServersSettings>(None)
-                .claude
-                .as_ref()
-                .map(|s| s.default_config_options.clone())
-                .unwrap_or_default()
-        });
-
-        cx.spawn(async move |cx| {
-            let (command, login) = store
-                .update(cx, |store, cx| {
-                    let agent = store
-                        .get_external_agent(&CLAUDE_AGENT_NAME.into())
-                        .context("Claude Agent is not registered")?;
-                    anyhow::Ok(agent.get_command(
-                        extra_env,
-                        delegate.status_tx,
-                        delegate.new_version_available,
-                        &mut cx.to_async(),
-                    ))
-                })??
-                .await?;
-            let connection = crate::acp::connect(
-                name.clone(),
-                name,
-                command,
-                default_mode,
-                default_model,
-                default_config_options,
-                cx,
-            )
-            .await?;
-            Ok((connection, login))
-        })
-    }
-
-    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
-        self
-    }
-}

crates/agent_servers/src/codex.rs πŸ”—

@@ -1,270 +0,0 @@
-use std::any::Any;
-use std::rc::Rc;
-use std::sync::Arc;
-
-use acp_thread::AgentConnection;
-use agent_client_protocol as acp;
-use anyhow::{Context as _, Result};
-use collections::HashSet;
-use fs::Fs;
-use gpui::{App, AppContext as _, SharedString, Task};
-use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
-use settings::{SettingsStore, update_settings_file};
-
-use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
-
-#[derive(Clone)]
-pub struct Codex;
-
-const CODEX_API_KEY_VAR_NAME: &str = "CODEX_API_KEY";
-const OPEN_AI_API_KEY_VAR_NAME: &str = "OPEN_AI_API_KEY";
-
-impl AgentServer for Codex {
-    fn name(&self) -> SharedString {
-        "Codex".into()
-    }
-
-    fn logo(&self) -> ui::IconName {
-        ui::IconName::AiOpenAi
-    }
-
-    fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).codex.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
-    }
-
-    fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        update_settings_file(fs, cx, |settings, _| {
-            settings
-                .agent_servers
-                .get_or_insert_default()
-                .codex
-                .get_or_insert_default()
-                .default_mode = mode_id.map(|m| m.to_string())
-        });
-    }
-
-    fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).codex.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
-    }
-
-    fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        update_settings_file(fs, cx, |settings, _| {
-            settings
-                .agent_servers
-                .get_or_insert_default()
-                .codex
-                .get_or_insert_default()
-                .default_model = model_id.map(|m| m.to_string())
-        });
-    }
-
-    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).codex.clone()
-        });
-
-        settings
-            .as_ref()
-            .map(|s| {
-                s.favorite_models
-                    .iter()
-                    .map(|id| acp::ModelId::new(id.clone()))
-                    .collect()
-            })
-            .unwrap_or_default()
-    }
-
-    fn toggle_favorite_model(
-        &self,
-        model_id: acp::ModelId,
-        should_be_favorite: bool,
-        fs: Arc<dyn Fs>,
-        cx: &App,
-    ) {
-        update_settings_file(fs, cx, move |settings, _| {
-            let favorite_models = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .codex
-                .get_or_insert_default()
-                .favorite_models;
-
-            let model_id_str = model_id.to_string();
-            if should_be_favorite {
-                if !favorite_models.contains(&model_id_str) {
-                    favorite_models.push(model_id_str);
-                }
-            } else {
-                favorite_models.retain(|id| id != &model_id_str);
-            }
-        });
-    }
-
-    fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).codex.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.default_config_options.get(config_id).cloned())
-    }
-
-    fn set_default_config_option(
-        &self,
-        config_id: &str,
-        value_id: Option<&str>,
-        fs: Arc<dyn Fs>,
-        cx: &mut App,
-    ) {
-        let config_id = config_id.to_string();
-        let value_id = value_id.map(|s| s.to_string());
-        update_settings_file(fs, cx, move |settings, _| {
-            let config_options = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .codex
-                .get_or_insert_default()
-                .default_config_options;
-
-            if let Some(value) = value_id.clone() {
-                config_options.insert(config_id.clone(), value);
-            } else {
-                config_options.remove(&config_id);
-            }
-        });
-    }
-
-    fn favorite_config_option_value_ids(
-        &self,
-        config_id: &acp::SessionConfigId,
-        cx: &mut App,
-    ) -> HashSet<acp::SessionConfigValueId> {
-        let settings = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<AllAgentServersSettings>(None).codex.clone()
-        });
-
-        settings
-            .as_ref()
-            .and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
-            .map(|values| {
-                values
-                    .iter()
-                    .cloned()
-                    .map(acp::SessionConfigValueId::new)
-                    .collect()
-            })
-            .unwrap_or_default()
-    }
-
-    fn toggle_favorite_config_option_value(
-        &self,
-        config_id: acp::SessionConfigId,
-        value_id: acp::SessionConfigValueId,
-        should_be_favorite: bool,
-        fs: Arc<dyn Fs>,
-        cx: &App,
-    ) {
-        let config_id = config_id.to_string();
-        let value_id = value_id.to_string();
-
-        update_settings_file(fs, cx, move |settings, _| {
-            let favorites = &mut settings
-                .agent_servers
-                .get_or_insert_default()
-                .codex
-                .get_or_insert_default()
-                .favorite_config_option_values;
-
-            let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
-
-            if should_be_favorite {
-                if !entry.iter().any(|v| v == &value_id) {
-                    entry.push(value_id.clone());
-                }
-            } else {
-                entry.retain(|v| v != &value_id);
-                if entry.is_empty() {
-                    favorites.remove(&config_id);
-                }
-            }
-        });
-    }
-
-    fn connect(
-        &self,
-        delegate: AgentServerDelegate,
-        cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
-        let name = self.name();
-        let store = delegate.store.downgrade();
-        let mut extra_env = load_proxy_env(cx);
-        let default_mode = self.default_mode(cx);
-        let default_model = self.default_model(cx);
-        let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
-            settings
-                .get::<AllAgentServersSettings>(None)
-                .codex
-                .as_ref()
-                .map(|s| s.default_config_options.clone())
-                .unwrap_or_default()
-        });
-        if let Ok(api_key) = std::env::var(CODEX_API_KEY_VAR_NAME) {
-            extra_env.insert(CODEX_API_KEY_VAR_NAME.into(), api_key);
-        }
-        if let Ok(api_key) = std::env::var(OPEN_AI_API_KEY_VAR_NAME) {
-            extra_env.insert(OPEN_AI_API_KEY_VAR_NAME.into(), api_key);
-        }
-
-        cx.spawn(async move |cx| {
-            let (command, login) = store
-                .update(cx, |store, cx| {
-                    let agent = store
-                        .get_external_agent(&CODEX_NAME.into())
-                        .context("Codex is not registered")?;
-                    anyhow::Ok(agent.get_command(
-                        extra_env,
-                        delegate.status_tx,
-                        delegate.new_version_available,
-                        &mut cx.to_async(),
-                    ))
-                })??
-                .await?;
-
-            let connection = crate::acp::connect(
-                name.clone(),
-                name,
-                command,
-                default_mode,
-                default_model,
-                default_config_options,
-                cx,
-            )
-            .await?;
-            Ok((connection, login))
-        })
-    }
-
-    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
-        self
-    }
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
-    use super::*;
-
-    crate::common_e2e_tests!(async |_, _| Codex, allow_option_id = "proceed_once");
-}

crates/agent_servers/src/custom.rs πŸ”—

@@ -3,9 +3,13 @@ use acp_thread::AgentConnection;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result};
 use collections::HashSet;
+use credentials_provider::CredentialsProvider;
 use fs::Fs;
 use gpui::{App, AppContext as _, SharedString, Task};
-use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
+use language_model::{ApiKey, EnvVar};
+use project::agent_server_store::{
+    AllAgentServersSettings, CLAUDE_AGENT_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
+};
 use settings::{SettingsStore, update_settings_file};
 use std::{rc::Rc, sync::Arc};
 use ui::IconName;
@@ -34,7 +38,6 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .custom
                 .get(self.name().as_ref())
                 .cloned()
         });
@@ -52,7 +55,6 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .custom
                 .get(self.name().as_ref())
                 .cloned()
         });
@@ -86,7 +88,6 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .custom
                 .entry(name.to_string())
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
@@ -135,7 +136,6 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .custom
                 .entry(name.to_string())
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
@@ -160,7 +160,6 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .custom
                 .get(self.name().as_ref())
                 .cloned()
         });
@@ -176,7 +175,6 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .custom
                 .entry(name.to_string())
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
@@ -201,7 +199,6 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .custom
                 .get(self.name().as_ref())
                 .cloned()
         });
@@ -229,7 +226,6 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .custom
                 .entry(name.to_string())
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
@@ -267,7 +263,6 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .custom
                 .get(self.name().as_ref())
                 .cloned()
         });
@@ -291,7 +286,6 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .custom
                 .entry(name.to_string())
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
@@ -329,7 +323,7 @@ impl AgentServer for CustomAgentServer {
         &self,
         delegate: AgentServerDelegate,
         cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+    ) -> Task<Result<Rc<dyn AgentConnection>>> {
         let name = self.name();
         let display_name = delegate
             .store
@@ -338,11 +332,12 @@ impl AgentServer for CustomAgentServer {
             .unwrap_or_else(|| name.clone());
         let default_mode = self.default_mode(cx);
         let default_model = self.default_model(cx);
+        let is_previous_built_in =
+            matches!(name.as_ref(), CLAUDE_AGENT_NAME | CODEX_NAME | GEMINI_NAME);
         let (default_config_options, is_registry_agent) =
             cx.read_global(|settings: &SettingsStore, _| {
                 let agent_settings = settings
                     .get::<AllAgentServersSettings>(None)
-                    .custom
                     .get(self.name().as_ref());
 
                 let is_registry = agent_settings
@@ -374,16 +369,46 @@ impl AgentServer for CustomAgentServer {
                 (config_options, is_registry)
             });
 
+        // Intermediate step to allow for previous built-ins to also be triggered if they aren't in settings yet.
+        let is_registry_agent = is_registry_agent || is_previous_built_in;
+
         if is_registry_agent {
             if let Some(registry_store) = project::AgentRegistryStore::try_global(cx) {
                 registry_store.update(cx, |store, cx| store.refresh_if_stale(cx));
             }
         }
 
+        let mut extra_env = load_proxy_env(cx);
+        if delegate.store.read(cx).no_browser() {
+            extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
+        }
+        if is_registry_agent {
+            match name.as_ref() {
+                CLAUDE_AGENT_NAME => {
+                    extra_env.insert("ANTHROPIC_API_KEY".into(), "".into());
+                }
+                CODEX_NAME => {
+                    if let Ok(api_key) = std::env::var("CODEX_API_KEY") {
+                        extra_env.insert("CODEX_API_KEY".into(), api_key);
+                    }
+                    if let Ok(api_key) = std::env::var("OPEN_AI_API_KEY") {
+                        extra_env.insert("OPEN_AI_API_KEY".into(), api_key);
+                    }
+                }
+                GEMINI_NAME => {
+                    extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
+                }
+                _ => {}
+            }
+        }
         let store = delegate.store.downgrade();
-        let extra_env = load_proxy_env(cx);
         cx.spawn(async move |cx| {
-            let (command, login) = store
+            if is_registry_agent && name.as_ref() == GEMINI_NAME {
+                if let Some(api_key) = cx.update(api_key_for_gemini_cli).await.ok() {
+                    extra_env.insert("GEMINI_API_KEY".into(), api_key);
+                }
+            }
+            let command = store
                 .update(cx, |store, cx| {
                     let agent = store
                         .get_external_agent(&ExternalAgentServerName(name.clone()))
@@ -408,7 +433,7 @@ impl AgentServer for CustomAgentServer {
                 cx,
             )
             .await?;
-            Ok((connection, login))
+            Ok(connection)
         })
     }
 
@@ -416,3 +441,20 @@ impl AgentServer for CustomAgentServer {
         self
     }
 }
+
+fn api_key_for_gemini_cli(cx: &mut App) -> Task<Result<String>> {
+    let env_var = EnvVar::new("GEMINI_API_KEY".into()).or(EnvVar::new("GOOGLE_AI_API_KEY".into()));
+    if let Some(key) = env_var.value {
+        return Task::ready(Ok(key));
+    }
+    let credentials_provider = <dyn CredentialsProvider>::global(cx);
+    let api_url = google_ai::API_URL.to_string();
+    cx.spawn(async move |cx| {
+        Ok(
+            ApiKey::load_from_system_keychain(&api_url, credentials_provider.as_ref(), cx)
+                .await?
+                .key()
+                .to_string(),
+        )
+    })
+}

crates/agent_servers/src/e2e_tests.rs πŸ”—

@@ -4,8 +4,6 @@ use agent_client_protocol as acp;
 use futures::{FutureExt, StreamExt, channel::mpsc, select};
 use gpui::{Entity, TestAppContext};
 use indoc::indoc;
-#[cfg(test)]
-use project::agent_server_store::BuiltinAgentServerSettings;
 use project::{FakeFs, Project};
 #[cfg(test)]
 use settings::Settings;
@@ -414,18 +412,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
 
         #[cfg(test)]
         project::agent_server_store::AllAgentServersSettings::override_global(
-            project::agent_server_store::AllAgentServersSettings {
-                claude: Some(BuiltinAgentServerSettings {
-                    path: Some("claude-agent-acp".into()),
-                    ..Default::default()
-                }),
-                gemini: Some(crate::gemini::tests::local_command().into()),
-                codex: Some(BuiltinAgentServerSettings {
-                    path: Some("codex-acp".into()),
-                    ..Default::default()
-                }),
-                custom: collections::HashMap::default(),
-            },
+            project::agent_server_store::AllAgentServersSettings(collections::HashMap::default()),
             cx,
         );
     });
@@ -444,7 +431,7 @@ pub async fn new_test_thread(
     let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
     let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
 
-    let (connection, _) = cx.update(|cx| server.connect(delegate, cx)).await.unwrap();
+    let connection = cx.update(|cx| server.connect(delegate, cx)).await.unwrap();
 
     cx.update(|cx| connection.new_session(project.clone(), current_dir.as_ref(), cx))
         .await

crates/agent_servers/src/gemini.rs πŸ”—

@@ -1,125 +0,0 @@
-use std::any::Any;
-use std::rc::Rc;
-
-use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
-use acp_thread::AgentConnection;
-use anyhow::{Context as _, Result};
-use credentials_provider::CredentialsProvider;
-use gpui::{App, AppContext as _, SharedString, Task};
-use language_model::{ApiKey, EnvVar};
-use project::agent_server_store::{AllAgentServersSettings, GEMINI_NAME};
-use settings::SettingsStore;
-
-const GEMINI_API_KEY_VAR_NAME: &str = "GEMINI_API_KEY";
-const GOOGLE_AI_API_KEY_VAR_NAME: &str = "GOOGLE_AI_API_KEY";
-
-fn api_key_for_gemini_cli(cx: &mut App) -> Task<Result<String>> {
-    let env_var = EnvVar::new(GEMINI_API_KEY_VAR_NAME.into())
-        .or(EnvVar::new(GOOGLE_AI_API_KEY_VAR_NAME.into()));
-    if let Some(key) = env_var.value {
-        return Task::ready(Ok(key));
-    }
-    let credentials_provider = <dyn CredentialsProvider>::global(cx);
-    let api_url = google_ai::API_URL.to_string();
-    cx.spawn(async move |cx| {
-        Ok(
-            ApiKey::load_from_system_keychain(&api_url, credentials_provider.as_ref(), cx)
-                .await?
-                .key()
-                .to_string(),
-        )
-    })
-}
-
-#[derive(Clone)]
-pub struct Gemini;
-
-impl AgentServer for Gemini {
-    fn name(&self) -> SharedString {
-        "Gemini CLI".into()
-    }
-
-    fn logo(&self) -> ui::IconName {
-        ui::IconName::AiGemini
-    }
-
-    fn connect(
-        &self,
-        delegate: AgentServerDelegate,
-        cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
-        let name = self.name();
-        let store = delegate.store.downgrade();
-        let mut extra_env = load_proxy_env(cx);
-        let default_mode = self.default_mode(cx);
-        let default_model = self.default_model(cx);
-        let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
-            settings
-                .get::<AllAgentServersSettings>(None)
-                .gemini
-                .as_ref()
-                .map(|s| s.default_config_options.clone())
-                .unwrap_or_default()
-        });
-
-        cx.spawn(async move |cx| {
-            extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
-
-            if let Some(api_key) = cx.update(api_key_for_gemini_cli).await.ok() {
-                extra_env.insert("GEMINI_API_KEY".into(), api_key);
-            }
-            let (command, login) = store
-                .update(cx, |store, cx| {
-                    let agent = store
-                        .get_external_agent(&GEMINI_NAME.into())
-                        .context("Gemini CLI is not registered")?;
-                    anyhow::Ok(agent.get_command(
-                        extra_env,
-                        delegate.status_tx,
-                        delegate.new_version_available,
-                        &mut cx.to_async(),
-                    ))
-                })??
-                .await?;
-
-            let connection = crate::acp::connect(
-                name.clone(),
-                name,
-                command,
-                default_mode,
-                default_model,
-                default_config_options,
-                cx,
-            )
-            .await?;
-            Ok((connection, login))
-        })
-    }
-
-    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
-        self
-    }
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
-    use project::agent_server_store::AgentServerCommand;
-
-    use super::*;
-    use std::path::Path;
-
-    crate::common_e2e_tests!(async |_, _| Gemini, allow_option_id = "proceed_once");
-
-    pub fn local_command() -> AgentServerCommand {
-        let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("../../../gemini-cli/packages/cli")
-            .to_string_lossy()
-            .to_string();
-
-        AgentServerCommand {
-            path: "node".into(),
-            args: vec![cli_path],
-            env: None,
-        }
-    }
-}

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -107,8 +107,8 @@ pub(crate) enum ThreadError {
     },
 }
 
-impl ThreadError {
-    fn from_err(error: anyhow::Error, agent_name: &str) -> Self {
+impl From<anyhow::Error> for ThreadError {
+    fn from(error: anyhow::Error) -> Self {
         if error.is::<language_model::PaymentRequiredError>() {
             Self::PaymentRequired
         } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
@@ -123,18 +123,9 @@ impl ThreadError {
                 .downcast_ref::<acp::Error>()
                 .map(|acp_error| SharedString::from(acp_error.code.to_string()));
 
-            // TODO: we should have Gemini return better errors here.
-            if agent_name == "Gemini CLI"
-                && message.contains("Could not load the default credentials")
-                || message.contains("API key not valid")
-                || message.contains("Request had invalid authentication credentials")
-            {
-                Self::AuthenticationRequired(message)
-            } else {
-                Self::Other {
-                    message,
-                    acp_error_code,
-                }
+            Self::Other {
+                message,
+                acp_error_code,
             }
         }
     }
@@ -286,7 +277,6 @@ pub struct AcpServerView {
     thread_store: Option<Entity<ThreadStore>>,
     prompt_store: Option<Entity<PromptStore>>,
     server_state: ServerState,
-    login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
     history: Entity<AcpThreadHistory>,
     focus_handle: FocusHandle,
     notifications: Vec<WindowHandle<AgentNotification>>,
@@ -296,6 +286,12 @@ pub struct AcpServerView {
 }
 
 impl AcpServerView {
+    pub fn has_auth_methods(&self) -> bool {
+        self.as_connected().map_or(false, |connected| {
+            !connected.connection.auth_methods().is_empty()
+        })
+    }
+
     pub fn active_thread(&self) -> Option<&Entity<AcpThreadView>> {
         match &self.server_state {
             ServerState::Connected(connected) => connected.active_view(),
@@ -487,7 +483,6 @@ impl AcpServerView {
                 window,
                 cx,
             ),
-            login: None,
             notifications: Vec::new(),
             notification_subscriptions: HashMap::default(),
             auth_task: None,
@@ -598,16 +593,13 @@ impl AcpServerView {
         let connect_task = agent.connect(delegate, cx);
         let load_task = cx.spawn_in(window, async move |this, cx| {
             let connection = match connect_task.await {
-                Ok((connection, login)) => {
-                    this.update(cx, |this, _| this.login = login).ok();
-                    connection
-                }
+                Ok(connection) => connection,
                 Err(err) => {
                     this.update_in(cx, |this, window, cx| {
                         if err.downcast_ref::<LoadError>().is_some() {
                             this.handle_load_error(err, window, cx);
                         } else if let Some(active) = this.active_thread() {
-                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
+                            active.update(cx, |active, cx| active.handle_thread_error(err, cx));
                         } else {
                             this.handle_load_error(err, window, cx);
                         }
@@ -922,7 +914,6 @@ impl AcpServerView {
                 parent_id,
                 thread,
                 conversation,
-                self.login.clone(),
                 weak,
                 agent_icon,
                 agent_name,
@@ -1480,7 +1471,7 @@ impl AcpServerView {
                                     }
                                     if let Some(active) = this.active_thread() {
                                         active.update(cx, |active, cx| {
-                                            active.handle_any_thread_error(err, cx);
+                                            active.handle_thread_error(err, cx);
                                         })
                                     }
                                 } else {
@@ -1496,79 +1487,10 @@ impl AcpServerView {
             }
         }
 
-        if method.0.as_ref() == "gemini-api-key" {
-            let registry = LanguageModelRegistry::global(cx);
-            let provider = registry
-                .read(cx)
-                .provider(&language_model::GOOGLE_PROVIDER_ID)
-                .unwrap();
-            if !provider.is_authenticated(cx) {
-                let this = cx.weak_entity();
-                let agent_name = self.agent.name();
-                let connection = connection.clone();
-                window.defer(cx, |window, cx| {
-                    Self::handle_auth_required(
-                        this,
-                        AuthRequired {
-                            description: Some("GEMINI_API_KEY must be set".to_owned()),
-                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
-                        },
-                        agent_name,
-                        connection,
-                        window,
-                        cx,
-                    );
-                });
-                return;
-            }
-        } else if method.0.as_ref() == "vertex-ai"
-            && std::env::var("GOOGLE_API_KEY").is_err()
-            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
-                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
-        {
-            let this = cx.weak_entity();
-            let agent_name = self.agent.name();
-            let connection = connection.clone();
-
-            window.defer(cx, |window, cx| {
-                    Self::handle_auth_required(
-                        this,
-                        AuthRequired {
-                            description: Some(
-                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
-                                    .to_owned(),
-                            ),
-                            provider_id: None,
-                        },
-                        agent_name,
-                        connection,
-                        window,
-                        cx,
-                    )
-                });
-            return;
-        }
-
         configuration_view.take();
         pending_auth_method.replace(method.clone());
-        let authenticate = if let Some(login) = self.login.clone() {
-            if let Some(workspace) = self.workspace.upgrade() {
-                let project = self.project.clone();
-                Self::spawn_external_agent_login(
-                    login,
-                    workspace,
-                    project,
-                    method.clone(),
-                    false,
-                    window,
-                    cx,
-                )
-            } else {
-                Task::ready(Ok(()))
-            }
-        } else {
-            connection.authenticate(method, cx)
-        };
+
+        let authenticate = connection.authenticate(method, cx);
         cx.notify();
         self.auth_task = Some(cx.spawn_in(window, {
             async move |this, cx| {
@@ -1598,7 +1520,7 @@ impl AcpServerView {
                             pending_auth_method.take();
                         }
                         if let Some(active) = this.active_thread() {
-                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
+                            active.update(cx, |active, cx| active.handle_thread_error(err, cx));
                         }
                     } else {
                         this.reset(window, cx);
@@ -1843,15 +1765,7 @@ impl AcpServerView {
                     .enumerate()
                     .rev()
                     .map(|(ix, method)| {
-                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
-                            && method.id.0.as_ref() == "oauth-personal"
-                            && method.name == "Log in with Google"
-                        {
-                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
-                        } else {
-                            (method.id.0.clone(), method.name.clone())
-                        };
-
+                        let (method_id, name) = (method.id.0.clone(), method.name.clone());
                         let agent_telemetry_id = connection.telemetry_id();
 
                         Button::new(method_id.clone(), name)
@@ -3617,8 +3531,8 @@ pub(crate) mod tests {
             &self,
             _delegate: AgentServerDelegate,
             _cx: &mut App,
-        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
-            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
+        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
+            Task::ready(Ok(Rc::new(self.connection.clone())))
         }
 
         fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
@@ -3641,7 +3555,7 @@ pub(crate) mod tests {
             &self,
             _delegate: AgentServerDelegate,
             _cx: &mut App,
-        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
             Task::ready(Err(anyhow!(
                 "extracting downloaded asset for \
                  https://github.com/zed-industries/codex-acp/releases/download/v0.9.4/\

crates/agent_ui/src/acp/thread_view/active_thread.rs πŸ”—

@@ -189,7 +189,6 @@ impl DiffStats {
 pub struct AcpThreadView {
     pub id: acp::SessionId,
     pub parent_id: Option<acp::SessionId>,
-    pub login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
     pub thread: Entity<AcpThread>,
     pub(crate) conversation: Entity<super::Conversation>,
     pub server_view: WeakEntity<AcpServerView>,
@@ -281,7 +280,6 @@ impl AcpThreadView {
         parent_id: Option<acp::SessionId>,
         thread: Entity<AcpThread>,
         conversation: Entity<super::Conversation>,
-        login: Option<task::SpawnInTerminal>,
         server_view: WeakEntity<AcpServerView>,
         agent_icon: IconName,
         agent_name: SharedString,
@@ -387,7 +385,6 @@ impl AcpThreadView {
             focus_handle: cx.focus_handle(),
             thread,
             conversation,
-            login,
             server_view,
             agent_icon,
             agent_name,
@@ -658,7 +655,7 @@ impl AcpThreadView {
         let text = text.trim();
         if text == "/login" || text == "/logout" {
             let connection = thread.read(cx).connection().clone();
-            let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
+            let can_login = !connection.auth_methods().is_empty();
             // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
             let logout_supported = text == "/logout"
                 && self
@@ -833,7 +830,7 @@ impl AcpThreadView {
         cx.spawn(async move |this, cx| {
             if let Err(err) = task.await {
                 this.update(cx, |this, cx| {
-                    this.handle_any_thread_error(err, cx);
+                    this.handle_thread_error(err, cx);
                 })
                 .ok();
             } else {
@@ -891,12 +888,12 @@ impl AcpThreadView {
         .detach();
     }
 
-    pub(crate) fn handle_any_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
-        let error = ThreadError::from_err(error, &self.agent_name);
-        self.handle_thread_error(error, cx);
-    }
-
-    pub(crate) fn handle_thread_error(&mut self, error: ThreadError, cx: &mut Context<Self>) {
+    pub(crate) fn handle_thread_error(
+        &mut self,
+        error: impl Into<ThreadError>,
+        cx: &mut Context<Self>,
+    ) {
+        let error = error.into();
         self.emit_thread_error_telemetry(&error, cx);
         self.thread_error = Some(error);
         cx.notify();
@@ -964,7 +961,7 @@ impl AcpThreadView {
 
             this.update(cx, |this, cx| {
                 if let Err(err) = result {
-                    this.handle_any_thread_error(err, cx);
+                    this.handle_thread_error(err, cx);
                 }
             })
         })

crates/agent_ui/src/agent_configuration.rs πŸ”—

@@ -8,7 +8,6 @@ use std::{ops::Range, sync::Arc};
 
 use agent::ContextServerRegistry;
 use anyhow::Result;
-use client::zed_urls;
 use cloud_api_types::Plan;
 use collections::HashMap;
 use context_server::ContextServerId;
@@ -20,6 +19,7 @@ use gpui::{
     Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
     ScrollHandle, Subscription, Task, WeakEntity,
 };
+use itertools::Itertools;
 use language::LanguageRegistry;
 use language_model::{
     IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
@@ -28,10 +28,7 @@ use language_model::{
 use language_models::AllLanguageModelSettings;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{
-        AgentServerStore, CLAUDE_AGENT_NAME, CODEX_NAME, ExternalAgentServerName,
-        ExternalAgentSource, GEMINI_NAME,
-    },
+    agent_server_store::{AgentServerStore, ExternalAgentServerName, ExternalAgentSource},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
@@ -941,9 +938,6 @@ impl AgentConfiguration {
 
         let user_defined_agents = agent_server_store
             .external_agents()
-            .filter(|name| {
-                name.0 != GEMINI_NAME && name.0 != CLAUDE_AGENT_NAME && name.0 != CODEX_NAME
-            })
             .cloned()
             .collect::<Vec<_>>();
 
@@ -961,6 +955,7 @@ impl AgentConfiguration {
                 let source = agent_server_store.agent_source(&name).unwrap_or_default();
                 (name, icon, display_name, source)
             })
+            .sorted_unstable_by_key(|(_, _, display_name, _)| display_name.to_lowercase())
             .collect();
 
         let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
@@ -998,22 +993,6 @@ impl AgentConfiguration {
                         })
                         .separator()
                         .header("Learn More")
-                        .item(
-                            ContextMenuEntry::new("Agent Servers Docs")
-                                .icon(IconName::ArrowUpRight)
-                                .icon_color(Color::Muted)
-                                .icon_position(IconPosition::End)
-                                .handler({
-                                    move |window, cx| {
-                                        window.dispatch_action(
-                                            Box::new(OpenBrowser {
-                                                url: zed_urls::agent_server_docs(cx),
-                                            }),
-                                            cx,
-                                        );
-                                    }
-                                }),
-                        )
                         .item(
                             ContextMenuEntry::new("ACP Docs")
                                 .icon(IconName::ArrowUpRight)
@@ -1049,51 +1028,24 @@ impl AgentConfiguration {
                         "All agents connected through the Agent Client Protocol.",
                         add_agent_popover.into_any_element(),
                     ))
-                    .child(
-                        v_flex()
-                            .p_4()
-                            .pt_0()
-                            .gap_2()
-                            .child(self.render_agent_server(
-                                AgentIcon::Name(IconName::AiClaude),
-                                "Claude Agent",
-                                "Claude Agent",
-                                ExternalAgentSource::Builtin,
-                                cx,
-                            ))
-                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
-                            .child(self.render_agent_server(
-                                AgentIcon::Name(IconName::AiOpenAi),
-                                "Codex CLI",
-                                "Codex CLI",
-                                ExternalAgentSource::Builtin,
-                                cx,
-                            ))
-                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
-                            .child(self.render_agent_server(
-                                AgentIcon::Name(IconName::AiGemini),
-                                "Gemini CLI",
-                                "Gemini CLI",
-                                ExternalAgentSource::Builtin,
+                    .child(v_flex().p_4().pt_0().gap_2().map(|mut parent| {
+                        let mut first = true;
+                        for (name, icon, display_name, source) in user_defined_agents {
+                            if !first {
+                                parent = parent
+                                    .child(Divider::horizontal().color(DividerColor::BorderFaded));
+                            }
+                            first = false;
+                            parent = parent.child(self.render_agent_server(
+                                icon,
+                                name,
+                                display_name,
+                                source,
                                 cx,
-                            ))
-                            .map(|mut parent| {
-                                for (name, icon, display_name, source) in user_defined_agents {
-                                    parent = parent
-                                        .child(
-                                            Divider::horizontal().color(DividerColor::BorderFaded),
-                                        )
-                                        .child(self.render_agent_server(
-                                            icon,
-                                            name,
-                                            display_name,
-                                            source,
-                                            cx,
-                                        ));
-                                }
-                                parent
-                            }),
-                    ),
+                            ));
+                        }
+                        parent
+                    })),
             )
     }
 
@@ -1134,7 +1086,7 @@ impl AgentConfiguration {
                 )),
                 IconName::AcpRegistry,
             )),
-            ExternalAgentSource::Builtin | ExternalAgentSource::Custom => None,
+            ExternalAgentSource::Custom => None,
         };
 
         let agent_server_name = ExternalAgentServerName(id.clone());
@@ -1176,19 +1128,19 @@ impl AgentConfiguration {
                             let Some(agent_servers) = settings.agent_servers.as_mut() else {
                                 return;
                             };
-                            if let Some(entry) = agent_servers.custom.get(agent_name.0.as_ref())
+                            if let Some(entry) = agent_servers.get(agent_name.0.as_ref())
                                 && matches!(
                                     entry,
                                     settings::CustomAgentServerSettings::Registry { .. }
                                 )
                             {
-                                agent_servers.custom.remove(agent_name.0.as_ref());
+                                agent_servers.remove(agent_name.0.as_ref());
                             }
                         });
                     })),
                 )
             }
-            ExternalAgentSource::Builtin | ExternalAgentSource::Custom => None,
+            ExternalAgentSource::Custom => None,
         };
 
         h_flex()
@@ -1367,29 +1319,23 @@ async fn open_new_agent_servers_entry_in_settings_editor(
                         !settings
                             .agent_servers
                             .as_ref()
-                            .is_some_and(|agent_servers| {
-                                agent_servers.custom.contains_key(name.as_str())
-                            })
+                            .is_some_and(|agent_servers| agent_servers.contains_key(name.as_str()))
                     });
                 if let Some(server_name) = server_name {
                     unique_server_name = Some(SharedString::from(server_name.clone()));
-                    settings
-                        .agent_servers
-                        .get_or_insert_default()
-                        .custom
-                        .insert(
-                            server_name,
-                            settings::CustomAgentServerSettings::Custom {
-                                path: "path_to_executable".into(),
-                                args: vec![],
-                                env: HashMap::default(),
-                                default_mode: None,
-                                default_model: None,
-                                favorite_models: vec![],
-                                default_config_options: Default::default(),
-                                favorite_config_option_values: Default::default(),
-                            },
-                        );
+                    settings.agent_servers.get_or_insert_default().insert(
+                        server_name,
+                        settings::CustomAgentServerSettings::Custom {
+                            path: "path_to_executable".into(),
+                            args: vec![],
+                            env: HashMap::default(),
+                            default_mode: None,
+                            default_model: None,
+                            favorite_models: vec![],
+                            default_config_options: Default::default(),
+                            favorite_config_option_values: Default::default(),
+                        },
+                    );
                 }
             });
 

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -14,6 +14,7 @@ use agent::{ContextServerRegistry, SharedThread, ThreadStore};
 use agent_client_protocol as acp;
 use agent_servers::AgentServer;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
+use itertools::Itertools;
 use project::{
     ExternalAgentServerName,
     agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
@@ -366,9 +367,6 @@ pub enum AgentType {
     #[default]
     NativeAgent,
     TextThread,
-    Gemini,
-    ClaudeAgent,
-    Codex,
     Custom {
         name: SharedString,
     },
@@ -378,9 +376,6 @@ impl AgentType {
     fn label(&self) -> SharedString {
         match self {
             Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
-            Self::Gemini => "Gemini CLI".into(),
-            Self::ClaudeAgent => "Claude Agent".into(),
-            Self::Codex => "Codex".into(),
             Self::Custom { name, .. } => name.into(),
         }
     }
@@ -388,9 +383,6 @@ impl AgentType {
     fn icon(&self) -> Option<IconName> {
         match self {
             Self::NativeAgent | Self::TextThread => None,
-            Self::Gemini => Some(IconName::AiGemini),
-            Self::ClaudeAgent => Some(IconName::AiClaude),
-            Self::Codex => Some(IconName::AiOpenAi),
             Self::Custom { .. } => Some(IconName::Sparkle),
         }
     }
@@ -399,9 +391,6 @@ impl AgentType {
 impl From<ExternalAgent> for AgentType {
     fn from(value: ExternalAgent) -> Self {
         match value {
-            ExternalAgent::Gemini => Self::Gemini,
-            ExternalAgent::ClaudeCode => Self::ClaudeAgent,
-            ExternalAgent::Codex => Self::Codex,
             ExternalAgent::Custom { name } => Self::Custom { name },
             ExternalAgent::NativeAgent => Self::NativeAgent,
         }
@@ -1117,10 +1106,7 @@ impl AgentPanel {
         match self.selected_agent {
             AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
             AgentType::TextThread => Some(HistoryKind::TextThreads),
-            AgentType::Gemini
-            | AgentType::ClaudeAgent
-            | AgentType::Codex
-            | AgentType::Custom { .. } => {
+            AgentType::Custom { .. } => {
                 if self.acp_history.read(cx).has_session_list() {
                     Some(HistoryKind::AgentThreads)
                 } else {
@@ -1759,9 +1745,6 @@ impl AgentPanel {
     fn selected_external_agent(&self) -> Option<ExternalAgent> {
         match &self.selected_agent {
             AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
-            AgentType::Gemini => Some(ExternalAgent::Gemini),
-            AgentType::ClaudeAgent => Some(ExternalAgent::ClaudeCode),
-            AgentType::Codex => Some(ExternalAgent::Codex),
             AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
             AgentType::TextThread => None,
         }
@@ -1827,25 +1810,6 @@ impl AgentPanel {
                 window,
                 cx,
             ),
-            AgentType::Gemini => {
-                self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
-            }
-            AgentType::ClaudeAgent => {
-                self.selected_agent = AgentType::ClaudeAgent;
-                self.serialize(cx);
-                self.external_thread(
-                    Some(crate::ExternalAgent::ClaudeCode),
-                    None,
-                    None,
-                    window,
-                    cx,
-                )
-            }
-            AgentType::Codex => {
-                self.selected_agent = AgentType::Codex;
-                self.serialize(cx);
-                self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
-            }
             AgentType::Custom { name } => self.external_thread(
                 Some(crate::ExternalAgent::Custom { name }),
                 None,
@@ -2196,8 +2160,6 @@ impl AgentPanel {
             "Enable Full Screen"
         };
 
-        let selected_agent = self.selected_agent.clone();
-
         let text_thread_view = match &self.active_view {
             ActiveView::TextThread {
                 text_thread_editor, ..
@@ -2226,6 +2188,10 @@ impl AgentPanel {
             }
             _ => false,
         };
+        let has_auth_methods = match &self.active_view {
+            ActiveView::AgentThread { server_view } => server_view.read(cx).has_auth_methods(),
+            _ => false,
+        };
 
         PopoverMenu::new("agent-options-menu")
             .trigger_with_tooltip(
@@ -2301,7 +2267,7 @@ impl AgentPanel {
                             .separator()
                             .action(full_screen_label, Box::new(ToggleZoom));
 
-                        if selected_agent == AgentType::Gemini {
+                        if has_auth_methods {
                             menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
                         }
 
@@ -2510,140 +2476,73 @@ impl AgentPanel {
                             )
                             .separator()
                             .header("External Agents")
-                            .item(
-                                ContextMenuEntry::new("Claude Agent")
-                                    .when(is_agent_selected(AgentType::ClaudeAgent), |this| {
-                                        this.action(Box::new(NewExternalAgentThread {
-                                            agent: None,
-                                        }))
-                                    })
-                                    .icon(IconName::AiClaude)
-                                    .disabled(is_via_collab)
-                                    .icon_color(Color::Muted)
-                                    .handler({
-                                        let workspace = workspace.clone();
-                                        move |window, cx| {
-                                            if let Some(workspace) = workspace.upgrade() {
-                                                workspace.update(cx, |workspace, cx| {
-                                                    if let Some(panel) =
-                                                        workspace.panel::<AgentPanel>(cx)
-                                                    {
-                                                        panel.update(cx, |panel, cx| {
-                                                            panel.new_agent_thread(
-                                                                AgentType::ClaudeAgent,
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        });
-                                                    }
-                                                });
-                                            }
-                                        }
-                                    }),
-                            )
-                            .item(
-                                ContextMenuEntry::new("Codex CLI")
-                                    .when(is_agent_selected(AgentType::Codex), |this| {
-                                        this.action(Box::new(NewExternalAgentThread {
-                                            agent: None,
-                                        }))
-                                    })
-                                    .icon(IconName::AiOpenAi)
-                                    .disabled(is_via_collab)
-                                    .icon_color(Color::Muted)
-                                    .handler({
-                                        let workspace = workspace.clone();
-                                        move |window, cx| {
-                                            if let Some(workspace) = workspace.upgrade() {
-                                                workspace.update(cx, |workspace, cx| {
-                                                    if let Some(panel) =
-                                                        workspace.panel::<AgentPanel>(cx)
-                                                    {
-                                                        panel.update(cx, |panel, cx| {
-                                                            panel.new_agent_thread(
-                                                                AgentType::Codex,
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        });
-                                                    }
-                                                });
-                                            }
-                                        }
-                                    }),
-                            )
-                            .item(
-                                ContextMenuEntry::new("Gemini CLI")
-                                    .when(is_agent_selected(AgentType::Gemini), |this| {
-                                        this.action(Box::new(NewExternalAgentThread {
-                                            agent: None,
-                                        }))
-                                    })
-                                    .icon(IconName::AiGemini)
-                                    .icon_color(Color::Muted)
-                                    .disabled(is_via_collab)
-                                    .handler({
-                                        let workspace = workspace.clone();
-                                        move |window, cx| {
-                                            if let Some(workspace) = workspace.upgrade() {
-                                                workspace.update(cx, |workspace, cx| {
-                                                    if let Some(panel) =
-                                                        workspace.panel::<AgentPanel>(cx)
-                                                    {
-                                                        panel.update(cx, |panel, cx| {
-                                                            panel.new_agent_thread(
-                                                                AgentType::Gemini,
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        });
-                                                    }
-                                                });
-                                            }
-                                        }
-                                    }),
-                            )
                             .map(|mut menu| {
                                 let agent_server_store = agent_server_store.read(cx);
-                                let agent_names = agent_server_store
+                                let registry_store =
+                                    project::AgentRegistryStore::try_global(cx);
+                                let registry_store_ref =
+                                    registry_store.as_ref().map(|s| s.read(cx));
+
+                                struct AgentMenuItem {
+                                    id: ExternalAgentServerName,
+                                    display_name: SharedString,
+                                }
+
+                                let agent_items = agent_server_store
                                     .external_agents()
-                                    .filter(|name| {
-                                        name.0 != GEMINI_NAME
-                                            && name.0 != CLAUDE_AGENT_NAME
-                                            && name.0 != CODEX_NAME
+                                    .map(|name| {
+                                        let display_name = agent_server_store
+                                            .agent_display_name(name)
+                                            .or_else(|| {
+                                                registry_store_ref
+                                                    .as_ref()
+                                                    .and_then(|store| store.agent(name.0.as_ref()))
+                                                    .map(|a| a.name().clone())
+                                            })
+                                            .unwrap_or_else(|| name.0.clone());
+                                        AgentMenuItem {
+                                            id: name.clone(),
+                                            display_name,
+                                        }
                                     })
-                                    .cloned()
+                                    .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
                                     .collect::<Vec<_>>();
 
-                                for agent_name in agent_names {
-                                    let icon_path = agent_server_store.agent_icon(&agent_name);
-                                    let display_name = agent_server_store
-                                        .agent_display_name(&agent_name)
-                                        .unwrap_or_else(|| agent_name.0.clone());
-
-                                    let mut entry = ContextMenuEntry::new(display_name);
+                                for item in &agent_items {
+                                    let mut entry =
+                                        ContextMenuEntry::new(item.display_name.clone());
+
+                                    let icon_path = agent_server_store
+                                        .agent_icon(&item.id)
+                                        .or_else(|| {
+                                            registry_store_ref
+                                                .as_ref()
+                                                .and_then(|store| store.agent(item.id.0.as_str()))
+                                                .and_then(|a| a.icon_path().cloned())
+                                        });
 
                                     if let Some(icon_path) = icon_path {
                                         entry = entry.custom_icon_svg(icon_path);
                                     } else {
                                         entry = entry.icon(IconName::Sparkle);
                                     }
+
                                     entry = entry
                                         .when(
                                             is_agent_selected(AgentType::Custom {
-                                                name: agent_name.0.clone(),
+                                                name: item.id.0.clone(),
                                             }),
                                             |this| {
-                                                this.action(Box::new(NewExternalAgentThread {
-                                                    agent: None,
-                                                }))
+                                                this.action(Box::new(
+                                                    NewExternalAgentThread { agent: None },
+                                                ))
                                             },
                                         )
                                         .icon_color(Color::Muted)
                                         .disabled(is_via_collab)
                                         .handler({
                                             let workspace = workspace.clone();
-                                            let agent_name = agent_name.clone();
+                                            let agent_id = item.id.clone();
                                             move |window, cx| {
                                                 if let Some(workspace) = workspace.upgrade() {
                                                     workspace.update(cx, |workspace, cx| {
@@ -2653,9 +2552,7 @@ impl AgentPanel {
                                                             panel.update(cx, |panel, cx| {
                                                                 panel.new_agent_thread(
                                                                     AgentType::Custom {
-                                                                        name: agent_name
-                                                                            .clone()
-                                                                            .into(),
+                                                                        name: agent_id.0.clone(),
                                                                     },
                                                                     window,
                                                                     cx,
@@ -2673,6 +2570,102 @@ impl AgentPanel {
                                 menu
                             })
                             .separator()
+                            .map(|mut menu| {
+                                let agent_server_store = agent_server_store.read(cx);
+                                let registry_store =
+                                    project::AgentRegistryStore::try_global(cx);
+                                let registry_store_ref =
+                                    registry_store.as_ref().map(|s| s.read(cx));
+
+                                let previous_built_in_ids: &[ExternalAgentServerName] =
+                                    &[CLAUDE_AGENT_NAME.into(), CODEX_NAME.into(), GEMINI_NAME.into()];
+
+                                let promoted_items = previous_built_in_ids
+                                    .iter()
+                                    .filter(|id| {
+                                        !agent_server_store.external_agents.contains_key(*id)
+                                    })
+                                    .map(|name| {
+                                        let display_name = registry_store_ref
+                                            .as_ref()
+                                            .and_then(|store| store.agent(name.0.as_ref()))
+                                            .map(|a| a.name().clone())
+                                            .unwrap_or_else(|| name.0.clone());
+                                        (name.clone(), display_name)
+                                    })
+                                    .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
+                                    .collect::<Vec<_>>();
+
+                                for (agent_id, display_name) in &promoted_items {
+                                    let mut entry =
+                                        ContextMenuEntry::new(display_name.clone());
+
+                                    let icon_path = registry_store_ref
+                                        .as_ref()
+                                        .and_then(|store| store.agent(agent_id.0.as_str()))
+                                        .and_then(|a| a.icon_path().cloned());
+
+                                    if let Some(icon_path) = icon_path {
+                                        entry = entry.custom_icon_svg(icon_path);
+                                    } else {
+                                        entry = entry.icon(IconName::Sparkle);
+                                    }
+
+                                    entry = entry
+                                        .icon_color(Color::Muted)
+                                        .disabled(is_via_collab)
+                                        .handler({
+                                            let workspace = workspace.clone();
+                                            let agent_id = agent_id.clone();
+                                            move |window, cx| {
+                                                let fs = <dyn fs::Fs>::global(cx);
+                                                let agent_id_string =
+                                                    agent_id.to_string();
+                                                settings::update_settings_file(
+                                                    fs,
+                                                    cx,
+                                                    move |settings, _| {
+                                                        let agent_servers = settings
+                                                            .agent_servers
+                                                            .get_or_insert_default();
+                                                        agent_servers.entry(agent_id_string).or_insert_with(|| {
+                                                            settings::CustomAgentServerSettings::Registry {
+                                                                default_mode: None,
+                                                                default_model: None,
+                                                                env: Default::default(),
+                                                                favorite_models: Vec::new(),
+                                                                default_config_options: Default::default(),
+                                                                favorite_config_option_values: Default::default(),
+                                                            }
+                                                        });
+                                                    },
+                                                );
+
+                                                if let Some(workspace) = workspace.upgrade() {
+                                                    workspace.update(cx, |workspace, cx| {
+                                                        if let Some(panel) =
+                                                            workspace.panel::<AgentPanel>(cx)
+                                                        {
+                                                            panel.update(cx, |panel, cx| {
+                                                                panel.new_agent_thread(
+                                                                    AgentType::Custom {
+                                                                        name: agent_id.0.clone(),
+                                                                    },
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                            });
+                                                        }
+                                                    });
+                                                }
+                                            }
+                                        });
+
+                                    menu = menu.item(entry);
+                                }
+
+                                menu
+                            })
                             .item(
                                 ContextMenuEntry::new("Add More Agents")
                                     .icon(IconName::Plus)
@@ -3565,7 +3558,9 @@ mod tests {
 
         panel_b.update(cx, |panel, _cx| {
             panel.width = Some(px(400.0));
-            panel.selected_agent = AgentType::ClaudeAgent;
+            panel.selected_agent = AgentType::Custom {
+                name: "claude-acp".into(),
+            };
         });
 
         // --- Serialize both panels ---
@@ -3612,7 +3607,9 @@ mod tests {
             );
             assert_eq!(
                 panel.selected_agent,
-                AgentType::ClaudeAgent,
+                AgentType::Custom {
+                    name: "claude-acp".into()
+                },
                 "workspace B agent type should be restored"
             );
             assert!(

crates/agent_ui/src/agent_registry_ui.rs πŸ”—

@@ -173,7 +173,7 @@ impl AgentRegistryPage {
             .global::<SettingsStore>()
             .get::<AllAgentServersSettings>(None);
         self.installed_statuses.clear();
-        for (id, settings) in &settings.custom {
+        for (id, settings) in settings.iter() {
             let status = match settings {
                 CustomAgentServerSettings::Registry { .. } => {
                     RegistryInstallStatus::InstalledRegistry
@@ -583,7 +583,7 @@ impl AgentRegistryPage {
                         let agent_id = agent_id.clone();
                         update_settings_file(fs.clone(), cx, move |settings, _| {
                             let agent_servers = settings.agent_servers.get_or_insert_default();
-                            agent_servers.custom.entry(agent_id).or_insert_with(|| {
+                            agent_servers.entry(agent_id).or_insert_with(|| {
                                 settings::CustomAgentServerSettings::Registry {
                                     default_mode: None,
                                     default_model: None,
@@ -607,13 +607,13 @@ impl AgentRegistryPage {
                             let Some(agent_servers) = settings.agent_servers.as_mut() else {
                                 return;
                             };
-                            if let Some(entry) = agent_servers.custom.get(agent_id.as_str())
+                            if let Some(entry) = agent_servers.get(agent_id.as_str())
                                 && matches!(
                                     entry,
                                     settings::CustomAgentServerSettings::Registry { .. }
                                 )
                             {
-                                agent_servers.custom.remove(agent_id.as_str());
+                                agent_servers.remove(agent_id.as_str());
                             }
                         });
                     })

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -203,9 +203,6 @@ pub struct NewNativeAgentThreadFromSummary {
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum ExternalAgent {
-    Gemini,
-    ClaudeCode,
-    Codex,
     NativeAgent,
     Custom { name: SharedString },
 }
@@ -217,9 +214,6 @@ impl ExternalAgent {
         thread_store: Entity<agent::ThreadStore>,
     ) -> Rc<dyn agent_servers::AgentServer> {
         match self {
-            Self::Gemini => Rc::new(agent_servers::Gemini),
-            Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
-            Self::Codex => Rc::new(agent_servers::Codex),
             Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
             Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
         }

crates/agent_ui/src/mention_set.rs πŸ”—

@@ -549,7 +549,7 @@ impl MentionSet {
         );
         let connection = server.connect(delegate, cx);
         cx.spawn(async move |_, cx| {
-            let (agent, _) = connection.await?;
+            let agent = connection.await?;
             let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
             let summary = agent
                 .0

crates/agent_ui/src/ui/acp_onboarding_modal.rs πŸ”—

@@ -1,8 +1,8 @@
-use client::zed_urls;
 use gpui::{
     ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
     linear_color_stop, linear_gradient,
 };
+use project::agent_server_store::GEMINI_NAME;
 use ui::{TintColor, Vector, VectorName, prelude::*};
 use workspace::{ModalView, Workspace};
 
@@ -37,7 +37,13 @@ impl AcpOnboardingModal {
 
             if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                 panel.update(cx, |panel, cx| {
-                    panel.new_agent_thread(AgentType::Gemini, window, cx);
+                    panel.new_agent_thread(
+                        AgentType::Custom {
+                            name: GEMINI_NAME.into(),
+                        },
+                        window,
+                        cx,
+                    );
                 });
             }
         });
@@ -47,11 +53,11 @@ impl AcpOnboardingModal {
         acp_onboarding_event!("Open Panel Clicked");
     }
 
-    fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
-        cx.open_url(&zed_urls::external_agents_docs(cx));
+    fn open_agent_registry(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+        window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx);
         cx.notify();
 
-        acp_onboarding_event!("Documentation Link Clicked");
+        acp_onboarding_event!("Open Agent Registry Clicked");
     }
 
     fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
@@ -197,7 +203,7 @@ impl Render for AcpOnboardingModal {
             .icon_size(IconSize::Indicator)
             .icon_color(Color::Muted)
             .full_width()
-            .on_click(cx.listener(Self::view_docs));
+            .on_click(cx.listener(Self::open_agent_registry));
 
         let close_button = h_flex().absolute().top_2().right_2().child(
             IconButton::new("cancel", IconName::Close).on_click(cx.listener(

crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs πŸ”—

@@ -1,8 +1,8 @@
-use client::zed_urls;
 use gpui::{
     ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
     linear_color_stop, linear_gradient,
 };
+use project::agent_server_store::CLAUDE_AGENT_NAME;
 use ui::{TintColor, Vector, VectorName, prelude::*};
 use workspace::{ModalView, Workspace};
 
@@ -37,7 +37,13 @@ impl ClaudeCodeOnboardingModal {
 
             if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                 panel.update(cx, |panel, cx| {
-                    panel.new_agent_thread(AgentType::ClaudeAgent, window, cx);
+                    panel.new_agent_thread(
+                        AgentType::Custom {
+                            name: CLAUDE_AGENT_NAME.into(),
+                        },
+                        window,
+                        cx,
+                    );
                 });
             }
         });
@@ -47,8 +53,8 @@ impl ClaudeCodeOnboardingModal {
         claude_agent_onboarding_event!("Open Panel Clicked");
     }
 
-    fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
-        cx.open_url(&zed_urls::external_agents_docs(cx));
+    fn view_docs(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+        window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx);
         cx.notify();
 
         claude_agent_onboarding_event!("Documentation Link Clicked");

crates/client/src/zed_urls.rs πŸ”—

@@ -44,22 +44,6 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
     )
 }
 
-/// Returns the URL to Zed AI's external agents documentation.
-pub fn external_agents_docs(cx: &App) -> String {
-    format!(
-        "{server_url}/docs/ai/external-agents",
-        server_url = server_url(cx)
-    )
-}
-
-/// Returns the URL to Zed agent servers documentation.
-pub fn agent_server_docs(cx: &App) -> String {
-    format!(
-        "{server_url}/docs/extensions/agent-servers",
-        server_url = server_url(cx)
-    )
-}
-
 /// Returns the URL to Zed's edit prediction documentation.
 pub fn edit_prediction_docs(cx: &App) -> String {
     format!(

crates/migrator/src/migrations.rs πŸ”—

@@ -292,3 +292,9 @@ pub(crate) mod m_2026_02_04 {
 
     pub(crate) use settings::migrate_tool_permission_defaults;
 }
+
+pub(crate) mod m_2026_02_25 {
+    mod settings;
+
+    pub(crate) use settings::migrate_builtin_agent_servers_to_registry;
+}

crates/migrator/src/migrations/m_2026_02_25/settings.rs πŸ”—

@@ -0,0 +1,161 @@
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::migrations::migrate_settings;
+
+const AGENT_SERVERS_KEY: &str = "agent_servers";
+
+struct BuiltinMapping {
+    old_key: &'static str,
+    registry_key: &'static str,
+}
+
+const BUILTIN_MAPPINGS: &[BuiltinMapping] = &[
+    BuiltinMapping {
+        old_key: "gemini",
+        registry_key: "gemini",
+    },
+    BuiltinMapping {
+        old_key: "claude",
+        registry_key: "claude-acp",
+    },
+    BuiltinMapping {
+        old_key: "codex",
+        registry_key: "codex-acp",
+    },
+];
+
+const REGISTRY_COMPATIBLE_FIELDS: &[&str] = &[
+    "env",
+    "default_mode",
+    "default_model",
+    "favorite_models",
+    "default_config_options",
+    "favorite_config_option_values",
+];
+
+pub fn migrate_builtin_agent_servers_to_registry(value: &mut Value) -> Result<()> {
+    migrate_settings(value, &mut migrate_one)
+}
+
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
+    let Some(agent_servers) = obj.get_mut(AGENT_SERVERS_KEY) else {
+        return Ok(());
+    };
+    let Some(servers_map) = agent_servers.as_object_mut() else {
+        return Ok(());
+    };
+
+    for mapping in BUILTIN_MAPPINGS {
+        migrate_builtin_entry(servers_map, mapping);
+    }
+
+    Ok(())
+}
+
+fn migrate_builtin_entry(
+    servers_map: &mut serde_json::Map<String, Value>,
+    mapping: &BuiltinMapping,
+) {
+    // Check if the old key exists and needs migration before taking ownership.
+    let needs_migration = servers_map
+        .get(mapping.old_key)
+        .and_then(|v| v.as_object())
+        .is_some_and(|obj| !obj.contains_key("type"));
+
+    if !needs_migration {
+        return;
+    }
+
+    // When the registry key differs from the old key and the target already
+    // exists, just remove the stale old entry to avoid overwriting user data.
+    if mapping.old_key != mapping.registry_key && servers_map.contains_key(mapping.registry_key) {
+        servers_map.remove(mapping.old_key);
+        return;
+    }
+
+    let Some(old_entry) = servers_map.remove(mapping.old_key) else {
+        return;
+    };
+    let Some(old_obj) = old_entry.as_object() else {
+        return;
+    };
+
+    let has_command = old_obj.contains_key("command");
+    let ignore_system_version = old_obj
+        .get("ignore_system_version")
+        .and_then(|v| v.as_bool());
+
+    // A custom entry is needed when the user configured a custom binary
+    // or explicitly opted into using the system version via
+    // `ignore_system_version: false` (only meaningful for gemini).
+    let needs_custom = has_command
+        || (mapping.old_key == "gemini" && matches!(ignore_system_version, Some(false)));
+
+    if needs_custom {
+        let local_key = format!("{}-custom", mapping.registry_key);
+
+        // Don't overwrite an existing `-custom` entry.
+        if servers_map.contains_key(&local_key) {
+            return;
+        }
+
+        let mut custom_obj = serde_json::Map::new();
+        custom_obj.insert("type".to_string(), Value::String("custom".to_string()));
+
+        if has_command {
+            if let Some(command) = old_obj.get("command") {
+                custom_obj.insert("command".to_string(), command.clone());
+            }
+            if let Some(args) = old_obj.get("args") {
+                if !args.as_array().is_some_and(|a| a.is_empty()) {
+                    custom_obj.insert("args".to_string(), args.clone());
+                }
+            }
+        } else {
+            // ignore_system_version: false β€” the user wants the binary from $PATH
+            custom_obj.insert(
+                "command".to_string(),
+                Value::String(mapping.old_key.to_string()),
+            );
+        }
+
+        // Carry over all compatible fields to the custom entry.
+        for &field in REGISTRY_COMPATIBLE_FIELDS {
+            if let Some(value) = old_obj.get(field) {
+                match value {
+                    Value::Array(arr) if arr.is_empty() => continue,
+                    Value::Object(map) if map.is_empty() => continue,
+                    Value::Null => continue,
+                    _ => {
+                        custom_obj.insert(field.to_string(), value.clone());
+                    }
+                }
+            }
+        }
+
+        servers_map.insert(local_key, Value::Object(custom_obj));
+    } else {
+        // Build a registry entry with compatible fields only.
+        let mut registry_obj = serde_json::Map::new();
+        registry_obj.insert("type".to_string(), Value::String("registry".to_string()));
+
+        for &field in REGISTRY_COMPATIBLE_FIELDS {
+            if let Some(value) = old_obj.get(field) {
+                match value {
+                    Value::Array(arr) if arr.is_empty() => continue,
+                    Value::Object(map) if map.is_empty() => continue,
+                    Value::Null => continue,
+                    _ => {
+                        registry_obj.insert(field.to_string(), value.clone());
+                    }
+                }
+            }
+        }
+
+        servers_map.insert(
+            mapping.registry_key.to_string(),
+            Value::Object(registry_obj),
+        );
+    }
+}

crates/migrator/src/migrator.rs πŸ”—

@@ -237,6 +237,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
         ),
         MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
         MigrationType::Json(migrations::m_2026_02_04::migrate_tool_permission_defaults),
+        MigrationType::Json(migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry),
     ];
     run_migrations(text, migrations)
 }
@@ -3820,4 +3821,415 @@ mod tests {
             ),
         );
     }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_to_registry_simple() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "default_model": "gemini-2.0-flash"
+        },
+        "claude": {
+            "default_mode": "plan"
+        },
+        "codex": {
+            "default_model": "o4-mini"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "codex-acp": {
+            "type": "registry",
+            "default_model": "o4-mini"
+        },
+        "claude-acp": {
+            "type": "registry",
+            "default_mode": "plan"
+        },
+        "gemini": {
+            "type": "registry",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_empty_entries() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {},
+        "claude": {},
+        "codex": {}
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "codex-acp": {
+            "type": "registry"
+        },
+        "claude-acp": {
+            "type": "registry"
+        },
+        "gemini": {
+            "type": "registry"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_with_command() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "claude": {
+            "command": "/usr/local/bin/claude",
+            "args": ["--verbose"],
+            "env": {"CLAUDE_KEY": "abc123"},
+            "default_mode": "plan",
+            "default_model": "claude-sonnet-4"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "claude-acp-custom": {
+            "type": "custom",
+            "command": "/usr/local/bin/claude",
+            "args": [
+                "--verbose"
+            ],
+            "env": {
+                "CLAUDE_KEY": "abc123"
+            },
+            "default_mode": "plan",
+            "default_model": "claude-sonnet-4"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_gemini_with_command() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "command": "/opt/gemini/bin/gemini",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "gemini-custom": {
+            "type": "custom",
+            "command": "/opt/gemini/bin/gemini",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_false() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "ignore_system_version": false,
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "gemini-custom": {
+            "type": "custom",
+            "command": "gemini",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_true() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "ignore_system_version": true,
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "gemini": {
+            "type": "registry",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_already_typed_unchanged() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "type": "registry",
+            "default_model": "gemini-2.0-flash"
+        },
+        "claude-acp": {
+            "type": "registry",
+            "default_mode": "plan"
+        }
+    }
+}"#,
+            None,
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_preserves_custom_entries() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "claude": {
+            "default_mode": "plan"
+        },
+        "my-custom-agent": {
+            "type": "custom",
+            "command": "/path/to/agent"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "claude-acp": {
+            "type": "registry",
+            "default_mode": "plan"
+        },
+        "my-custom-agent": {
+            "type": "custom",
+            "command": "/path/to/agent"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_target_already_exists() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "claude": {
+            "default_mode": "plan"
+        },
+        "claude-acp": {
+            "type": "registry",
+            "default_model": "claude-sonnet-4"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "claude-acp": {
+            "type": "registry",
+            "default_model": "claude-sonnet-4"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_no_agent_servers_key() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent": {
+        "enabled": true
+    }
+}"#,
+            None,
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_all_fields() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "codex": {
+            "env": {"OPENAI_API_KEY": "sk-123"},
+            "default_mode": "read-only",
+            "default_model": "o4-mini",
+            "favorite_models": ["o4-mini", "codex-mini-latest"],
+            "default_config_options": {"approval_mode": "auto-edit"},
+            "favorite_config_option_values": {"approval_mode": ["auto-edit", "suggest"]}
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "codex-acp": {
+            "type": "registry",
+            "env": {
+                "OPENAI_API_KEY": "sk-123"
+            },
+            "default_mode": "read-only",
+            "default_model": "o4-mini",
+            "favorite_models": [
+                "o4-mini",
+                "codex-mini-latest"
+            ],
+            "default_config_options": {
+                "approval_mode": "auto-edit"
+            },
+            "favorite_config_option_values": {
+                "approval_mode": [
+                    "auto-edit",
+                    "suggest"
+                ]
+            }
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_codex_with_command() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "codex": {
+            "command": "/usr/local/bin/codex",
+            "args": ["--full-auto"],
+            "default_model": "o4-mini"
+        }
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "codex-acp-custom": {
+            "type": "custom",
+            "command": "/usr/local/bin/codex",
+            "args": [
+                "--full-auto"
+            ],
+            "default_model": "o4-mini"
+        }
+    }
+}"#,
+            ),
+        );
+    }
+
+    #[test]
+    fn test_migrate_builtin_agent_servers_mixed_migrated_and_not() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
+            )],
+            r#"{
+    "agent_servers": {
+        "gemini": {
+            "type": "registry",
+            "default_model": "gemini-2.0-flash"
+        },
+        "claude": {
+            "default_mode": "plan"
+        },
+        "codex": {}
+    }
+}"#,
+            Some(
+                r#"{
+    "agent_servers": {
+        "codex-acp": {
+            "type": "registry"
+        },
+        "claude-acp": {
+            "type": "registry",
+            "default_mode": "plan"
+        },
+        "gemini": {
+            "type": "registry",
+            "default_model": "gemini-2.0-flash"
+        }
+    }
+}"#,
+            ),
+        );
+    }
 }

crates/project/src/agent_registry_store.rs πŸ”—

@@ -9,7 +9,7 @@ use futures::AsyncReadExt;
 use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
 use http_client::{AsyncBody, HttpClient};
 use serde::Deserialize;
-use settings::Settings;
+use settings::Settings as _;
 
 use crate::agent_server_store::AllAgentServersSettings;
 

crates/project/src/agent_server_store.rs πŸ”—

@@ -3,18 +3,14 @@ use std::{
     any::Any,
     borrow::Borrow,
     path::{Path, PathBuf},
-    str::FromStr as _,
     sync::Arc,
     time::Duration,
 };
 
 use anyhow::{Context as _, Result, bail};
 use collections::HashMap;
-use fs::{Fs, RemoveOptions, RenameOptions};
-use futures::StreamExt as _;
-use gpui::{
-    AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
-};
+use fs::Fs;
+use gpui::{AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task};
 use http_client::{HttpClient, github::AssetKind};
 use node_runtime::NodeRuntime;
 use remote::RemoteClient;
@@ -23,10 +19,9 @@ use rpc::{
     proto::{self, ExternalExtensionAgent},
 };
 use schemars::JsonSchema;
-use semver::Version;
 use serde::{Deserialize, Serialize};
 use settings::{RegisterSetting, SettingsStore};
-use task::{Shell, SpawnInTerminal};
+use task::Shell;
 use util::{ResultExt as _, debug_panic};
 
 use crate::ProjectEnvironment;
@@ -66,7 +61,7 @@ impl std::fmt::Debug for AgentServerCommand {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct ExternalAgentServerName(pub SharedString);
 
 impl std::fmt::Display for ExternalAgentServerName {
@@ -95,7 +90,6 @@ impl Borrow<str> for ExternalAgentServerName {
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum ExternalAgentSource {
-    Builtin,
     #[default]
     Custom,
     Extension,
@@ -109,7 +103,7 @@ pub trait ExternalAgentServer {
         status_tx: Option<watch::Sender<SharedString>>,
         new_version_available_tx: Option<watch::Sender<Option<String>>>,
         cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>>;
+    ) -> Task<Result<AgentServerCommand>>;
 
     fn as_any_mut(&mut self) -> &mut dyn Any;
 }
@@ -409,86 +403,13 @@ impl AgentServerStore {
 
         // If we don't have agents from the registry loaded yet, trigger a
         // refresh, which will cause this function to be called again
+        let registry_store = AgentRegistryStore::try_global(cx);
         if new_settings.has_registry_agents()
-            && let Some(registry) = AgentRegistryStore::try_global(cx)
+            && let Some(registry) = registry_store.as_ref()
         {
             registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
         }
 
-        self.external_agents.clear();
-        self.external_agents.insert(
-            GEMINI_NAME.into(),
-            ExternalAgentEntry::new(
-                Box::new(LocalGemini {
-                    fs: fs.clone(),
-                    node_runtime: node_runtime.clone(),
-                    project_environment: project_environment.clone(),
-                    custom_command: new_settings
-                        .gemini
-                        .clone()
-                        .and_then(|settings| settings.custom_command()),
-                    settings_env: new_settings
-                        .gemini
-                        .as_ref()
-                        .and_then(|settings| settings.env.clone()),
-                    ignore_system_version: new_settings
-                        .gemini
-                        .as_ref()
-                        .and_then(|settings| settings.ignore_system_version)
-                        .unwrap_or(true),
-                }),
-                ExternalAgentSource::Builtin,
-                None,
-                None,
-            ),
-        );
-        self.external_agents.insert(
-            CODEX_NAME.into(),
-            ExternalAgentEntry::new(
-                Box::new(LocalCodex {
-                    fs: fs.clone(),
-                    project_environment: project_environment.clone(),
-                    custom_command: new_settings
-                        .codex
-                        .clone()
-                        .and_then(|settings| settings.custom_command()),
-                    settings_env: new_settings
-                        .codex
-                        .as_ref()
-                        .and_then(|settings| settings.env.clone()),
-                    http_client: http_client.clone(),
-                    no_browser: downstream_client
-                        .as_ref()
-                        .is_some_and(|(_, client)| !client.has_wsl_interop()),
-                }),
-                ExternalAgentSource::Builtin,
-                None,
-                None,
-            ),
-        );
-        self.external_agents.insert(
-            CLAUDE_AGENT_NAME.into(),
-            ExternalAgentEntry::new(
-                Box::new(LocalClaudeCode {
-                    fs: fs.clone(),
-                    node_runtime: node_runtime.clone(),
-                    project_environment: project_environment.clone(),
-                    custom_command: new_settings
-                        .claude
-                        .clone()
-                        .and_then(|settings| settings.custom_command()),
-                    settings_env: new_settings
-                        .claude
-                        .as_ref()
-                        .and_then(|settings| settings.env.clone()),
-                }),
-                ExternalAgentSource::Builtin,
-                None,
-                None,
-            ),
-        );
-
-        let registry_store = AgentRegistryStore::try_global(cx);
         let registry_agents_by_id = registry_store
             .as_ref()
             .map(|store| {
@@ -502,13 +423,14 @@ impl AgentServerStore {
             })
             .unwrap_or_default();
 
+        self.external_agents.clear();
+
         // Insert extension agents before custom/registry so registry entries override extensions.
         for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
             let name = ExternalAgentServerName(agent_name.clone().into());
             let mut env = env.clone();
             if let Some(settings_env) =
                 new_settings
-                    .custom
                     .get(agent_name.as_ref())
                     .and_then(|settings| match settings {
                         CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
@@ -541,7 +463,7 @@ impl AgentServerStore {
             );
         }
 
-        for (name, settings) in &new_settings.custom {
+        for (name, settings) in new_settings.iter() {
             match settings {
                 CustomAgentServerSettings::Custom { command, .. } => {
                     let agent_name = ExternalAgentServerName(name.clone().into());
@@ -671,7 +593,7 @@ impl AgentServerStore {
                 extension_agents: vec![],
                 _subscriptions: subscriptions,
             },
-            external_agents: Default::default(),
+            external_agents: HashMap::default(),
         };
         if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
         this.agent_servers_settings_changed(cx);
@@ -679,70 +601,19 @@ impl AgentServerStore {
     }
 
     pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
-        // Set up the builtin agents here so they're immediately available in
-        // remote projects--we know that the HeadlessProject on the other end
-        // will have them.
-        let external_agents: [(ExternalAgentServerName, ExternalAgentEntry); 3] = [
-            (
-                CLAUDE_AGENT_NAME.into(),
-                ExternalAgentEntry::new(
-                    Box::new(RemoteExternalAgentServer {
-                        project_id,
-                        upstream_client: upstream_client.clone(),
-                        name: CLAUDE_AGENT_NAME.into(),
-                        status_tx: None,
-                        new_version_available_tx: None,
-                    }) as Box<dyn ExternalAgentServer>,
-                    ExternalAgentSource::Builtin,
-                    None,
-                    None,
-                ),
-            ),
-            (
-                CODEX_NAME.into(),
-                ExternalAgentEntry::new(
-                    Box::new(RemoteExternalAgentServer {
-                        project_id,
-                        upstream_client: upstream_client.clone(),
-                        name: CODEX_NAME.into(),
-                        status_tx: None,
-                        new_version_available_tx: None,
-                    }) as Box<dyn ExternalAgentServer>,
-                    ExternalAgentSource::Builtin,
-                    None,
-                    None,
-                ),
-            ),
-            (
-                GEMINI_NAME.into(),
-                ExternalAgentEntry::new(
-                    Box::new(RemoteExternalAgentServer {
-                        project_id,
-                        upstream_client: upstream_client.clone(),
-                        name: GEMINI_NAME.into(),
-                        status_tx: None,
-                        new_version_available_tx: None,
-                    }) as Box<dyn ExternalAgentServer>,
-                    ExternalAgentSource::Builtin,
-                    None,
-                    None,
-                ),
-            ),
-        ];
-
         Self {
             state: AgentServerStoreState::Remote {
                 project_id,
                 upstream_client,
             },
-            external_agents: external_agents.into_iter().collect(),
+            external_agents: HashMap::default(),
         }
     }
 
     pub fn collab() -> Self {
         Self {
             state: AgentServerStoreState::Collab,
-            external_agents: Default::default(),
+            external_agents: HashMap::default(),
         }
     }
 
@@ -789,6 +660,17 @@ impl AgentServerStore {
             .map(|entry| entry.server.as_mut())
     }
 
+    pub fn no_browser(&self) -> bool {
+        match &self.state {
+            AgentServerStoreState::Local {
+                downstream_client, ..
+            } => downstream_client
+                .as_ref()
+                .is_some_and(|(_, client)| !client.has_wsl_interop()),
+            _ => false,
+        }
+    }
+
     pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
         self.external_agents.keys()
     }
@@ -798,7 +680,7 @@ impl AgentServerStore {
         envelope: TypedEnvelope<proto::GetAgentServerCommand>,
         mut cx: AsyncApp,
     ) -> Result<proto::AgentServerCommand> {
-        let (command, login_command) = this
+        let command = this
             .update(&mut cx, |this, cx| {
                 let AgentServerStoreState::Local {
                     downstream_client, ..
@@ -807,6 +689,7 @@ impl AgentServerStore {
                     debug_panic!("should not receive GetAgentServerCommand in a non-local project");
                     bail!("unexpected GetAgentServerCommand request in a non-local project");
                 };
+                let no_browser = this.no_browser();
                 let agent = this
                     .external_agents
                     .get_mut(&*envelope.payload.name)
@@ -856,8 +739,12 @@ impl AgentServerStore {
                         (status_tx, new_version_available_tx)
                     })
                     .unzip();
+                let mut extra_env = HashMap::default();
+                if no_browser {
+                    extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
+                }
                 anyhow::Ok(agent.get_command(
-                    HashMap::default(),
+                    extra_env,
                     status_tx,
                     new_version_available_tx,
                     &mut cx.to_async(),
@@ -871,9 +758,9 @@ impl AgentServerStore {
                 .env
                 .map(|env| env.into_iter().collect())
                 .unwrap_or_default(),
-            // This is no longer used, but returned for backwards compatibility
+            // root_dir and login are no longer used, but returned for backwards compatibility
             root_dir: paths::home_dir().to_string_lossy().to_string(),
-            login: login_command.map(|cmd| cmd.to_proto()),
+            login: None,
         })
     }
 
@@ -914,13 +801,7 @@ impl AgentServerStore {
                 .names
                 .into_iter()
                 .map(|name| {
-                    let agent_name = ExternalAgentServerName(name.clone().into());
-                    let fallback_source =
-                        if name == GEMINI_NAME || name == CLAUDE_AGENT_NAME || name == CODEX_NAME {
-                            ExternalAgentSource::Builtin
-                        } else {
-                            ExternalAgentSource::Custom
-                        };
+                    let agent_name = ExternalAgentServerName(name.into());
                     let (icon, display_name, source) = metadata
                         .remove(&agent_name)
                         .or_else(|| {
@@ -934,12 +815,7 @@ impl AgentServerStore {
                                     )
                                 })
                         })
-                        .unwrap_or((None, None, fallback_source));
-                    let source = if fallback_source == ExternalAgentSource::Builtin {
-                        ExternalAgentSource::Builtin
-                    } else {
-                        source
-                    };
+                        .unwrap_or((None, None, ExternalAgentSource::default()));
                     let agent = RemoteExternalAgentServer {
                         project_id: *project_id,
                         upstream_client: upstream_client.clone(),
@@ -1056,192 +932,6 @@ impl AgentServerStore {
     }
 }
 
-fn get_or_npm_install_builtin_agent(
-    binary_name: SharedString,
-    package_name: SharedString,
-    entrypoint_path: PathBuf,
-    minimum_version: Option<semver::Version>,
-    status_tx: Option<watch::Sender<SharedString>>,
-    new_version_available: Option<watch::Sender<Option<String>>>,
-    fs: Arc<dyn Fs>,
-    node_runtime: NodeRuntime,
-    cx: &mut AsyncApp,
-) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
-    cx.spawn(async move |cx| {
-        let node_path = node_runtime.binary_path().await?;
-        let dir = paths::external_agents_dir().join(binary_name.as_str());
-        fs.create_dir(&dir).await?;
-
-        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(name) = file_name.to_str()
-                && let Some(version) = semver::Version::from_str(name).ok()
-                && fs
-                    .is_file(&dir.join(file_name).join(&entrypoint_path))
-                    .await
-            {
-                versions.push((version, file_name.to_owned()));
-            } else {
-                to_delete.push(file_name.to_owned())
-            }
-        }
-
-        versions.sort();
-        let newest_version = if let Some((version, _)) = versions.last().cloned()
-            && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
-        {
-            versions.pop()
-        } else {
-            None
-        };
-        log::debug!("existing version of {package_name}: {newest_version:?}");
-        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((version, file_name)) = newest_version {
-            cx.background_spawn({
-                let dir = dir.clone();
-                let fs = fs.clone();
-                async move {
-                    let latest_version = node_runtime
-                        .npm_package_latest_version(&package_name)
-                        .await
-                        .ok();
-                    if let Some(latest_version) = latest_version
-                        && latest_version != version
-                    {
-                        let download_result = download_latest_version(
-                            fs,
-                            dir.clone(),
-                            node_runtime,
-                            package_name.clone(),
-                        )
-                        .await
-                        .log_err();
-                        if let Some(mut new_version_available) = new_version_available
-                            && download_result.is_some()
-                        {
-                            new_version_available
-                                .send(Some(latest_version.to_string()))
-                                .ok();
-                        }
-                    }
-                }
-            })
-            .detach();
-            file_name
-        } else {
-            if let Some(mut status_tx) = status_tx {
-                status_tx.send("Installing…".into()).ok();
-            }
-            let dir = dir.clone();
-            cx.background_spawn(download_latest_version(
-                fs.clone(),
-                dir.clone(),
-                node_runtime,
-                package_name.clone(),
-            ))
-            .await?
-            .to_string()
-            .into()
-        };
-
-        let agent_server_path = dir.join(version).join(entrypoint_path);
-        let agent_server_path_exists = fs.is_file(&agent_server_path).await;
-        anyhow::ensure!(
-            agent_server_path_exists,
-            "Missing entrypoint path {} after installation",
-            agent_server_path.to_string_lossy()
-        );
-
-        anyhow::Ok(AgentServerCommand {
-            path: node_path,
-            args: vec![agent_server_path.to_string_lossy().into_owned()],
-            env: None,
-        })
-    })
-}
-
-fn find_bin_in_path(
-    bin_name: SharedString,
-    root_dir: PathBuf,
-    env: HashMap<String, String>,
-    cx: &mut AsyncApp,
-) -> Task<Option<PathBuf>> {
-    cx.background_executor().spawn(async move {
-        let which_result = if cfg!(windows) {
-            which::which(bin_name.as_str())
-        } else {
-            let shell_path = env.get("PATH").cloned();
-            which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
-        };
-
-        if let Err(which::Error::CannotFindBinaryPath) = which_result {
-            return None;
-        }
-
-        which_result.log_err()
-    })
-}
-
-async fn download_latest_version(
-    fs: Arc<dyn Fs>,
-    dir: PathBuf,
-    node_runtime: NodeRuntime,
-    package_name: SharedString,
-) -> Result<Version> {
-    log::debug!("downloading latest version of {package_name}");
-
-    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.to_string()),
-        RenameOptions {
-            ignore_if_exists: true,
-            overwrite: true,
-            create_parents: false,
-        },
-    )
-    .await?;
-
-    anyhow::Ok(version)
-}
-
 struct RemoteExternalAgentServer {
     project_id: u64,
     upstream_client: Entity<RemoteClient>,
@@ -1257,7 +947,7 @@ impl ExternalAgentServer for RemoteExternalAgentServer {
         status_tx: Option<watch::Sender<SharedString>>,
         new_version_available_tx: Option<watch::Sender<Option<String>>>,
         cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
+    ) -> Task<Result<AgentServerCommand>> {
         let project_id = self.project_id;
         let name = self.name.to_string();
         let upstream_client = self.upstream_client.downgrade();
@@ -1287,358 +977,11 @@ impl ExternalAgentServer for RemoteExternalAgentServer {
                     Interactive::No,
                 )
             })??;
-            Ok((
-                AgentServerCommand {
-                    path: command.program.into(),
-                    args: command.args,
-                    env: Some(command.env),
-                },
-                response.login.map(SpawnInTerminal::from_proto),
-            ))
-        })
-    }
-
-    fn as_any_mut(&mut self) -> &mut dyn Any {
-        self
-    }
-}
-
-struct LocalGemini {
-    fs: Arc<dyn Fs>,
-    node_runtime: NodeRuntime,
-    project_environment: Entity<ProjectEnvironment>,
-    custom_command: Option<AgentServerCommand>,
-    settings_env: Option<HashMap<String, String>>,
-    ignore_system_version: bool,
-}
-
-impl ExternalAgentServer for LocalGemini {
-    fn get_command(
-        &mut self,
-        extra_env: HashMap<String, String>,
-        status_tx: Option<watch::Sender<SharedString>>,
-        new_version_available_tx: Option<watch::Sender<Option<String>>>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
-        let fs = self.fs.clone();
-        let node_runtime = self.node_runtime.clone();
-        let project_environment = self.project_environment.downgrade();
-        let custom_command = self.custom_command.clone();
-        let settings_env = self.settings_env.clone();
-        let ignore_system_version = self.ignore_system_version;
-        let home_dir = paths::home_dir();
-
-        cx.spawn(async move |cx| {
-            let mut env = project_environment
-                .update(cx, |project_environment, cx| {
-                    project_environment.local_directory_environment(
-                        &Shell::System,
-                        home_dir.as_path().into(),
-                        cx,
-                    )
-                })?
-                .await
-                .unwrap_or_default();
-
-            env.extend(settings_env.unwrap_or_default());
-
-            let mut command = if let Some(mut custom_command) = custom_command {
-                custom_command.env = Some(env);
-                custom_command
-            } else if !ignore_system_version
-                && let Some(bin) =
-                    find_bin_in_path("gemini".into(), home_dir.to_path_buf(), env.clone(), cx).await
-            {
-                AgentServerCommand {
-                    path: bin,
-                    args: Vec::new(),
-                    env: Some(env),
-                }
-            } else {
-                let mut command = get_or_npm_install_builtin_agent(
-                    GEMINI_NAME.into(),
-                    "@google/gemini-cli".into(),
-                    "node_modules/@google/gemini-cli/dist/index.js".into(),
-                    if cfg!(windows) {
-                        // v0.8.x on Windows has a bug that causes the initialize request to hang forever
-                        Some("0.9.0".parse().unwrap())
-                    } else {
-                        Some("0.2.1".parse().unwrap())
-                    },
-                    status_tx,
-                    new_version_available_tx,
-                    fs,
-                    node_runtime,
-                    cx,
-                )
-                .await?;
-                command.env = Some(env);
-                command
-            };
-
-            // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
-            let login = task::SpawnInTerminal {
-                command: Some(command.path.to_string_lossy().into_owned()),
-                args: command.args.clone(),
-                env: command.env.clone().unwrap_or_default(),
-                label: "gemini /auth".into(),
-                ..Default::default()
-            };
-
-            command.env.get_or_insert_default().extend(extra_env);
-            command.args.push("--experimental-acp".into());
-            Ok((command, Some(login)))
-        })
-    }
-
-    fn as_any_mut(&mut self) -> &mut dyn Any {
-        self
-    }
-}
-
-struct LocalClaudeCode {
-    fs: Arc<dyn Fs>,
-    node_runtime: NodeRuntime,
-    project_environment: Entity<ProjectEnvironment>,
-    custom_command: Option<AgentServerCommand>,
-    settings_env: Option<HashMap<String, String>>,
-}
-
-impl ExternalAgentServer for LocalClaudeCode {
-    fn get_command(
-        &mut self,
-        extra_env: HashMap<String, String>,
-        status_tx: Option<watch::Sender<SharedString>>,
-        new_version_available_tx: Option<watch::Sender<Option<String>>>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
-        let fs = self.fs.clone();
-        let node_runtime = self.node_runtime.clone();
-        let project_environment = self.project_environment.downgrade();
-        let custom_command = self.custom_command.clone();
-        let settings_env = self.settings_env.clone();
-
-        cx.spawn(async move |cx| {
-            let mut env = project_environment
-                .update(cx, |project_environment, cx| {
-                    project_environment.local_directory_environment(
-                        &Shell::System,
-                        paths::home_dir().as_path().into(),
-                        cx,
-                    )
-                })?
-                .await
-                .unwrap_or_default();
-            env.insert("ANTHROPIC_API_KEY".into(), "".into());
-
-            env.extend(settings_env.unwrap_or_default());
-
-            let (mut command, login_command) = if let Some(mut custom_command) = custom_command {
-                custom_command.env = Some(env);
-                (custom_command, None)
-            } else {
-                let mut command = get_or_npm_install_builtin_agent(
-                    "claude-agent-acp".into(),
-                    "@zed-industries/claude-agent-acp".into(),
-                    "node_modules/@zed-industries/claude-agent-acp/dist/index.js".into(),
-                    Some("0.17.0".parse().unwrap()),
-                    status_tx,
-                    new_version_available_tx,
-                    fs,
-                    node_runtime,
-                    cx,
-                )
-                .await?;
-                command.env = Some(env);
-
-                (command, None)
-            };
-
-            command.env.get_or_insert_default().extend(extra_env);
-            Ok((command, login_command))
-        })
-    }
-
-    fn as_any_mut(&mut self) -> &mut dyn Any {
-        self
-    }
-}
-
-struct LocalCodex {
-    fs: Arc<dyn Fs>,
-    project_environment: Entity<ProjectEnvironment>,
-    http_client: Arc<dyn HttpClient>,
-    custom_command: Option<AgentServerCommand>,
-    settings_env: Option<HashMap<String, String>>,
-    no_browser: bool,
-}
-
-impl ExternalAgentServer for LocalCodex {
-    fn get_command(
-        &mut self,
-        extra_env: HashMap<String, String>,
-        mut status_tx: Option<watch::Sender<SharedString>>,
-        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
-        let fs = self.fs.clone();
-        let project_environment = self.project_environment.downgrade();
-        let http = self.http_client.clone();
-        let custom_command = self.custom_command.clone();
-        let settings_env = self.settings_env.clone();
-        let no_browser = self.no_browser;
-
-        cx.spawn(async move |cx| {
-            let mut env = project_environment
-                .update(cx, |project_environment, cx| {
-                    project_environment.local_directory_environment(
-                        &Shell::System,
-                        paths::home_dir().as_path().into(),
-                        cx,
-                    )
-                })?
-                .await
-                .unwrap_or_default();
-            if no_browser {
-                env.insert("NO_BROWSER".to_owned(), "1".to_owned());
-            }
-
-            env.extend(settings_env.unwrap_or_default());
-
-            let mut command = if let Some(mut custom_command) = custom_command {
-                custom_command.env = Some(env);
-                custom_command
-            } else {
-                let dir = paths::external_agents_dir().join(CODEX_NAME);
-                fs.create_dir(&dir).await?;
-
-                let bin_name = if cfg!(windows) {
-                    "codex-acp.exe"
-                } else {
-                    "codex-acp"
-                };
-
-                let find_latest_local_version = async || -> Option<PathBuf> {
-                    let mut local_versions: Vec<(semver::Version, String)> = Vec::new();
-                    let mut stream = fs.read_dir(&dir).await.ok()?;
-                    while let Some(entry) = stream.next().await {
-                        let Ok(entry) = entry else { continue };
-                        let Some(file_name) = entry.file_name() else {
-                            continue;
-                        };
-                        let version_path = dir.join(&file_name);
-                        if fs.is_file(&version_path.join(bin_name)).await {
-                            let version_str = file_name.to_string_lossy();
-                            if let Ok(version) =
-                                semver::Version::from_str(version_str.trim_start_matches('v'))
-                            {
-                                local_versions.push((version, version_str.into_owned()));
-                            }
-                        }
-                    }
-                    local_versions.sort_by(|(a, _), (b, _)| a.cmp(b));
-                    local_versions.last().map(|(_, v)| dir.join(v))
-                };
-
-                let fallback_to_latest_local_version =
-                    async |err: anyhow::Error| -> Result<PathBuf, anyhow::Error> {
-                        if let Some(local) = find_latest_local_version().await {
-                            log::info!(
-                                "Falling back to locally installed Codex version: {}",
-                                local.display()
-                            );
-                            Ok(local)
-                        } else {
-                            Err(err)
-                        }
-                    };
-
-                let version_dir = match ::http_client::github::latest_github_release(
-                    CODEX_ACP_REPO,
-                    true,
-                    false,
-                    http.clone(),
-                )
-                .await
-                {
-                    Ok(release) => {
-                        let version_dir = dir.join(&release.tag_name);
-                        if !fs.is_dir(&version_dir).await {
-                            if let Some(ref mut status_tx) = status_tx {
-                                status_tx.send("Installing…".into()).ok();
-                            }
-
-                            let tag = release.tag_name.clone();
-                            let version_number = tag.trim_start_matches('v');
-                            let asset_name = asset_name(version_number)
-                                .context("codex acp is not supported for this architecture")?;
-                            let asset = release
-                                .assets
-                                .into_iter()
-                                .find(|asset| asset.name == asset_name)
-                                .with_context(|| {
-                                    format!("no asset found matching `{asset_name:?}`")
-                                })?;
-                            // Strip "sha256:" prefix from digest if present (GitHub API format)
-                            let digest = asset
-                                .digest
-                                .as_deref()
-                                .and_then(|d| d.strip_prefix("sha256:").or(Some(d)));
-                            match ::http_client::github_download::download_server_binary(
-                                &*http,
-                                &asset.browser_download_url,
-                                digest,
-                                &version_dir,
-                                if cfg!(target_os = "windows") {
-                                    AssetKind::Zip
-                                } else {
-                                    AssetKind::TarGz
-                                },
-                            )
-                            .await
-                            {
-                                Ok(()) => {
-                                    // remove older versions
-                                    util::fs::remove_matching(&dir, |entry| entry != version_dir)
-                                        .await;
-                                    version_dir
-                                }
-                                Err(err) => {
-                                    log::error!(
-                                        "Failed to download Codex release {}: {err:#}",
-                                        release.tag_name
-                                    );
-                                    fallback_to_latest_local_version(err).await?
-                                }
-                            }
-                        } else {
-                            version_dir
-                        }
-                    }
-                    Err(err) => {
-                        log::error!("Failed to fetch Codex latest release: {err:#}");
-                        fallback_to_latest_local_version(err).await?
-                    }
-                };
-
-                let bin_path = version_dir.join(bin_name);
-                anyhow::ensure!(
-                    fs.is_file(&bin_path).await,
-                    "Missing Codex binary at {} after installation",
-                    bin_path.to_string_lossy()
-                );
-
-                let mut cmd = AgentServerCommand {
-                    path: bin_path,
-                    args: Vec::new(),
-                    env: None,
-                };
-                cmd.env = Some(env);
-                cmd
-            };
-
-            command.env.get_or_insert_default().extend(extra_env);
-            Ok((command, None))
+            Ok(AgentServerCommand {
+                path: command.program.into(),
+                args: command.args,
+                env: Some(command.env),
+            })
         })
     }
 
@@ -1647,42 +990,6 @@ impl ExternalAgentServer for LocalCodex {
     }
 }
 
-pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp";
-
-fn get_platform_info() -> Option<(&'static str, &'static str, &'static str)> {
-    let arch = if cfg!(target_arch = "x86_64") {
-        "x86_64"
-    } else if cfg!(target_arch = "aarch64") {
-        "aarch64"
-    } else {
-        return None;
-    };
-
-    let platform = if cfg!(target_os = "macos") {
-        "apple-darwin"
-    } else if cfg!(target_os = "windows") {
-        "pc-windows-msvc"
-    } else if cfg!(target_os = "linux") {
-        "unknown-linux-gnu"
-    } else {
-        return None;
-    };
-
-    // Windows uses .zip in release assets
-    let ext = if cfg!(target_os = "windows") {
-        "zip"
-    } else {
-        "tar.gz"
-    };
-
-    Some((arch, platform, ext))
-}
-
-fn asset_name(version: &str) -> Option<String> {
-    let (arch, platform, ext) = get_platform_info()?;
-    Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}"))
-}
-
 pub struct LocalExtensionArchiveAgent {
     pub fs: Arc<dyn Fs>,
     pub http_client: Arc<dyn HttpClient>,
@@ -1701,7 +1008,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
         _status_tx: Option<watch::Sender<SharedString>>,
         _new_version_available_tx: Option<watch::Sender<Option<String>>>,
         cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
+    ) -> Task<Result<AgentServerCommand>> {
         let fs = self.fs.clone();
         let http_client = self.http_client.clone();
         let node_runtime = self.node_runtime.clone();
@@ -1877,7 +1184,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
                 env: Some(env),
             };
 
-            Ok((command, None))
+            Ok(command)
         })
     }
 
@@ -1903,7 +1210,7 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
         _status_tx: Option<watch::Sender<SharedString>>,
         _new_version_available_tx: Option<watch::Sender<Option<String>>>,
         cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
+    ) -> Task<Result<AgentServerCommand>> {
         let fs = self.fs.clone();
         let http_client = self.http_client.clone();
         let node_runtime = self.node_runtime.clone();
@@ -2061,7 +1368,7 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
                 env: Some(env),
             };
 
-            Ok((command, None))
+            Ok(command)
         })
     }
 
@@ -2086,7 +1393,7 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
         _status_tx: Option<watch::Sender<SharedString>>,
         _new_version_available_tx: Option<watch::Sender<Option<String>>>,
         cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
+    ) -> Task<Result<AgentServerCommand>> {
         let node_runtime = self.node_runtime.clone();
         let project_environment = self.project_environment.downgrade();
         let package = self.package.clone();
@@ -2132,7 +1439,7 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
                 env: Some(env),
             };
 
-            Ok((command, None))
+            Ok(command)
         })
     }
 

crates/project/tests/integration/ext_agent_tests.rs πŸ”—

@@ -13,15 +13,12 @@ impl ExternalAgentServer for NoopExternalAgent {
         _status_tx: Option<watch::Sender<SharedString>>,
         _new_version_available_tx: Option<watch::Sender<Option<String>>>,
         _cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
-        Task::ready(Ok((
-            AgentServerCommand {
-                path: PathBuf::from("noop"),
-                args: Vec::new(),
-                env: None,
-            },
-            None,
-        )))
+    ) -> Task<Result<AgentServerCommand>> {
+        Task::ready(Ok(AgentServerCommand {
+            path: PathBuf::from("noop"),
+            args: Vec::new(),
+            env: None,
+        }))
     }
 
     fn as_any_mut(&mut self) -> &mut dyn Any {

crates/project/tests/integration/extension_agent_tests.rs πŸ”—

@@ -29,15 +29,12 @@ impl ExternalAgentServer for NoopExternalAgent {
         _status_tx: Option<watch::Sender<SharedString>>,
         _new_version_available_tx: Option<watch::Sender<Option<String>>>,
         _cx: &mut AsyncApp,
-    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
-        Task::ready(Ok((
-            AgentServerCommand {
-                path: PathBuf::from("noop"),
-                args: Vec::new(),
-                env: None,
-            },
-            None,
-        )))
+    ) -> Task<Result<AgentServerCommand>> {
+        Task::ready(Ok(AgentServerCommand {
+            path: PathBuf::from("noop"),
+            args: Vec::new(),
+            env: None,
+        }))
     }
 
     fn as_any_mut(&mut self) -> &mut dyn Any {
@@ -299,26 +296,6 @@ async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) {
 
 #[test]
 fn test_tilde_expansion_in_settings() {
-    let settings = settings::BuiltinAgentServerSettings {
-        path: Some(PathBuf::from("~/bin/agent")),
-        args: Some(vec!["--flag".into()]),
-        env: None,
-        ignore_system_version: None,
-        default_mode: None,
-        default_model: None,
-        favorite_models: vec![],
-        default_config_options: Default::default(),
-        favorite_config_option_values: Default::default(),
-    };
-
-    let BuiltinAgentServerSettings { path, .. } = settings.into();
-
-    let path = path.unwrap();
-    assert!(
-        !path.to_string_lossy().starts_with("~"),
-        "Tilde should be expanded for builtin agent path"
-    );
-
     let settings = settings::CustomAgentServerSettings::Custom {
         path: PathBuf::from("~/custom/agent"),
         args: vec!["serve".into()],

crates/remote_server/src/remote_editing_tests.rs πŸ”—

@@ -2129,7 +2129,7 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["codex", "gemini", "claude"]);
+    pretty_assertions::assert_eq!(names, Vec::<String>::new());
     server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
         settings_store
             .set_server_settings(
@@ -2160,8 +2160,8 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]);
-    let (command, login) = project
+    pretty_assertions::assert_eq!(names, ["foo"]);
+    let command = project
         .update(cx, |project, cx| {
             project.agent_server_store().update(cx, |store, cx| {
                 store
@@ -2183,12 +2183,12 @@ async fn test_remote_external_agent_server(
             path: "mock".into(),
             args: vec!["foo-cli".into(), "--flag".into()],
             env: Some(HashMap::from_iter([
+                ("NO_BROWSER".into(), "1".into()),
                 ("VAR".into(), "val".into()),
                 ("OTHER_VAR".into(), "other-val".into())
             ]))
         }
     );
-    assert!(login.is_none());
 }
 
 pub async fn init_test(

crates/settings_content/src/agent.rs πŸ”—

@@ -316,73 +316,21 @@ impl From<&str> for LanguageModelProviderSetting {
 
 #[with_fallible_options]
 #[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
-pub struct AllAgentServersSettings {
-    pub gemini: Option<BuiltinAgentServerSettings>,
-    pub claude: Option<BuiltinAgentServerSettings>,
-    pub codex: Option<BuiltinAgentServerSettings>,
-
-    /// Custom agent servers configured by the user
-    #[serde(flatten)]
-    pub custom: HashMap<String, CustomAgentServerSettings>,
+#[serde(transparent)]
+pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
+
+impl std::ops::Deref for AllAgentServersSettings {
+    type Target = HashMap<String, CustomAgentServerSettings>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
 }
 
-#[with_fallible_options]
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
-pub struct BuiltinAgentServerSettings {
-    /// Absolute path to a binary to be used when launching this agent.
-    ///
-    /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
-    #[serde(rename = "command")]
-    pub path: Option<PathBuf>,
-    /// If a binary is specified in `command`, it will be passed these arguments.
-    pub args: Option<Vec<String>>,
-    /// If a binary is specified in `command`, it will be passed these environment variables.
-    pub env: Option<HashMap<String, String>>,
-    /// Whether to skip searching `$PATH` for an agent server binary when
-    /// launching this agent.
-    ///
-    /// This has no effect if a `command` is specified. Otherwise, when this is
-    /// `false`, Zed will search `$PATH` for an agent server binary and, if one
-    /// is found, use it for threads with this agent. If no agent binary is
-    /// found on `$PATH`, Zed will automatically install and use its own binary.
-    /// When this is `true`, Zed will not search `$PATH`, and will always use
-    /// its own binary.
-    ///
-    /// Default: true
-    pub ignore_system_version: Option<bool>,
-    /// The default mode to use for this agent.
-    ///
-    /// Note: Not only all agents support modes.
-    ///
-    /// Default: None
-    pub default_mode: Option<String>,
-    /// The default model to use for this agent.
-    ///
-    /// This should be the model ID as reported by the agent.
-    ///
-    /// Default: None
-    pub default_model: Option<String>,
-    /// The favorite models for this agent.
-    ///
-    /// These are the model IDs as reported by the agent.
-    ///
-    /// Default: []
-    #[serde(default, skip_serializing_if = "Vec::is_empty")]
-    pub favorite_models: Vec<String>,
-    /// Default values for session config options.
-    ///
-    /// This is a map from config option ID to value ID.
-    ///
-    /// Default: {}
-    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
-    pub default_config_options: HashMap<String, String>,
-    /// Favorited values for session config options.
-    ///
-    /// This is a map from config option ID to a list of favorited value IDs.
-    ///
-    /// Default: {}
-    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
-    pub favorite_config_option_values: HashMap<String, Vec<String>>,
+impl std::ops::DerefMut for AllAgentServersSettings {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
 }
 
 #[with_fallible_options]

crates/zed/src/visual_test_runner.rs πŸ”—

@@ -1947,8 +1947,8 @@ impl AgentServer for StubAgentServer {
         &self,
         _delegate: AgentServerDelegate,
         _cx: &mut App,
-    ) -> gpui::Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
-        gpui::Task::ready(Ok((Rc::new(self.connection.clone()), None)))
+    ) -> gpui::Task<gpui::Result<Rc<dyn AgentConnection>>> {
+        gpui::Task::ready(Ok(Rc::new(self.connection.clone())))
     }
 
     fn into_any(self: Rc<Self>) -> Rc<dyn Any> {

docs/src/ai/external-agents.md πŸ”—

@@ -27,7 +27,10 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
 [
   {
     "bindings": {
-      "cmd-alt-g": ["agent::NewExternalAgentThread", { "agent": "gemini" }]
+      "cmd-alt-g": [
+        "agent::NewExternalAgentThread",
+        { "agent": { "custom": { "name": "gemini" } } }
+      ]
     }
   }
 ]
@@ -38,32 +41,14 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
 The first time you create a Gemini CLI thread, Zed will install [@google/gemini-cli](https://github.com/google-gemini/gemini-cli).
 This installation is only available to Zed and is kept up to date as you use the agent.
 
-By default, Zed will use this managed version of Gemini CLI even if you have it installed globally.
-However, you can configure it to use a version in your `PATH` by adding this to your settings:
-
-```json [settings]
-{
-  "agent_servers": {
-    "gemini": {
-      "ignore_system_version": false
-    }
-  }
-}
-```
-
 #### Authentication
 
-After you have Gemini CLI running, you'll be prompted to choose your authentication method.
+After you have Gemini CLI running, you'll be prompted to authenticate.
 
-Most users should click the "Log in with Google".
-This will cause a browser window to pop-up and auth directly with Gemini CLI.
+Click the "Login" button to open the Gemini CLI interactively, where you can log in with your Google account or [Vertex AI](https://cloud.google.com/vertex-ai) credentials.
 Zed does not see your OAuth or access tokens in this case.
 
-You can also use the "Gemini API Key".
-If you select this, and have the `GEMINI_API_KEY` set, then we will use that.
-Otherwise Zed will prompt you for an API key which will be stored securely in your keychain, and used to start Gemini CLI from within Zed.
-
-The "Vertex AI" option is for those who are using [Vertex AI](https://cloud.google.com/vertex-ai), and have already configured their environment correctly.
+If the `GEMINI_API_KEY` environment variable (or `GOOGLE_AI_API_KEY`) is already set, or you have configured a Google AI API key in Zed's [language model provider settings](./llm-providers.md#google-ai), it will be passed to Gemini CLI automatically.
 
 For more information, see the [Gemini CLI docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md).
 
@@ -88,7 +73,10 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
 [
   {
     "bindings": {
-      "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude_code" }]
+      "cmd-alt-c": [
+        "agent::NewExternalAgentThread",
+        { "agent": { "custom": { "name": "claude-acp" } } }
+      ]
     }
   }
 ]
@@ -114,7 +102,8 @@ If you want to override the executable used by the adapter, you can set the `CLA
 ```json
 {
   "agent_servers": {
-    "claude": {
+    "claude-acp": {
+      "type": "registry",
       "env": {
         "CLAUDE_CODE_EXECUTABLE": "/path/to/alternate-claude-code-executable"
       }
@@ -159,7 +148,10 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
 [
   {
     "bindings": {
-      "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "codex" }]
+      "cmd-alt-c": [
+        "agent::NewExternalAgentThread",
+        { "agent": { "custom": { "name": "codex-acp" } } }
+      ]
     }
   }
 ]
@@ -248,7 +240,7 @@ You can also add agents through your settings file ([how to edit](../configuring
 
 This can be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it.
 
-It's also possible to specify a custom path, arguments, or environment for the builtin integrations by using the `claude` and `gemini` names.
+It's also possible to customize environment variables for registry-installed agents like Claude Agent, Codex, and Gemini CLI by using their registry names (`claude-acp`, `codex-acp`, `gemini`) with `"type": "registry"` in your settings.
 
 ## Debugging Agents