1use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
2use acp_thread::AgentConnection;
3use agent_client_protocol as acp;
4use anyhow::{Context as _, Result};
5use collections::HashSet;
6use credentials_provider::CredentialsProvider;
7use fs::Fs;
8use gpui::{App, AppContext as _, SharedString, Task};
9use language_model::{ApiKey, EnvVar};
10use project::agent_server_store::{
11 AllAgentServersSettings, CLAUDE_AGENT_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
12};
13use settings::{SettingsStore, update_settings_file};
14use std::{rc::Rc, sync::Arc};
15use ui::IconName;
16
17/// A generic agent server implementation for custom user-defined agents
18pub struct CustomAgentServer {
19 name: SharedString,
20}
21
22impl CustomAgentServer {
23 pub fn new(name: SharedString) -> Self {
24 Self { name }
25 }
26}
27
28impl AgentServer for CustomAgentServer {
29 fn name(&self) -> SharedString {
30 self.name.clone()
31 }
32
33 fn logo(&self) -> IconName {
34 IconName::Terminal
35 }
36
37 fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
38 let settings = cx.read_global(|settings: &SettingsStore, _| {
39 settings
40 .get::<AllAgentServersSettings>(None)
41 .get(self.name().as_ref())
42 .cloned()
43 });
44
45 settings
46 .as_ref()
47 .and_then(|s| s.default_mode().map(acp::SessionModeId::new))
48 }
49
50 fn favorite_config_option_value_ids(
51 &self,
52 config_id: &acp::SessionConfigId,
53 cx: &mut App,
54 ) -> HashSet<acp::SessionConfigValueId> {
55 let settings = cx.read_global(|settings: &SettingsStore, _| {
56 settings
57 .get::<AllAgentServersSettings>(None)
58 .get(self.name().as_ref())
59 .cloned()
60 });
61
62 settings
63 .as_ref()
64 .and_then(|s| s.favorite_config_option_values(config_id.0.as_ref()))
65 .map(|values| {
66 values
67 .iter()
68 .cloned()
69 .map(acp::SessionConfigValueId::new)
70 .collect()
71 })
72 .unwrap_or_default()
73 }
74
75 fn toggle_favorite_config_option_value(
76 &self,
77 config_id: acp::SessionConfigId,
78 value_id: acp::SessionConfigValueId,
79 should_be_favorite: bool,
80 fs: Arc<dyn Fs>,
81 cx: &App,
82 ) {
83 let name = self.name();
84 let config_id = config_id.to_string();
85 let value_id = value_id.to_string();
86
87 update_settings_file(fs, cx, move |settings, cx| {
88 let settings = settings
89 .agent_servers
90 .get_or_insert_default()
91 .entry(name.to_string())
92 .or_insert_with(|| default_settings_for_agent(&name, cx));
93
94 match settings {
95 settings::CustomAgentServerSettings::Custom {
96 favorite_config_option_values,
97 ..
98 }
99 | settings::CustomAgentServerSettings::Extension {
100 favorite_config_option_values,
101 ..
102 }
103 | settings::CustomAgentServerSettings::Registry {
104 favorite_config_option_values,
105 ..
106 } => {
107 let entry = favorite_config_option_values
108 .entry(config_id.clone())
109 .or_insert_with(Vec::new);
110
111 if should_be_favorite {
112 if !entry.iter().any(|v| v == &value_id) {
113 entry.push(value_id.clone());
114 }
115 } else {
116 entry.retain(|v| v != &value_id);
117 if entry.is_empty() {
118 favorite_config_option_values.remove(&config_id);
119 }
120 }
121 }
122 }
123 });
124 }
125
126 fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
127 let name = self.name();
128 update_settings_file(fs, cx, move |settings, cx| {
129 let settings = settings
130 .agent_servers
131 .get_or_insert_default()
132 .entry(name.to_string())
133 .or_insert_with(|| default_settings_for_agent(&name, cx));
134
135 match settings {
136 settings::CustomAgentServerSettings::Custom { default_mode, .. }
137 | settings::CustomAgentServerSettings::Extension { default_mode, .. }
138 | settings::CustomAgentServerSettings::Registry { default_mode, .. } => {
139 *default_mode = mode_id.map(|m| m.to_string());
140 }
141 }
142 });
143 }
144
145 fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
146 let settings = cx.read_global(|settings: &SettingsStore, _| {
147 settings
148 .get::<AllAgentServersSettings>(None)
149 .get(self.name().as_ref())
150 .cloned()
151 });
152
153 settings
154 .as_ref()
155 .and_then(|s| s.default_model().map(acp::ModelId::new))
156 }
157
158 fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
159 let name = self.name();
160 update_settings_file(fs, cx, move |settings, cx| {
161 let settings = settings
162 .agent_servers
163 .get_or_insert_default()
164 .entry(name.to_string())
165 .or_insert_with(|| default_settings_for_agent(&name, cx));
166
167 match settings {
168 settings::CustomAgentServerSettings::Custom { default_model, .. }
169 | settings::CustomAgentServerSettings::Extension { default_model, .. }
170 | settings::CustomAgentServerSettings::Registry { default_model, .. } => {
171 *default_model = model_id.map(|m| m.to_string());
172 }
173 }
174 });
175 }
176
177 fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
178 let settings = cx.read_global(|settings: &SettingsStore, _| {
179 settings
180 .get::<AllAgentServersSettings>(None)
181 .get(self.name().as_ref())
182 .cloned()
183 });
184
185 settings
186 .as_ref()
187 .map(|s| {
188 s.favorite_models()
189 .iter()
190 .map(|id| acp::ModelId::new(id.clone()))
191 .collect()
192 })
193 .unwrap_or_default()
194 }
195
196 fn toggle_favorite_model(
197 &self,
198 model_id: acp::ModelId,
199 should_be_favorite: bool,
200 fs: Arc<dyn Fs>,
201 cx: &App,
202 ) {
203 let name = self.name();
204 update_settings_file(fs, cx, move |settings, cx| {
205 let settings = settings
206 .agent_servers
207 .get_or_insert_default()
208 .entry(name.to_string())
209 .or_insert_with(|| default_settings_for_agent(&name, cx));
210
211 let favorite_models = match settings {
212 settings::CustomAgentServerSettings::Custom {
213 favorite_models, ..
214 }
215 | settings::CustomAgentServerSettings::Extension {
216 favorite_models, ..
217 }
218 | settings::CustomAgentServerSettings::Registry {
219 favorite_models, ..
220 } => favorite_models,
221 };
222
223 let model_id_str = model_id.to_string();
224 if should_be_favorite {
225 if !favorite_models.contains(&model_id_str) {
226 favorite_models.push(model_id_str);
227 }
228 } else {
229 favorite_models.retain(|id| id != &model_id_str);
230 }
231 });
232 }
233
234 fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
235 let settings = cx.read_global(|settings: &SettingsStore, _| {
236 settings
237 .get::<AllAgentServersSettings>(None)
238 .get(self.name().as_ref())
239 .cloned()
240 });
241
242 settings
243 .as_ref()
244 .and_then(|s| s.default_config_option(config_id).map(|s| s.to_string()))
245 }
246
247 fn set_default_config_option(
248 &self,
249 config_id: &str,
250 value_id: Option<&str>,
251 fs: Arc<dyn Fs>,
252 cx: &mut App,
253 ) {
254 let name = self.name();
255 let config_id = config_id.to_string();
256 let value_id = value_id.map(|s| s.to_string());
257 update_settings_file(fs, cx, move |settings, cx| {
258 let settings = settings
259 .agent_servers
260 .get_or_insert_default()
261 .entry(name.to_string())
262 .or_insert_with(|| default_settings_for_agent(&name, cx));
263
264 match settings {
265 settings::CustomAgentServerSettings::Custom {
266 default_config_options,
267 ..
268 }
269 | settings::CustomAgentServerSettings::Extension {
270 default_config_options,
271 ..
272 }
273 | settings::CustomAgentServerSettings::Registry {
274 default_config_options,
275 ..
276 } => {
277 if let Some(value) = value_id.clone() {
278 default_config_options.insert(config_id.clone(), value);
279 } else {
280 default_config_options.remove(&config_id);
281 }
282 }
283 }
284 });
285 }
286
287 fn connect(
288 &self,
289 delegate: AgentServerDelegate,
290 cx: &mut App,
291 ) -> Task<Result<Rc<dyn AgentConnection>>> {
292 let name = self.name();
293 let display_name = delegate
294 .store
295 .read(cx)
296 .agent_display_name(&ExternalAgentServerName(name.clone()))
297 .unwrap_or_else(|| name.clone());
298 let default_mode = self.default_mode(cx);
299 let default_model = self.default_model(cx);
300 let is_registry_agent = is_registry_agent(&name, cx);
301 let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
302 settings
303 .get::<AllAgentServersSettings>(None)
304 .get(self.name().as_ref())
305 .map(|s| match s {
306 project::agent_server_store::CustomAgentServerSettings::Custom {
307 default_config_options,
308 ..
309 }
310 | project::agent_server_store::CustomAgentServerSettings::Extension {
311 default_config_options,
312 ..
313 }
314 | project::agent_server_store::CustomAgentServerSettings::Registry {
315 default_config_options,
316 ..
317 } => default_config_options.clone(),
318 })
319 .unwrap_or_default()
320 });
321
322 if is_registry_agent {
323 if let Some(registry_store) = project::AgentRegistryStore::try_global(cx) {
324 registry_store.update(cx, |store, cx| store.refresh_if_stale(cx));
325 }
326 }
327
328 let mut extra_env = load_proxy_env(cx);
329 if delegate.store.read(cx).no_browser() {
330 extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
331 }
332 if is_registry_agent {
333 match name.as_ref() {
334 CLAUDE_AGENT_NAME => {
335 extra_env.insert("ANTHROPIC_API_KEY".into(), "".into());
336 }
337 CODEX_NAME => {
338 if let Ok(api_key) = std::env::var("CODEX_API_KEY") {
339 extra_env.insert("CODEX_API_KEY".into(), api_key);
340 }
341 if let Ok(api_key) = std::env::var("OPEN_AI_API_KEY") {
342 extra_env.insert("OPEN_AI_API_KEY".into(), api_key);
343 }
344 }
345 GEMINI_NAME => {
346 extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
347 }
348 _ => {}
349 }
350 }
351 let store = delegate.store.downgrade();
352 cx.spawn(async move |cx| {
353 if is_registry_agent && name.as_ref() == GEMINI_NAME {
354 if let Some(api_key) = cx.update(api_key_for_gemini_cli).await.ok() {
355 extra_env.insert("GEMINI_API_KEY".into(), api_key);
356 }
357 }
358 let command = store
359 .update(cx, |store, cx| {
360 let agent = store
361 .get_external_agent(&ExternalAgentServerName(name.clone()))
362 .with_context(|| {
363 format!("Custom agent server `{}` is not registered", name)
364 })?;
365 anyhow::Ok(agent.get_command(
366 extra_env,
367 delegate.status_tx,
368 delegate.new_version_available,
369 &mut cx.to_async(),
370 ))
371 })??
372 .await?;
373 let connection = crate::acp::connect(
374 name,
375 display_name,
376 command,
377 default_mode,
378 default_model,
379 default_config_options,
380 cx,
381 )
382 .await?;
383 Ok(connection)
384 })
385 }
386
387 fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
388 self
389 }
390}
391
392fn api_key_for_gemini_cli(cx: &mut App) -> Task<Result<String>> {
393 let env_var = EnvVar::new("GEMINI_API_KEY".into()).or(EnvVar::new("GOOGLE_AI_API_KEY".into()));
394 if let Some(key) = env_var.value {
395 return Task::ready(Ok(key));
396 }
397 let credentials_provider = <dyn CredentialsProvider>::global(cx);
398 let api_url = google_ai::API_URL.to_string();
399 cx.spawn(async move |cx| {
400 Ok(
401 ApiKey::load_from_system_keychain(&api_url, credentials_provider.as_ref(), cx)
402 .await?
403 .key()
404 .to_string(),
405 )
406 })
407}
408
409fn is_registry_agent(name: &str, cx: &App) -> bool {
410 let is_previous_built_in = matches!(name, CLAUDE_AGENT_NAME | CODEX_NAME | GEMINI_NAME);
411 let is_in_registry = project::AgentRegistryStore::try_global(cx)
412 .map(|store| store.read(cx).agent(name).is_some())
413 .unwrap_or(false);
414 let is_settings_registry = cx.read_global(|settings: &SettingsStore, _| {
415 settings
416 .get::<AllAgentServersSettings>(None)
417 .get(name)
418 .is_some_and(|s| {
419 matches!(
420 s,
421 project::agent_server_store::CustomAgentServerSettings::Registry { .. }
422 )
423 })
424 });
425 is_previous_built_in || is_in_registry || is_settings_registry
426}
427
428fn default_settings_for_agent(name: &str, cx: &App) -> settings::CustomAgentServerSettings {
429 if is_registry_agent(name, cx) {
430 settings::CustomAgentServerSettings::Registry {
431 default_model: None,
432 default_mode: None,
433 env: Default::default(),
434 favorite_models: Vec::new(),
435 default_config_options: Default::default(),
436 favorite_config_option_values: Default::default(),
437 }
438 } else {
439 settings::CustomAgentServerSettings::Extension {
440 default_model: None,
441 default_mode: None,
442 env: Default::default(),
443 favorite_models: Vec::new(),
444 default_config_options: Default::default(),
445 favorite_config_option_values: Default::default(),
446 }
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use collections::HashMap;
454 use gpui::TestAppContext;
455 use project::agent_registry_store::{
456 AgentRegistryStore, RegistryAgent, RegistryAgentMetadata, RegistryNpxAgent,
457 };
458 use settings::Settings as _;
459
460 fn init_test(cx: &mut TestAppContext) {
461 cx.update(|cx| {
462 let settings_store = SettingsStore::test(cx);
463 cx.set_global(settings_store);
464 });
465 }
466
467 fn init_registry_with_agents(cx: &mut TestAppContext, agent_ids: &[&str]) {
468 let agents: Vec<RegistryAgent> = agent_ids
469 .iter()
470 .map(|id| {
471 let id = SharedString::from(id.to_string());
472 RegistryAgent::Npx(RegistryNpxAgent {
473 metadata: RegistryAgentMetadata {
474 id: id.clone(),
475 name: id.clone(),
476 description: SharedString::from(""),
477 version: SharedString::from("1.0.0"),
478 repository: None,
479 icon_path: None,
480 },
481 package: id,
482 args: Vec::new(),
483 env: HashMap::default(),
484 })
485 })
486 .collect();
487 cx.update(|cx| {
488 AgentRegistryStore::init_test_global(cx, agents);
489 });
490 }
491
492 fn set_agent_server_settings(
493 cx: &mut TestAppContext,
494 entries: Vec<(&str, settings::CustomAgentServerSettings)>,
495 ) {
496 cx.update(|cx| {
497 AllAgentServersSettings::override_global(
498 project::agent_server_store::AllAgentServersSettings(
499 entries
500 .into_iter()
501 .map(|(name, settings)| (name.to_string(), settings.into()))
502 .collect(),
503 ),
504 cx,
505 );
506 });
507 }
508
509 #[gpui::test]
510 fn test_previous_builtins_are_registry(cx: &mut TestAppContext) {
511 init_test(cx);
512 cx.update(|cx| {
513 assert!(is_registry_agent(CLAUDE_AGENT_NAME, cx));
514 assert!(is_registry_agent(CODEX_NAME, cx));
515 assert!(is_registry_agent(GEMINI_NAME, cx));
516 });
517 }
518
519 #[gpui::test]
520 fn test_unknown_agent_is_not_registry(cx: &mut TestAppContext) {
521 init_test(cx);
522 cx.update(|cx| {
523 assert!(!is_registry_agent("my-custom-agent", cx));
524 });
525 }
526
527 #[gpui::test]
528 fn test_agent_in_registry_store_is_registry(cx: &mut TestAppContext) {
529 init_test(cx);
530 init_registry_with_agents(cx, &["some-new-registry-agent"]);
531 cx.update(|cx| {
532 assert!(is_registry_agent("some-new-registry-agent", cx));
533 assert!(!is_registry_agent("not-in-registry", cx));
534 });
535 }
536
537 #[gpui::test]
538 fn test_agent_with_registry_settings_type_is_registry(cx: &mut TestAppContext) {
539 init_test(cx);
540 set_agent_server_settings(
541 cx,
542 vec![(
543 "agent-from-settings",
544 settings::CustomAgentServerSettings::Registry {
545 env: HashMap::default(),
546 default_mode: None,
547 default_model: None,
548 favorite_models: Vec::new(),
549 default_config_options: HashMap::default(),
550 favorite_config_option_values: HashMap::default(),
551 },
552 )],
553 );
554 cx.update(|cx| {
555 assert!(is_registry_agent("agent-from-settings", cx));
556 });
557 }
558
559 #[gpui::test]
560 fn test_agent_with_extension_settings_type_is_not_registry(cx: &mut TestAppContext) {
561 init_test(cx);
562 set_agent_server_settings(
563 cx,
564 vec![(
565 "my-extension-agent",
566 settings::CustomAgentServerSettings::Extension {
567 env: HashMap::default(),
568 default_mode: None,
569 default_model: None,
570 favorite_models: Vec::new(),
571 default_config_options: HashMap::default(),
572 favorite_config_option_values: HashMap::default(),
573 },
574 )],
575 );
576 cx.update(|cx| {
577 assert!(!is_registry_agent("my-extension-agent", cx));
578 });
579 }
580
581 #[gpui::test]
582 fn test_default_settings_for_builtin_agent(cx: &mut TestAppContext) {
583 init_test(cx);
584 cx.update(|cx| {
585 assert!(matches!(
586 default_settings_for_agent(CODEX_NAME, cx),
587 settings::CustomAgentServerSettings::Registry { .. }
588 ));
589 assert!(matches!(
590 default_settings_for_agent(CLAUDE_AGENT_NAME, cx),
591 settings::CustomAgentServerSettings::Registry { .. }
592 ));
593 assert!(matches!(
594 default_settings_for_agent(GEMINI_NAME, cx),
595 settings::CustomAgentServerSettings::Registry { .. }
596 ));
597 });
598 }
599
600 #[gpui::test]
601 fn test_default_settings_for_extension_agent(cx: &mut TestAppContext) {
602 init_test(cx);
603 cx.update(|cx| {
604 assert!(matches!(
605 default_settings_for_agent("some-extension-agent", cx),
606 settings::CustomAgentServerSettings::Extension { .. }
607 ));
608 });
609 }
610
611 #[gpui::test]
612 fn test_default_settings_for_agent_in_registry(cx: &mut TestAppContext) {
613 init_test(cx);
614 init_registry_with_agents(cx, &["new-registry-agent"]);
615 cx.update(|cx| {
616 assert!(matches!(
617 default_settings_for_agent("new-registry-agent", cx),
618 settings::CustomAgentServerSettings::Registry { .. }
619 ));
620 assert!(matches!(
621 default_settings_for_agent("not-in-registry", cx),
622 settings::CustomAgentServerSettings::Extension { .. }
623 ));
624 });
625 }
626}