AllAgentServerSettings

Conrad Irwin and Ben Kunkle created

Co-authored-by: Ben Kunkle <ben@zed.dev>

Change summary

crates/project/src/agent_server_store.rs      | 116 +++++++++-----------
crates/settings/src/settings_content.rs       |   1 
crates/settings/src/settings_content/agent.rs |  60 ++++++++++
3 files changed, 114 insertions(+), 63 deletions(-)

Detailed changes

crates/project/src/agent_server_store.rs 🔗

@@ -22,8 +22,8 @@ use rpc::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{SettingsKey, SettingsSources, SettingsStore, SettingsUi};
-use util::{ResultExt as _, debug_panic};
+use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi};
+use util::{MergeFrom, ResultExt as _, debug_panic};
 
 use crate::ProjectEnvironment;
 
@@ -994,47 +994,19 @@ impl ExternalAgentServer for LocalCustomAgent {
 pub const GEMINI_NAME: &'static str = "gemini";
 pub const CLAUDE_CODE_NAME: &'static str = "claude";
 
-#[derive(
-    Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq,
-)]
+#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)]
 #[settings_key(key = "agent_servers")]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
-
-    /// Custom agent servers configured by the user
-    #[serde(flatten)]
     pub custom: HashMap<SharedString, CustomAgentServerSettings>,
 }
-
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Default, Clone, JsonSchema, 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>,
 }
 
@@ -1048,6 +1020,18 @@ impl BuiltinAgentServerSettings {
     }
 }
 
+impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
+    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
+        BuiltinAgentServerSettings {
+            path: value.path,
+            args: value.args,
+            env: value.env,
+            ignore_system_version: value.ignore_system_version,
+            default_mode: value.default_mode,
+        }
+    }
+}
+
 impl From<AgentServerCommand> for BuiltinAgentServerSettings {
     fn from(value: AgentServerCommand) -> Self {
         BuiltinAgentServerSettings {
@@ -1059,9 +1043,8 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
     }
 }
 
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, JsonSchema, Debug, PartialEq)]
 pub struct CustomAgentServerSettings {
-    #[serde(flatten)]
     pub command: AgentServerCommand,
     /// The default mode to use for this agent.
     ///
@@ -1071,36 +1054,47 @@ pub struct CustomAgentServerSettings {
     pub default_mode: Option<String>,
 }
 
-impl settings::Settings for AllAgentServersSettings {
-    type FileContent = Self;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        let mut settings = AllAgentServersSettings::default();
-
-        for AllAgentServersSettings {
-            gemini,
-            claude,
-            custom,
-        } in sources.defaults_and_customizations()
-        {
-            if gemini.is_some() {
-                settings.gemini = gemini.clone();
-            }
-            if claude.is_some() {
-                settings.claude = claude.clone();
-            }
+impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
+    fn from(value: settings::CustomAgentServerSettings) -> Self {
+        CustomAgentServerSettings {
+            command: AgentServerCommand {
+                path: value.path,
+                args: value.args,
+                env: value.env,
+            },
+            default_mode: value.default_mode,
+        }
+    }
+}
 
-            // Merge custom agents
-            for (name, config) in custom {
-                // Skip built-in agent names to avoid conflicts
-                if name != GEMINI_NAME && name != CLAUDE_CODE_NAME {
-                    settings.custom.insert(name.clone(), config.clone());
-                }
-            }
+impl settings::Settings for AllAgentServersSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let agent_settings = content.agent_servers.clone().unwrap();
+        Self {
+            gemini: agent_settings.gemini.map(Into::into),
+            claude: agent_settings.claude.map(Into::into),
+            custom: agent_settings
+                .custom
+                .into_iter()
+                .map(|(k, v)| (k.clone(), v.into()))
+                .collect(),
         }
+    }
 
-        Ok(settings)
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(content) = &content.agent_servers else {
+            return;
+        };
+        if let Some(gemini) = content.gemini.clone() {
+            self.gemini = Some(gemini.into())
+        };
+        if let Some(claude) = content.claude.clone() {
+            self.claude = Some(claude.into());
+        }
+        for (name, config) in content.custom.clone() {
+            self.custom.insert(name, config.into());
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/settings/src/settings_content.rs 🔗

@@ -26,6 +26,7 @@ pub struct SettingsContent {
     pub theme: ThemeSettingsContent,
 
     pub agent: Option<AgentSettingsContent>,
+    pub agent_servers: Option<AllAgentServersSettings>,
 
     /// Configuration of audio in Zed.
     pub audio: Option<AudioSettingsContent>,

crates/settings/src/settings_content/agent.rs 🔗

@@ -1,8 +1,8 @@
-use collections::IndexMap;
+use collections::{HashMap, IndexMap};
 use gpui::SharedString;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use std::{borrow::Cow, sync::Arc};
+use std::{borrow::Cow, path::PathBuf, sync::Arc};
 
 #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
 pub struct AgentSettingsContent {
@@ -262,3 +262,59 @@ impl From<&str> for LanguageModelProviderSetting {
         Self(provider.to_string())
     }
 }
+
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct AllAgentServersSettings {
+    pub gemini: Option<BuiltinAgentServerSettings>,
+    pub claude: Option<BuiltinAgentServerSettings>,
+
+    /// Custom agent servers configured by the user
+    #[serde(flatten)]
+    pub custom: HashMap<SharedString, CustomAgentServerSettings>,
+}
+
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, 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>,
+}
+
+#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct CustomAgentServerSettings {
+    #[serde(rename = "command")]
+    pub path: PathBuf,
+    #[serde(default)]
+    pub args: Vec<String>,
+    pub env: Option<HashMap<String, String>>,
+    /// The default mode to use for this agent.
+    ///
+    /// Note: Not only all agents support modes.
+    ///
+    /// Default: None
+    pub default_mode: Option<String>,
+}