agent_server_store.rs

   1use std::{
   2    any::Any,
   3    borrow::Borrow,
   4    path::{Path, PathBuf},
   5    str::FromStr as _,
   6    sync::Arc,
   7    time::Duration,
   8};
   9
  10use anyhow::{Context as _, Result, bail};
  11use collections::HashMap;
  12use fs::{Fs, RemoveOptions, RenameOptions};
  13use futures::StreamExt as _;
  14use gpui::{
  15    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
  16};
  17use node_runtime::NodeRuntime;
  18use remote::RemoteClient;
  19use rpc::{AnyProtoClient, TypedEnvelope, proto};
  20use schemars::JsonSchema;
  21use serde::{Deserialize, Serialize};
  22use settings::{SettingsContent, SettingsStore};
  23use util::{ResultExt as _, debug_panic};
  24
  25use crate::ProjectEnvironment;
  26
  27#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
  28pub struct AgentServerCommand {
  29    #[serde(rename = "command")]
  30    pub path: PathBuf,
  31    #[serde(default)]
  32    pub args: Vec<String>,
  33    pub env: Option<HashMap<String, String>>,
  34}
  35
  36impl std::fmt::Debug for AgentServerCommand {
  37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  38        let filtered_env = self.env.as_ref().map(|env| {
  39            env.iter()
  40                .map(|(k, v)| {
  41                    (
  42                        k,
  43                        if util::redact::should_redact(k) {
  44                            "[REDACTED]"
  45                        } else {
  46                            v
  47                        },
  48                    )
  49                })
  50                .collect::<Vec<_>>()
  51        });
  52
  53        f.debug_struct("AgentServerCommand")
  54            .field("path", &self.path)
  55            .field("args", &self.args)
  56            .field("env", &filtered_env)
  57            .finish()
  58    }
  59}
  60
  61#[derive(Clone, Debug, PartialEq, Eq, Hash)]
  62pub struct ExternalAgentServerName(pub SharedString);
  63
  64impl std::fmt::Display for ExternalAgentServerName {
  65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  66        write!(f, "{}", self.0)
  67    }
  68}
  69
  70impl From<&'static str> for ExternalAgentServerName {
  71    fn from(value: &'static str) -> Self {
  72        ExternalAgentServerName(value.into())
  73    }
  74}
  75
  76impl From<ExternalAgentServerName> for SharedString {
  77    fn from(value: ExternalAgentServerName) -> Self {
  78        value.0
  79    }
  80}
  81
  82impl Borrow<str> for ExternalAgentServerName {
  83    fn borrow(&self) -> &str {
  84        &self.0
  85    }
  86}
  87
  88pub trait ExternalAgentServer {
  89    fn get_command(
  90        &mut self,
  91        root_dir: Option<&str>,
  92        extra_env: HashMap<String, String>,
  93        status_tx: Option<watch::Sender<SharedString>>,
  94        new_version_available_tx: Option<watch::Sender<Option<String>>>,
  95        cx: &mut AsyncApp,
  96    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>>;
  97
  98    fn as_any_mut(&mut self) -> &mut dyn Any;
  99}
 100
 101impl dyn ExternalAgentServer {
 102    fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
 103        self.as_any_mut().downcast_mut()
 104    }
 105}
 106
 107enum AgentServerStoreState {
 108    Local {
 109        node_runtime: NodeRuntime,
 110        fs: Arc<dyn Fs>,
 111        project_environment: Entity<ProjectEnvironment>,
 112        downstream_client: Option<(u64, AnyProtoClient)>,
 113        settings: Option<AllAgentServersSettings>,
 114        _subscriptions: [Subscription; 1],
 115    },
 116    Remote {
 117        project_id: u64,
 118        upstream_client: Entity<RemoteClient>,
 119    },
 120    Collab,
 121}
 122
 123pub struct AgentServerStore {
 124    state: AgentServerStoreState,
 125    external_agents: HashMap<ExternalAgentServerName, Box<dyn ExternalAgentServer>>,
 126}
 127
 128pub struct AgentServersUpdated;
 129
 130impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
 131
 132impl AgentServerStore {
 133    pub fn init_remote(session: &AnyProtoClient) {
 134        session.add_entity_message_handler(Self::handle_external_agents_updated);
 135        session.add_entity_message_handler(Self::handle_loading_status_updated);
 136        session.add_entity_message_handler(Self::handle_new_version_available);
 137    }
 138
 139    pub fn init_headless(session: &AnyProtoClient) {
 140        session.add_entity_request_handler(Self::handle_get_agent_server_command);
 141    }
 142
 143    fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
 144        let AgentServerStoreState::Local {
 145            node_runtime,
 146            fs,
 147            project_environment,
 148            downstream_client,
 149            settings: old_settings,
 150            ..
 151        } = &mut self.state
 152        else {
 153            debug_panic!(
 154                "should not be subscribed to agent server settings changes in non-local project"
 155            );
 156            return;
 157        };
 158
 159        let new_settings = cx
 160            .global::<SettingsStore>()
 161            .get::<AllAgentServersSettings>(None)
 162            .clone();
 163        if Some(&new_settings) == old_settings.as_ref() {
 164            return;
 165        }
 166
 167        self.external_agents.clear();
 168        self.external_agents.insert(
 169            GEMINI_NAME.into(),
 170            Box::new(LocalGemini {
 171                fs: fs.clone(),
 172                node_runtime: node_runtime.clone(),
 173                project_environment: project_environment.clone(),
 174                custom_command: new_settings
 175                    .gemini
 176                    .clone()
 177                    .and_then(|settings| settings.custom_command()),
 178                ignore_system_version: new_settings
 179                    .gemini
 180                    .as_ref()
 181                    .and_then(|settings| settings.ignore_system_version)
 182                    .unwrap_or(true),
 183            }),
 184        );
 185        self.external_agents.insert(
 186            CLAUDE_CODE_NAME.into(),
 187            Box::new(LocalClaudeCode {
 188                fs: fs.clone(),
 189                node_runtime: node_runtime.clone(),
 190                project_environment: project_environment.clone(),
 191                custom_command: new_settings
 192                    .claude
 193                    .clone()
 194                    .and_then(|settings| settings.custom_command()),
 195            }),
 196        );
 197        self.external_agents
 198            .extend(new_settings.custom.iter().map(|(name, settings)| {
 199                (
 200                    ExternalAgentServerName(name.clone()),
 201                    Box::new(LocalCustomAgent {
 202                        command: settings.command.clone(),
 203                        project_environment: project_environment.clone(),
 204                    }) as Box<dyn ExternalAgentServer>,
 205                )
 206            }));
 207
 208        *old_settings = Some(new_settings.clone());
 209
 210        if let Some((project_id, downstream_client)) = downstream_client {
 211            downstream_client
 212                .send(proto::ExternalAgentsUpdated {
 213                    project_id: *project_id,
 214                    names: self
 215                        .external_agents
 216                        .keys()
 217                        .map(|name| name.to_string())
 218                        .collect(),
 219                })
 220                .log_err();
 221        }
 222        cx.emit(AgentServersUpdated);
 223    }
 224
 225    pub fn local(
 226        node_runtime: NodeRuntime,
 227        fs: Arc<dyn Fs>,
 228        project_environment: Entity<ProjectEnvironment>,
 229        cx: &mut Context<Self>,
 230    ) -> Self {
 231        let subscription = cx.observe_global::<SettingsStore>(|this, cx| {
 232            this.agent_servers_settings_changed(cx);
 233        });
 234        let mut this = Self {
 235            state: AgentServerStoreState::Local {
 236                node_runtime,
 237                fs,
 238                project_environment,
 239                downstream_client: None,
 240                settings: None,
 241                _subscriptions: [subscription],
 242            },
 243            external_agents: Default::default(),
 244        };
 245        this.agent_servers_settings_changed(cx);
 246        this
 247    }
 248
 249    pub(crate) fn remote(
 250        project_id: u64,
 251        upstream_client: Entity<RemoteClient>,
 252        _cx: &mut Context<Self>,
 253    ) -> Self {
 254        // Set up the builtin agents here so they're immediately available in
 255        // remote projects--we know that the HeadlessProject on the other end
 256        // will have them.
 257        let external_agents = [
 258            (
 259                GEMINI_NAME.into(),
 260                Box::new(RemoteExternalAgentServer {
 261                    project_id,
 262                    upstream_client: upstream_client.clone(),
 263                    name: GEMINI_NAME.into(),
 264                    status_tx: None,
 265                    new_version_available_tx: None,
 266                }) as Box<dyn ExternalAgentServer>,
 267            ),
 268            (
 269                CLAUDE_CODE_NAME.into(),
 270                Box::new(RemoteExternalAgentServer {
 271                    project_id,
 272                    upstream_client: upstream_client.clone(),
 273                    name: CLAUDE_CODE_NAME.into(),
 274                    status_tx: None,
 275                    new_version_available_tx: None,
 276                }) as Box<dyn ExternalAgentServer>,
 277            ),
 278        ]
 279        .into_iter()
 280        .collect();
 281
 282        Self {
 283            state: AgentServerStoreState::Remote {
 284                project_id,
 285                upstream_client,
 286            },
 287            external_agents,
 288        }
 289    }
 290
 291    pub(crate) fn collab(_cx: &mut Context<Self>) -> Self {
 292        Self {
 293            state: AgentServerStoreState::Collab,
 294            external_agents: Default::default(),
 295        }
 296    }
 297
 298    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 299        match &mut self.state {
 300            AgentServerStoreState::Local {
 301                downstream_client, ..
 302            } => {
 303                *downstream_client = Some((project_id, client.clone()));
 304                // Send the current list of external agents downstream, but only after a delay,
 305                // to avoid having the message arrive before the downstream project's agent server store
 306                // sets up its handlers.
 307                cx.spawn(async move |this, cx| {
 308                    cx.background_executor().timer(Duration::from_secs(1)).await;
 309                    let names = this.update(cx, |this, _| {
 310                        this.external_agents
 311                            .keys()
 312                            .map(|name| name.to_string())
 313                            .collect()
 314                    })?;
 315                    client
 316                        .send(proto::ExternalAgentsUpdated { project_id, names })
 317                        .log_err();
 318                    anyhow::Ok(())
 319                })
 320                .detach();
 321            }
 322            AgentServerStoreState::Remote { .. } => {
 323                debug_panic!(
 324                    "external agents over collab not implemented, remote project should not be shared"
 325                );
 326            }
 327            AgentServerStoreState::Collab => {
 328                debug_panic!("external agents over collab not implemented, should not be shared");
 329            }
 330        }
 331    }
 332
 333    pub fn get_external_agent(
 334        &mut self,
 335        name: &ExternalAgentServerName,
 336    ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
 337        self.external_agents
 338            .get_mut(name)
 339            .map(|agent| agent.as_mut())
 340    }
 341
 342    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
 343        self.external_agents.keys()
 344    }
 345
 346    async fn handle_get_agent_server_command(
 347        this: Entity<Self>,
 348        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 349        mut cx: AsyncApp,
 350    ) -> Result<proto::AgentServerCommand> {
 351        let (command, root_dir, login) = this
 352            .update(&mut cx, |this, cx| {
 353                let AgentServerStoreState::Local {
 354                    downstream_client, ..
 355                } = &this.state
 356                else {
 357                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 358                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 359                };
 360                let agent = this
 361                    .external_agents
 362                    .get_mut(&*envelope.payload.name)
 363                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 364                let (status_tx, new_version_available_tx) = downstream_client
 365                    .clone()
 366                    .map(|(project_id, downstream_client)| {
 367                        let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
 368                        let (new_version_available_tx, mut new_version_available_rx) =
 369                            watch::channel(None);
 370                        cx.spawn({
 371                            let downstream_client = downstream_client.clone();
 372                            let name = envelope.payload.name.clone();
 373                            async move |_, _| {
 374                                while let Some(status) = status_rx.recv().await.ok() {
 375                                    downstream_client.send(
 376                                        proto::ExternalAgentLoadingStatusUpdated {
 377                                            project_id,
 378                                            name: name.clone(),
 379                                            status: status.to_string(),
 380                                        },
 381                                    )?;
 382                                }
 383                                anyhow::Ok(())
 384                            }
 385                        })
 386                        .detach_and_log_err(cx);
 387                        cx.spawn({
 388                            let name = envelope.payload.name.clone();
 389                            async move |_, _| {
 390                                if let Some(version) =
 391                                    new_version_available_rx.recv().await.ok().flatten()
 392                                {
 393                                    downstream_client.send(
 394                                        proto::NewExternalAgentVersionAvailable {
 395                                            project_id,
 396                                            name: name.clone(),
 397                                            version,
 398                                        },
 399                                    )?;
 400                                }
 401                                anyhow::Ok(())
 402                            }
 403                        })
 404                        .detach_and_log_err(cx);
 405                        (status_tx, new_version_available_tx)
 406                    })
 407                    .unzip();
 408                anyhow::Ok(agent.get_command(
 409                    envelope.payload.root_dir.as_deref(),
 410                    HashMap::default(),
 411                    status_tx,
 412                    new_version_available_tx,
 413                    &mut cx.to_async(),
 414                ))
 415            })??
 416            .await?;
 417        Ok(proto::AgentServerCommand {
 418            path: command.path.to_string_lossy().into_owned(),
 419            args: command.args,
 420            env: command
 421                .env
 422                .map(|env| env.into_iter().collect())
 423                .unwrap_or_default(),
 424            root_dir: root_dir,
 425            login: login.map(|login| login.to_proto()),
 426        })
 427    }
 428
 429    async fn handle_external_agents_updated(
 430        this: Entity<Self>,
 431        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 432        mut cx: AsyncApp,
 433    ) -> Result<()> {
 434        this.update(&mut cx, |this, cx| {
 435            let AgentServerStoreState::Remote {
 436                project_id,
 437                upstream_client,
 438            } = &this.state
 439            else {
 440                debug_panic!(
 441                    "handle_external_agents_updated should not be called for a non-remote project"
 442                );
 443                bail!("unexpected ExternalAgentsUpdated message")
 444            };
 445
 446            let mut status_txs = this
 447                .external_agents
 448                .iter_mut()
 449                .filter_map(|(name, agent)| {
 450                    Some((
 451                        name.clone(),
 452                        agent
 453                            .downcast_mut::<RemoteExternalAgentServer>()?
 454                            .status_tx
 455                            .take(),
 456                    ))
 457                })
 458                .collect::<HashMap<_, _>>();
 459            let mut new_version_available_txs = this
 460                .external_agents
 461                .iter_mut()
 462                .filter_map(|(name, agent)| {
 463                    Some((
 464                        name.clone(),
 465                        agent
 466                            .downcast_mut::<RemoteExternalAgentServer>()?
 467                            .new_version_available_tx
 468                            .take(),
 469                    ))
 470                })
 471                .collect::<HashMap<_, _>>();
 472
 473            this.external_agents = envelope
 474                .payload
 475                .names
 476                .into_iter()
 477                .map(|name| {
 478                    let agent = RemoteExternalAgentServer {
 479                        project_id: *project_id,
 480                        upstream_client: upstream_client.clone(),
 481                        name: ExternalAgentServerName(name.clone().into()),
 482                        status_tx: status_txs.remove(&*name).flatten(),
 483                        new_version_available_tx: new_version_available_txs
 484                            .remove(&*name)
 485                            .flatten(),
 486                    };
 487                    (
 488                        ExternalAgentServerName(name.into()),
 489                        Box::new(agent) as Box<dyn ExternalAgentServer>,
 490                    )
 491                })
 492                .collect();
 493            cx.emit(AgentServersUpdated);
 494            Ok(())
 495        })?
 496    }
 497
 498    async fn handle_loading_status_updated(
 499        this: Entity<Self>,
 500        envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
 501        mut cx: AsyncApp,
 502    ) -> Result<()> {
 503        this.update(&mut cx, |this, _| {
 504            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
 505                && let Some(agent) = agent.downcast_mut::<RemoteExternalAgentServer>()
 506                && let Some(status_tx) = &mut agent.status_tx
 507            {
 508                status_tx.send(envelope.payload.status.into()).ok();
 509            }
 510        })
 511    }
 512
 513    async fn handle_new_version_available(
 514        this: Entity<Self>,
 515        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
 516        mut cx: AsyncApp,
 517    ) -> Result<()> {
 518        this.update(&mut cx, |this, _| {
 519            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
 520                && let Some(agent) = agent.downcast_mut::<RemoteExternalAgentServer>()
 521                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
 522            {
 523                new_version_available_tx
 524                    .send(Some(envelope.payload.version))
 525                    .ok();
 526            }
 527        })
 528    }
 529}
 530
 531fn get_or_npm_install_builtin_agent(
 532    binary_name: SharedString,
 533    package_name: SharedString,
 534    entrypoint_path: PathBuf,
 535    minimum_version: Option<semver::Version>,
 536    status_tx: Option<watch::Sender<SharedString>>,
 537    new_version_available: Option<watch::Sender<Option<String>>>,
 538    fs: Arc<dyn Fs>,
 539    node_runtime: NodeRuntime,
 540    cx: &mut AsyncApp,
 541) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
 542    cx.spawn(async move |cx| {
 543        let node_path = node_runtime.binary_path().await?;
 544        let dir = paths::data_dir()
 545            .join("external_agents")
 546            .join(binary_name.as_str());
 547        fs.create_dir(&dir).await?;
 548
 549        let mut stream = fs.read_dir(&dir).await?;
 550        let mut versions = Vec::new();
 551        let mut to_delete = Vec::new();
 552        while let Some(entry) = stream.next().await {
 553            let Ok(entry) = entry else { continue };
 554            let Some(file_name) = entry.file_name() else {
 555                continue;
 556            };
 557
 558            if let Some(name) = file_name.to_str()
 559                && let Some(version) = semver::Version::from_str(name).ok()
 560                && fs
 561                    .is_file(&dir.join(file_name).join(&entrypoint_path))
 562                    .await
 563            {
 564                versions.push((version, file_name.to_owned()));
 565            } else {
 566                to_delete.push(file_name.to_owned())
 567            }
 568        }
 569
 570        versions.sort();
 571        let newest_version = if let Some((version, file_name)) = versions.last().cloned()
 572            && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
 573        {
 574            versions.pop();
 575            Some(file_name)
 576        } else {
 577            None
 578        };
 579        log::debug!("existing version of {package_name}: {newest_version:?}");
 580        to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
 581
 582        cx.background_spawn({
 583            let fs = fs.clone();
 584            let dir = dir.clone();
 585            async move {
 586                for file_name in to_delete {
 587                    fs.remove_dir(
 588                        &dir.join(file_name),
 589                        RemoveOptions {
 590                            recursive: true,
 591                            ignore_if_not_exists: false,
 592                        },
 593                    )
 594                    .await
 595                    .ok();
 596                }
 597            }
 598        })
 599        .detach();
 600
 601        let version = if let Some(file_name) = newest_version {
 602            cx.background_spawn({
 603                let file_name = file_name.clone();
 604                let dir = dir.clone();
 605                let fs = fs.clone();
 606                async move {
 607                    let latest_version =
 608                        node_runtime.npm_package_latest_version(&package_name).await;
 609                    if let Ok(latest_version) = latest_version
 610                        && &latest_version != &file_name.to_string_lossy()
 611                    {
 612                        let download_result = download_latest_version(
 613                            fs,
 614                            dir.clone(),
 615                            node_runtime,
 616                            package_name.clone(),
 617                        )
 618                        .await
 619                        .log_err();
 620                        if let Some(mut new_version_available) = new_version_available
 621                            && download_result.is_some()
 622                        {
 623                            new_version_available.send(Some(latest_version)).ok();
 624                        }
 625                    }
 626                }
 627            })
 628            .detach();
 629            file_name
 630        } else {
 631            if let Some(mut status_tx) = status_tx {
 632                status_tx.send("Installing…".into()).ok();
 633            }
 634            let dir = dir.clone();
 635            cx.background_spawn(download_latest_version(
 636                fs.clone(),
 637                dir.clone(),
 638                node_runtime,
 639                package_name.clone(),
 640            ))
 641            .await?
 642            .into()
 643        };
 644
 645        let agent_server_path = dir.join(version).join(entrypoint_path);
 646        let agent_server_path_exists = fs.is_file(&agent_server_path).await;
 647        anyhow::ensure!(
 648            agent_server_path_exists,
 649            "Missing entrypoint path {} after installation",
 650            agent_server_path.to_string_lossy()
 651        );
 652
 653        anyhow::Ok(AgentServerCommand {
 654            path: node_path,
 655            args: vec![agent_server_path.to_string_lossy().into_owned()],
 656            env: None,
 657        })
 658    })
 659}
 660
 661fn find_bin_in_path(
 662    bin_name: SharedString,
 663    root_dir: PathBuf,
 664    env: HashMap<String, String>,
 665    cx: &mut AsyncApp,
 666) -> Task<Option<PathBuf>> {
 667    cx.background_executor().spawn(async move {
 668        let which_result = if cfg!(windows) {
 669            which::which(bin_name.as_str())
 670        } else {
 671            let shell_path = env.get("PATH").cloned();
 672            which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
 673        };
 674
 675        if let Err(which::Error::CannotFindBinaryPath) = which_result {
 676            return None;
 677        }
 678
 679        which_result.log_err()
 680    })
 681}
 682
 683async fn download_latest_version(
 684    fs: Arc<dyn Fs>,
 685    dir: PathBuf,
 686    node_runtime: NodeRuntime,
 687    package_name: SharedString,
 688) -> Result<String> {
 689    log::debug!("downloading latest version of {package_name}");
 690
 691    let tmp_dir = tempfile::tempdir_in(&dir)?;
 692
 693    node_runtime
 694        .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
 695        .await?;
 696
 697    let version = node_runtime
 698        .npm_package_installed_version(tmp_dir.path(), &package_name)
 699        .await?
 700        .context("expected package to be installed")?;
 701
 702    fs.rename(
 703        &tmp_dir.keep(),
 704        &dir.join(&version),
 705        RenameOptions {
 706            ignore_if_exists: true,
 707            overwrite: true,
 708        },
 709    )
 710    .await?;
 711
 712    anyhow::Ok(version)
 713}
 714
 715struct RemoteExternalAgentServer {
 716    project_id: u64,
 717    upstream_client: Entity<RemoteClient>,
 718    name: ExternalAgentServerName,
 719    status_tx: Option<watch::Sender<SharedString>>,
 720    new_version_available_tx: Option<watch::Sender<Option<String>>>,
 721}
 722
 723impl ExternalAgentServer for RemoteExternalAgentServer {
 724    fn get_command(
 725        &mut self,
 726        root_dir: Option<&str>,
 727        extra_env: HashMap<String, String>,
 728        status_tx: Option<watch::Sender<SharedString>>,
 729        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 730        cx: &mut AsyncApp,
 731    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
 732        let project_id = self.project_id;
 733        let name = self.name.to_string();
 734        let upstream_client = self.upstream_client.downgrade();
 735        let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
 736        self.status_tx = status_tx;
 737        self.new_version_available_tx = new_version_available_tx;
 738        cx.spawn(async move |cx| {
 739            let mut response = upstream_client
 740                .update(cx, |upstream_client, _| {
 741                    upstream_client
 742                        .proto_client()
 743                        .request(proto::GetAgentServerCommand {
 744                            project_id,
 745                            name,
 746                            root_dir: root_dir.clone(),
 747                        })
 748                })?
 749                .await?;
 750            let root_dir = response.root_dir;
 751            response.env.extend(extra_env);
 752            let command = upstream_client.update(cx, |client, _| {
 753                client.build_command(
 754                    Some(response.path),
 755                    &response.args,
 756                    &response.env.into_iter().collect(),
 757                    Some(root_dir.clone()),
 758                    None,
 759                )
 760            })??;
 761            Ok((
 762                AgentServerCommand {
 763                    path: command.program.into(),
 764                    args: command.args,
 765                    env: Some(command.env),
 766                },
 767                root_dir,
 768                response
 769                    .login
 770                    .map(|login| task::SpawnInTerminal::from_proto(login)),
 771            ))
 772        })
 773    }
 774
 775    fn as_any_mut(&mut self) -> &mut dyn Any {
 776        self
 777    }
 778}
 779
 780struct LocalGemini {
 781    fs: Arc<dyn Fs>,
 782    node_runtime: NodeRuntime,
 783    project_environment: Entity<ProjectEnvironment>,
 784    custom_command: Option<AgentServerCommand>,
 785    ignore_system_version: bool,
 786}
 787
 788impl ExternalAgentServer for LocalGemini {
 789    fn get_command(
 790        &mut self,
 791        root_dir: Option<&str>,
 792        extra_env: HashMap<String, String>,
 793        status_tx: Option<watch::Sender<SharedString>>,
 794        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 795        cx: &mut AsyncApp,
 796    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
 797        let fs = self.fs.clone();
 798        let node_runtime = self.node_runtime.clone();
 799        let project_environment = self.project_environment.downgrade();
 800        let custom_command = self.custom_command.clone();
 801        let ignore_system_version = self.ignore_system_version;
 802        let root_dir: Arc<Path> = root_dir
 803            .map(|root_dir| Path::new(root_dir))
 804            .unwrap_or(paths::home_dir())
 805            .into();
 806
 807        cx.spawn(async move |cx| {
 808            let mut env = project_environment
 809                .update(cx, |project_environment, cx| {
 810                    project_environment.get_directory_environment(root_dir.clone(), cx)
 811                })?
 812                .await
 813                .unwrap_or_default();
 814
 815            let mut command = if let Some(mut custom_command) = custom_command {
 816                env.extend(custom_command.env.unwrap_or_default());
 817                custom_command.env = Some(env);
 818                custom_command
 819            } else if !ignore_system_version
 820                && let Some(bin) =
 821                    find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
 822            {
 823                AgentServerCommand {
 824                    path: bin,
 825                    args: Vec::new(),
 826                    env: Some(env),
 827                }
 828            } else {
 829                let mut command = get_or_npm_install_builtin_agent(
 830                    GEMINI_NAME.into(),
 831                    "@google/gemini-cli".into(),
 832                    "node_modules/@google/gemini-cli/dist/index.js".into(),
 833                    Some("0.2.1".parse().unwrap()),
 834                    status_tx,
 835                    new_version_available_tx,
 836                    fs,
 837                    node_runtime,
 838                    cx,
 839                )
 840                .await?;
 841                command.env = Some(env);
 842                command
 843            };
 844
 845            // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
 846            let login = task::SpawnInTerminal {
 847                command: Some(command.path.to_string_lossy().into_owned()),
 848                args: command.args.clone(),
 849                env: command.env.clone().unwrap_or_default(),
 850                label: "gemini /auth".into(),
 851                ..Default::default()
 852            };
 853
 854            command.env.get_or_insert_default().extend(extra_env);
 855            command.args.push("--experimental-acp".into());
 856            Ok((
 857                command,
 858                root_dir.to_string_lossy().into_owned(),
 859                Some(login),
 860            ))
 861        })
 862    }
 863
 864    fn as_any_mut(&mut self) -> &mut dyn Any {
 865        self
 866    }
 867}
 868
 869struct LocalClaudeCode {
 870    fs: Arc<dyn Fs>,
 871    node_runtime: NodeRuntime,
 872    project_environment: Entity<ProjectEnvironment>,
 873    custom_command: Option<AgentServerCommand>,
 874}
 875
 876impl ExternalAgentServer for LocalClaudeCode {
 877    fn get_command(
 878        &mut self,
 879        root_dir: Option<&str>,
 880        extra_env: HashMap<String, String>,
 881        status_tx: Option<watch::Sender<SharedString>>,
 882        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 883        cx: &mut AsyncApp,
 884    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
 885        let fs = self.fs.clone();
 886        let node_runtime = self.node_runtime.clone();
 887        let project_environment = self.project_environment.downgrade();
 888        let custom_command = self.custom_command.clone();
 889        let root_dir: Arc<Path> = root_dir
 890            .map(|root_dir| Path::new(root_dir))
 891            .unwrap_or(paths::home_dir())
 892            .into();
 893
 894        cx.spawn(async move |cx| {
 895            let mut env = project_environment
 896                .update(cx, |project_environment, cx| {
 897                    project_environment.get_directory_environment(root_dir.clone(), cx)
 898                })?
 899                .await
 900                .unwrap_or_default();
 901            env.insert("ANTHROPIC_API_KEY".into(), "".into());
 902
 903            let (mut command, login) = if let Some(mut custom_command) = custom_command {
 904                env.extend(custom_command.env.unwrap_or_default());
 905                custom_command.env = Some(env);
 906                (custom_command, None)
 907            } else {
 908                let mut command = get_or_npm_install_builtin_agent(
 909                    "claude-code-acp".into(),
 910                    "@zed-industries/claude-code-acp".into(),
 911                    "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
 912                    Some("0.2.5".parse().unwrap()),
 913                    status_tx,
 914                    new_version_available_tx,
 915                    fs,
 916                    node_runtime,
 917                    cx,
 918                )
 919                .await?;
 920                command.env = Some(env);
 921                let login = command
 922                    .args
 923                    .first()
 924                    .and_then(|path| {
 925                        path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
 926                    })
 927                    .map(|path_prefix| task::SpawnInTerminal {
 928                        command: Some(command.path.to_string_lossy().into_owned()),
 929                        args: vec![
 930                            Path::new(path_prefix)
 931                                .join("@anthropic-ai/claude-code/cli.js")
 932                                .to_string_lossy()
 933                                .to_string(),
 934                            "/login".into(),
 935                        ],
 936                        env: command.env.clone().unwrap_or_default(),
 937                        label: "claude /login".into(),
 938                        ..Default::default()
 939                    });
 940                (command, login)
 941            };
 942
 943            command.env.get_or_insert_default().extend(extra_env);
 944            Ok((command, root_dir.to_string_lossy().into_owned(), login))
 945        })
 946    }
 947
 948    fn as_any_mut(&mut self) -> &mut dyn Any {
 949        self
 950    }
 951}
 952
 953struct LocalCustomAgent {
 954    project_environment: Entity<ProjectEnvironment>,
 955    command: AgentServerCommand,
 956}
 957
 958impl ExternalAgentServer for LocalCustomAgent {
 959    fn get_command(
 960        &mut self,
 961        root_dir: Option<&str>,
 962        extra_env: HashMap<String, String>,
 963        _status_tx: Option<watch::Sender<SharedString>>,
 964        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
 965        cx: &mut AsyncApp,
 966    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
 967        let mut command = self.command.clone();
 968        let root_dir: Arc<Path> = root_dir
 969            .map(|root_dir| Path::new(root_dir))
 970            .unwrap_or(paths::home_dir())
 971            .into();
 972        let project_environment = self.project_environment.downgrade();
 973        cx.spawn(async move |cx| {
 974            let mut env = project_environment
 975                .update(cx, |project_environment, cx| {
 976                    project_environment.get_directory_environment(root_dir.clone(), cx)
 977                })?
 978                .await
 979                .unwrap_or_default();
 980            env.extend(command.env.unwrap_or_default());
 981            env.extend(extra_env);
 982            command.env = Some(env);
 983            Ok((command, root_dir.to_string_lossy().into_owned(), None))
 984        })
 985    }
 986
 987    fn as_any_mut(&mut self) -> &mut dyn Any {
 988        self
 989    }
 990}
 991
 992pub const GEMINI_NAME: &'static str = "gemini";
 993pub const CLAUDE_CODE_NAME: &'static str = "claude";
 994
 995#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
 996pub struct AllAgentServersSettings {
 997    pub gemini: Option<BuiltinAgentServerSettings>,
 998    pub claude: Option<BuiltinAgentServerSettings>,
 999    pub custom: HashMap<SharedString, CustomAgentServerSettings>,
1000}
1001#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
1002pub struct BuiltinAgentServerSettings {
1003    pub path: Option<PathBuf>,
1004    pub args: Option<Vec<String>>,
1005    pub env: Option<HashMap<String, String>>,
1006    pub ignore_system_version: Option<bool>,
1007    pub default_mode: Option<String>,
1008}
1009
1010impl BuiltinAgentServerSettings {
1011    pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
1012        self.path.map(|path| AgentServerCommand {
1013            path,
1014            args: self.args.unwrap_or_default(),
1015            env: self.env,
1016        })
1017    }
1018}
1019
1020impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
1021    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
1022        BuiltinAgentServerSettings {
1023            path: value.path,
1024            args: value.args,
1025            env: value.env,
1026            ignore_system_version: value.ignore_system_version,
1027            default_mode: value.default_mode,
1028        }
1029    }
1030}
1031
1032impl From<AgentServerCommand> for BuiltinAgentServerSettings {
1033    fn from(value: AgentServerCommand) -> Self {
1034        BuiltinAgentServerSettings {
1035            path: Some(value.path),
1036            args: Some(value.args),
1037            env: value.env,
1038            ..Default::default()
1039        }
1040    }
1041}
1042
1043#[derive(Clone, JsonSchema, Debug, PartialEq)]
1044pub struct CustomAgentServerSettings {
1045    pub command: AgentServerCommand,
1046    /// The default mode to use for this agent.
1047    ///
1048    /// Note: Not only all agents support modes.
1049    ///
1050    /// Default: None
1051    pub default_mode: Option<String>,
1052}
1053
1054impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1055    fn from(value: settings::CustomAgentServerSettings) -> Self {
1056        CustomAgentServerSettings {
1057            command: AgentServerCommand {
1058                path: value.path,
1059                args: value.args,
1060                env: value.env,
1061            },
1062            default_mode: value.default_mode,
1063        }
1064    }
1065}
1066
1067impl settings::Settings for AllAgentServersSettings {
1068    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
1069        let agent_settings = content.agent_servers.clone().unwrap();
1070        Self {
1071            gemini: agent_settings.gemini.map(Into::into),
1072            claude: agent_settings.claude.map(Into::into),
1073            custom: agent_settings
1074                .custom
1075                .into_iter()
1076                .map(|(k, v)| (k, v.into()))
1077                .collect(),
1078        }
1079    }
1080
1081    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
1082}