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        // If we don't have agents from the registry loaded yet, trigger a
 412        // refresh, which will cause this function to be called again
 413        if new_settings.has_registry_agents()
 414            && let Some(registry) = AgentRegistryStore::try_global(cx)
 415        {
 416            registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
 417        }
 418
 419        self.external_agents.clear();
 420        self.external_agents.insert(
 421            GEMINI_NAME.into(),
 422            ExternalAgentEntry::new(
 423                Box::new(LocalGemini {
 424                    fs: fs.clone(),
 425                    node_runtime: node_runtime.clone(),
 426                    project_environment: project_environment.clone(),
 427                    custom_command: new_settings
 428                        .gemini
 429                        .clone()
 430                        .and_then(|settings| settings.custom_command()),
 431                    settings_env: new_settings
 432                        .gemini
 433                        .as_ref()
 434                        .and_then(|settings| settings.env.clone()),
 435                    ignore_system_version: new_settings
 436                        .gemini
 437                        .as_ref()
 438                        .and_then(|settings| settings.ignore_system_version)
 439                        .unwrap_or(true),
 440                }),
 441                ExternalAgentSource::Builtin,
 442                None,
 443                None,
 444            ),
 445        );
 446        self.external_agents.insert(
 447            CODEX_NAME.into(),
 448            ExternalAgentEntry::new(
 449                Box::new(LocalCodex {
 450                    fs: fs.clone(),
 451                    project_environment: project_environment.clone(),
 452                    custom_command: new_settings
 453                        .codex
 454                        .clone()
 455                        .and_then(|settings| settings.custom_command()),
 456                    settings_env: new_settings
 457                        .codex
 458                        .as_ref()
 459                        .and_then(|settings| settings.env.clone()),
 460                    http_client: http_client.clone(),
 461                    no_browser: downstream_client
 462                        .as_ref()
 463                        .is_some_and(|(_, client)| !client.has_wsl_interop()),
 464                }),
 465                ExternalAgentSource::Builtin,
 466                None,
 467                None,
 468            ),
 469        );
 470        self.external_agents.insert(
 471            CLAUDE_AGENT_NAME.into(),
 472            ExternalAgentEntry::new(
 473                Box::new(LocalClaudeCode {
 474                    fs: fs.clone(),
 475                    node_runtime: node_runtime.clone(),
 476                    project_environment: project_environment.clone(),
 477                    custom_command: new_settings
 478                        .claude
 479                        .clone()
 480                        .and_then(|settings| settings.custom_command()),
 481                    settings_env: new_settings
 482                        .claude
 483                        .as_ref()
 484                        .and_then(|settings| settings.env.clone()),
 485                }),
 486                ExternalAgentSource::Builtin,
 487                None,
 488                None,
 489            ),
 490        );
 491
 492        let registry_store = AgentRegistryStore::try_global(cx);
 493        let registry_agents_by_id = registry_store
 494            .as_ref()
 495            .map(|store| {
 496                store
 497                    .read(cx)
 498                    .agents()
 499                    .iter()
 500                    .cloned()
 501                    .map(|agent| (agent.id().to_string(), agent))
 502                    .collect::<HashMap<_, _>>()
 503            })
 504            .unwrap_or_default();
 505
 506        // Insert extension agents before custom/registry so registry entries override extensions.
 507        for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
 508            let name = ExternalAgentServerName(agent_name.clone().into());
 509            let mut env = env.clone();
 510            if let Some(settings_env) =
 511                new_settings
 512                    .custom
 513                    .get(agent_name.as_ref())
 514                    .and_then(|settings| match settings {
 515                        CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
 516                        _ => None,
 517                    })
 518            {
 519                env.extend(settings_env);
 520            }
 521            let icon = icon_path
 522                .as_ref()
 523                .map(|path| SharedString::from(path.clone()));
 524
 525            self.external_agents.insert(
 526                name.clone(),
 527                ExternalAgentEntry::new(
 528                    Box::new(LocalExtensionArchiveAgent {
 529                        fs: fs.clone(),
 530                        http_client: http_client.clone(),
 531                        node_runtime: node_runtime.clone(),
 532                        project_environment: project_environment.clone(),
 533                        extension_id: Arc::from(&**ext_id),
 534                        targets: targets.clone(),
 535                        env,
 536                        agent_id: agent_name.clone(),
 537                    }) as Box<dyn ExternalAgentServer>,
 538                    ExternalAgentSource::Extension,
 539                    icon,
 540                    display_name.clone(),
 541                ),
 542            );
 543        }
 544
 545        for (name, settings) in &new_settings.custom {
 546            match settings {
 547                CustomAgentServerSettings::Custom { command, .. } => {
 548                    let agent_name = ExternalAgentServerName(name.clone().into());
 549                    self.external_agents.insert(
 550                        agent_name.clone(),
 551                        ExternalAgentEntry::new(
 552                            Box::new(LocalCustomAgent {
 553                                command: command.clone(),
 554                                project_environment: project_environment.clone(),
 555                            }) as Box<dyn ExternalAgentServer>,
 556                            ExternalAgentSource::Custom,
 557                            None,
 558                            None,
 559                        ),
 560                    );
 561                }
 562                CustomAgentServerSettings::Registry { env, .. } => {
 563                    let Some(agent) = registry_agents_by_id.get(name) else {
 564                        if registry_store.is_some() {
 565                            log::debug!("Registry agent '{}' not found in ACP registry", name);
 566                        }
 567                        continue;
 568                    };
 569
 570                    let agent_name = ExternalAgentServerName(name.clone().into());
 571                    match agent {
 572                        RegistryAgent::Binary(agent) => {
 573                            if !agent.supports_current_platform {
 574                                log::warn!(
 575                                    "Registry agent '{}' has no compatible binary for this platform",
 576                                    name
 577                                );
 578                                continue;
 579                            }
 580
 581                            self.external_agents.insert(
 582                                agent_name.clone(),
 583                                ExternalAgentEntry::new(
 584                                    Box::new(LocalRegistryArchiveAgent {
 585                                        fs: fs.clone(),
 586                                        http_client: http_client.clone(),
 587                                        node_runtime: node_runtime.clone(),
 588                                        project_environment: project_environment.clone(),
 589                                        registry_id: Arc::from(name.as_str()),
 590                                        targets: agent.targets.clone(),
 591                                        env: env.clone(),
 592                                    })
 593                                        as Box<dyn ExternalAgentServer>,
 594                                    ExternalAgentSource::Registry,
 595                                    agent.metadata.icon_path.clone(),
 596                                    Some(agent.metadata.name.clone()),
 597                                ),
 598                            );
 599                        }
 600                        RegistryAgent::Npx(agent) => {
 601                            self.external_agents.insert(
 602                                agent_name.clone(),
 603                                ExternalAgentEntry::new(
 604                                    Box::new(LocalRegistryNpxAgent {
 605                                        node_runtime: node_runtime.clone(),
 606                                        project_environment: project_environment.clone(),
 607                                        package: agent.package.clone(),
 608                                        args: agent.args.clone(),
 609                                        distribution_env: agent.env.clone(),
 610                                        settings_env: env.clone(),
 611                                    })
 612                                        as Box<dyn ExternalAgentServer>,
 613                                    ExternalAgentSource::Registry,
 614                                    agent.metadata.icon_path.clone(),
 615                                    Some(agent.metadata.name.clone()),
 616                                ),
 617                            );
 618                        }
 619                    }
 620                }
 621                CustomAgentServerSettings::Extension { .. } => {}
 622            }
 623        }
 624
 625        *old_settings = Some(new_settings);
 626
 627        if let Some((project_id, downstream_client)) = downstream_client {
 628            downstream_client
 629                .send(proto::ExternalAgentsUpdated {
 630                    project_id: *project_id,
 631                    names: self
 632                        .external_agents
 633                        .keys()
 634                        .map(|name| name.to_string())
 635                        .collect(),
 636                })
 637                .log_err();
 638        }
 639        cx.emit(AgentServersUpdated);
 640    }
 641
 642    pub fn node_runtime(&self) -> Option<NodeRuntime> {
 643        match &self.state {
 644            AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
 645            _ => None,
 646        }
 647    }
 648
 649    pub fn local(
 650        node_runtime: NodeRuntime,
 651        fs: Arc<dyn Fs>,
 652        project_environment: Entity<ProjectEnvironment>,
 653        http_client: Arc<dyn HttpClient>,
 654        cx: &mut Context<Self>,
 655    ) -> Self {
 656        let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
 657            this.agent_servers_settings_changed(cx);
 658        })];
 659        if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
 660            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
 661                this.reregister_agents(cx);
 662            }));
 663        }
 664        let mut this = Self {
 665            state: AgentServerStoreState::Local {
 666                node_runtime,
 667                fs,
 668                project_environment,
 669                http_client,
 670                downstream_client: None,
 671                settings: None,
 672                extension_agents: vec![],
 673                _subscriptions: subscriptions,
 674            },
 675            external_agents: Default::default(),
 676        };
 677        if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
 678        this.agent_servers_settings_changed(cx);
 679        this
 680    }
 681
 682    pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
 683        // Set up the builtin agents here so they're immediately available in
 684        // remote projects--we know that the HeadlessProject on the other end
 685        // will have them.
 686        let external_agents: [(ExternalAgentServerName, ExternalAgentEntry); 3] = [
 687            (
 688                CLAUDE_AGENT_NAME.into(),
 689                ExternalAgentEntry::new(
 690                    Box::new(RemoteExternalAgentServer {
 691                        project_id,
 692                        upstream_client: upstream_client.clone(),
 693                        name: CLAUDE_AGENT_NAME.into(),
 694                        status_tx: None,
 695                        new_version_available_tx: None,
 696                    }) as Box<dyn ExternalAgentServer>,
 697                    ExternalAgentSource::Builtin,
 698                    None,
 699                    None,
 700                ),
 701            ),
 702            (
 703                CODEX_NAME.into(),
 704                ExternalAgentEntry::new(
 705                    Box::new(RemoteExternalAgentServer {
 706                        project_id,
 707                        upstream_client: upstream_client.clone(),
 708                        name: CODEX_NAME.into(),
 709                        status_tx: None,
 710                        new_version_available_tx: None,
 711                    }) as Box<dyn ExternalAgentServer>,
 712                    ExternalAgentSource::Builtin,
 713                    None,
 714                    None,
 715                ),
 716            ),
 717            (
 718                GEMINI_NAME.into(),
 719                ExternalAgentEntry::new(
 720                    Box::new(RemoteExternalAgentServer {
 721                        project_id,
 722                        upstream_client: upstream_client.clone(),
 723                        name: GEMINI_NAME.into(),
 724                        status_tx: None,
 725                        new_version_available_tx: None,
 726                    }) as Box<dyn ExternalAgentServer>,
 727                    ExternalAgentSource::Builtin,
 728                    None,
 729                    None,
 730                ),
 731            ),
 732        ];
 733
 734        Self {
 735            state: AgentServerStoreState::Remote {
 736                project_id,
 737                upstream_client,
 738            },
 739            external_agents: external_agents.into_iter().collect(),
 740        }
 741    }
 742
 743    pub fn collab() -> Self {
 744        Self {
 745            state: AgentServerStoreState::Collab,
 746            external_agents: Default::default(),
 747        }
 748    }
 749
 750    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 751        match &mut self.state {
 752            AgentServerStoreState::Local {
 753                downstream_client, ..
 754            } => {
 755                *downstream_client = Some((project_id, client.clone()));
 756                // Send the current list of external agents downstream, but only after a delay,
 757                // to avoid having the message arrive before the downstream project's agent server store
 758                // sets up its handlers.
 759                cx.spawn(async move |this, cx| {
 760                    cx.background_executor().timer(Duration::from_secs(1)).await;
 761                    let names = this.update(cx, |this, _| {
 762                        this.external_agents()
 763                            .map(|name| name.to_string())
 764                            .collect()
 765                    })?;
 766                    client
 767                        .send(proto::ExternalAgentsUpdated { project_id, names })
 768                        .log_err();
 769                    anyhow::Ok(())
 770                })
 771                .detach();
 772            }
 773            AgentServerStoreState::Remote { .. } => {
 774                debug_panic!(
 775                    "external agents over collab not implemented, remote project should not be shared"
 776                );
 777            }
 778            AgentServerStoreState::Collab => {
 779                debug_panic!("external agents over collab not implemented, should not be shared");
 780            }
 781        }
 782    }
 783
 784    pub fn get_external_agent(
 785        &mut self,
 786        name: &ExternalAgentServerName,
 787    ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
 788        self.external_agents
 789            .get_mut(name)
 790            .map(|entry| entry.server.as_mut())
 791    }
 792
 793    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
 794        self.external_agents.keys()
 795    }
 796
 797    async fn handle_get_agent_server_command(
 798        this: Entity<Self>,
 799        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 800        mut cx: AsyncApp,
 801    ) -> Result<proto::AgentServerCommand> {
 802        let (command, root_dir, login_command) = this
 803            .update(&mut cx, |this, cx| {
 804                let AgentServerStoreState::Local {
 805                    downstream_client, ..
 806                } = &this.state
 807                else {
 808                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 809                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 810                };
 811                let agent = this
 812                    .external_agents
 813                    .get_mut(&*envelope.payload.name)
 814                    .map(|entry| entry.server.as_mut())
 815                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 816                let (status_tx, new_version_available_tx) = downstream_client
 817                    .clone()
 818                    .map(|(project_id, downstream_client)| {
 819                        let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
 820                        let (new_version_available_tx, mut new_version_available_rx) =
 821                            watch::channel(None);
 822                        cx.spawn({
 823                            let downstream_client = downstream_client.clone();
 824                            let name = envelope.payload.name.clone();
 825                            async move |_, _| {
 826                                while let Some(status) = status_rx.recv().await.ok() {
 827                                    downstream_client.send(
 828                                        proto::ExternalAgentLoadingStatusUpdated {
 829                                            project_id,
 830                                            name: name.clone(),
 831                                            status: status.to_string(),
 832                                        },
 833                                    )?;
 834                                }
 835                                anyhow::Ok(())
 836                            }
 837                        })
 838                        .detach_and_log_err(cx);
 839                        cx.spawn({
 840                            let name = envelope.payload.name.clone();
 841                            async move |_, _| {
 842                                if let Some(version) =
 843                                    new_version_available_rx.recv().await.ok().flatten()
 844                                {
 845                                    downstream_client.send(
 846                                        proto::NewExternalAgentVersionAvailable {
 847                                            project_id,
 848                                            name: name.clone(),
 849                                            version,
 850                                        },
 851                                    )?;
 852                                }
 853                                anyhow::Ok(())
 854                            }
 855                        })
 856                        .detach_and_log_err(cx);
 857                        (status_tx, new_version_available_tx)
 858                    })
 859                    .unzip();
 860                anyhow::Ok(agent.get_command(
 861                    envelope.payload.root_dir.as_deref(),
 862                    HashMap::default(),
 863                    status_tx,
 864                    new_version_available_tx,
 865                    &mut cx.to_async(),
 866                ))
 867            })?
 868            .await?;
 869        Ok(proto::AgentServerCommand {
 870            path: command.path.to_string_lossy().into_owned(),
 871            args: command.args,
 872            env: command
 873                .env
 874                .map(|env| env.into_iter().collect())
 875                .unwrap_or_default(),
 876            root_dir: root_dir,
 877            login: login_command.map(|cmd| cmd.to_proto()),
 878        })
 879    }
 880
 881    async fn handle_external_agents_updated(
 882        this: Entity<Self>,
 883        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 884        mut cx: AsyncApp,
 885    ) -> Result<()> {
 886        this.update(&mut cx, |this, cx| {
 887            let AgentServerStoreState::Remote {
 888                project_id,
 889                upstream_client,
 890            } = &this.state
 891            else {
 892                debug_panic!(
 893                    "handle_external_agents_updated should not be called for a non-remote project"
 894                );
 895                bail!("unexpected ExternalAgentsUpdated message")
 896            };
 897
 898            let mut previous_entries = std::mem::take(&mut this.external_agents);
 899            let mut status_txs = HashMap::default();
 900            let mut new_version_available_txs = HashMap::default();
 901            let mut metadata = HashMap::default();
 902
 903            for (name, mut entry) in previous_entries.drain() {
 904                if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
 905                    status_txs.insert(name.clone(), agent.status_tx.take());
 906                    new_version_available_txs
 907                        .insert(name.clone(), agent.new_version_available_tx.take());
 908                }
 909
 910                metadata.insert(name, (entry.icon, entry.display_name, entry.source));
 911            }
 912
 913            this.external_agents = envelope
 914                .payload
 915                .names
 916                .into_iter()
 917                .map(|name| {
 918                    let agent_name = ExternalAgentServerName(name.clone().into());
 919                    let fallback_source =
 920                        if name == GEMINI_NAME || name == CLAUDE_AGENT_NAME || name == CODEX_NAME {
 921                            ExternalAgentSource::Builtin
 922                        } else {
 923                            ExternalAgentSource::Custom
 924                        };
 925                    let (icon, display_name, source) = metadata
 926                        .remove(&agent_name)
 927                        .or_else(|| {
 928                            AgentRegistryStore::try_global(cx)
 929                                .and_then(|store| store.read(cx).agent(&agent_name.0))
 930                                .map(|s| {
 931                                    (
 932                                        s.icon_path().cloned(),
 933                                        Some(s.name().clone()),
 934                                        ExternalAgentSource::Registry,
 935                                    )
 936                                })
 937                        })
 938                        .unwrap_or((None, None, fallback_source));
 939                    let source = if fallback_source == ExternalAgentSource::Builtin {
 940                        ExternalAgentSource::Builtin
 941                    } else {
 942                        source
 943                    };
 944                    let agent = RemoteExternalAgentServer {
 945                        project_id: *project_id,
 946                        upstream_client: upstream_client.clone(),
 947                        name: agent_name.clone(),
 948                        status_tx: status_txs.remove(&agent_name).flatten(),
 949                        new_version_available_tx: new_version_available_txs
 950                            .remove(&agent_name)
 951                            .flatten(),
 952                    };
 953                    (
 954                        agent_name,
 955                        ExternalAgentEntry::new(
 956                            Box::new(agent) as Box<dyn ExternalAgentServer>,
 957                            source,
 958                            icon,
 959                            display_name,
 960                        ),
 961                    )
 962                })
 963                .collect();
 964            cx.emit(AgentServersUpdated);
 965            Ok(())
 966        })
 967    }
 968
 969    async fn handle_external_extension_agents_updated(
 970        this: Entity<Self>,
 971        envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
 972        mut cx: AsyncApp,
 973    ) -> Result<()> {
 974        this.update(&mut cx, |this, cx| {
 975            let AgentServerStoreState::Local {
 976                extension_agents, ..
 977            } = &mut this.state
 978            else {
 979                panic!(
 980                    "handle_external_extension_agents_updated \
 981                    should not be called for a non-remote project"
 982                );
 983            };
 984
 985            for ExternalExtensionAgent {
 986                name,
 987                icon_path,
 988                extension_id,
 989                targets,
 990                env,
 991            } in envelope.payload.agents
 992            {
 993                extension_agents.push((
 994                    Arc::from(&*name),
 995                    extension_id,
 996                    targets
 997                        .into_iter()
 998                        .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
 999                        .collect(),
1000                    env.into_iter().collect(),
1001                    icon_path,
1002                    None,
1003                ));
1004            }
1005
1006            this.reregister_agents(cx);
1007            cx.emit(AgentServersUpdated);
1008            Ok(())
1009        })
1010    }
1011
1012    async fn handle_loading_status_updated(
1013        this: Entity<Self>,
1014        envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
1015        mut cx: AsyncApp,
1016    ) -> Result<()> {
1017        this.update(&mut cx, |this, _| {
1018            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1019                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1020                && let Some(status_tx) = &mut agent.status_tx
1021            {
1022                status_tx.send(envelope.payload.status.into()).ok();
1023            }
1024        });
1025        Ok(())
1026    }
1027
1028    async fn handle_new_version_available(
1029        this: Entity<Self>,
1030        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
1031        mut cx: AsyncApp,
1032    ) -> Result<()> {
1033        this.update(&mut cx, |this, _| {
1034            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1035                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1036                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
1037            {
1038                new_version_available_tx
1039                    .send(Some(envelope.payload.version))
1040                    .ok();
1041            }
1042        });
1043        Ok(())
1044    }
1045
1046    pub fn get_extension_id_for_agent(
1047        &mut self,
1048        name: &ExternalAgentServerName,
1049    ) -> Option<Arc<str>> {
1050        self.external_agents.get_mut(name).and_then(|entry| {
1051            entry
1052                .server
1053                .as_any_mut()
1054                .downcast_ref::<LocalExtensionArchiveAgent>()
1055                .map(|ext_agent| ext_agent.extension_id.clone())
1056        })
1057    }
1058}
1059
1060fn get_or_npm_install_builtin_agent(
1061    binary_name: SharedString,
1062    package_name: SharedString,
1063    entrypoint_path: PathBuf,
1064    minimum_version: Option<semver::Version>,
1065    status_tx: Option<watch::Sender<SharedString>>,
1066    new_version_available: Option<watch::Sender<Option<String>>>,
1067    fs: Arc<dyn Fs>,
1068    node_runtime: NodeRuntime,
1069    cx: &mut AsyncApp,
1070) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
1071    cx.spawn(async move |cx| {
1072        let node_path = node_runtime.binary_path().await?;
1073        let dir = paths::external_agents_dir().join(binary_name.as_str());
1074        fs.create_dir(&dir).await?;
1075
1076        let mut stream = fs.read_dir(&dir).await?;
1077        let mut versions = Vec::new();
1078        let mut to_delete = Vec::new();
1079        while let Some(entry) = stream.next().await {
1080            let Ok(entry) = entry else { continue };
1081            let Some(file_name) = entry.file_name() else {
1082                continue;
1083            };
1084
1085            if let Some(name) = file_name.to_str()
1086                && let Some(version) = semver::Version::from_str(name).ok()
1087                && fs
1088                    .is_file(&dir.join(file_name).join(&entrypoint_path))
1089                    .await
1090            {
1091                versions.push((version, file_name.to_owned()));
1092            } else {
1093                to_delete.push(file_name.to_owned())
1094            }
1095        }
1096
1097        versions.sort();
1098        let newest_version = if let Some((version, _)) = versions.last().cloned()
1099            && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
1100        {
1101            versions.pop()
1102        } else {
1103            None
1104        };
1105        log::debug!("existing version of {package_name}: {newest_version:?}");
1106        to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
1107
1108        cx.background_spawn({
1109            let fs = fs.clone();
1110            let dir = dir.clone();
1111            async move {
1112                for file_name in to_delete {
1113                    fs.remove_dir(
1114                        &dir.join(file_name),
1115                        RemoveOptions {
1116                            recursive: true,
1117                            ignore_if_not_exists: false,
1118                        },
1119                    )
1120                    .await
1121                    .ok();
1122                }
1123            }
1124        })
1125        .detach();
1126
1127        let version = if let Some((version, file_name)) = newest_version {
1128            cx.background_spawn({
1129                let dir = dir.clone();
1130                let fs = fs.clone();
1131                async move {
1132                    let latest_version = node_runtime
1133                        .npm_package_latest_version(&package_name)
1134                        .await
1135                        .ok();
1136                    if let Some(latest_version) = latest_version
1137                        && latest_version != version
1138                    {
1139                        let download_result = download_latest_version(
1140                            fs,
1141                            dir.clone(),
1142                            node_runtime,
1143                            package_name.clone(),
1144                        )
1145                        .await
1146                        .log_err();
1147                        if let Some(mut new_version_available) = new_version_available
1148                            && download_result.is_some()
1149                        {
1150                            new_version_available
1151                                .send(Some(latest_version.to_string()))
1152                                .ok();
1153                        }
1154                    }
1155                }
1156            })
1157            .detach();
1158            file_name
1159        } else {
1160            if let Some(mut status_tx) = status_tx {
1161                status_tx.send("Installing…".into()).ok();
1162            }
1163            let dir = dir.clone();
1164            cx.background_spawn(download_latest_version(
1165                fs.clone(),
1166                dir.clone(),
1167                node_runtime,
1168                package_name.clone(),
1169            ))
1170            .await?
1171            .to_string()
1172            .into()
1173        };
1174
1175        let agent_server_path = dir.join(version).join(entrypoint_path);
1176        let agent_server_path_exists = fs.is_file(&agent_server_path).await;
1177        anyhow::ensure!(
1178            agent_server_path_exists,
1179            "Missing entrypoint path {} after installation",
1180            agent_server_path.to_string_lossy()
1181        );
1182
1183        anyhow::Ok(AgentServerCommand {
1184            path: node_path,
1185            args: vec![agent_server_path.to_string_lossy().into_owned()],
1186            env: None,
1187        })
1188    })
1189}
1190
1191fn find_bin_in_path(
1192    bin_name: SharedString,
1193    root_dir: PathBuf,
1194    env: HashMap<String, String>,
1195    cx: &mut AsyncApp,
1196) -> Task<Option<PathBuf>> {
1197    cx.background_executor().spawn(async move {
1198        let which_result = if cfg!(windows) {
1199            which::which(bin_name.as_str())
1200        } else {
1201            let shell_path = env.get("PATH").cloned();
1202            which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
1203        };
1204
1205        if let Err(which::Error::CannotFindBinaryPath) = which_result {
1206            return None;
1207        }
1208
1209        which_result.log_err()
1210    })
1211}
1212
1213async fn download_latest_version(
1214    fs: Arc<dyn Fs>,
1215    dir: PathBuf,
1216    node_runtime: NodeRuntime,
1217    package_name: SharedString,
1218) -> Result<Version> {
1219    log::debug!("downloading latest version of {package_name}");
1220
1221    let tmp_dir = tempfile::tempdir_in(&dir)?;
1222
1223    node_runtime
1224        .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
1225        .await?;
1226
1227    let version = node_runtime
1228        .npm_package_installed_version(tmp_dir.path(), &package_name)
1229        .await?
1230        .context("expected package to be installed")?;
1231
1232    fs.rename(
1233        &tmp_dir.keep(),
1234        &dir.join(version.to_string()),
1235        RenameOptions {
1236            ignore_if_exists: true,
1237            overwrite: true,
1238            create_parents: false,
1239        },
1240    )
1241    .await?;
1242
1243    anyhow::Ok(version)
1244}
1245
1246struct RemoteExternalAgentServer {
1247    project_id: u64,
1248    upstream_client: Entity<RemoteClient>,
1249    name: ExternalAgentServerName,
1250    status_tx: Option<watch::Sender<SharedString>>,
1251    new_version_available_tx: Option<watch::Sender<Option<String>>>,
1252}
1253
1254impl ExternalAgentServer for RemoteExternalAgentServer {
1255    fn get_command(
1256        &mut self,
1257        root_dir: Option<&str>,
1258        extra_env: HashMap<String, String>,
1259        status_tx: Option<watch::Sender<SharedString>>,
1260        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1261        cx: &mut AsyncApp,
1262    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1263        let project_id = self.project_id;
1264        let name = self.name.to_string();
1265        let upstream_client = self.upstream_client.downgrade();
1266        let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
1267        self.status_tx = status_tx;
1268        self.new_version_available_tx = new_version_available_tx;
1269        cx.spawn(async move |cx| {
1270            let mut response = upstream_client
1271                .update(cx, |upstream_client, _| {
1272                    upstream_client
1273                        .proto_client()
1274                        .request(proto::GetAgentServerCommand {
1275                            project_id,
1276                            name,
1277                            root_dir: root_dir.clone(),
1278                        })
1279                })?
1280                .await?;
1281            let root_dir = response.root_dir;
1282            response.env.extend(extra_env);
1283            let command = upstream_client.update(cx, |client, _| {
1284                client.build_command_with_options(
1285                    Some(response.path),
1286                    &response.args,
1287                    &response.env.into_iter().collect(),
1288                    Some(root_dir.clone()),
1289                    None,
1290                    Interactive::No,
1291                )
1292            })??;
1293            Ok((
1294                AgentServerCommand {
1295                    path: command.program.into(),
1296                    args: command.args,
1297                    env: Some(command.env),
1298                },
1299                root_dir,
1300                response.login.map(SpawnInTerminal::from_proto),
1301            ))
1302        })
1303    }
1304
1305    fn as_any_mut(&mut self) -> &mut dyn Any {
1306        self
1307    }
1308}
1309
1310struct LocalGemini {
1311    fs: Arc<dyn Fs>,
1312    node_runtime: NodeRuntime,
1313    project_environment: Entity<ProjectEnvironment>,
1314    custom_command: Option<AgentServerCommand>,
1315    settings_env: Option<HashMap<String, String>>,
1316    ignore_system_version: bool,
1317}
1318
1319impl ExternalAgentServer for LocalGemini {
1320    fn get_command(
1321        &mut self,
1322        root_dir: Option<&str>,
1323        extra_env: HashMap<String, String>,
1324        status_tx: Option<watch::Sender<SharedString>>,
1325        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1326        cx: &mut AsyncApp,
1327    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1328        let fs = self.fs.clone();
1329        let node_runtime = self.node_runtime.clone();
1330        let project_environment = self.project_environment.downgrade();
1331        let custom_command = self.custom_command.clone();
1332        let settings_env = self.settings_env.clone();
1333        let ignore_system_version = self.ignore_system_version;
1334        let root_dir: Arc<Path> = root_dir
1335            .map(|root_dir| Path::new(root_dir))
1336            .unwrap_or(paths::home_dir())
1337            .into();
1338
1339        cx.spawn(async move |cx| {
1340            let mut env = project_environment
1341                .update(cx, |project_environment, cx| {
1342                    project_environment.local_directory_environment(
1343                        &Shell::System,
1344                        root_dir.clone(),
1345                        cx,
1346                    )
1347                })?
1348                .await
1349                .unwrap_or_default();
1350
1351            env.extend(settings_env.unwrap_or_default());
1352
1353            let mut command = if let Some(mut custom_command) = custom_command {
1354                custom_command.env = Some(env);
1355                custom_command
1356            } else if !ignore_system_version
1357                && let Some(bin) =
1358                    find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
1359            {
1360                AgentServerCommand {
1361                    path: bin,
1362                    args: Vec::new(),
1363                    env: Some(env),
1364                }
1365            } else {
1366                let mut command = get_or_npm_install_builtin_agent(
1367                    GEMINI_NAME.into(),
1368                    "@google/gemini-cli".into(),
1369                    "node_modules/@google/gemini-cli/dist/index.js".into(),
1370                    if cfg!(windows) {
1371                        // v0.8.x on Windows has a bug that causes the initialize request to hang forever
1372                        Some("0.9.0".parse().unwrap())
1373                    } else {
1374                        Some("0.2.1".parse().unwrap())
1375                    },
1376                    status_tx,
1377                    new_version_available_tx,
1378                    fs,
1379                    node_runtime,
1380                    cx,
1381                )
1382                .await?;
1383                command.env = Some(env);
1384                command
1385            };
1386
1387            // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
1388            let login = task::SpawnInTerminal {
1389                command: Some(command.path.to_string_lossy().into_owned()),
1390                args: command.args.clone(),
1391                env: command.env.clone().unwrap_or_default(),
1392                label: "gemini /auth".into(),
1393                ..Default::default()
1394            };
1395
1396            command.env.get_or_insert_default().extend(extra_env);
1397            command.args.push("--experimental-acp".into());
1398            Ok((
1399                command,
1400                root_dir.to_string_lossy().into_owned(),
1401                Some(login),
1402            ))
1403        })
1404    }
1405
1406    fn as_any_mut(&mut self) -> &mut dyn Any {
1407        self
1408    }
1409}
1410
1411struct LocalClaudeCode {
1412    fs: Arc<dyn Fs>,
1413    node_runtime: NodeRuntime,
1414    project_environment: Entity<ProjectEnvironment>,
1415    custom_command: Option<AgentServerCommand>,
1416    settings_env: Option<HashMap<String, String>>,
1417}
1418
1419impl ExternalAgentServer for LocalClaudeCode {
1420    fn get_command(
1421        &mut self,
1422        root_dir: Option<&str>,
1423        extra_env: HashMap<String, String>,
1424        status_tx: Option<watch::Sender<SharedString>>,
1425        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1426        cx: &mut AsyncApp,
1427    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1428        let fs = self.fs.clone();
1429        let node_runtime = self.node_runtime.clone();
1430        let project_environment = self.project_environment.downgrade();
1431        let custom_command = self.custom_command.clone();
1432        let settings_env = self.settings_env.clone();
1433        let root_dir: Arc<Path> = root_dir
1434            .map(|root_dir| Path::new(root_dir))
1435            .unwrap_or(paths::home_dir())
1436            .into();
1437
1438        cx.spawn(async move |cx| {
1439            let mut env = project_environment
1440                .update(cx, |project_environment, cx| {
1441                    project_environment.local_directory_environment(
1442                        &Shell::System,
1443                        root_dir.clone(),
1444                        cx,
1445                    )
1446                })?
1447                .await
1448                .unwrap_or_default();
1449            env.insert("ANTHROPIC_API_KEY".into(), "".into());
1450
1451            env.extend(settings_env.unwrap_or_default());
1452
1453            let (mut command, login_command) = if let Some(mut custom_command) = custom_command {
1454                custom_command.env = Some(env);
1455                (custom_command, None)
1456            } else {
1457                let mut command = get_or_npm_install_builtin_agent(
1458                    "claude-agent-acp".into(),
1459                    "@zed-industries/claude-agent-acp".into(),
1460                    "node_modules/@zed-industries/claude-agent-acp/dist/index.js".into(),
1461                    Some("0.17.0".parse().unwrap()),
1462                    status_tx,
1463                    new_version_available_tx,
1464                    fs,
1465                    node_runtime,
1466                    cx,
1467                )
1468                .await?;
1469                command.env = Some(env);
1470
1471                (command, None)
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") {
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().map(|d| {
1840                                        d.strip_prefix("sha256:")
1841                                            .map(|s| s.to_string())
1842                                            .unwrap_or_else(|| 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_AGENT_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
2243impl AllAgentServersSettings {
2244    pub fn has_registry_agents(&self) -> bool {
2245        self.custom
2246            .values()
2247            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
2248    }
2249}
2250
2251#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
2252pub struct BuiltinAgentServerSettings {
2253    pub path: Option<PathBuf>,
2254    pub args: Option<Vec<String>>,
2255    pub env: Option<HashMap<String, String>>,
2256    pub ignore_system_version: Option<bool>,
2257    pub default_mode: Option<String>,
2258    pub default_model: Option<String>,
2259    pub favorite_models: Vec<String>,
2260    pub default_config_options: HashMap<String, String>,
2261    pub favorite_config_option_values: HashMap<String, Vec<String>>,
2262}
2263
2264impl BuiltinAgentServerSettings {
2265    fn custom_command(self) -> Option<AgentServerCommand> {
2266        self.path.map(|path| AgentServerCommand {
2267            path,
2268            args: self.args.unwrap_or_default(),
2269            // Settings env are always applied, so we don't need to supply them here as well
2270            env: None,
2271        })
2272    }
2273}
2274
2275impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
2276    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
2277        BuiltinAgentServerSettings {
2278            path: value
2279                .path
2280                .map(|p| PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref())),
2281            args: value.args,
2282            env: value.env,
2283            ignore_system_version: value.ignore_system_version,
2284            default_mode: value.default_mode,
2285            default_model: value.default_model,
2286            favorite_models: value.favorite_models,
2287            default_config_options: value.default_config_options,
2288            favorite_config_option_values: value.favorite_config_option_values,
2289        }
2290    }
2291}
2292
2293impl From<AgentServerCommand> for BuiltinAgentServerSettings {
2294    fn from(value: AgentServerCommand) -> Self {
2295        BuiltinAgentServerSettings {
2296            path: Some(value.path),
2297            args: Some(value.args),
2298            env: value.env,
2299            ..Default::default()
2300        }
2301    }
2302}
2303
2304#[derive(Clone, JsonSchema, Debug, PartialEq)]
2305pub enum CustomAgentServerSettings {
2306    Custom {
2307        command: AgentServerCommand,
2308        /// The default mode to use for this agent.
2309        ///
2310        /// Note: Not only all agents support modes.
2311        ///
2312        /// Default: None
2313        default_mode: Option<String>,
2314        /// The default model to use for this agent.
2315        ///
2316        /// This should be the model ID as reported by the agent.
2317        ///
2318        /// Default: None
2319        default_model: Option<String>,
2320        /// The favorite models for this agent.
2321        ///
2322        /// Default: []
2323        favorite_models: Vec<String>,
2324        /// Default values for session config options.
2325        ///
2326        /// This is a map from config option ID to value ID.
2327        ///
2328        /// Default: {}
2329        default_config_options: HashMap<String, String>,
2330        /// Favorited values for session config options.
2331        ///
2332        /// This is a map from config option ID to a list of favorited value IDs.
2333        ///
2334        /// Default: {}
2335        favorite_config_option_values: HashMap<String, Vec<String>>,
2336    },
2337    Extension {
2338        /// Additional environment variables to pass to the agent.
2339        ///
2340        /// Default: {}
2341        env: HashMap<String, String>,
2342        /// The default mode to use for this agent.
2343        ///
2344        /// Note: Not only all agents support modes.
2345        ///
2346        /// Default: None
2347        default_mode: Option<String>,
2348        /// The default model to use for this agent.
2349        ///
2350        /// This should be the model ID as reported by the agent.
2351        ///
2352        /// Default: None
2353        default_model: Option<String>,
2354        /// The favorite models for this agent.
2355        ///
2356        /// Default: []
2357        favorite_models: Vec<String>,
2358        /// Default values for session config options.
2359        ///
2360        /// This is a map from config option ID to value ID.
2361        ///
2362        /// Default: {}
2363        default_config_options: HashMap<String, String>,
2364        /// Favorited values for session config options.
2365        ///
2366        /// This is a map from config option ID to a list of favorited value IDs.
2367        ///
2368        /// Default: {}
2369        favorite_config_option_values: HashMap<String, Vec<String>>,
2370    },
2371    Registry {
2372        /// Additional environment variables to pass to the agent.
2373        ///
2374        /// Default: {}
2375        env: HashMap<String, String>,
2376        /// The default mode to use for this agent.
2377        ///
2378        /// Note: Not only all agents support modes.
2379        ///
2380        /// Default: None
2381        default_mode: Option<String>,
2382        /// The default model to use for this agent.
2383        ///
2384        /// This should be the model ID as reported by the agent.
2385        ///
2386        /// Default: None
2387        default_model: Option<String>,
2388        /// The favorite models for this agent.
2389        ///
2390        /// Default: []
2391        favorite_models: Vec<String>,
2392        /// Default values for session config options.
2393        ///
2394        /// This is a map from config option ID to value ID.
2395        ///
2396        /// Default: {}
2397        default_config_options: HashMap<String, String>,
2398        /// Favorited values for session config options.
2399        ///
2400        /// This is a map from config option ID to a list of favorited value IDs.
2401        ///
2402        /// Default: {}
2403        favorite_config_option_values: HashMap<String, Vec<String>>,
2404    },
2405}
2406
2407impl CustomAgentServerSettings {
2408    pub fn command(&self) -> Option<&AgentServerCommand> {
2409        match self {
2410            CustomAgentServerSettings::Custom { command, .. } => Some(command),
2411            CustomAgentServerSettings::Extension { .. }
2412            | CustomAgentServerSettings::Registry { .. } => None,
2413        }
2414    }
2415
2416    pub fn default_mode(&self) -> Option<&str> {
2417        match self {
2418            CustomAgentServerSettings::Custom { default_mode, .. }
2419            | CustomAgentServerSettings::Extension { default_mode, .. }
2420            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
2421        }
2422    }
2423
2424    pub fn default_model(&self) -> Option<&str> {
2425        match self {
2426            CustomAgentServerSettings::Custom { default_model, .. }
2427            | CustomAgentServerSettings::Extension { default_model, .. }
2428            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
2429        }
2430    }
2431
2432    pub fn favorite_models(&self) -> &[String] {
2433        match self {
2434            CustomAgentServerSettings::Custom {
2435                favorite_models, ..
2436            }
2437            | CustomAgentServerSettings::Extension {
2438                favorite_models, ..
2439            }
2440            | CustomAgentServerSettings::Registry {
2441                favorite_models, ..
2442            } => favorite_models,
2443        }
2444    }
2445
2446    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
2447        match self {
2448            CustomAgentServerSettings::Custom {
2449                default_config_options,
2450                ..
2451            }
2452            | CustomAgentServerSettings::Extension {
2453                default_config_options,
2454                ..
2455            }
2456            | CustomAgentServerSettings::Registry {
2457                default_config_options,
2458                ..
2459            } => default_config_options.get(config_id).map(|s| s.as_str()),
2460        }
2461    }
2462
2463    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
2464        match self {
2465            CustomAgentServerSettings::Custom {
2466                favorite_config_option_values,
2467                ..
2468            }
2469            | CustomAgentServerSettings::Extension {
2470                favorite_config_option_values,
2471                ..
2472            }
2473            | CustomAgentServerSettings::Registry {
2474                favorite_config_option_values,
2475                ..
2476            } => favorite_config_option_values
2477                .get(config_id)
2478                .map(|v| v.as_slice()),
2479        }
2480    }
2481}
2482
2483impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
2484    fn from(value: settings::CustomAgentServerSettings) -> Self {
2485        match value {
2486            settings::CustomAgentServerSettings::Custom {
2487                path,
2488                args,
2489                env,
2490                default_mode,
2491                default_model,
2492                favorite_models,
2493                default_config_options,
2494                favorite_config_option_values,
2495            } => CustomAgentServerSettings::Custom {
2496                command: AgentServerCommand {
2497                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
2498                    args,
2499                    env: Some(env),
2500                },
2501                default_mode,
2502                default_model,
2503                favorite_models,
2504                default_config_options,
2505                favorite_config_option_values,
2506            },
2507            settings::CustomAgentServerSettings::Extension {
2508                env,
2509                default_mode,
2510                default_model,
2511                default_config_options,
2512                favorite_models,
2513                favorite_config_option_values,
2514            } => CustomAgentServerSettings::Extension {
2515                env,
2516                default_mode,
2517                default_model,
2518                default_config_options,
2519                favorite_models,
2520                favorite_config_option_values,
2521            },
2522            settings::CustomAgentServerSettings::Registry {
2523                env,
2524                default_mode,
2525                default_model,
2526                default_config_options,
2527                favorite_models,
2528                favorite_config_option_values,
2529            } => CustomAgentServerSettings::Registry {
2530                env,
2531                default_mode,
2532                default_model,
2533                default_config_options,
2534                favorite_models,
2535                favorite_config_option_values,
2536            },
2537        }
2538    }
2539}
2540
2541impl settings::Settings for AllAgentServersSettings {
2542    fn from_settings(content: &settings::SettingsContent) -> Self {
2543        let agent_settings = content.agent_servers.clone().unwrap();
2544        Self {
2545            gemini: agent_settings.gemini.map(Into::into),
2546            claude: agent_settings.claude.map(Into::into),
2547            codex: agent_settings.codex.map(Into::into),
2548            custom: agent_settings
2549                .custom
2550                .into_iter()
2551                .map(|(k, v)| (k, v.into()))
2552                .collect(),
2553        }
2554    }
2555}