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