1use std::path::PathBuf;
2
3use crate::AgentServerCommand;
4use anyhow::Result;
5use collections::HashMap;
6use gpui::{App, SharedString};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
10
11pub fn init(cx: &mut App) {
12 AllAgentServersSettings::register(cx);
13}
14
15#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
16#[settings_key(key = "agent_servers")]
17pub struct AllAgentServersSettingsContent {
18 gemini: Option<GeminiSettingsContent>,
19 claude: Option<AgentServerCommand>,
20 /// Custom agent servers configured by the user
21 #[serde(flatten)]
22 pub custom: HashMap<SharedString, AgentServerCommand>,
23}
24
25#[derive(Clone, Debug, Default)]
26pub struct AllAgentServersSettings {
27 pub commands: HashMap<SharedString, AgentServerCommand>,
28 pub gemini_is_system: bool,
29}
30
31impl AllAgentServersSettings {
32 pub fn is_system(this: &Self, name: &str) -> bool {
33 if name == "gemini" {
34 this.gemini_is_system
35 } else {
36 false
37 }
38 }
39}
40
41impl std::ops::Deref for AllAgentServersSettings {
42 type Target = HashMap<SharedString, AgentServerCommand>;
43 fn deref(&self) -> &Self::Target {
44 &self.commands
45 }
46}
47
48impl std::ops::DerefMut for AllAgentServersSettings {
49 fn deref_mut(&mut self) -> &mut Self::Target {
50 &mut self.commands
51 }
52}
53
54#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
55pub struct GeminiSettingsContent {
56 ignore_system_version: Option<bool>,
57 #[serde(flatten)]
58 inner: Option<AgentServerCommand>,
59}
60
61#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
62pub struct BuiltinAgentServerSettings {
63 /// Absolute path to a binary to be used when launching this agent.
64 ///
65 /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
66 #[serde(rename = "command")]
67 pub path: Option<PathBuf>,
68 /// If a binary is specified in `command`, it will be passed these arguments.
69 pub args: Option<Vec<String>>,
70 /// If a binary is specified in `command`, it will be passed these environment variables.
71 pub env: Option<HashMap<String, String>>,
72 /// Whether to skip searching `$PATH` for an agent server binary when
73 /// launching this agent.
74 ///
75 /// This has no effect if a `command` is specified. Otherwise, when this is
76 /// `false`, Zed will search `$PATH` for an agent server binary and, if one
77 /// is found, use it for threads with this agent. If no agent binary is
78 /// found on `$PATH`, Zed will automatically install and use its own binary.
79 /// When this is `true`, Zed will not search `$PATH`, and will always use
80 /// its own binary.
81 ///
82 /// Default: true
83 pub ignore_system_version: Option<bool>,
84}
85
86impl BuiltinAgentServerSettings {
87 pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
88 self.path.map(|path| AgentServerCommand {
89 path,
90 args: self.args.unwrap_or_default(),
91 env: self.env,
92 })
93 }
94}
95
96impl From<AgentServerCommand> for BuiltinAgentServerSettings {
97 fn from(value: AgentServerCommand) -> Self {
98 BuiltinAgentServerSettings {
99 path: Some(value.path),
100 args: Some(value.args),
101 env: value.env,
102 ..Default::default()
103 }
104 }
105}
106
107#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
108pub struct AgentServerSettings {
109 #[serde(flatten)]
110 pub command: AgentServerCommand,
111}
112
113impl settings::Settings for AllAgentServersSettings {
114 type FileContent = AllAgentServersSettingsContent;
115
116 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
117 let mut settings = AllAgentServersSettings::default();
118
119 for AllAgentServersSettingsContent {
120 gemini,
121 claude,
122 custom,
123 } in sources.defaults_and_customizations()
124 {
125 if let Some(gemini) = gemini {
126 if let Some(ignore) = gemini.ignore_system_version {
127 settings.gemini_is_system = !ignore;
128 }
129 if let Some(gemini) = gemini.inner.as_ref() {
130 settings.insert("gemini".into(), gemini.clone());
131 }
132 }
133 if let Some(claude) = claude.clone() {
134 settings.insert("claude".into(), claude);
135 }
136
137 // Merge custom agents
138 for (name, command) in custom {
139 // Skip built-in agent names to avoid conflicts
140 if name != "gemini" && name != "claude" {
141 settings.commands.insert(name.clone(), command.clone());
142 }
143 }
144 }
145
146 Ok(settings)
147 }
148
149 fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
150}
151
152#[cfg(test)]
153mod tests {
154 use serde_json::json;
155
156 use crate::{AgentServerCommand, GeminiSettingsContent};
157
158 #[test]
159 fn test_deserialization() {
160 let value = json!({
161 "command": "foo",
162 "args": ["bar"],
163 "ignore_system_version": false
164 });
165 let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
166 assert_eq!(
167 settings,
168 GeminiSettingsContent {
169 ignore_system_version: Some(false),
170 inner: Some(AgentServerCommand {
171 path: "foo".into(),
172 args: vec!["bar".into()],
173 env: Default::default(),
174 })
175 }
176 )
177 }
178}