1use remote::Interactive;
2use std::{
3 any::Any,
4 path::{Path, PathBuf},
5 sync::Arc,
6 time::Duration,
7};
8
9use anyhow::{Context as _, Result, bail};
10use collections::HashMap;
11use fs::Fs;
12use gpui::{AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task};
13use http_client::{HttpClient, github::AssetKind};
14use node_runtime::NodeRuntime;
15use remote::RemoteClient;
16use rpc::{
17 AnyProtoClient, TypedEnvelope,
18 proto::{self, ExternalExtensionAgent},
19};
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use settings::{RegisterSetting, SettingsStore};
23use sha2::{Digest, Sha256};
24use task::Shell;
25use util::{ResultExt as _, debug_panic};
26
27use crate::ProjectEnvironment;
28use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig};
29
30#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
31pub struct AgentServerCommand {
32 #[serde(rename = "command")]
33 pub path: PathBuf,
34 #[serde(default)]
35 pub args: Vec<String>,
36 pub env: Option<HashMap<String, String>>,
37}
38
39impl std::fmt::Debug for AgentServerCommand {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 let filtered_env = self.env.as_ref().map(|env| {
42 env.iter()
43 .map(|(k, v)| {
44 (
45 k,
46 if util::redact::should_redact(k) {
47 "[REDACTED]"
48 } else {
49 v
50 },
51 )
52 })
53 .collect::<Vec<_>>()
54 });
55
56 f.debug_struct("AgentServerCommand")
57 .field("path", &self.path)
58 .field("args", &self.args)
59 .field("env", &filtered_env)
60 .finish()
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub struct ExternalAgentServerName(pub SharedString);
66
67impl std::fmt::Display for ExternalAgentServerName {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73impl From<&'static str> for ExternalAgentServerName {
74 fn from(value: &'static str) -> Self {
75 ExternalAgentServerName(value.into())
76 }
77}
78
79impl From<ExternalAgentServerName> for SharedString {
80 fn from(value: ExternalAgentServerName) -> Self {
81 value.0
82 }
83}
84
85impl std::borrow::Borrow<str> for ExternalAgentServerName {
86 fn borrow(&self) -> &str {
87 &self.0
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum ExternalAgentSource {
93 #[default]
94 Custom,
95 Extension,
96 Registry,
97}
98
99pub trait ExternalAgentServer {
100 fn get_command(
101 &mut self,
102 extra_env: HashMap<String, String>,
103 new_version_available_tx: Option<watch::Sender<Option<String>>>,
104 cx: &mut AsyncApp,
105 ) -> Task<Result<AgentServerCommand>>;
106
107 fn as_any_mut(&mut self) -> &mut dyn Any;
108}
109
110impl dyn ExternalAgentServer {
111 fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
112 self.as_any_mut().downcast_mut()
113 }
114}
115
116enum AgentServerStoreState {
117 Local {
118 node_runtime: NodeRuntime,
119 fs: Arc<dyn Fs>,
120 project_environment: Entity<ProjectEnvironment>,
121 downstream_client: Option<(u64, AnyProtoClient)>,
122 settings: Option<AllAgentServersSettings>,
123 http_client: Arc<dyn HttpClient>,
124 extension_agents: Vec<(
125 Arc<str>,
126 String,
127 HashMap<String, extension::TargetConfig>,
128 HashMap<String, String>,
129 Option<String>,
130 Option<SharedString>,
131 )>,
132 _subscriptions: Vec<Subscription>,
133 },
134 Remote {
135 project_id: u64,
136 upstream_client: Entity<RemoteClient>,
137 },
138 Collab,
139}
140
141pub struct ExternalAgentEntry {
142 server: Box<dyn ExternalAgentServer>,
143 icon: Option<SharedString>,
144 display_name: Option<SharedString>,
145 pub source: ExternalAgentSource,
146}
147
148impl ExternalAgentEntry {
149 pub fn new(
150 server: Box<dyn ExternalAgentServer>,
151 source: ExternalAgentSource,
152 icon: Option<SharedString>,
153 display_name: Option<SharedString>,
154 ) -> Self {
155 Self {
156 server,
157 icon,
158 display_name,
159 source,
160 }
161 }
162}
163
164pub struct AgentServerStore {
165 state: AgentServerStoreState,
166 pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
167}
168
169pub struct AgentServersUpdated;
170
171impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
172
173impl AgentServerStore {
174 /// Synchronizes extension-provided agent servers with the store.
175 pub fn sync_extension_agents<'a, I>(
176 &mut self,
177 manifests: I,
178 extensions_dir: PathBuf,
179 cx: &mut Context<Self>,
180 ) where
181 I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
182 {
183 // Collect manifests first so we can iterate twice
184 let manifests: Vec<_> = manifests.into_iter().collect();
185
186 // Remove all extension-provided agents
187 // (They will be re-added below if they're in the currently installed extensions)
188 self.external_agents
189 .retain(|_, entry| entry.source != ExternalAgentSource::Extension);
190
191 // Insert agent servers from extension manifests
192 match &mut self.state {
193 AgentServerStoreState::Local {
194 extension_agents, ..
195 } => {
196 extension_agents.clear();
197 for (ext_id, manifest) in manifests {
198 for (agent_name, agent_entry) in &manifest.agent_servers {
199 let display_name = SharedString::from(agent_entry.name.clone());
200 let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
201 resolve_extension_icon_path(&extensions_dir, ext_id, icon)
202 });
203
204 extension_agents.push((
205 agent_name.clone(),
206 ext_id.to_owned(),
207 agent_entry.targets.clone(),
208 agent_entry.env.clone(),
209 icon_path,
210 Some(display_name),
211 ));
212 }
213 }
214 self.reregister_agents(cx);
215 }
216 AgentServerStoreState::Remote {
217 project_id,
218 upstream_client,
219 } => {
220 let mut agents = vec![];
221 for (ext_id, manifest) in manifests {
222 for (agent_name, agent_entry) in &manifest.agent_servers {
223 let display_name = SharedString::from(agent_entry.name.clone());
224 let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
225 resolve_extension_icon_path(&extensions_dir, ext_id, icon)
226 });
227 let icon_shared = icon_path
228 .as_ref()
229 .map(|path| SharedString::from(path.clone()));
230 let icon = icon_path;
231 let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
232 self.external_agents
233 .entry(agent_server_name.clone())
234 .and_modify(|entry| {
235 entry.icon = icon_shared.clone();
236 entry.display_name = Some(display_name.clone());
237 entry.source = ExternalAgentSource::Extension;
238 })
239 .or_insert_with(|| {
240 ExternalAgentEntry::new(
241 Box::new(RemoteExternalAgentServer {
242 project_id: *project_id,
243 upstream_client: upstream_client.clone(),
244 name: agent_server_name.clone(),
245 new_version_available_tx: None,
246 })
247 as Box<dyn ExternalAgentServer>,
248 ExternalAgentSource::Extension,
249 icon_shared.clone(),
250 Some(display_name.clone()),
251 )
252 });
253
254 agents.push(ExternalExtensionAgent {
255 name: agent_name.to_string(),
256 icon_path: icon,
257 extension_id: ext_id.to_string(),
258 targets: agent_entry
259 .targets
260 .iter()
261 .map(|(k, v)| (k.clone(), v.to_proto()))
262 .collect(),
263 env: agent_entry
264 .env
265 .iter()
266 .map(|(k, v)| (k.clone(), v.clone()))
267 .collect(),
268 });
269 }
270 }
271 upstream_client
272 .read(cx)
273 .proto_client()
274 .send(proto::ExternalExtensionAgentsUpdated {
275 project_id: *project_id,
276 agents,
277 })
278 .log_err();
279 }
280 AgentServerStoreState::Collab => {
281 // Do nothing
282 }
283 }
284
285 cx.emit(AgentServersUpdated);
286 }
287
288 pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
289 self.external_agents
290 .get(name)
291 .and_then(|entry| entry.icon.clone())
292 }
293
294 pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
295 self.external_agents.get(name).map(|entry| entry.source)
296 }
297}
298
299/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
300/// Returns `None` if the path would escape the extension directory (path traversal attack).
301pub fn resolve_extension_icon_path(
302 extensions_dir: &Path,
303 extension_id: &str,
304 icon_relative_path: &str,
305) -> Option<String> {
306 let extension_root = extensions_dir.join(extension_id);
307 let icon_path = extension_root.join(icon_relative_path);
308
309 // Canonicalize both paths to resolve symlinks and normalize the paths.
310 // For the extension root, we need to handle the case where it might be a symlink
311 // (common for dev extensions).
312 let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
313 let canonical_icon_path = match icon_path.canonicalize() {
314 Ok(path) => path,
315 Err(err) => {
316 log::warn!(
317 "Failed to canonicalize icon path for extension '{}': {} (path: {})",
318 extension_id,
319 err,
320 icon_relative_path
321 );
322 return None;
323 }
324 };
325
326 // Verify the resolved icon path is within the extension directory
327 if canonical_icon_path.starts_with(&canonical_extension_root) {
328 Some(canonical_icon_path.to_string_lossy().to_string())
329 } else {
330 log::warn!(
331 "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
332 icon_relative_path,
333 extension_id
334 );
335 None
336 }
337}
338
339impl AgentServerStore {
340 pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
341 self.external_agents
342 .get(name)
343 .and_then(|entry| entry.display_name.clone())
344 }
345
346 pub fn init_remote(session: &AnyProtoClient) {
347 session.add_entity_message_handler(Self::handle_external_agents_updated);
348 session.add_entity_message_handler(Self::handle_new_version_available);
349 }
350
351 pub fn init_headless(session: &AnyProtoClient) {
352 session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
353 session.add_entity_request_handler(Self::handle_get_agent_server_command);
354 }
355
356 fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
357 let AgentServerStoreState::Local {
358 settings: old_settings,
359 ..
360 } = &mut self.state
361 else {
362 debug_panic!(
363 "should not be subscribed to agent server settings changes in non-local project"
364 );
365 return;
366 };
367
368 let new_settings = cx
369 .global::<SettingsStore>()
370 .get::<AllAgentServersSettings>(None)
371 .clone();
372 if Some(&new_settings) == old_settings.as_ref() {
373 return;
374 }
375
376 self.reregister_agents(cx);
377 }
378
379 fn reregister_agents(&mut self, cx: &mut Context<Self>) {
380 let AgentServerStoreState::Local {
381 node_runtime,
382 fs,
383 project_environment,
384 downstream_client,
385 settings: old_settings,
386 http_client,
387 extension_agents,
388 ..
389 } = &mut self.state
390 else {
391 debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");
392
393 return;
394 };
395
396 let new_settings = cx
397 .global::<SettingsStore>()
398 .get::<AllAgentServersSettings>(None)
399 .clone();
400
401 // If we don't have agents from the registry loaded yet, trigger a
402 // refresh, which will cause this function to be called again
403 let registry_store = AgentRegistryStore::try_global(cx);
404 if new_settings.has_registry_agents()
405 && let Some(registry) = registry_store.as_ref()
406 {
407 registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
408 }
409
410 let registry_agents_by_id = registry_store
411 .as_ref()
412 .map(|store| {
413 store
414 .read(cx)
415 .agents()
416 .iter()
417 .cloned()
418 .map(|agent| (agent.id().to_string(), agent))
419 .collect::<HashMap<_, _>>()
420 })
421 .unwrap_or_default();
422
423 self.external_agents.clear();
424
425 // Insert extension agents before custom/registry so registry entries override extensions.
426 for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
427 let name = ExternalAgentServerName(agent_name.clone().into());
428 let mut env = env.clone();
429 if let Some(settings_env) =
430 new_settings
431 .get(agent_name.as_ref())
432 .and_then(|settings| match settings {
433 CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
434 _ => None,
435 })
436 {
437 env.extend(settings_env);
438 }
439 let icon = icon_path
440 .as_ref()
441 .map(|path| SharedString::from(path.clone()));
442
443 self.external_agents.insert(
444 name.clone(),
445 ExternalAgentEntry::new(
446 Box::new(LocalExtensionArchiveAgent {
447 fs: fs.clone(),
448 http_client: http_client.clone(),
449 node_runtime: node_runtime.clone(),
450 project_environment: project_environment.clone(),
451 extension_id: Arc::from(&**ext_id),
452 targets: targets.clone(),
453 env,
454 agent_id: agent_name.clone(),
455 }) as Box<dyn ExternalAgentServer>,
456 ExternalAgentSource::Extension,
457 icon,
458 display_name.clone(),
459 ),
460 );
461 }
462
463 for (name, settings) in new_settings.iter() {
464 match settings {
465 CustomAgentServerSettings::Custom { command, .. } => {
466 let agent_name = ExternalAgentServerName(name.clone().into());
467 self.external_agents.insert(
468 agent_name.clone(),
469 ExternalAgentEntry::new(
470 Box::new(LocalCustomAgent {
471 command: command.clone(),
472 project_environment: project_environment.clone(),
473 }) as Box<dyn ExternalAgentServer>,
474 ExternalAgentSource::Custom,
475 None,
476 None,
477 ),
478 );
479 }
480 CustomAgentServerSettings::Registry { env, .. } => {
481 let Some(agent) = registry_agents_by_id.get(name) else {
482 if registry_store.is_some() {
483 log::debug!("Registry agent '{}' not found in ACP registry", name);
484 }
485 continue;
486 };
487
488 let agent_name = ExternalAgentServerName(name.clone().into());
489 match agent {
490 RegistryAgent::Binary(agent) => {
491 if !agent.supports_current_platform {
492 log::warn!(
493 "Registry agent '{}' has no compatible binary for this platform",
494 name
495 );
496 continue;
497 }
498
499 self.external_agents.insert(
500 agent_name.clone(),
501 ExternalAgentEntry::new(
502 Box::new(LocalRegistryArchiveAgent {
503 fs: fs.clone(),
504 http_client: http_client.clone(),
505 node_runtime: node_runtime.clone(),
506 project_environment: project_environment.clone(),
507 registry_id: Arc::from(name.as_str()),
508 targets: agent.targets.clone(),
509 env: env.clone(),
510 })
511 as Box<dyn ExternalAgentServer>,
512 ExternalAgentSource::Registry,
513 agent.metadata.icon_path.clone(),
514 Some(agent.metadata.name.clone()),
515 ),
516 );
517 }
518 RegistryAgent::Npx(agent) => {
519 self.external_agents.insert(
520 agent_name.clone(),
521 ExternalAgentEntry::new(
522 Box::new(LocalRegistryNpxAgent {
523 node_runtime: node_runtime.clone(),
524 project_environment: project_environment.clone(),
525 package: agent.package.clone(),
526 args: agent.args.clone(),
527 distribution_env: agent.env.clone(),
528 settings_env: env.clone(),
529 })
530 as Box<dyn ExternalAgentServer>,
531 ExternalAgentSource::Registry,
532 agent.metadata.icon_path.clone(),
533 Some(agent.metadata.name.clone()),
534 ),
535 );
536 }
537 }
538 }
539 CustomAgentServerSettings::Extension { .. } => {}
540 }
541 }
542
543 *old_settings = Some(new_settings);
544
545 if let Some((project_id, downstream_client)) = downstream_client {
546 downstream_client
547 .send(proto::ExternalAgentsUpdated {
548 project_id: *project_id,
549 names: self
550 .external_agents
551 .keys()
552 .map(|name| name.to_string())
553 .collect(),
554 })
555 .log_err();
556 }
557 cx.emit(AgentServersUpdated);
558 }
559
560 pub fn node_runtime(&self) -> Option<NodeRuntime> {
561 match &self.state {
562 AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
563 _ => None,
564 }
565 }
566
567 pub fn local(
568 node_runtime: NodeRuntime,
569 fs: Arc<dyn Fs>,
570 project_environment: Entity<ProjectEnvironment>,
571 http_client: Arc<dyn HttpClient>,
572 cx: &mut Context<Self>,
573 ) -> Self {
574 let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
575 this.agent_servers_settings_changed(cx);
576 })];
577 if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
578 subscriptions.push(cx.observe(®istry_store, |this, _, cx| {
579 this.reregister_agents(cx);
580 }));
581 }
582 let mut this = Self {
583 state: AgentServerStoreState::Local {
584 node_runtime,
585 fs,
586 project_environment,
587 http_client,
588 downstream_client: None,
589 settings: None,
590 extension_agents: vec![],
591 _subscriptions: subscriptions,
592 },
593 external_agents: HashMap::default(),
594 };
595 if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
596 this.agent_servers_settings_changed(cx);
597 this
598 }
599
600 pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
601 Self {
602 state: AgentServerStoreState::Remote {
603 project_id,
604 upstream_client,
605 },
606 external_agents: HashMap::default(),
607 }
608 }
609
610 pub fn collab() -> Self {
611 Self {
612 state: AgentServerStoreState::Collab,
613 external_agents: HashMap::default(),
614 }
615 }
616
617 pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
618 match &mut self.state {
619 AgentServerStoreState::Local {
620 downstream_client, ..
621 } => {
622 *downstream_client = Some((project_id, client.clone()));
623 // Send the current list of external agents downstream, but only after a delay,
624 // to avoid having the message arrive before the downstream project's agent server store
625 // sets up its handlers.
626 cx.spawn(async move |this, cx| {
627 cx.background_executor().timer(Duration::from_secs(1)).await;
628 let names = this.update(cx, |this, _| {
629 this.external_agents()
630 .map(|name| name.to_string())
631 .collect()
632 })?;
633 client
634 .send(proto::ExternalAgentsUpdated { project_id, names })
635 .log_err();
636 anyhow::Ok(())
637 })
638 .detach();
639 }
640 AgentServerStoreState::Remote { .. } => {
641 debug_panic!(
642 "external agents over collab not implemented, remote project should not be shared"
643 );
644 }
645 AgentServerStoreState::Collab => {
646 debug_panic!("external agents over collab not implemented, should not be shared");
647 }
648 }
649 }
650
651 pub fn get_external_agent(
652 &mut self,
653 name: &ExternalAgentServerName,
654 ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
655 self.external_agents
656 .get_mut(name)
657 .map(|entry| entry.server.as_mut())
658 }
659
660 pub fn no_browser(&self) -> bool {
661 match &self.state {
662 AgentServerStoreState::Local {
663 downstream_client, ..
664 } => downstream_client
665 .as_ref()
666 .is_some_and(|(_, client)| !client.has_wsl_interop()),
667 _ => false,
668 }
669 }
670
671 pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
672 self.external_agents.keys()
673 }
674
675 async fn handle_get_agent_server_command(
676 this: Entity<Self>,
677 envelope: TypedEnvelope<proto::GetAgentServerCommand>,
678 mut cx: AsyncApp,
679 ) -> Result<proto::AgentServerCommand> {
680 let command = this
681 .update(&mut cx, |this, cx| {
682 let AgentServerStoreState::Local {
683 downstream_client, ..
684 } = &this.state
685 else {
686 debug_panic!("should not receive GetAgentServerCommand in a non-local project");
687 bail!("unexpected GetAgentServerCommand request in a non-local project");
688 };
689 let no_browser = this.no_browser();
690 let agent = this
691 .external_agents
692 .get_mut(&*envelope.payload.name)
693 .map(|entry| entry.server.as_mut())
694 .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
695 let new_version_available_tx =
696 downstream_client
697 .clone()
698 .map(|(project_id, downstream_client)| {
699 let (new_version_available_tx, mut new_version_available_rx) =
700 watch::channel(None);
701 cx.spawn({
702 let name = envelope.payload.name.clone();
703 async move |_, _| {
704 if let Some(version) =
705 new_version_available_rx.recv().await.ok().flatten()
706 {
707 downstream_client.send(
708 proto::NewExternalAgentVersionAvailable {
709 project_id,
710 name: name.clone(),
711 version,
712 },
713 )?;
714 }
715 anyhow::Ok(())
716 }
717 })
718 .detach_and_log_err(cx);
719 new_version_available_tx
720 });
721 let mut extra_env = HashMap::default();
722 if no_browser {
723 extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
724 }
725 anyhow::Ok(agent.get_command(
726 extra_env,
727 new_version_available_tx,
728 &mut cx.to_async(),
729 ))
730 })?
731 .await?;
732 Ok(proto::AgentServerCommand {
733 path: command.path.to_string_lossy().into_owned(),
734 args: command.args,
735 env: command
736 .env
737 .map(|env| env.into_iter().collect())
738 .unwrap_or_default(),
739 // root_dir and login are no longer used, but returned for backwards compatibility
740 root_dir: paths::home_dir().to_string_lossy().to_string(),
741 login: None,
742 })
743 }
744
745 async fn handle_external_agents_updated(
746 this: Entity<Self>,
747 envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
748 mut cx: AsyncApp,
749 ) -> Result<()> {
750 this.update(&mut cx, |this, cx| {
751 let AgentServerStoreState::Remote {
752 project_id,
753 upstream_client,
754 } = &this.state
755 else {
756 debug_panic!(
757 "handle_external_agents_updated should not be called for a non-remote project"
758 );
759 bail!("unexpected ExternalAgentsUpdated message")
760 };
761
762 let mut previous_entries = std::mem::take(&mut this.external_agents);
763 let mut new_version_available_txs = HashMap::default();
764 let mut metadata = HashMap::default();
765
766 for (name, mut entry) in previous_entries.drain() {
767 if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
768 new_version_available_txs
769 .insert(name.clone(), agent.new_version_available_tx.take());
770 }
771
772 metadata.insert(name, (entry.icon, entry.display_name, entry.source));
773 }
774
775 this.external_agents = envelope
776 .payload
777 .names
778 .into_iter()
779 .map(|name| {
780 let agent_name = ExternalAgentServerName(name.into());
781 let (icon, display_name, source) = metadata
782 .remove(&agent_name)
783 .or_else(|| {
784 AgentRegistryStore::try_global(cx)
785 .and_then(|store| store.read(cx).agent(&agent_name.0))
786 .map(|s| {
787 (
788 s.icon_path().cloned(),
789 Some(s.name().clone()),
790 ExternalAgentSource::Registry,
791 )
792 })
793 })
794 .unwrap_or((None, None, ExternalAgentSource::default()));
795 let agent = RemoteExternalAgentServer {
796 project_id: *project_id,
797 upstream_client: upstream_client.clone(),
798 name: agent_name.clone(),
799 new_version_available_tx: new_version_available_txs
800 .remove(&agent_name)
801 .flatten(),
802 };
803 (
804 agent_name,
805 ExternalAgentEntry::new(
806 Box::new(agent) as Box<dyn ExternalAgentServer>,
807 source,
808 icon,
809 display_name,
810 ),
811 )
812 })
813 .collect();
814 cx.emit(AgentServersUpdated);
815 Ok(())
816 })
817 }
818
819 async fn handle_external_extension_agents_updated(
820 this: Entity<Self>,
821 envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
822 mut cx: AsyncApp,
823 ) -> Result<()> {
824 this.update(&mut cx, |this, cx| {
825 let AgentServerStoreState::Local {
826 extension_agents, ..
827 } = &mut this.state
828 else {
829 panic!(
830 "handle_external_extension_agents_updated \
831 should not be called for a non-remote project"
832 );
833 };
834
835 for ExternalExtensionAgent {
836 name,
837 icon_path,
838 extension_id,
839 targets,
840 env,
841 } in envelope.payload.agents
842 {
843 extension_agents.push((
844 Arc::from(&*name),
845 extension_id,
846 targets
847 .into_iter()
848 .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
849 .collect(),
850 env.into_iter().collect(),
851 icon_path,
852 None,
853 ));
854 }
855
856 this.reregister_agents(cx);
857 cx.emit(AgentServersUpdated);
858 Ok(())
859 })
860 }
861
862 async fn handle_new_version_available(
863 this: Entity<Self>,
864 envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
865 mut cx: AsyncApp,
866 ) -> Result<()> {
867 this.update(&mut cx, |this, _| {
868 if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
869 && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
870 && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
871 {
872 new_version_available_tx
873 .send(Some(envelope.payload.version))
874 .ok();
875 }
876 });
877 Ok(())
878 }
879
880 pub fn get_extension_id_for_agent(
881 &mut self,
882 name: &ExternalAgentServerName,
883 ) -> Option<Arc<str>> {
884 self.external_agents.get_mut(name).and_then(|entry| {
885 entry
886 .server
887 .as_any_mut()
888 .downcast_ref::<LocalExtensionArchiveAgent>()
889 .map(|ext_agent| ext_agent.extension_id.clone())
890 })
891 }
892}
893
894struct RemoteExternalAgentServer {
895 project_id: u64,
896 upstream_client: Entity<RemoteClient>,
897 name: ExternalAgentServerName,
898 new_version_available_tx: Option<watch::Sender<Option<String>>>,
899}
900
901impl ExternalAgentServer for RemoteExternalAgentServer {
902 fn get_command(
903 &mut self,
904 extra_env: HashMap<String, String>,
905 new_version_available_tx: Option<watch::Sender<Option<String>>>,
906 cx: &mut AsyncApp,
907 ) -> Task<Result<AgentServerCommand>> {
908 let project_id = self.project_id;
909 let name = self.name.to_string();
910 let upstream_client = self.upstream_client.downgrade();
911 self.new_version_available_tx = new_version_available_tx;
912 cx.spawn(async move |cx| {
913 let mut response = upstream_client
914 .update(cx, |upstream_client, _| {
915 upstream_client
916 .proto_client()
917 .request(proto::GetAgentServerCommand {
918 project_id,
919 name,
920 root_dir: None,
921 })
922 })?
923 .await?;
924 let root_dir = response.root_dir;
925 response.env.extend(extra_env);
926 let command = upstream_client.update(cx, |client, _| {
927 client.build_command_with_options(
928 Some(response.path),
929 &response.args,
930 &response.env.into_iter().collect(),
931 Some(root_dir.clone()),
932 None,
933 Interactive::No,
934 )
935 })??;
936 Ok(AgentServerCommand {
937 path: command.program.into(),
938 args: command.args,
939 env: Some(command.env),
940 })
941 })
942 }
943
944 fn as_any_mut(&mut self) -> &mut dyn Any {
945 self
946 }
947}
948
949pub struct LocalExtensionArchiveAgent {
950 pub fs: Arc<dyn Fs>,
951 pub http_client: Arc<dyn HttpClient>,
952 pub node_runtime: NodeRuntime,
953 pub project_environment: Entity<ProjectEnvironment>,
954 pub extension_id: Arc<str>,
955 pub agent_id: Arc<str>,
956 pub targets: HashMap<String, extension::TargetConfig>,
957 pub env: HashMap<String, String>,
958}
959
960impl ExternalAgentServer for LocalExtensionArchiveAgent {
961 fn get_command(
962 &mut self,
963 extra_env: HashMap<String, String>,
964 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
965 cx: &mut AsyncApp,
966 ) -> Task<Result<AgentServerCommand>> {
967 let fs = self.fs.clone();
968 let http_client = self.http_client.clone();
969 let node_runtime = self.node_runtime.clone();
970 let project_environment = self.project_environment.downgrade();
971 let extension_id = self.extension_id.clone();
972 let agent_id = self.agent_id.clone();
973 let targets = self.targets.clone();
974 let base_env = self.env.clone();
975
976 cx.spawn(async move |cx| {
977 // Get project environment
978 let mut env = project_environment
979 .update(cx, |project_environment, cx| {
980 project_environment.local_directory_environment(
981 &Shell::System,
982 paths::home_dir().as_path().into(),
983 cx,
984 )
985 })?
986 .await
987 .unwrap_or_default();
988
989 // Merge manifest env and extra env
990 env.extend(base_env);
991 env.extend(extra_env);
992
993 let cache_key = format!("{}/{}", extension_id, agent_id);
994 let dir = paths::external_agents_dir().join(&cache_key);
995 fs.create_dir(&dir).await?;
996
997 // Determine platform key
998 let os = if cfg!(target_os = "macos") {
999 "darwin"
1000 } else if cfg!(target_os = "linux") {
1001 "linux"
1002 } else if cfg!(target_os = "windows") {
1003 "windows"
1004 } else {
1005 anyhow::bail!("unsupported OS");
1006 };
1007
1008 let arch = if cfg!(target_arch = "aarch64") {
1009 "aarch64"
1010 } else if cfg!(target_arch = "x86_64") {
1011 "x86_64"
1012 } else {
1013 anyhow::bail!("unsupported architecture");
1014 };
1015
1016 let platform_key = format!("{}-{}", os, arch);
1017 let target_config = targets.get(&platform_key).with_context(|| {
1018 format!(
1019 "no target specified for platform '{}'. Available platforms: {}",
1020 platform_key,
1021 targets
1022 .keys()
1023 .map(|k| k.as_str())
1024 .collect::<Vec<_>>()
1025 .join(", ")
1026 )
1027 })?;
1028
1029 let archive_url = &target_config.archive;
1030
1031 // Use URL as version identifier for caching
1032 // Hash the URL to get a stable directory name
1033 let mut hasher = Sha256::new();
1034 hasher.update(archive_url.as_bytes());
1035 let url_hash = format!("{:x}", hasher.finalize());
1036 let version_dir = dir.join(format!("v_{}", url_hash));
1037
1038 if !fs.is_dir(&version_dir).await {
1039 // Determine SHA256 for verification
1040 let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1041 // Use provided SHA256
1042 Some(provided_sha.clone())
1043 } else if archive_url.starts_with("https://github.com/") {
1044 // Try to fetch SHA256 from GitHub API
1045 // Parse URL to extract repo and tag/file info
1046 // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1047 if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1048 let parts: Vec<&str> = caps.split('/').collect();
1049 if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1050 let repo = format!("{}/{}", parts[0], parts[1]);
1051 let tag = parts[4];
1052 let filename = parts[5..].join("/");
1053
1054 // Try to get release info from GitHub
1055 if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1056 &repo,
1057 tag,
1058 http_client.clone(),
1059 )
1060 .await
1061 {
1062 // Find matching asset
1063 if let Some(asset) =
1064 release.assets.iter().find(|a| a.name == filename)
1065 {
1066 // Strip "sha256:" prefix if present
1067 asset.digest.as_ref().map(|d| {
1068 d.strip_prefix("sha256:")
1069 .map(|s| s.to_string())
1070 .unwrap_or_else(|| d.clone())
1071 })
1072 } else {
1073 None
1074 }
1075 } else {
1076 None
1077 }
1078 } else {
1079 None
1080 }
1081 } else {
1082 None
1083 }
1084 } else {
1085 None
1086 };
1087
1088 // Determine archive type from URL
1089 let asset_kind = if archive_url.ends_with(".zip") {
1090 AssetKind::Zip
1091 } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1092 AssetKind::TarGz
1093 } else {
1094 anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1095 };
1096
1097 // Download and extract
1098 ::http_client::github_download::download_server_binary(
1099 &*http_client,
1100 archive_url,
1101 sha256.as_deref(),
1102 &version_dir,
1103 asset_kind,
1104 )
1105 .await?;
1106 }
1107
1108 // Validate and resolve cmd path
1109 let cmd = &target_config.cmd;
1110
1111 let cmd_path = if cmd == "node" {
1112 // Use Zed's managed Node.js runtime
1113 node_runtime.binary_path().await?
1114 } else {
1115 if cmd.contains("..") {
1116 anyhow::bail!("command path cannot contain '..': {}", cmd);
1117 }
1118
1119 if cmd.starts_with("./") || cmd.starts_with(".\\") {
1120 // Relative to extraction directory
1121 let cmd_path = version_dir.join(&cmd[2..]);
1122 anyhow::ensure!(
1123 fs.is_file(&cmd_path).await,
1124 "Missing command {} after extraction",
1125 cmd_path.to_string_lossy()
1126 );
1127 cmd_path
1128 } else {
1129 // On PATH
1130 anyhow::bail!("command must be relative (start with './'): {}", cmd);
1131 }
1132 };
1133
1134 let command = AgentServerCommand {
1135 path: cmd_path,
1136 args: target_config.args.clone(),
1137 env: Some(env),
1138 };
1139
1140 Ok(command)
1141 })
1142 }
1143
1144 fn as_any_mut(&mut self) -> &mut dyn Any {
1145 self
1146 }
1147}
1148
1149struct LocalRegistryArchiveAgent {
1150 fs: Arc<dyn Fs>,
1151 http_client: Arc<dyn HttpClient>,
1152 node_runtime: NodeRuntime,
1153 project_environment: Entity<ProjectEnvironment>,
1154 registry_id: Arc<str>,
1155 targets: HashMap<String, RegistryTargetConfig>,
1156 env: HashMap<String, String>,
1157}
1158
1159impl ExternalAgentServer for LocalRegistryArchiveAgent {
1160 fn get_command(
1161 &mut self,
1162 extra_env: HashMap<String, String>,
1163 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1164 cx: &mut AsyncApp,
1165 ) -> Task<Result<AgentServerCommand>> {
1166 let fs = self.fs.clone();
1167 let http_client = self.http_client.clone();
1168 let node_runtime = self.node_runtime.clone();
1169 let project_environment = self.project_environment.downgrade();
1170 let registry_id = self.registry_id.clone();
1171 let targets = self.targets.clone();
1172 let settings_env = self.env.clone();
1173
1174 cx.spawn(async move |cx| {
1175 let mut env = project_environment
1176 .update(cx, |project_environment, cx| {
1177 project_environment.local_directory_environment(
1178 &Shell::System,
1179 paths::home_dir().as_path().into(),
1180 cx,
1181 )
1182 })?
1183 .await
1184 .unwrap_or_default();
1185
1186 let dir = paths::external_agents_dir()
1187 .join("registry")
1188 .join(registry_id.as_ref());
1189 fs.create_dir(&dir).await?;
1190
1191 let os = if cfg!(target_os = "macos") {
1192 "darwin"
1193 } else if cfg!(target_os = "linux") {
1194 "linux"
1195 } else if cfg!(target_os = "windows") {
1196 "windows"
1197 } else {
1198 anyhow::bail!("unsupported OS");
1199 };
1200
1201 let arch = if cfg!(target_arch = "aarch64") {
1202 "aarch64"
1203 } else if cfg!(target_arch = "x86_64") {
1204 "x86_64"
1205 } else {
1206 anyhow::bail!("unsupported architecture");
1207 };
1208
1209 let platform_key = format!("{}-{}", os, arch);
1210 let target_config = targets.get(&platform_key).with_context(|| {
1211 format!(
1212 "no target specified for platform '{}'. Available platforms: {}",
1213 platform_key,
1214 targets
1215 .keys()
1216 .map(|k| k.as_str())
1217 .collect::<Vec<_>>()
1218 .join(", ")
1219 )
1220 })?;
1221
1222 env.extend(target_config.env.clone());
1223 env.extend(extra_env);
1224 env.extend(settings_env);
1225
1226 let archive_url = &target_config.archive;
1227
1228 let mut hasher = Sha256::new();
1229 hasher.update(archive_url.as_bytes());
1230 let url_hash = format!("{:x}", hasher.finalize());
1231 let version_dir = dir.join(format!("v_{}", url_hash));
1232
1233 if !fs.is_dir(&version_dir).await {
1234 let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1235 Some(provided_sha.clone())
1236 } else if archive_url.starts_with("https://github.com/") {
1237 if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1238 let parts: Vec<&str> = caps.split('/').collect();
1239 if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1240 let repo = format!("{}/{}", parts[0], parts[1]);
1241 let tag = parts[4];
1242 let filename = parts[5..].join("/");
1243
1244 if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1245 &repo,
1246 tag,
1247 http_client.clone(),
1248 )
1249 .await
1250 {
1251 if let Some(asset) =
1252 release.assets.iter().find(|a| a.name == filename)
1253 {
1254 asset.digest.as_ref().and_then(|d| {
1255 d.strip_prefix("sha256:")
1256 .map(|s| s.to_string())
1257 .or_else(|| Some(d.clone()))
1258 })
1259 } else {
1260 None
1261 }
1262 } else {
1263 None
1264 }
1265 } else {
1266 None
1267 }
1268 } else {
1269 None
1270 }
1271 } else {
1272 None
1273 };
1274
1275 let asset_kind = if archive_url.ends_with(".zip") {
1276 AssetKind::Zip
1277 } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1278 AssetKind::TarGz
1279 } else {
1280 anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1281 };
1282
1283 ::http_client::github_download::download_server_binary(
1284 &*http_client,
1285 archive_url,
1286 sha256.as_deref(),
1287 &version_dir,
1288 asset_kind,
1289 )
1290 .await?;
1291 }
1292
1293 let cmd = &target_config.cmd;
1294
1295 let cmd_path = if cmd == "node" {
1296 node_runtime.binary_path().await?
1297 } else {
1298 if cmd.contains("..") {
1299 anyhow::bail!("command path cannot contain '..': {}", cmd);
1300 }
1301
1302 if cmd.starts_with("./") || cmd.starts_with(".\\") {
1303 let cmd_path = version_dir.join(&cmd[2..]);
1304 anyhow::ensure!(
1305 fs.is_file(&cmd_path).await,
1306 "Missing command {} after extraction",
1307 cmd_path.to_string_lossy()
1308 );
1309 cmd_path
1310 } else {
1311 anyhow::bail!("command must be relative (start with './'): {}", cmd);
1312 }
1313 };
1314
1315 let command = AgentServerCommand {
1316 path: cmd_path,
1317 args: target_config.args.clone(),
1318 env: Some(env),
1319 };
1320
1321 Ok(command)
1322 })
1323 }
1324
1325 fn as_any_mut(&mut self) -> &mut dyn Any {
1326 self
1327 }
1328}
1329
1330struct LocalRegistryNpxAgent {
1331 node_runtime: NodeRuntime,
1332 project_environment: Entity<ProjectEnvironment>,
1333 package: SharedString,
1334 args: Vec<String>,
1335 distribution_env: HashMap<String, String>,
1336 settings_env: HashMap<String, String>,
1337}
1338
1339impl ExternalAgentServer for LocalRegistryNpxAgent {
1340 fn get_command(
1341 &mut self,
1342 extra_env: HashMap<String, String>,
1343 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1344 cx: &mut AsyncApp,
1345 ) -> Task<Result<AgentServerCommand>> {
1346 let node_runtime = self.node_runtime.clone();
1347 let project_environment = self.project_environment.downgrade();
1348 let package = self.package.clone();
1349 let args = self.args.clone();
1350 let distribution_env = self.distribution_env.clone();
1351 let settings_env = self.settings_env.clone();
1352
1353 cx.spawn(async move |cx| {
1354 let mut env = project_environment
1355 .update(cx, |project_environment, cx| {
1356 project_environment.local_directory_environment(
1357 &Shell::System,
1358 paths::home_dir().as_path().into(),
1359 cx,
1360 )
1361 })?
1362 .await
1363 .unwrap_or_default();
1364
1365 let mut exec_args = Vec::new();
1366 exec_args.push("--yes".to_string());
1367 exec_args.push(package.to_string());
1368 if !args.is_empty() {
1369 exec_args.push("--".to_string());
1370 exec_args.extend(args);
1371 }
1372
1373 let npm_command = node_runtime
1374 .npm_command(
1375 "exec",
1376 &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1377 )
1378 .await?;
1379
1380 env.extend(npm_command.env);
1381 env.extend(distribution_env);
1382 env.extend(extra_env);
1383 env.extend(settings_env);
1384
1385 let command = AgentServerCommand {
1386 path: npm_command.path,
1387 args: npm_command.args,
1388 env: Some(env),
1389 };
1390
1391 Ok(command)
1392 })
1393 }
1394
1395 fn as_any_mut(&mut self) -> &mut dyn Any {
1396 self
1397 }
1398}
1399
1400struct LocalCustomAgent {
1401 project_environment: Entity<ProjectEnvironment>,
1402 command: AgentServerCommand,
1403}
1404
1405impl ExternalAgentServer for LocalCustomAgent {
1406 fn get_command(
1407 &mut self,
1408 extra_env: HashMap<String, String>,
1409 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1410 cx: &mut AsyncApp,
1411 ) -> Task<Result<AgentServerCommand>> {
1412 let mut command = self.command.clone();
1413 let project_environment = self.project_environment.downgrade();
1414 cx.spawn(async move |cx| {
1415 let mut env = project_environment
1416 .update(cx, |project_environment, cx| {
1417 project_environment.local_directory_environment(
1418 &Shell::System,
1419 paths::home_dir().as_path().into(),
1420 cx,
1421 )
1422 })?
1423 .await
1424 .unwrap_or_default();
1425 env.extend(command.env.unwrap_or_default());
1426 env.extend(extra_env);
1427 command.env = Some(env);
1428 Ok(command)
1429 })
1430 }
1431
1432 fn as_any_mut(&mut self) -> &mut dyn Any {
1433 self
1434 }
1435}
1436
1437pub const GEMINI_NAME: &str = "gemini";
1438pub const CLAUDE_AGENT_NAME: &str = "claude-acp";
1439pub const CODEX_NAME: &str = "codex-acp";
1440
1441#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1442pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1443
1444impl std::ops::Deref for AllAgentServersSettings {
1445 type Target = HashMap<String, CustomAgentServerSettings>;
1446
1447 fn deref(&self) -> &Self::Target {
1448 &self.0
1449 }
1450}
1451
1452impl std::ops::DerefMut for AllAgentServersSettings {
1453 fn deref_mut(&mut self) -> &mut Self::Target {
1454 &mut self.0
1455 }
1456}
1457
1458impl AllAgentServersSettings {
1459 pub fn has_registry_agents(&self) -> bool {
1460 self.values()
1461 .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1462 }
1463}
1464
1465#[derive(Clone, JsonSchema, Debug, PartialEq)]
1466pub enum CustomAgentServerSettings {
1467 Custom {
1468 command: AgentServerCommand,
1469 /// The default mode to use for this agent.
1470 ///
1471 /// Note: Not only all agents support modes.
1472 ///
1473 /// Default: None
1474 default_mode: Option<String>,
1475 /// The default model to use for this agent.
1476 ///
1477 /// This should be the model ID as reported by the agent.
1478 ///
1479 /// Default: None
1480 default_model: Option<String>,
1481 /// The favorite models for this agent.
1482 ///
1483 /// Default: []
1484 favorite_models: Vec<String>,
1485 /// Default values for session config options.
1486 ///
1487 /// This is a map from config option ID to value ID.
1488 ///
1489 /// Default: {}
1490 default_config_options: HashMap<String, String>,
1491 /// Favorited values for session config options.
1492 ///
1493 /// This is a map from config option ID to a list of favorited value IDs.
1494 ///
1495 /// Default: {}
1496 favorite_config_option_values: HashMap<String, Vec<String>>,
1497 },
1498 Extension {
1499 /// Additional environment variables to pass to the agent.
1500 ///
1501 /// Default: {}
1502 env: HashMap<String, String>,
1503 /// The default mode to use for this agent.
1504 ///
1505 /// Note: Not only all agents support modes.
1506 ///
1507 /// Default: None
1508 default_mode: Option<String>,
1509 /// The default model to use for this agent.
1510 ///
1511 /// This should be the model ID as reported by the agent.
1512 ///
1513 /// Default: None
1514 default_model: Option<String>,
1515 /// The favorite models for this agent.
1516 ///
1517 /// Default: []
1518 favorite_models: Vec<String>,
1519 /// Default values for session config options.
1520 ///
1521 /// This is a map from config option ID to value ID.
1522 ///
1523 /// Default: {}
1524 default_config_options: HashMap<String, String>,
1525 /// Favorited values for session config options.
1526 ///
1527 /// This is a map from config option ID to a list of favorited value IDs.
1528 ///
1529 /// Default: {}
1530 favorite_config_option_values: HashMap<String, Vec<String>>,
1531 },
1532 Registry {
1533 /// Additional environment variables to pass to the agent.
1534 ///
1535 /// Default: {}
1536 env: HashMap<String, String>,
1537 /// The default mode to use for this agent.
1538 ///
1539 /// Note: Not only all agents support modes.
1540 ///
1541 /// Default: None
1542 default_mode: Option<String>,
1543 /// The default model to use for this agent.
1544 ///
1545 /// This should be the model ID as reported by the agent.
1546 ///
1547 /// Default: None
1548 default_model: Option<String>,
1549 /// The favorite models for this agent.
1550 ///
1551 /// Default: []
1552 favorite_models: Vec<String>,
1553 /// Default values for session config options.
1554 ///
1555 /// This is a map from config option ID to value ID.
1556 ///
1557 /// Default: {}
1558 default_config_options: HashMap<String, String>,
1559 /// Favorited values for session config options.
1560 ///
1561 /// This is a map from config option ID to a list of favorited value IDs.
1562 ///
1563 /// Default: {}
1564 favorite_config_option_values: HashMap<String, Vec<String>>,
1565 },
1566}
1567
1568impl CustomAgentServerSettings {
1569 pub fn command(&self) -> Option<&AgentServerCommand> {
1570 match self {
1571 CustomAgentServerSettings::Custom { command, .. } => Some(command),
1572 CustomAgentServerSettings::Extension { .. }
1573 | CustomAgentServerSettings::Registry { .. } => None,
1574 }
1575 }
1576
1577 pub fn default_mode(&self) -> Option<&str> {
1578 match self {
1579 CustomAgentServerSettings::Custom { default_mode, .. }
1580 | CustomAgentServerSettings::Extension { default_mode, .. }
1581 | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1582 }
1583 }
1584
1585 pub fn default_model(&self) -> Option<&str> {
1586 match self {
1587 CustomAgentServerSettings::Custom { default_model, .. }
1588 | CustomAgentServerSettings::Extension { default_model, .. }
1589 | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1590 }
1591 }
1592
1593 pub fn favorite_models(&self) -> &[String] {
1594 match self {
1595 CustomAgentServerSettings::Custom {
1596 favorite_models, ..
1597 }
1598 | CustomAgentServerSettings::Extension {
1599 favorite_models, ..
1600 }
1601 | CustomAgentServerSettings::Registry {
1602 favorite_models, ..
1603 } => favorite_models,
1604 }
1605 }
1606
1607 pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1608 match self {
1609 CustomAgentServerSettings::Custom {
1610 default_config_options,
1611 ..
1612 }
1613 | CustomAgentServerSettings::Extension {
1614 default_config_options,
1615 ..
1616 }
1617 | CustomAgentServerSettings::Registry {
1618 default_config_options,
1619 ..
1620 } => default_config_options.get(config_id).map(|s| s.as_str()),
1621 }
1622 }
1623
1624 pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1625 match self {
1626 CustomAgentServerSettings::Custom {
1627 favorite_config_option_values,
1628 ..
1629 }
1630 | CustomAgentServerSettings::Extension {
1631 favorite_config_option_values,
1632 ..
1633 }
1634 | CustomAgentServerSettings::Registry {
1635 favorite_config_option_values,
1636 ..
1637 } => favorite_config_option_values
1638 .get(config_id)
1639 .map(|v| v.as_slice()),
1640 }
1641 }
1642}
1643
1644impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1645 fn from(value: settings::CustomAgentServerSettings) -> Self {
1646 match value {
1647 settings::CustomAgentServerSettings::Custom {
1648 path,
1649 args,
1650 env,
1651 default_mode,
1652 default_model,
1653 favorite_models,
1654 default_config_options,
1655 favorite_config_option_values,
1656 } => CustomAgentServerSettings::Custom {
1657 command: AgentServerCommand {
1658 path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1659 args,
1660 env: Some(env),
1661 },
1662 default_mode,
1663 default_model,
1664 favorite_models,
1665 default_config_options,
1666 favorite_config_option_values,
1667 },
1668 settings::CustomAgentServerSettings::Extension {
1669 env,
1670 default_mode,
1671 default_model,
1672 default_config_options,
1673 favorite_models,
1674 favorite_config_option_values,
1675 } => CustomAgentServerSettings::Extension {
1676 env,
1677 default_mode,
1678 default_model,
1679 default_config_options,
1680 favorite_models,
1681 favorite_config_option_values,
1682 },
1683 settings::CustomAgentServerSettings::Registry {
1684 env,
1685 default_mode,
1686 default_model,
1687 default_config_options,
1688 favorite_models,
1689 favorite_config_option_values,
1690 } => CustomAgentServerSettings::Registry {
1691 env,
1692 default_mode,
1693 default_model,
1694 default_config_options,
1695 favorite_models,
1696 favorite_config_option_values,
1697 },
1698 }
1699 }
1700}
1701
1702impl settings::Settings for AllAgentServersSettings {
1703 fn from_settings(content: &settings::SettingsContent) -> Self {
1704 let agent_settings = content.agent_servers.clone().unwrap();
1705 Self(
1706 agent_settings
1707 .0
1708 .into_iter()
1709 .map(|(k, v)| (k, v.into()))
1710 .collect(),
1711 )
1712 }
1713}