Detailed changes
@@ -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>)
})
}
@@ -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,
@@ -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>;
@@ -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
- }
-}
@@ -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");
-}
@@ -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(),
+ )
+ })
+}
@@ -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
@@ -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,
- }
- }
-}
@@ -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/\
@@ -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);
}
})
})
@@ -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(),
+ },
+ );
}
});
@@ -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!(
@@ -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());
}
});
})
@@ -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())),
}
@@ -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
@@ -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(
@@ -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");
@@ -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!(
@@ -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;
+}
@@ -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),
+ );
+ }
+}
@@ -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"
+ }
+ }
+}"#,
+ ),
+ );
+ }
}
@@ -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;
@@ -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)
})
}
@@ -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 {
@@ -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()],
@@ -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(
@@ -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]
@@ -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> {
@@ -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