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