Detailed changes
@@ -70,7 +70,7 @@ impl AgentServerDelegate {
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
- ignore_system_version: bool,
+ search_path: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
@@ -85,7 +85,7 @@ impl AgentServerDelegate {
let new_version_available = self.new_version_available;
cx.spawn(async move |cx| {
- if !ignore_system_version {
+ if search_path {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,
@@ -3,7 +3,7 @@ use std::path::Path;
use std::rc::Rc;
use std::{any::Any, path::PathBuf};
-use anyhow::Result;
+use anyhow::{Result, bail};
use gpui::{App, AppContext as _, SharedString, Task};
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
@@ -25,20 +25,23 @@ impl ClaudeCode {
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<ClaudeCodeLoginCommand>> {
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::<AllAgentServersSettings>(None).claude.clone()
+ let custom_command = cx.read_global(|settings: &SettingsStore, _| {
+ settings
+ .get::<AllAgentServersSettings>(None)
+ .get("claude")
+ .cloned()
});
cx.spawn(async move |cx| {
- let mut command = if let Some(settings) = settings {
- settings.command
+ let mut command = if custom_command.is_some() {
+ bail!("Cannot construct login command because a custom command was specified for claude-code-acp in settings")
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
- true,
+ false,
Some("0.2.5".parse().unwrap()),
cx,
)
@@ -77,20 +80,23 @@ impl AgentServer for ClaudeCode {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::<AllAgentServersSettings>(None).claude.clone()
+ let custom_command = cx.read_global(|settings: &SettingsStore, _| {
+ settings
+ .get::<AllAgentServersSettings>(None)
+ .get("claude")
+ .cloned()
});
cx.spawn(async move |cx| {
- let mut command = if let Some(settings) = settings {
- settings.command
+ let mut command = if let Some(custom_command) = custom_command {
+ custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
- true,
+ false,
None,
cx,
)
@@ -1,6 +1,6 @@
-use crate::{AgentServer, AgentServerDelegate};
#[cfg(test)]
-use crate::{AgentServerCommand, CustomAgentServerSettings};
+use crate::AgentServerCommand;
+use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
@@ -473,15 +473,20 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
- claude: Some(CustomAgentServerSettings {
- command: AgentServerCommand {
- path: "claude-code-acp".into(),
- args: vec![],
- env: None,
- },
- }),
- gemini: Some(crate::gemini::tests::local_command().into()),
- custom: collections::HashMap::default(),
+ commands: [
+ (
+ "claude".into(),
+ AgentServerCommand {
+ path: "claude-code-acp".into(),
+ args: vec![],
+ env: None,
+ },
+ ),
+ ("gemini".into(), crate::gemini::tests::local_command()),
+ ]
+ .into_iter()
+ .collect(),
+ gemini_is_system: false,
},
cx,
);
@@ -2,15 +2,13 @@ use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
-use crate::{AgentServer, AgentServerDelegate};
+use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use settings::SettingsStore;
-use crate::AllAgentServersSettings;
-
#[derive(Clone)]
pub struct Gemini;
@@ -38,26 +36,25 @@ impl AgentServer for Gemini {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::<AllAgentServersSettings>(None).gemini.clone()
+ let (custom_command, is_system) = cx.read_global(|settings: &SettingsStore, _| {
+ let s = settings.get::<AllAgentServersSettings>(None);
+ (
+ s.get("gemini").cloned(),
+ AllAgentServersSettings::is_system(s, "gemini"),
+ )
});
cx.spawn(async move |cx| {
- let ignore_system_version = settings
- .as_ref()
- .and_then(|settings| settings.ignore_system_version)
- .unwrap_or(true);
- let mut command = if let Some(settings) = settings
- && let Some(command) = settings.custom_command()
+ let mut command = if let Some(custom_command) = custom_command
{
- command
+ custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
- ignore_system_version,
+ is_system,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
@@ -14,13 +14,48 @@ pub fn init(cx: &mut App) {
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
+pub struct AllAgentServersSettingsContent {
+ gemini: Option<GeminiSettingsContent>,
+ claude: Option<AgentServerCommand>,
+ /// Custom agent servers configured by the user
+ #[serde(flatten)]
+ pub custom: HashMap<SharedString, AgentServerCommand>,
+}
+
+#[derive(Clone, Debug, Default)]
pub struct AllAgentServersSettings {
- pub gemini: Option<BuiltinAgentServerSettings>,
- pub claude: Option<CustomAgentServerSettings>,
+ pub commands: HashMap<SharedString, AgentServerCommand>,
+ pub gemini_is_system: bool,
+}
- /// Custom agent servers configured by the user
+impl AllAgentServersSettings {
+ pub fn is_system(this: &Self, name: &str) -> bool {
+ if name == "gemini" {
+ this.gemini_is_system
+ } else {
+ false
+ }
+ }
+}
+
+impl std::ops::Deref for AllAgentServersSettings {
+ type Target = HashMap<SharedString, AgentServerCommand>;
+ fn deref(&self) -> &Self::Target {
+ &self.commands
+ }
+}
+
+impl std::ops::DerefMut for AllAgentServersSettings {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.commands
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
+pub struct GeminiSettingsContent {
+ ignore_system_version: Option<bool>,
#[serde(flatten)]
- pub custom: HashMap<SharedString, CustomAgentServerSettings>,
+ inner: Option<AgentServerCommand>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
@@ -70,35 +105,40 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
-pub struct CustomAgentServerSettings {
+pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}
impl settings::Settings for AllAgentServersSettings {
- type FileContent = Self;
+ type FileContent = AllAgentServersSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
- for AllAgentServersSettings {
+ for AllAgentServersSettingsContent {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
- if gemini.is_some() {
- settings.gemini = gemini.clone();
+ if let Some(gemini) = gemini {
+ if let Some(ignore) = gemini.ignore_system_version {
+ settings.gemini_is_system = !ignore;
+ }
+ if let Some(gemini) = gemini.inner.as_ref() {
+ settings.insert("gemini".into(), gemini.clone());
+ }
}
- if claude.is_some() {
- settings.claude = claude.clone();
+ if let Some(claude) = claude.clone() {
+ settings.insert("claude".into(), claude);
}
// Merge custom agents
- for (name, config) in custom {
+ for (name, command) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
- settings.custom.insert(name.clone(), config.clone());
+ settings.commands.insert(name.clone(), command.clone());
}
}
}
@@ -108,3 +148,31 @@ impl settings::Settings for AllAgentServersSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use crate::{AgentServerCommand, GeminiSettingsContent};
+
+ #[test]
+ fn test_deserialization() {
+ let value = json!({
+ "command": "foo",
+ "args": ["bar"],
+ "ignore_system_version": false
+ });
+ let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
+ assert_eq!(
+ settings,
+ GeminiSettingsContent {
+ ignore_system_version: Some(false),
+ inner: Some(AgentServerCommand {
+ path: "foo".into(),
+ args: vec!["bar".into()],
+ env: Default::default(),
+ })
+ }
+ )
+ }
+}
@@ -5,7 +5,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
-use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
+use agent_servers::{AgentServerCommand, AllAgentServersSettings};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -20,6 +20,7 @@ use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
};
+use itertools::Itertools as _;
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
@@ -993,15 +994,16 @@ impl AgentConfiguration {
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
- .custom
.iter()
- .map(|(name, settings)| {
+ .filter(|(name, _)| *name != "gemini" && *name != "claude")
+ .sorted_by(|(l, _), (r, _)| l.cmp(r))
+ .map(|(name, command)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
- command: settings.command.clone(),
+ command: command.clone(),
},
cx,
)
@@ -1280,6 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
+ // FIXME test that this still works
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
@@ -1294,12 +1297,10 @@ async fn open_new_agent_servers_entry_in_settings_editor(
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
- CustomAgentServerSettings {
- command: AgentServerCommand {
- path: "path_to_executable".into(),
- args: vec![],
- env: Some(HashMap::default()),
- },
+ AgentServerCommand {
+ path: "path_to_executable".into(),
+ args: vec![],
+ env: Some(HashMap::default()),
},
);
}
@@ -8,6 +8,7 @@ use acp_thread::AcpThread;
use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
+use itertools::Itertools;
use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
@@ -2683,7 +2684,13 @@ impl AgentPanel {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
- for (agent_name, agent_settings) in &settings.custom {
+ for (agent_name, command) in
+ settings.iter().sorted_by(|(l, _), (r, _)| l.cmp(r))
+ {
+ if agent_name == "gemini" || agent_name == "claude" {
+ continue;
+ }
+
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
@@ -2692,7 +2699,7 @@ impl AgentPanel {
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
- let agent_settings = agent_settings.clone();
+ let command = command.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2704,8 +2711,7 @@ impl AgentPanel {
AgentType::Custom {
name: agent_name
.clone(),
- command: agent_settings
- .command
+ command: command
.clone(),
},
window,