agent_servers: Migrate all built-in agents to go via registry (#50094)
This has lots of benefits, but mainly allows users to uninstall agents. Release Notes: - N/A --------- Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
29 files changed
+1031
-2054
No files matched your search
@@ -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,
|
||||
.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,
|
||||
))
|
||||
.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,
|
||||
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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+153
-156
@@ -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());
|
||||
for item in &agent_items {
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(item.display_name.clone());
|
||||
|
||||
let mut entry = ContextMenuEntry::new(display_name);
|
||||
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;
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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>,
|
||||
#[serde(transparent)]
|
||||
pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: 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
|
||||
|
||||
|
||||
Reference in new issue
Block a user