agent_server_store.rs

   1use remote::Interactive;
   2use std::{
   3    any::Any,
   4    borrow::Borrow,
   5    path::{Path, PathBuf},
   6    str::FromStr as _,
   7    sync::Arc,
   8    time::Duration,
   9};
  10
  11use anyhow::{Context as _, Result, bail};
  12use collections::HashMap;
  13use fs::{Fs, RemoveOptions, RenameOptions};
  14use futures::StreamExt as _;
  15use gpui::{
  16    AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
  17};
  18use http_client::{HttpClient, github::AssetKind};
  19use node_runtime::NodeRuntime;
  20use remote::RemoteClient;
  21use rpc::{
  22    AnyProtoClient, TypedEnvelope,
  23    proto::{self, ExternalExtensionAgent},
  24};
  25use schemars::JsonSchema;
  26use semver::Version;
  27use serde::{Deserialize, Serialize};
  28use settings::{RegisterSetting, SettingsStore};
  29use task::{Shell, SpawnInTerminal};
  30use util::{ResultExt as _, debug_panic};
  31
  32use crate::ProjectEnvironment;
  33use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig};
  34
  35#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
  36pub struct AgentServerCommand {
  37    #[serde(rename = "command")]
  38    pub path: PathBuf,
  39    #[serde(default)]
  40    pub args: Vec<String>,
  41    pub env: Option<HashMap<String, String>>,
  42}
  43
  44impl std::fmt::Debug for AgentServerCommand {
  45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  46        let filtered_env = self.env.as_ref().map(|env| {
  47            env.iter()
  48                .map(|(k, v)| {
  49                    (
  50                        k,
  51                        if util::redact::should_redact(k) {
  52                            "[REDACTED]"
  53                        } else {
  54                            v
  55                        },
  56                    )
  57                })
  58                .collect::<Vec<_>>()
  59        });
  60
  61        f.debug_struct("AgentServerCommand")
  62            .field("path", &self.path)
  63            .field("args", &self.args)
  64            .field("env", &filtered_env)
  65            .finish()
  66    }
  67}
  68
  69#[derive(Clone, Debug, PartialEq, Eq, Hash)]
  70pub struct ExternalAgentServerName(pub SharedString);
  71
  72impl std::fmt::Display for ExternalAgentServerName {
  73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  74        write!(f, "{}", self.0)
  75    }
  76}
  77
  78impl From<&'static str> for ExternalAgentServerName {
  79    fn from(value: &'static str) -> Self {
  80        ExternalAgentServerName(value.into())
  81    }
  82}
  83
  84impl From<ExternalAgentServerName> for SharedString {
  85    fn from(value: ExternalAgentServerName) -> Self {
  86        value.0
  87    }
  88}
  89
  90impl Borrow<str> for ExternalAgentServerName {
  91    fn borrow(&self) -> &str {
  92        &self.0
  93    }
  94}
  95
  96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
  97pub enum ExternalAgentSource {
  98    Builtin,
  99    #[default]
 100    Custom,
 101    Extension,
 102    Registry,
 103}
 104
 105pub trait ExternalAgentServer {
 106    fn get_command(
 107        &mut self,
 108        extra_env: HashMap<String, String>,
 109        status_tx: Option<watch::Sender<SharedString>>,
 110        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 111        cx: &mut AsyncApp,
 112    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>>;
 113
 114    fn as_any_mut(&mut self) -> &mut dyn Any;
 115}
 116
 117impl dyn ExternalAgentServer {
 118    fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
 119        self.as_any_mut().downcast_mut()
 120    }
 121}
 122
 123enum AgentServerStoreState {
 124    Local {
 125        node_runtime: NodeRuntime,
 126        fs: Arc<dyn Fs>,
 127        project_environment: Entity<ProjectEnvironment>,
 128        downstream_client: Option<(u64, AnyProtoClient)>,
 129        settings: Option<AllAgentServersSettings>,
 130        http_client: Arc<dyn HttpClient>,
 131        extension_agents: Vec<(
 132            Arc<str>,
 133            String,
 134            HashMap<String, extension::TargetConfig>,
 135            HashMap<String, String>,
 136            Option<String>,
 137            Option<SharedString>,
 138        )>,
 139        _subscriptions: Vec<Subscription>,
 140    },
 141    Remote {
 142        project_id: u64,
 143        upstream_client: Entity<RemoteClient>,
 144    },
 145    Collab,
 146}
 147
 148pub struct ExternalAgentEntry {
 149    server: Box<dyn ExternalAgentServer>,
 150    icon: Option<SharedString>,
 151    display_name: Option<SharedString>,
 152    pub source: ExternalAgentSource,
 153}
 154
 155impl ExternalAgentEntry {
 156    pub fn new(
 157        server: Box<dyn ExternalAgentServer>,
 158        source: ExternalAgentSource,
 159        icon: Option<SharedString>,
 160        display_name: Option<SharedString>,
 161    ) -> Self {
 162        Self {
 163            server,
 164            icon,
 165            display_name,
 166            source,
 167        }
 168    }
 169}
 170
 171pub struct AgentServerStore {
 172    state: AgentServerStoreState,
 173    pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
 174}
 175
 176pub struct AgentServersUpdated;
 177
 178impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
 179
 180impl AgentServerStore {
 181    /// Synchronizes extension-provided agent servers with the store.
 182    pub fn sync_extension_agents<'a, I>(
 183        &mut self,
 184        manifests: I,
 185        extensions_dir: PathBuf,
 186        cx: &mut Context<Self>,
 187    ) where
 188        I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
 189    {
 190        // Collect manifests first so we can iterate twice
 191        let manifests: Vec<_> = manifests.into_iter().collect();
 192
 193        // Remove all extension-provided agents
 194        // (They will be re-added below if they're in the currently installed extensions)
 195        self.external_agents
 196            .retain(|_, entry| entry.source != ExternalAgentSource::Extension);
 197
 198        // Insert agent servers from extension manifests
 199        match &mut self.state {
 200            AgentServerStoreState::Local {
 201                extension_agents, ..
 202            } => {
 203                extension_agents.clear();
 204                for (ext_id, manifest) in manifests {
 205                    for (agent_name, agent_entry) in &manifest.agent_servers {
 206                        let display_name = SharedString::from(agent_entry.name.clone());
 207                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 208                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 209                        });
 210
 211                        extension_agents.push((
 212                            agent_name.clone(),
 213                            ext_id.to_owned(),
 214                            agent_entry.targets.clone(),
 215                            agent_entry.env.clone(),
 216                            icon_path,
 217                            Some(display_name),
 218                        ));
 219                    }
 220                }
 221                self.reregister_agents(cx);
 222            }
 223            AgentServerStoreState::Remote {
 224                project_id,
 225                upstream_client,
 226            } => {
 227                let mut agents = vec![];
 228                for (ext_id, manifest) in manifests {
 229                    for (agent_name, agent_entry) in &manifest.agent_servers {
 230                        let display_name = SharedString::from(agent_entry.name.clone());
 231                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 232                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 233                        });
 234                        let icon_shared = icon_path
 235                            .as_ref()
 236                            .map(|path| SharedString::from(path.clone()));
 237                        let icon = icon_path;
 238                        let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
 239                        self.external_agents
 240                            .entry(agent_server_name.clone())
 241                            .and_modify(|entry| {
 242                                entry.icon = icon_shared.clone();
 243                                entry.display_name = Some(display_name.clone());
 244                                entry.source = ExternalAgentSource::Extension;
 245                            })
 246                            .or_insert_with(|| {
 247                                ExternalAgentEntry::new(
 248                                    Box::new(RemoteExternalAgentServer {
 249                                        project_id: *project_id,
 250                                        upstream_client: upstream_client.clone(),
 251                                        name: agent_server_name.clone(),
 252                                        status_tx: None,
 253                                        new_version_available_tx: None,
 254                                    })
 255                                        as Box<dyn ExternalAgentServer>,
 256                                    ExternalAgentSource::Extension,
 257                                    icon_shared.clone(),
 258                                    Some(display_name.clone()),
 259                                )
 260                            });
 261
 262                        agents.push(ExternalExtensionAgent {
 263                            name: agent_name.to_string(),
 264                            icon_path: icon,
 265                            extension_id: ext_id.to_string(),
 266                            targets: agent_entry
 267                                .targets
 268                                .iter()
 269                                .map(|(k, v)| (k.clone(), v.to_proto()))
 270                                .collect(),
 271                            env: agent_entry
 272                                .env
 273                                .iter()
 274                                .map(|(k, v)| (k.clone(), v.clone()))
 275                                .collect(),
 276                        });
 277                    }
 278                }
 279                upstream_client
 280                    .read(cx)
 281                    .proto_client()
 282                    .send(proto::ExternalExtensionAgentsUpdated {
 283                        project_id: *project_id,
 284                        agents,
 285                    })
 286                    .log_err();
 287            }
 288            AgentServerStoreState::Collab => {
 289                // Do nothing
 290            }
 291        }
 292
 293        cx.emit(AgentServersUpdated);
 294    }
 295
 296    pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 297        self.external_agents
 298            .get(name)
 299            .and_then(|entry| entry.icon.clone())
 300    }
 301
 302    pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
 303        self.external_agents.get(name).map(|entry| entry.source)
 304    }
 305}
 306
 307/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
 308/// Returns `None` if the path would escape the extension directory (path traversal attack).
 309pub fn resolve_extension_icon_path(
 310    extensions_dir: &Path,
 311    extension_id: &str,
 312    icon_relative_path: &str,
 313) -> Option<String> {
 314    let extension_root = extensions_dir.join(extension_id);
 315    let icon_path = extension_root.join(icon_relative_path);
 316
 317    // Canonicalize both paths to resolve symlinks and normalize the paths.
 318    // For the extension root, we need to handle the case where it might be a symlink
 319    // (common for dev extensions).
 320    let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
 321    let canonical_icon_path = match icon_path.canonicalize() {
 322        Ok(path) => path,
 323        Err(err) => {
 324            log::warn!(
 325                "Failed to canonicalize icon path for extension '{}': {} (path: {})",
 326                extension_id,
 327                err,
 328                icon_relative_path
 329            );
 330            return None;
 331        }
 332    };
 333
 334    // Verify the resolved icon path is within the extension directory
 335    if canonical_icon_path.starts_with(&canonical_extension_root) {
 336        Some(canonical_icon_path.to_string_lossy().to_string())
 337    } else {
 338        log::warn!(
 339            "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
 340            icon_relative_path,
 341            extension_id
 342        );
 343        None
 344    }
 345}
 346
 347impl AgentServerStore {
 348    pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 349        self.external_agents
 350            .get(name)
 351            .and_then(|entry| entry.display_name.clone())
 352    }
 353
 354    pub fn init_remote(session: &AnyProtoClient) {
 355        session.add_entity_message_handler(Self::handle_external_agents_updated);
 356        session.add_entity_message_handler(Self::handle_loading_status_updated);
 357        session.add_entity_message_handler(Self::handle_new_version_available);
 358    }
 359
 360    pub fn init_headless(session: &AnyProtoClient) {
 361        session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
 362        session.add_entity_request_handler(Self::handle_get_agent_server_command);
 363    }
 364
 365    fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
 366        let AgentServerStoreState::Local {
 367            settings: old_settings,
 368            ..
 369        } = &mut self.state
 370        else {
 371            debug_panic!(
 372                "should not be subscribed to agent server settings changes in non-local project"
 373            );
 374            return;
 375        };
 376
 377        let new_settings = cx
 378            .global::<SettingsStore>()
 379            .get::<AllAgentServersSettings>(None)
 380            .clone();
 381        if Some(&new_settings) == old_settings.as_ref() {
 382            return;
 383        }
 384
 385        self.reregister_agents(cx);
 386    }
 387
 388    fn reregister_agents(&mut self, cx: &mut Context<Self>) {
 389        let AgentServerStoreState::Local {
 390            node_runtime,
 391            fs,
 392            project_environment,
 393            downstream_client,
 394            settings: old_settings,
 395            http_client,
 396            extension_agents,
 397            ..
 398        } = &mut self.state
 399        else {
 400            debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");
 401
 402            return;
 403        };
 404
 405        let new_settings = cx
 406            .global::<SettingsStore>()
 407            .get::<AllAgentServersSettings>(None)
 408            .clone();
 409
 410        // If we don't have agents from the registry loaded yet, trigger a
 411        // refresh, which will cause this function to be called again
 412        if new_settings.has_registry_agents()
 413            && let Some(registry) = AgentRegistryStore::try_global(cx)
 414        {
 415            registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
 416        }
 417
 418        self.external_agents.clear();
 419        self.external_agents.insert(
 420            GEMINI_NAME.into(),
 421            ExternalAgentEntry::new(
 422                Box::new(LocalGemini {
 423                    fs: fs.clone(),
 424                    node_runtime: node_runtime.clone(),
 425                    project_environment: project_environment.clone(),
 426                    custom_command: new_settings
 427                        .gemini
 428                        .clone()
 429                        .and_then(|settings| settings.custom_command()),
 430                    settings_env: new_settings
 431                        .gemini
 432                        .as_ref()
 433                        .and_then(|settings| settings.env.clone()),
 434                    ignore_system_version: new_settings
 435                        .gemini
 436                        .as_ref()
 437                        .and_then(|settings| settings.ignore_system_version)
 438                        .unwrap_or(true),
 439                }),
 440                ExternalAgentSource::Builtin,
 441                None,
 442                None,
 443            ),
 444        );
 445        self.external_agents.insert(
 446            CODEX_NAME.into(),
 447            ExternalAgentEntry::new(
 448                Box::new(LocalCodex {
 449                    fs: fs.clone(),
 450                    project_environment: project_environment.clone(),
 451                    custom_command: new_settings
 452                        .codex
 453                        .clone()
 454                        .and_then(|settings| settings.custom_command()),
 455                    settings_env: new_settings
 456                        .codex
 457                        .as_ref()
 458                        .and_then(|settings| settings.env.clone()),
 459                    http_client: http_client.clone(),
 460                    no_browser: downstream_client
 461                        .as_ref()
 462                        .is_some_and(|(_, client)| !client.has_wsl_interop()),
 463                }),
 464                ExternalAgentSource::Builtin,
 465                None,
 466                None,
 467            ),
 468        );
 469        self.external_agents.insert(
 470            CLAUDE_AGENT_NAME.into(),
 471            ExternalAgentEntry::new(
 472                Box::new(LocalClaudeCode {
 473                    fs: fs.clone(),
 474                    node_runtime: node_runtime.clone(),
 475                    project_environment: project_environment.clone(),
 476                    custom_command: new_settings
 477                        .claude
 478                        .clone()
 479                        .and_then(|settings| settings.custom_command()),
 480                    settings_env: new_settings
 481                        .claude
 482                        .as_ref()
 483                        .and_then(|settings| settings.env.clone()),
 484                }),
 485                ExternalAgentSource::Builtin,
 486                None,
 487                None,
 488            ),
 489        );
 490
 491        let registry_store = AgentRegistryStore::try_global(cx);
 492        let registry_agents_by_id = registry_store
 493            .as_ref()
 494            .map(|store| {
 495                store
 496                    .read(cx)
 497                    .agents()
 498                    .iter()
 499                    .cloned()
 500                    .map(|agent| (agent.id().to_string(), agent))
 501                    .collect::<HashMap<_, _>>()
 502            })
 503            .unwrap_or_default();
 504
 505        // Insert extension agents before custom/registry so registry entries override extensions.
 506        for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
 507            let name = ExternalAgentServerName(agent_name.clone().into());
 508            let mut env = env.clone();
 509            if let Some(settings_env) =
 510                new_settings
 511                    .custom
 512                    .get(agent_name.as_ref())
 513                    .and_then(|settings| match settings {
 514                        CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
 515                        _ => None,
 516                    })
 517            {
 518                env.extend(settings_env);
 519            }
 520            let icon = icon_path
 521                .as_ref()
 522                .map(|path| SharedString::from(path.clone()));
 523
 524            self.external_agents.insert(
 525                name.clone(),
 526                ExternalAgentEntry::new(
 527                    Box::new(LocalExtensionArchiveAgent {
 528                        fs: fs.clone(),
 529                        http_client: http_client.clone(),
 530                        node_runtime: node_runtime.clone(),
 531                        project_environment: project_environment.clone(),
 532                        extension_id: Arc::from(&**ext_id),
 533                        targets: targets.clone(),
 534                        env,
 535                        agent_id: agent_name.clone(),
 536                    }) as Box<dyn ExternalAgentServer>,
 537                    ExternalAgentSource::Extension,
 538                    icon,
 539                    display_name.clone(),
 540                ),
 541            );
 542        }
 543
 544        for (name, settings) in &new_settings.custom {
 545            match settings {
 546                CustomAgentServerSettings::Custom { command, .. } => {
 547                    let agent_name = ExternalAgentServerName(name.clone().into());
 548                    self.external_agents.insert(
 549                        agent_name.clone(),
 550                        ExternalAgentEntry::new(
 551                            Box::new(LocalCustomAgent {
 552                                command: command.clone(),
 553                                project_environment: project_environment.clone(),
 554                            }) as Box<dyn ExternalAgentServer>,
 555                            ExternalAgentSource::Custom,
 556                            None,
 557                            None,
 558                        ),
 559                    );
 560                }
 561                CustomAgentServerSettings::Registry { env, .. } => {
 562                    let Some(agent) = registry_agents_by_id.get(name) else {
 563                        if registry_store.is_some() {
 564                            log::debug!("Registry agent '{}' not found in ACP registry", name);
 565                        }
 566                        continue;
 567                    };
 568
 569                    let agent_name = ExternalAgentServerName(name.clone().into());
 570                    match agent {
 571                        RegistryAgent::Binary(agent) => {
 572                            if !agent.supports_current_platform {
 573                                log::warn!(
 574                                    "Registry agent '{}' has no compatible binary for this platform",
 575                                    name
 576                                );
 577                                continue;
 578                            }
 579
 580                            self.external_agents.insert(
 581                                agent_name.clone(),
 582                                ExternalAgentEntry::new(
 583                                    Box::new(LocalRegistryArchiveAgent {
 584                                        fs: fs.clone(),
 585                                        http_client: http_client.clone(),
 586                                        node_runtime: node_runtime.clone(),
 587                                        project_environment: project_environment.clone(),
 588                                        registry_id: Arc::from(name.as_str()),
 589                                        targets: agent.targets.clone(),
 590                                        env: env.clone(),
 591                                    })
 592                                        as Box<dyn ExternalAgentServer>,
 593                                    ExternalAgentSource::Registry,
 594                                    agent.metadata.icon_path.clone(),
 595                                    Some(agent.metadata.name.clone()),
 596                                ),
 597                            );
 598                        }
 599                        RegistryAgent::Npx(agent) => {
 600                            self.external_agents.insert(
 601                                agent_name.clone(),
 602                                ExternalAgentEntry::new(
 603                                    Box::new(LocalRegistryNpxAgent {
 604                                        node_runtime: node_runtime.clone(),
 605                                        project_environment: project_environment.clone(),
 606                                        package: agent.package.clone(),
 607                                        args: agent.args.clone(),
 608                                        distribution_env: agent.env.clone(),
 609                                        settings_env: env.clone(),
 610                                    })
 611                                        as Box<dyn ExternalAgentServer>,
 612                                    ExternalAgentSource::Registry,
 613                                    agent.metadata.icon_path.clone(),
 614                                    Some(agent.metadata.name.clone()),
 615                                ),
 616                            );
 617                        }
 618                    }
 619                }
 620                CustomAgentServerSettings::Extension { .. } => {}
 621            }
 622        }
 623
 624        *old_settings = Some(new_settings);
 625
 626        if let Some((project_id, downstream_client)) = downstream_client {
 627            downstream_client
 628                .send(proto::ExternalAgentsUpdated {
 629                    project_id: *project_id,
 630                    names: self
 631                        .external_agents
 632                        .keys()
 633                        .map(|name| name.to_string())
 634                        .collect(),
 635                })
 636                .log_err();
 637        }
 638        cx.emit(AgentServersUpdated);
 639    }
 640
 641    pub fn node_runtime(&self) -> Option<NodeRuntime> {
 642        match &self.state {
 643            AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
 644            _ => None,
 645        }
 646    }
 647
 648    pub fn local(
 649        node_runtime: NodeRuntime,
 650        fs: Arc<dyn Fs>,
 651        project_environment: Entity<ProjectEnvironment>,
 652        http_client: Arc<dyn HttpClient>,
 653        cx: &mut Context<Self>,
 654    ) -> Self {
 655        let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
 656            this.agent_servers_settings_changed(cx);
 657        })];
 658        if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
 659            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
 660                this.reregister_agents(cx);
 661            }));
 662        }
 663        let mut this = Self {
 664            state: AgentServerStoreState::Local {
 665                node_runtime,
 666                fs,
 667                project_environment,
 668                http_client,
 669                downstream_client: None,
 670                settings: None,
 671                extension_agents: vec![],
 672                _subscriptions: subscriptions,
 673            },
 674            external_agents: Default::default(),
 675        };
 676        if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
 677        this.agent_servers_settings_changed(cx);
 678        this
 679    }
 680
 681    pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
 682        // Set up the builtin agents here so they're immediately available in
 683        // remote projects--we know that the HeadlessProject on the other end
 684        // will have them.
 685        let external_agents: [(ExternalAgentServerName, ExternalAgentEntry); 3] = [
 686            (
 687                CLAUDE_AGENT_NAME.into(),
 688                ExternalAgentEntry::new(
 689                    Box::new(RemoteExternalAgentServer {
 690                        project_id,
 691                        upstream_client: upstream_client.clone(),
 692                        name: CLAUDE_AGENT_NAME.into(),
 693                        status_tx: None,
 694                        new_version_available_tx: None,
 695                    }) as Box<dyn ExternalAgentServer>,
 696                    ExternalAgentSource::Builtin,
 697                    None,
 698                    None,
 699                ),
 700            ),
 701            (
 702                CODEX_NAME.into(),
 703                ExternalAgentEntry::new(
 704                    Box::new(RemoteExternalAgentServer {
 705                        project_id,
 706                        upstream_client: upstream_client.clone(),
 707                        name: CODEX_NAME.into(),
 708                        status_tx: None,
 709                        new_version_available_tx: None,
 710                    }) as Box<dyn ExternalAgentServer>,
 711                    ExternalAgentSource::Builtin,
 712                    None,
 713                    None,
 714                ),
 715            ),
 716            (
 717                GEMINI_NAME.into(),
 718                ExternalAgentEntry::new(
 719                    Box::new(RemoteExternalAgentServer {
 720                        project_id,
 721                        upstream_client: upstream_client.clone(),
 722                        name: GEMINI_NAME.into(),
 723                        status_tx: None,
 724                        new_version_available_tx: None,
 725                    }) as Box<dyn ExternalAgentServer>,
 726                    ExternalAgentSource::Builtin,
 727                    None,
 728                    None,
 729                ),
 730            ),
 731        ];
 732
 733        Self {
 734            state: AgentServerStoreState::Remote {
 735                project_id,
 736                upstream_client,
 737            },
 738            external_agents: external_agents.into_iter().collect(),
 739        }
 740    }
 741
 742    pub fn collab() -> Self {
 743        Self {
 744            state: AgentServerStoreState::Collab,
 745            external_agents: Default::default(),
 746        }
 747    }
 748
 749    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 750        match &mut self.state {
 751            AgentServerStoreState::Local {
 752                downstream_client, ..
 753            } => {
 754                *downstream_client = Some((project_id, client.clone()));
 755                // Send the current list of external agents downstream, but only after a delay,
 756                // to avoid having the message arrive before the downstream project's agent server store
 757                // sets up its handlers.
 758                cx.spawn(async move |this, cx| {
 759                    cx.background_executor().timer(Duration::from_secs(1)).await;
 760                    let names = this.update(cx, |this, _| {
 761                        this.external_agents()
 762                            .map(|name| name.to_string())
 763                            .collect()
 764                    })?;
 765                    client
 766                        .send(proto::ExternalAgentsUpdated { project_id, names })
 767                        .log_err();
 768                    anyhow::Ok(())
 769                })
 770                .detach();
 771            }
 772            AgentServerStoreState::Remote { .. } => {
 773                debug_panic!(
 774                    "external agents over collab not implemented, remote project should not be shared"
 775                );
 776            }
 777            AgentServerStoreState::Collab => {
 778                debug_panic!("external agents over collab not implemented, should not be shared");
 779            }
 780        }
 781    }
 782
 783    pub fn get_external_agent(
 784        &mut self,
 785        name: &ExternalAgentServerName,
 786    ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
 787        self.external_agents
 788            .get_mut(name)
 789            .map(|entry| entry.server.as_mut())
 790    }
 791
 792    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
 793        self.external_agents.keys()
 794    }
 795
 796    async fn handle_get_agent_server_command(
 797        this: Entity<Self>,
 798        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 799        mut cx: AsyncApp,
 800    ) -> Result<proto::AgentServerCommand> {
 801        let (command, login_command) = this
 802            .update(&mut cx, |this, cx| {
 803                let AgentServerStoreState::Local {
 804                    downstream_client, ..
 805                } = &this.state
 806                else {
 807                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 808                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 809                };
 810                let agent = this
 811                    .external_agents
 812                    .get_mut(&*envelope.payload.name)
 813                    .map(|entry| entry.server.as_mut())
 814                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 815                let (status_tx, new_version_available_tx) = downstream_client
 816                    .clone()
 817                    .map(|(project_id, downstream_client)| {
 818                        let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
 819                        let (new_version_available_tx, mut new_version_available_rx) =
 820                            watch::channel(None);
 821                        cx.spawn({
 822                            let downstream_client = downstream_client.clone();
 823                            let name = envelope.payload.name.clone();
 824                            async move |_, _| {
 825                                while let Some(status) = status_rx.recv().await.ok() {
 826                                    downstream_client.send(
 827                                        proto::ExternalAgentLoadingStatusUpdated {
 828                                            project_id,
 829                                            name: name.clone(),
 830                                            status: status.to_string(),
 831                                        },
 832                                    )?;
 833                                }
 834                                anyhow::Ok(())
 835                            }
 836                        })
 837                        .detach_and_log_err(cx);
 838                        cx.spawn({
 839                            let name = envelope.payload.name.clone();
 840                            async move |_, _| {
 841                                if let Some(version) =
 842                                    new_version_available_rx.recv().await.ok().flatten()
 843                                {
 844                                    downstream_client.send(
 845                                        proto::NewExternalAgentVersionAvailable {
 846                                            project_id,
 847                                            name: name.clone(),
 848                                            version,
 849                                        },
 850                                    )?;
 851                                }
 852                                anyhow::Ok(())
 853                            }
 854                        })
 855                        .detach_and_log_err(cx);
 856                        (status_tx, new_version_available_tx)
 857                    })
 858                    .unzip();
 859                anyhow::Ok(agent.get_command(
 860                    HashMap::default(),
 861                    status_tx,
 862                    new_version_available_tx,
 863                    &mut cx.to_async(),
 864                ))
 865            })?
 866            .await?;
 867        Ok(proto::AgentServerCommand {
 868            path: command.path.to_string_lossy().into_owned(),
 869            args: command.args,
 870            env: command
 871                .env
 872                .map(|env| env.into_iter().collect())
 873                .unwrap_or_default(),
 874            // This is no longer used, but returned for backwards compatibility
 875            root_dir: paths::home_dir().to_string_lossy().to_string(),
 876            login: login_command.map(|cmd| cmd.to_proto()),
 877        })
 878    }
 879
 880    async fn handle_external_agents_updated(
 881        this: Entity<Self>,
 882        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 883        mut cx: AsyncApp,
 884    ) -> Result<()> {
 885        this.update(&mut cx, |this, cx| {
 886            let AgentServerStoreState::Remote {
 887                project_id,
 888                upstream_client,
 889            } = &this.state
 890            else {
 891                debug_panic!(
 892                    "handle_external_agents_updated should not be called for a non-remote project"
 893                );
 894                bail!("unexpected ExternalAgentsUpdated message")
 895            };
 896
 897            let mut previous_entries = std::mem::take(&mut this.external_agents);
 898            let mut status_txs = HashMap::default();
 899            let mut new_version_available_txs = HashMap::default();
 900            let mut metadata = HashMap::default();
 901
 902            for (name, mut entry) in previous_entries.drain() {
 903                if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
 904                    status_txs.insert(name.clone(), agent.status_tx.take());
 905                    new_version_available_txs
 906                        .insert(name.clone(), agent.new_version_available_tx.take());
 907                }
 908
 909                metadata.insert(name, (entry.icon, entry.display_name, entry.source));
 910            }
 911
 912            this.external_agents = envelope
 913                .payload
 914                .names
 915                .into_iter()
 916                .map(|name| {
 917                    let agent_name = ExternalAgentServerName(name.clone().into());
 918                    let fallback_source =
 919                        if name == GEMINI_NAME || name == CLAUDE_AGENT_NAME || name == CODEX_NAME {
 920                            ExternalAgentSource::Builtin
 921                        } else {
 922                            ExternalAgentSource::Custom
 923                        };
 924                    let (icon, display_name, source) = metadata
 925                        .remove(&agent_name)
 926                        .or_else(|| {
 927                            AgentRegistryStore::try_global(cx)
 928                                .and_then(|store| store.read(cx).agent(&agent_name.0))
 929                                .map(|s| {
 930                                    (
 931                                        s.icon_path().cloned(),
 932                                        Some(s.name().clone()),
 933                                        ExternalAgentSource::Registry,
 934                                    )
 935                                })
 936                        })
 937                        .unwrap_or((None, None, fallback_source));
 938                    let source = if fallback_source == ExternalAgentSource::Builtin {
 939                        ExternalAgentSource::Builtin
 940                    } else {
 941                        source
 942                    };
 943                    let agent = RemoteExternalAgentServer {
 944                        project_id: *project_id,
 945                        upstream_client: upstream_client.clone(),
 946                        name: agent_name.clone(),
 947                        status_tx: status_txs.remove(&agent_name).flatten(),
 948                        new_version_available_tx: new_version_available_txs
 949                            .remove(&agent_name)
 950                            .flatten(),
 951                    };
 952                    (
 953                        agent_name,
 954                        ExternalAgentEntry::new(
 955                            Box::new(agent) as Box<dyn ExternalAgentServer>,
 956                            source,
 957                            icon,
 958                            display_name,
 959                        ),
 960                    )
 961                })
 962                .collect();
 963            cx.emit(AgentServersUpdated);
 964            Ok(())
 965        })
 966    }
 967
 968    async fn handle_external_extension_agents_updated(
 969        this: Entity<Self>,
 970        envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
 971        mut cx: AsyncApp,
 972    ) -> Result<()> {
 973        this.update(&mut cx, |this, cx| {
 974            let AgentServerStoreState::Local {
 975                extension_agents, ..
 976            } = &mut this.state
 977            else {
 978                panic!(
 979                    "handle_external_extension_agents_updated \
 980                    should not be called for a non-remote project"
 981                );
 982            };
 983
 984            for ExternalExtensionAgent {
 985                name,
 986                icon_path,
 987                extension_id,
 988                targets,
 989                env,
 990            } in envelope.payload.agents
 991            {
 992                extension_agents.push((
 993                    Arc::from(&*name),
 994                    extension_id,
 995                    targets
 996                        .into_iter()
 997                        .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
 998                        .collect(),
 999                    env.into_iter().collect(),
1000                    icon_path,
1001                    None,
1002                ));
1003            }
1004
1005            this.reregister_agents(cx);
1006            cx.emit(AgentServersUpdated);
1007            Ok(())
1008        })
1009    }
1010
1011    async fn handle_loading_status_updated(
1012        this: Entity<Self>,
1013        envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
1014        mut cx: AsyncApp,
1015    ) -> Result<()> {
1016        this.update(&mut cx, |this, _| {
1017            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1018                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1019                && let Some(status_tx) = &mut agent.status_tx
1020            {
1021                status_tx.send(envelope.payload.status.into()).ok();
1022            }
1023        });
1024        Ok(())
1025    }
1026
1027    async fn handle_new_version_available(
1028        this: Entity<Self>,
1029        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
1030        mut cx: AsyncApp,
1031    ) -> Result<()> {
1032        this.update(&mut cx, |this, _| {
1033            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1034                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1035                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
1036            {
1037                new_version_available_tx
1038                    .send(Some(envelope.payload.version))
1039                    .ok();
1040            }
1041        });
1042        Ok(())
1043    }
1044
1045    pub fn get_extension_id_for_agent(
1046        &mut self,
1047        name: &ExternalAgentServerName,
1048    ) -> Option<Arc<str>> {
1049        self.external_agents.get_mut(name).and_then(|entry| {
1050            entry
1051                .server
1052                .as_any_mut()
1053                .downcast_ref::<LocalExtensionArchiveAgent>()
1054                .map(|ext_agent| ext_agent.extension_id.clone())
1055        })
1056    }
1057}
1058
1059fn get_or_npm_install_builtin_agent(
1060    binary_name: SharedString,
1061    package_name: SharedString,
1062    entrypoint_path: PathBuf,
1063    minimum_version: Option<semver::Version>,
1064    status_tx: Option<watch::Sender<SharedString>>,
1065    new_version_available: Option<watch::Sender<Option<String>>>,
1066    fs: Arc<dyn Fs>,
1067    node_runtime: NodeRuntime,
1068    cx: &mut AsyncApp,
1069) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
1070    cx.spawn(async move |cx| {
1071        let node_path = node_runtime.binary_path().await?;
1072        let dir = paths::external_agents_dir().join(binary_name.as_str());
1073        fs.create_dir(&dir).await?;
1074
1075        let mut stream = fs.read_dir(&dir).await?;
1076        let mut versions = Vec::new();
1077        let mut to_delete = Vec::new();
1078        while let Some(entry) = stream.next().await {
1079            let Ok(entry) = entry else { continue };
1080            let Some(file_name) = entry.file_name() else {
1081                continue;
1082            };
1083
1084            if let Some(name) = file_name.to_str()
1085                && let Some(version) = semver::Version::from_str(name).ok()
1086                && fs
1087                    .is_file(&dir.join(file_name).join(&entrypoint_path))
1088                    .await
1089            {
1090                versions.push((version, file_name.to_owned()));
1091            } else {
1092                to_delete.push(file_name.to_owned())
1093            }
1094        }
1095
1096        versions.sort();
1097        let newest_version = if let Some((version, _)) = versions.last().cloned()
1098            && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
1099        {
1100            versions.pop()
1101        } else {
1102            None
1103        };
1104        log::debug!("existing version of {package_name}: {newest_version:?}");
1105        to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
1106
1107        cx.background_spawn({
1108            let fs = fs.clone();
1109            let dir = dir.clone();
1110            async move {
1111                for file_name in to_delete {
1112                    fs.remove_dir(
1113                        &dir.join(file_name),
1114                        RemoveOptions {
1115                            recursive: true,
1116                            ignore_if_not_exists: false,
1117                        },
1118                    )
1119                    .await
1120                    .ok();
1121                }
1122            }
1123        })
1124        .detach();
1125
1126        let version = if let Some((version, file_name)) = newest_version {
1127            cx.background_spawn({
1128                let dir = dir.clone();
1129                let fs = fs.clone();
1130                async move {
1131                    let latest_version = node_runtime
1132                        .npm_package_latest_version(&package_name)
1133                        .await
1134                        .ok();
1135                    if let Some(latest_version) = latest_version
1136                        && latest_version != version
1137                    {
1138                        let download_result = download_latest_version(
1139                            fs,
1140                            dir.clone(),
1141                            node_runtime,
1142                            package_name.clone(),
1143                        )
1144                        .await
1145                        .log_err();
1146                        if let Some(mut new_version_available) = new_version_available
1147                            && download_result.is_some()
1148                        {
1149                            new_version_available
1150                                .send(Some(latest_version.to_string()))
1151                                .ok();
1152                        }
1153                    }
1154                }
1155            })
1156            .detach();
1157            file_name
1158        } else {
1159            if let Some(mut status_tx) = status_tx {
1160                status_tx.send("Installing…".into()).ok();
1161            }
1162            let dir = dir.clone();
1163            cx.background_spawn(download_latest_version(
1164                fs.clone(),
1165                dir.clone(),
1166                node_runtime,
1167                package_name.clone(),
1168            ))
1169            .await?
1170            .to_string()
1171            .into()
1172        };
1173
1174        let agent_server_path = dir.join(version).join(entrypoint_path);
1175        let agent_server_path_exists = fs.is_file(&agent_server_path).await;
1176        anyhow::ensure!(
1177            agent_server_path_exists,
1178            "Missing entrypoint path {} after installation",
1179            agent_server_path.to_string_lossy()
1180        );
1181
1182        anyhow::Ok(AgentServerCommand {
1183            path: node_path,
1184            args: vec![agent_server_path.to_string_lossy().into_owned()],
1185            env: None,
1186        })
1187    })
1188}
1189
1190fn find_bin_in_path(
1191    bin_name: SharedString,
1192    root_dir: PathBuf,
1193    env: HashMap<String, String>,
1194    cx: &mut AsyncApp,
1195) -> Task<Option<PathBuf>> {
1196    cx.background_executor().spawn(async move {
1197        let which_result = if cfg!(windows) {
1198            which::which(bin_name.as_str())
1199        } else {
1200            let shell_path = env.get("PATH").cloned();
1201            which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
1202        };
1203
1204        if let Err(which::Error::CannotFindBinaryPath) = which_result {
1205            return None;
1206        }
1207
1208        which_result.log_err()
1209    })
1210}
1211
1212async fn download_latest_version(
1213    fs: Arc<dyn Fs>,
1214    dir: PathBuf,
1215    node_runtime: NodeRuntime,
1216    package_name: SharedString,
1217) -> Result<Version> {
1218    log::debug!("downloading latest version of {package_name}");
1219
1220    let tmp_dir = tempfile::tempdir_in(&dir)?;
1221
1222    node_runtime
1223        .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
1224        .await?;
1225
1226    let version = node_runtime
1227        .npm_package_installed_version(tmp_dir.path(), &package_name)
1228        .await?
1229        .context("expected package to be installed")?;
1230
1231    fs.rename(
1232        &tmp_dir.keep(),
1233        &dir.join(version.to_string()),
1234        RenameOptions {
1235            ignore_if_exists: true,
1236            overwrite: true,
1237            create_parents: false,
1238        },
1239    )
1240    .await?;
1241
1242    anyhow::Ok(version)
1243}
1244
1245struct RemoteExternalAgentServer {
1246    project_id: u64,
1247    upstream_client: Entity<RemoteClient>,
1248    name: ExternalAgentServerName,
1249    status_tx: Option<watch::Sender<SharedString>>,
1250    new_version_available_tx: Option<watch::Sender<Option<String>>>,
1251}
1252
1253impl ExternalAgentServer for RemoteExternalAgentServer {
1254    fn get_command(
1255        &mut self,
1256        extra_env: HashMap<String, String>,
1257        status_tx: Option<watch::Sender<SharedString>>,
1258        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1259        cx: &mut AsyncApp,
1260    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1261        let project_id = self.project_id;
1262        let name = self.name.to_string();
1263        let upstream_client = self.upstream_client.downgrade();
1264        self.status_tx = status_tx;
1265        self.new_version_available_tx = new_version_available_tx;
1266        cx.spawn(async move |cx| {
1267            let mut response = upstream_client
1268                .update(cx, |upstream_client, _| {
1269                    upstream_client
1270                        .proto_client()
1271                        .request(proto::GetAgentServerCommand {
1272                            project_id,
1273                            name,
1274                            root_dir: None,
1275                        })
1276                })?
1277                .await?;
1278            let root_dir = response.root_dir;
1279            response.env.extend(extra_env);
1280            let command = upstream_client.update(cx, |client, _| {
1281                client.build_command_with_options(
1282                    Some(response.path),
1283                    &response.args,
1284                    &response.env.into_iter().collect(),
1285                    Some(root_dir.clone()),
1286                    None,
1287                    Interactive::No,
1288                )
1289            })??;
1290            Ok((
1291                AgentServerCommand {
1292                    path: command.program.into(),
1293                    args: command.args,
1294                    env: Some(command.env),
1295                },
1296                response.login.map(SpawnInTerminal::from_proto),
1297            ))
1298        })
1299    }
1300
1301    fn as_any_mut(&mut self) -> &mut dyn Any {
1302        self
1303    }
1304}
1305
1306struct LocalGemini {
1307    fs: Arc<dyn Fs>,
1308    node_runtime: NodeRuntime,
1309    project_environment: Entity<ProjectEnvironment>,
1310    custom_command: Option<AgentServerCommand>,
1311    settings_env: Option<HashMap<String, String>>,
1312    ignore_system_version: bool,
1313}
1314
1315impl ExternalAgentServer for LocalGemini {
1316    fn get_command(
1317        &mut self,
1318        extra_env: HashMap<String, String>,
1319        status_tx: Option<watch::Sender<SharedString>>,
1320        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1321        cx: &mut AsyncApp,
1322    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1323        let fs = self.fs.clone();
1324        let node_runtime = self.node_runtime.clone();
1325        let project_environment = self.project_environment.downgrade();
1326        let custom_command = self.custom_command.clone();
1327        let settings_env = self.settings_env.clone();
1328        let ignore_system_version = self.ignore_system_version;
1329        let home_dir = paths::home_dir();
1330
1331        cx.spawn(async move |cx| {
1332            let mut env = project_environment
1333                .update(cx, |project_environment, cx| {
1334                    project_environment.local_directory_environment(
1335                        &Shell::System,
1336                        home_dir.as_path().into(),
1337                        cx,
1338                    )
1339                })?
1340                .await
1341                .unwrap_or_default();
1342
1343            env.extend(settings_env.unwrap_or_default());
1344
1345            let mut command = if let Some(mut custom_command) = custom_command {
1346                custom_command.env = Some(env);
1347                custom_command
1348            } else if !ignore_system_version
1349                && let Some(bin) =
1350                    find_bin_in_path("gemini".into(), home_dir.to_path_buf(), env.clone(), cx).await
1351            {
1352                AgentServerCommand {
1353                    path: bin,
1354                    args: Vec::new(),
1355                    env: Some(env),
1356                }
1357            } else {
1358                let mut command = get_or_npm_install_builtin_agent(
1359                    GEMINI_NAME.into(),
1360                    "@google/gemini-cli".into(),
1361                    "node_modules/@google/gemini-cli/dist/index.js".into(),
1362                    if cfg!(windows) {
1363                        // v0.8.x on Windows has a bug that causes the initialize request to hang forever
1364                        Some("0.9.0".parse().unwrap())
1365                    } else {
1366                        Some("0.2.1".parse().unwrap())
1367                    },
1368                    status_tx,
1369                    new_version_available_tx,
1370                    fs,
1371                    node_runtime,
1372                    cx,
1373                )
1374                .await?;
1375                command.env = Some(env);
1376                command
1377            };
1378
1379            // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
1380            let login = task::SpawnInTerminal {
1381                command: Some(command.path.to_string_lossy().into_owned()),
1382                args: command.args.clone(),
1383                env: command.env.clone().unwrap_or_default(),
1384                label: "gemini /auth".into(),
1385                ..Default::default()
1386            };
1387
1388            command.env.get_or_insert_default().extend(extra_env);
1389            command.args.push("--experimental-acp".into());
1390            Ok((command, Some(login)))
1391        })
1392    }
1393
1394    fn as_any_mut(&mut self) -> &mut dyn Any {
1395        self
1396    }
1397}
1398
1399struct LocalClaudeCode {
1400    fs: Arc<dyn Fs>,
1401    node_runtime: NodeRuntime,
1402    project_environment: Entity<ProjectEnvironment>,
1403    custom_command: Option<AgentServerCommand>,
1404    settings_env: Option<HashMap<String, String>>,
1405}
1406
1407impl ExternalAgentServer for LocalClaudeCode {
1408    fn get_command(
1409        &mut self,
1410        extra_env: HashMap<String, String>,
1411        status_tx: Option<watch::Sender<SharedString>>,
1412        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1413        cx: &mut AsyncApp,
1414    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1415        let fs = self.fs.clone();
1416        let node_runtime = self.node_runtime.clone();
1417        let project_environment = self.project_environment.downgrade();
1418        let custom_command = self.custom_command.clone();
1419        let settings_env = self.settings_env.clone();
1420
1421        cx.spawn(async move |cx| {
1422            let mut env = project_environment
1423                .update(cx, |project_environment, cx| {
1424                    project_environment.local_directory_environment(
1425                        &Shell::System,
1426                        paths::home_dir().as_path().into(),
1427                        cx,
1428                    )
1429                })?
1430                .await
1431                .unwrap_or_default();
1432            env.insert("ANTHROPIC_API_KEY".into(), "".into());
1433
1434            env.extend(settings_env.unwrap_or_default());
1435
1436            let (mut command, login_command) = if let Some(mut custom_command) = custom_command {
1437                custom_command.env = Some(env);
1438                (custom_command, None)
1439            } else {
1440                let mut command = get_or_npm_install_builtin_agent(
1441                    "claude-agent-acp".into(),
1442                    "@zed-industries/claude-agent-acp".into(),
1443                    "node_modules/@zed-industries/claude-agent-acp/dist/index.js".into(),
1444                    Some("0.17.0".parse().unwrap()),
1445                    status_tx,
1446                    new_version_available_tx,
1447                    fs,
1448                    node_runtime,
1449                    cx,
1450                )
1451                .await?;
1452                command.env = Some(env);
1453
1454                (command, None)
1455            };
1456
1457            command.env.get_or_insert_default().extend(extra_env);
1458            Ok((command, login_command))
1459        })
1460    }
1461
1462    fn as_any_mut(&mut self) -> &mut dyn Any {
1463        self
1464    }
1465}
1466
1467struct LocalCodex {
1468    fs: Arc<dyn Fs>,
1469    project_environment: Entity<ProjectEnvironment>,
1470    http_client: Arc<dyn HttpClient>,
1471    custom_command: Option<AgentServerCommand>,
1472    settings_env: Option<HashMap<String, String>>,
1473    no_browser: bool,
1474}
1475
1476impl ExternalAgentServer for LocalCodex {
1477    fn get_command(
1478        &mut self,
1479        extra_env: HashMap<String, String>,
1480        mut status_tx: Option<watch::Sender<SharedString>>,
1481        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1482        cx: &mut AsyncApp,
1483    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1484        let fs = self.fs.clone();
1485        let project_environment = self.project_environment.downgrade();
1486        let http = self.http_client.clone();
1487        let custom_command = self.custom_command.clone();
1488        let settings_env = self.settings_env.clone();
1489        let no_browser = self.no_browser;
1490
1491        cx.spawn(async move |cx| {
1492            let mut env = project_environment
1493                .update(cx, |project_environment, cx| {
1494                    project_environment.local_directory_environment(
1495                        &Shell::System,
1496                        paths::home_dir().as_path().into(),
1497                        cx,
1498                    )
1499                })?
1500                .await
1501                .unwrap_or_default();
1502            if no_browser {
1503                env.insert("NO_BROWSER".to_owned(), "1".to_owned());
1504            }
1505
1506            env.extend(settings_env.unwrap_or_default());
1507
1508            let mut command = if let Some(mut custom_command) = custom_command {
1509                custom_command.env = Some(env);
1510                custom_command
1511            } else {
1512                let dir = paths::external_agents_dir().join(CODEX_NAME);
1513                fs.create_dir(&dir).await?;
1514
1515                let bin_name = if cfg!(windows) {
1516                    "codex-acp.exe"
1517                } else {
1518                    "codex-acp"
1519                };
1520
1521                let find_latest_local_version = async || -> Option<PathBuf> {
1522                    let mut local_versions: Vec<(semver::Version, String)> = Vec::new();
1523                    let mut stream = fs.read_dir(&dir).await.ok()?;
1524                    while let Some(entry) = stream.next().await {
1525                        let Ok(entry) = entry else { continue };
1526                        let Some(file_name) = entry.file_name() else {
1527                            continue;
1528                        };
1529                        let version_path = dir.join(&file_name);
1530                        if fs.is_file(&version_path.join(bin_name)).await {
1531                            let version_str = file_name.to_string_lossy();
1532                            if let Ok(version) =
1533                                semver::Version::from_str(version_str.trim_start_matches('v'))
1534                            {
1535                                local_versions.push((version, version_str.into_owned()));
1536                            }
1537                        }
1538                    }
1539                    local_versions.sort_by(|(a, _), (b, _)| a.cmp(b));
1540                    local_versions.last().map(|(_, v)| dir.join(v))
1541                };
1542
1543                let fallback_to_latest_local_version =
1544                    async |err: anyhow::Error| -> Result<PathBuf, anyhow::Error> {
1545                        if let Some(local) = find_latest_local_version().await {
1546                            log::info!(
1547                                "Falling back to locally installed Codex version: {}",
1548                                local.display()
1549                            );
1550                            Ok(local)
1551                        } else {
1552                            Err(err)
1553                        }
1554                    };
1555
1556                let version_dir = match ::http_client::github::latest_github_release(
1557                    CODEX_ACP_REPO,
1558                    true,
1559                    false,
1560                    http.clone(),
1561                )
1562                .await
1563                {
1564                    Ok(release) => {
1565                        let version_dir = dir.join(&release.tag_name);
1566                        if !fs.is_dir(&version_dir).await {
1567                            if let Some(ref mut status_tx) = status_tx {
1568                                status_tx.send("Installing…".into()).ok();
1569                            }
1570
1571                            let tag = release.tag_name.clone();
1572                            let version_number = tag.trim_start_matches('v');
1573                            let asset_name = asset_name(version_number)
1574                                .context("codex acp is not supported for this architecture")?;
1575                            let asset = release
1576                                .assets
1577                                .into_iter()
1578                                .find(|asset| asset.name == asset_name)
1579                                .with_context(|| {
1580                                    format!("no asset found matching `{asset_name:?}`")
1581                                })?;
1582                            // Strip "sha256:" prefix from digest if present (GitHub API format)
1583                            let digest = asset
1584                                .digest
1585                                .as_deref()
1586                                .and_then(|d| d.strip_prefix("sha256:").or(Some(d)));
1587                            match ::http_client::github_download::download_server_binary(
1588                                &*http,
1589                                &asset.browser_download_url,
1590                                digest,
1591                                &version_dir,
1592                                if cfg!(target_os = "windows") {
1593                                    AssetKind::Zip
1594                                } else {
1595                                    AssetKind::TarGz
1596                                },
1597                            )
1598                            .await
1599                            {
1600                                Ok(()) => {
1601                                    // remove older versions
1602                                    util::fs::remove_matching(&dir, |entry| entry != version_dir)
1603                                        .await;
1604                                    version_dir
1605                                }
1606                                Err(err) => {
1607                                    log::error!(
1608                                        "Failed to download Codex release {}: {err:#}",
1609                                        release.tag_name
1610                                    );
1611                                    fallback_to_latest_local_version(err).await?
1612                                }
1613                            }
1614                        } else {
1615                            version_dir
1616                        }
1617                    }
1618                    Err(err) => {
1619                        log::error!("Failed to fetch Codex latest release: {err:#}");
1620                        fallback_to_latest_local_version(err).await?
1621                    }
1622                };
1623
1624                let bin_path = version_dir.join(bin_name);
1625                anyhow::ensure!(
1626                    fs.is_file(&bin_path).await,
1627                    "Missing Codex binary at {} after installation",
1628                    bin_path.to_string_lossy()
1629                );
1630
1631                let mut cmd = AgentServerCommand {
1632                    path: bin_path,
1633                    args: Vec::new(),
1634                    env: None,
1635                };
1636                cmd.env = Some(env);
1637                cmd
1638            };
1639
1640            command.env.get_or_insert_default().extend(extra_env);
1641            Ok((command, None))
1642        })
1643    }
1644
1645    fn as_any_mut(&mut self) -> &mut dyn Any {
1646        self
1647    }
1648}
1649
1650pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp";
1651
1652fn get_platform_info() -> Option<(&'static str, &'static str, &'static str)> {
1653    let arch = if cfg!(target_arch = "x86_64") {
1654        "x86_64"
1655    } else if cfg!(target_arch = "aarch64") {
1656        "aarch64"
1657    } else {
1658        return None;
1659    };
1660
1661    let platform = if cfg!(target_os = "macos") {
1662        "apple-darwin"
1663    } else if cfg!(target_os = "windows") {
1664        "pc-windows-msvc"
1665    } else if cfg!(target_os = "linux") {
1666        "unknown-linux-gnu"
1667    } else {
1668        return None;
1669    };
1670
1671    // Windows uses .zip in release assets
1672    let ext = if cfg!(target_os = "windows") {
1673        "zip"
1674    } else {
1675        "tar.gz"
1676    };
1677
1678    Some((arch, platform, ext))
1679}
1680
1681fn asset_name(version: &str) -> Option<String> {
1682    let (arch, platform, ext) = get_platform_info()?;
1683    Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}"))
1684}
1685
1686pub struct LocalExtensionArchiveAgent {
1687    pub fs: Arc<dyn Fs>,
1688    pub http_client: Arc<dyn HttpClient>,
1689    pub node_runtime: NodeRuntime,
1690    pub project_environment: Entity<ProjectEnvironment>,
1691    pub extension_id: Arc<str>,
1692    pub agent_id: Arc<str>,
1693    pub targets: HashMap<String, extension::TargetConfig>,
1694    pub env: HashMap<String, String>,
1695}
1696
1697impl ExternalAgentServer for LocalExtensionArchiveAgent {
1698    fn get_command(
1699        &mut self,
1700        extra_env: HashMap<String, String>,
1701        _status_tx: Option<watch::Sender<SharedString>>,
1702        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1703        cx: &mut AsyncApp,
1704    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1705        let fs = self.fs.clone();
1706        let http_client = self.http_client.clone();
1707        let node_runtime = self.node_runtime.clone();
1708        let project_environment = self.project_environment.downgrade();
1709        let extension_id = self.extension_id.clone();
1710        let agent_id = self.agent_id.clone();
1711        let targets = self.targets.clone();
1712        let base_env = self.env.clone();
1713
1714        cx.spawn(async move |cx| {
1715            // Get project environment
1716            let mut env = project_environment
1717                .update(cx, |project_environment, cx| {
1718                    project_environment.local_directory_environment(
1719                        &Shell::System,
1720                        paths::home_dir().as_path().into(),
1721                        cx,
1722                    )
1723                })?
1724                .await
1725                .unwrap_or_default();
1726
1727            // Merge manifest env and extra env
1728            env.extend(base_env);
1729            env.extend(extra_env);
1730
1731            let cache_key = format!("{}/{}", extension_id, agent_id);
1732            let dir = paths::external_agents_dir().join(&cache_key);
1733            fs.create_dir(&dir).await?;
1734
1735            // Determine platform key
1736            let os = if cfg!(target_os = "macos") {
1737                "darwin"
1738            } else if cfg!(target_os = "linux") {
1739                "linux"
1740            } else if cfg!(target_os = "windows") {
1741                "windows"
1742            } else {
1743                anyhow::bail!("unsupported OS");
1744            };
1745
1746            let arch = if cfg!(target_arch = "aarch64") {
1747                "aarch64"
1748            } else if cfg!(target_arch = "x86_64") {
1749                "x86_64"
1750            } else {
1751                anyhow::bail!("unsupported architecture");
1752            };
1753
1754            let platform_key = format!("{}-{}", os, arch);
1755            let target_config = targets.get(&platform_key).with_context(|| {
1756                format!(
1757                    "no target specified for platform '{}'. Available platforms: {}",
1758                    platform_key,
1759                    targets
1760                        .keys()
1761                        .map(|k| k.as_str())
1762                        .collect::<Vec<_>>()
1763                        .join(", ")
1764                )
1765            })?;
1766
1767            let archive_url = &target_config.archive;
1768
1769            // Use URL as version identifier for caching
1770            // Hash the URL to get a stable directory name
1771            use std::collections::hash_map::DefaultHasher;
1772            use std::hash::{Hash, Hasher};
1773            let mut hasher = DefaultHasher::new();
1774            archive_url.hash(&mut hasher);
1775            let url_hash = hasher.finish();
1776            let version_dir = dir.join(format!("v_{:x}", url_hash));
1777
1778            if !fs.is_dir(&version_dir).await {
1779                // Determine SHA256 for verification
1780                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1781                    // Use provided SHA256
1782                    Some(provided_sha.clone())
1783                } else if archive_url.starts_with("https://github.com/") {
1784                    // Try to fetch SHA256 from GitHub API
1785                    // Parse URL to extract repo and tag/file info
1786                    // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1787                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1788                        let parts: Vec<&str> = caps.split('/').collect();
1789                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1790                            let repo = format!("{}/{}", parts[0], parts[1]);
1791                            let tag = parts[4];
1792                            let filename = parts[5..].join("/");
1793
1794                            // Try to get release info from GitHub
1795                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1796                                &repo,
1797                                tag,
1798                                http_client.clone(),
1799                            )
1800                            .await
1801                            {
1802                                // Find matching asset
1803                                if let Some(asset) =
1804                                    release.assets.iter().find(|a| a.name == filename)
1805                                {
1806                                    // Strip "sha256:" prefix if present
1807                                    asset.digest.as_ref().map(|d| {
1808                                        d.strip_prefix("sha256:")
1809                                            .map(|s| s.to_string())
1810                                            .unwrap_or_else(|| d.clone())
1811                                    })
1812                                } else {
1813                                    None
1814                                }
1815                            } else {
1816                                None
1817                            }
1818                        } else {
1819                            None
1820                        }
1821                    } else {
1822                        None
1823                    }
1824                } else {
1825                    None
1826                };
1827
1828                // Determine archive type from URL
1829                let asset_kind = if archive_url.ends_with(".zip") {
1830                    AssetKind::Zip
1831                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1832                    AssetKind::TarGz
1833                } else {
1834                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1835                };
1836
1837                // Download and extract
1838                ::http_client::github_download::download_server_binary(
1839                    &*http_client,
1840                    archive_url,
1841                    sha256.as_deref(),
1842                    &version_dir,
1843                    asset_kind,
1844                )
1845                .await?;
1846            }
1847
1848            // Validate and resolve cmd path
1849            let cmd = &target_config.cmd;
1850
1851            let cmd_path = if cmd == "node" {
1852                // Use Zed's managed Node.js runtime
1853                node_runtime.binary_path().await?
1854            } else {
1855                if cmd.contains("..") {
1856                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1857                }
1858
1859                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1860                    // Relative to extraction directory
1861                    let cmd_path = version_dir.join(&cmd[2..]);
1862                    anyhow::ensure!(
1863                        fs.is_file(&cmd_path).await,
1864                        "Missing command {} after extraction",
1865                        cmd_path.to_string_lossy()
1866                    );
1867                    cmd_path
1868                } else {
1869                    // On PATH
1870                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1871                }
1872            };
1873
1874            let command = AgentServerCommand {
1875                path: cmd_path,
1876                args: target_config.args.clone(),
1877                env: Some(env),
1878            };
1879
1880            Ok((command, None))
1881        })
1882    }
1883
1884    fn as_any_mut(&mut self) -> &mut dyn Any {
1885        self
1886    }
1887}
1888
1889struct LocalRegistryArchiveAgent {
1890    fs: Arc<dyn Fs>,
1891    http_client: Arc<dyn HttpClient>,
1892    node_runtime: NodeRuntime,
1893    project_environment: Entity<ProjectEnvironment>,
1894    registry_id: Arc<str>,
1895    targets: HashMap<String, RegistryTargetConfig>,
1896    env: HashMap<String, String>,
1897}
1898
1899impl ExternalAgentServer for LocalRegistryArchiveAgent {
1900    fn get_command(
1901        &mut self,
1902        extra_env: HashMap<String, String>,
1903        _status_tx: Option<watch::Sender<SharedString>>,
1904        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1905        cx: &mut AsyncApp,
1906    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
1907        let fs = self.fs.clone();
1908        let http_client = self.http_client.clone();
1909        let node_runtime = self.node_runtime.clone();
1910        let project_environment = self.project_environment.downgrade();
1911        let registry_id = self.registry_id.clone();
1912        let targets = self.targets.clone();
1913        let settings_env = self.env.clone();
1914
1915        cx.spawn(async move |cx| {
1916            let mut env = project_environment
1917                .update(cx, |project_environment, cx| {
1918                    project_environment.local_directory_environment(
1919                        &Shell::System,
1920                        paths::home_dir().as_path().into(),
1921                        cx,
1922                    )
1923                })?
1924                .await
1925                .unwrap_or_default();
1926
1927            let dir = paths::external_agents_dir()
1928                .join("registry")
1929                .join(registry_id.as_ref());
1930            fs.create_dir(&dir).await?;
1931
1932            let os = if cfg!(target_os = "macos") {
1933                "darwin"
1934            } else if cfg!(target_os = "linux") {
1935                "linux"
1936            } else if cfg!(target_os = "windows") {
1937                "windows"
1938            } else {
1939                anyhow::bail!("unsupported OS");
1940            };
1941
1942            let arch = if cfg!(target_arch = "aarch64") {
1943                "aarch64"
1944            } else if cfg!(target_arch = "x86_64") {
1945                "x86_64"
1946            } else {
1947                anyhow::bail!("unsupported architecture");
1948            };
1949
1950            let platform_key = format!("{}-{}", os, arch);
1951            let target_config = targets.get(&platform_key).with_context(|| {
1952                format!(
1953                    "no target specified for platform '{}'. Available platforms: {}",
1954                    platform_key,
1955                    targets
1956                        .keys()
1957                        .map(|k| k.as_str())
1958                        .collect::<Vec<_>>()
1959                        .join(", ")
1960                )
1961            })?;
1962
1963            env.extend(target_config.env.clone());
1964            env.extend(extra_env);
1965            env.extend(settings_env);
1966
1967            let archive_url = &target_config.archive;
1968
1969            use std::collections::hash_map::DefaultHasher;
1970            use std::hash::{Hash, Hasher};
1971            let mut hasher = DefaultHasher::new();
1972            archive_url.hash(&mut hasher);
1973            let url_hash = hasher.finish();
1974            let version_dir = dir.join(format!("v_{:x}", url_hash));
1975
1976            if !fs.is_dir(&version_dir).await {
1977                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1978                    Some(provided_sha.clone())
1979                } else if archive_url.starts_with("https://github.com/") {
1980                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1981                        let parts: Vec<&str> = caps.split('/').collect();
1982                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1983                            let repo = format!("{}/{}", parts[0], parts[1]);
1984                            let tag = parts[4];
1985                            let filename = parts[5..].join("/");
1986
1987                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1988                                &repo,
1989                                tag,
1990                                http_client.clone(),
1991                            )
1992                            .await
1993                            {
1994                                if let Some(asset) =
1995                                    release.assets.iter().find(|a| a.name == filename)
1996                                {
1997                                    asset.digest.as_ref().and_then(|d| {
1998                                        d.strip_prefix("sha256:")
1999                                            .map(|s| s.to_string())
2000                                            .or_else(|| Some(d.clone()))
2001                                    })
2002                                } else {
2003                                    None
2004                                }
2005                            } else {
2006                                None
2007                            }
2008                        } else {
2009                            None
2010                        }
2011                    } else {
2012                        None
2013                    }
2014                } else {
2015                    None
2016                };
2017
2018                let asset_kind = if archive_url.ends_with(".zip") {
2019                    AssetKind::Zip
2020                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
2021                    AssetKind::TarGz
2022                } else {
2023                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
2024                };
2025
2026                ::http_client::github_download::download_server_binary(
2027                    &*http_client,
2028                    archive_url,
2029                    sha256.as_deref(),
2030                    &version_dir,
2031                    asset_kind,
2032                )
2033                .await?;
2034            }
2035
2036            let cmd = &target_config.cmd;
2037
2038            let cmd_path = if cmd == "node" {
2039                node_runtime.binary_path().await?
2040            } else {
2041                if cmd.contains("..") {
2042                    anyhow::bail!("command path cannot contain '..': {}", cmd);
2043                }
2044
2045                if cmd.starts_with("./") || cmd.starts_with(".\\") {
2046                    let cmd_path = version_dir.join(&cmd[2..]);
2047                    anyhow::ensure!(
2048                        fs.is_file(&cmd_path).await,
2049                        "Missing command {} after extraction",
2050                        cmd_path.to_string_lossy()
2051                    );
2052                    cmd_path
2053                } else {
2054                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
2055                }
2056            };
2057
2058            let command = AgentServerCommand {
2059                path: cmd_path,
2060                args: target_config.args.clone(),
2061                env: Some(env),
2062            };
2063
2064            Ok((command, None))
2065        })
2066    }
2067
2068    fn as_any_mut(&mut self) -> &mut dyn Any {
2069        self
2070    }
2071}
2072
2073struct LocalRegistryNpxAgent {
2074    node_runtime: NodeRuntime,
2075    project_environment: Entity<ProjectEnvironment>,
2076    package: SharedString,
2077    args: Vec<String>,
2078    distribution_env: HashMap<String, String>,
2079    settings_env: HashMap<String, String>,
2080}
2081
2082impl ExternalAgentServer for LocalRegistryNpxAgent {
2083    fn get_command(
2084        &mut self,
2085        extra_env: HashMap<String, String>,
2086        _status_tx: Option<watch::Sender<SharedString>>,
2087        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
2088        cx: &mut AsyncApp,
2089    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
2090        let node_runtime = self.node_runtime.clone();
2091        let project_environment = self.project_environment.downgrade();
2092        let package = self.package.clone();
2093        let args = self.args.clone();
2094        let distribution_env = self.distribution_env.clone();
2095        let settings_env = self.settings_env.clone();
2096
2097        cx.spawn(async move |cx| {
2098            let mut env = project_environment
2099                .update(cx, |project_environment, cx| {
2100                    project_environment.local_directory_environment(
2101                        &Shell::System,
2102                        paths::home_dir().as_path().into(),
2103                        cx,
2104                    )
2105                })?
2106                .await
2107                .unwrap_or_default();
2108
2109            let mut exec_args = Vec::new();
2110            exec_args.push("--yes".to_string());
2111            exec_args.push(package.to_string());
2112            if !args.is_empty() {
2113                exec_args.push("--".to_string());
2114                exec_args.extend(args);
2115            }
2116
2117            let npm_command = node_runtime
2118                .npm_command(
2119                    "exec",
2120                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
2121                )
2122                .await?;
2123
2124            env.extend(npm_command.env);
2125            env.extend(distribution_env);
2126            env.extend(extra_env);
2127            env.extend(settings_env);
2128
2129            let command = AgentServerCommand {
2130                path: npm_command.path,
2131                args: npm_command.args,
2132                env: Some(env),
2133            };
2134
2135            Ok((command, None))
2136        })
2137    }
2138
2139    fn as_any_mut(&mut self) -> &mut dyn Any {
2140        self
2141    }
2142}
2143
2144struct LocalCustomAgent {
2145    project_environment: Entity<ProjectEnvironment>,
2146    command: AgentServerCommand,
2147}
2148
2149impl ExternalAgentServer for LocalCustomAgent {
2150    fn get_command(
2151        &mut self,
2152        extra_env: HashMap<String, String>,
2153        _status_tx: Option<watch::Sender<SharedString>>,
2154        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
2155        cx: &mut AsyncApp,
2156    ) -> Task<Result<(AgentServerCommand, Option<task::SpawnInTerminal>)>> {
2157        let mut command = self.command.clone();
2158        let project_environment = self.project_environment.downgrade();
2159        cx.spawn(async move |cx| {
2160            let mut env = project_environment
2161                .update(cx, |project_environment, cx| {
2162                    project_environment.local_directory_environment(
2163                        &Shell::System,
2164                        paths::home_dir().as_path().into(),
2165                        cx,
2166                    )
2167                })?
2168                .await
2169                .unwrap_or_default();
2170            env.extend(command.env.unwrap_or_default());
2171            env.extend(extra_env);
2172            command.env = Some(env);
2173            Ok((command, None))
2174        })
2175    }
2176
2177    fn as_any_mut(&mut self) -> &mut dyn Any {
2178        self
2179    }
2180}
2181
2182pub const GEMINI_NAME: &'static str = "gemini";
2183pub const CLAUDE_AGENT_NAME: &'static str = "claude";
2184pub const CODEX_NAME: &'static str = "codex";
2185
2186#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
2187pub struct AllAgentServersSettings {
2188    pub gemini: Option<BuiltinAgentServerSettings>,
2189    pub claude: Option<BuiltinAgentServerSettings>,
2190    pub codex: Option<BuiltinAgentServerSettings>,
2191    pub custom: HashMap<String, CustomAgentServerSettings>,
2192}
2193
2194impl AllAgentServersSettings {
2195    pub fn has_registry_agents(&self) -> bool {
2196        self.custom
2197            .values()
2198            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
2199    }
2200}
2201
2202#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
2203pub struct BuiltinAgentServerSettings {
2204    pub path: Option<PathBuf>,
2205    pub args: Option<Vec<String>>,
2206    pub env: Option<HashMap<String, String>>,
2207    pub ignore_system_version: Option<bool>,
2208    pub default_mode: Option<String>,
2209    pub default_model: Option<String>,
2210    pub favorite_models: Vec<String>,
2211    pub default_config_options: HashMap<String, String>,
2212    pub favorite_config_option_values: HashMap<String, Vec<String>>,
2213}
2214
2215impl BuiltinAgentServerSettings {
2216    fn custom_command(self) -> Option<AgentServerCommand> {
2217        self.path.map(|path| AgentServerCommand {
2218            path,
2219            args: self.args.unwrap_or_default(),
2220            // Settings env are always applied, so we don't need to supply them here as well
2221            env: None,
2222        })
2223    }
2224}
2225
2226impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
2227    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
2228        BuiltinAgentServerSettings {
2229            path: value
2230                .path
2231                .map(|p| PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref())),
2232            args: value.args,
2233            env: value.env,
2234            ignore_system_version: value.ignore_system_version,
2235            default_mode: value.default_mode,
2236            default_model: value.default_model,
2237            favorite_models: value.favorite_models,
2238            default_config_options: value.default_config_options,
2239            favorite_config_option_values: value.favorite_config_option_values,
2240        }
2241    }
2242}
2243
2244impl From<AgentServerCommand> for BuiltinAgentServerSettings {
2245    fn from(value: AgentServerCommand) -> Self {
2246        BuiltinAgentServerSettings {
2247            path: Some(value.path),
2248            args: Some(value.args),
2249            env: value.env,
2250            ..Default::default()
2251        }
2252    }
2253}
2254
2255#[derive(Clone, JsonSchema, Debug, PartialEq)]
2256pub enum CustomAgentServerSettings {
2257    Custom {
2258        command: AgentServerCommand,
2259        /// The default mode to use for this agent.
2260        ///
2261        /// Note: Not only all agents support modes.
2262        ///
2263        /// Default: None
2264        default_mode: Option<String>,
2265        /// The default model to use for this agent.
2266        ///
2267        /// This should be the model ID as reported by the agent.
2268        ///
2269        /// Default: None
2270        default_model: Option<String>,
2271        /// The favorite models for this agent.
2272        ///
2273        /// Default: []
2274        favorite_models: Vec<String>,
2275        /// Default values for session config options.
2276        ///
2277        /// This is a map from config option ID to value ID.
2278        ///
2279        /// Default: {}
2280        default_config_options: HashMap<String, String>,
2281        /// Favorited values for session config options.
2282        ///
2283        /// This is a map from config option ID to a list of favorited value IDs.
2284        ///
2285        /// Default: {}
2286        favorite_config_option_values: HashMap<String, Vec<String>>,
2287    },
2288    Extension {
2289        /// Additional environment variables to pass to the agent.
2290        ///
2291        /// Default: {}
2292        env: HashMap<String, String>,
2293        /// The default mode to use for this agent.
2294        ///
2295        /// Note: Not only all agents support modes.
2296        ///
2297        /// Default: None
2298        default_mode: Option<String>,
2299        /// The default model to use for this agent.
2300        ///
2301        /// This should be the model ID as reported by the agent.
2302        ///
2303        /// Default: None
2304        default_model: Option<String>,
2305        /// The favorite models for this agent.
2306        ///
2307        /// Default: []
2308        favorite_models: Vec<String>,
2309        /// Default values for session config options.
2310        ///
2311        /// This is a map from config option ID to value ID.
2312        ///
2313        /// Default: {}
2314        default_config_options: HashMap<String, String>,
2315        /// Favorited values for session config options.
2316        ///
2317        /// This is a map from config option ID to a list of favorited value IDs.
2318        ///
2319        /// Default: {}
2320        favorite_config_option_values: HashMap<String, Vec<String>>,
2321    },
2322    Registry {
2323        /// Additional environment variables to pass to the agent.
2324        ///
2325        /// Default: {}
2326        env: HashMap<String, String>,
2327        /// The default mode to use for this agent.
2328        ///
2329        /// Note: Not only all agents support modes.
2330        ///
2331        /// Default: None
2332        default_mode: Option<String>,
2333        /// The default model to use for this agent.
2334        ///
2335        /// This should be the model ID as reported by the agent.
2336        ///
2337        /// Default: None
2338        default_model: Option<String>,
2339        /// The favorite models for this agent.
2340        ///
2341        /// Default: []
2342        favorite_models: Vec<String>,
2343        /// Default values for session config options.
2344        ///
2345        /// This is a map from config option ID to value ID.
2346        ///
2347        /// Default: {}
2348        default_config_options: HashMap<String, String>,
2349        /// Favorited values for session config options.
2350        ///
2351        /// This is a map from config option ID to a list of favorited value IDs.
2352        ///
2353        /// Default: {}
2354        favorite_config_option_values: HashMap<String, Vec<String>>,
2355    },
2356}
2357
2358impl CustomAgentServerSettings {
2359    pub fn command(&self) -> Option<&AgentServerCommand> {
2360        match self {
2361            CustomAgentServerSettings::Custom { command, .. } => Some(command),
2362            CustomAgentServerSettings::Extension { .. }
2363            | CustomAgentServerSettings::Registry { .. } => None,
2364        }
2365    }
2366
2367    pub fn default_mode(&self) -> Option<&str> {
2368        match self {
2369            CustomAgentServerSettings::Custom { default_mode, .. }
2370            | CustomAgentServerSettings::Extension { default_mode, .. }
2371            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
2372        }
2373    }
2374
2375    pub fn default_model(&self) -> Option<&str> {
2376        match self {
2377            CustomAgentServerSettings::Custom { default_model, .. }
2378            | CustomAgentServerSettings::Extension { default_model, .. }
2379            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
2380        }
2381    }
2382
2383    pub fn favorite_models(&self) -> &[String] {
2384        match self {
2385            CustomAgentServerSettings::Custom {
2386                favorite_models, ..
2387            }
2388            | CustomAgentServerSettings::Extension {
2389                favorite_models, ..
2390            }
2391            | CustomAgentServerSettings::Registry {
2392                favorite_models, ..
2393            } => favorite_models,
2394        }
2395    }
2396
2397    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
2398        match self {
2399            CustomAgentServerSettings::Custom {
2400                default_config_options,
2401                ..
2402            }
2403            | CustomAgentServerSettings::Extension {
2404                default_config_options,
2405                ..
2406            }
2407            | CustomAgentServerSettings::Registry {
2408                default_config_options,
2409                ..
2410            } => default_config_options.get(config_id).map(|s| s.as_str()),
2411        }
2412    }
2413
2414    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
2415        match self {
2416            CustomAgentServerSettings::Custom {
2417                favorite_config_option_values,
2418                ..
2419            }
2420            | CustomAgentServerSettings::Extension {
2421                favorite_config_option_values,
2422                ..
2423            }
2424            | CustomAgentServerSettings::Registry {
2425                favorite_config_option_values,
2426                ..
2427            } => favorite_config_option_values
2428                .get(config_id)
2429                .map(|v| v.as_slice()),
2430        }
2431    }
2432}
2433
2434impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
2435    fn from(value: settings::CustomAgentServerSettings) -> Self {
2436        match value {
2437            settings::CustomAgentServerSettings::Custom {
2438                path,
2439                args,
2440                env,
2441                default_mode,
2442                default_model,
2443                favorite_models,
2444                default_config_options,
2445                favorite_config_option_values,
2446            } => CustomAgentServerSettings::Custom {
2447                command: AgentServerCommand {
2448                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
2449                    args,
2450                    env: Some(env),
2451                },
2452                default_mode,
2453                default_model,
2454                favorite_models,
2455                default_config_options,
2456                favorite_config_option_values,
2457            },
2458            settings::CustomAgentServerSettings::Extension {
2459                env,
2460                default_mode,
2461                default_model,
2462                default_config_options,
2463                favorite_models,
2464                favorite_config_option_values,
2465            } => CustomAgentServerSettings::Extension {
2466                env,
2467                default_mode,
2468                default_model,
2469                default_config_options,
2470                favorite_models,
2471                favorite_config_option_values,
2472            },
2473            settings::CustomAgentServerSettings::Registry {
2474                env,
2475                default_mode,
2476                default_model,
2477                default_config_options,
2478                favorite_models,
2479                favorite_config_option_values,
2480            } => CustomAgentServerSettings::Registry {
2481                env,
2482                default_mode,
2483                default_model,
2484                default_config_options,
2485                favorite_models,
2486                favorite_config_option_values,
2487            },
2488        }
2489    }
2490}
2491
2492impl settings::Settings for AllAgentServersSettings {
2493    fn from_settings(content: &settings::SettingsContent) -> Self {
2494        let agent_settings = content.agent_servers.clone().unwrap();
2495        Self {
2496            gemini: agent_settings.gemini.map(Into::into),
2497            claude: agent_settings.claude.map(Into::into),
2498            codex: agent_settings.codex.map(Into::into),
2499            custom: agent_settings
2500                .custom
2501                .into_iter()
2502                .map(|(k, v)| (k, v.into()))
2503                .collect(),
2504        }
2505    }
2506}