agent_servers.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::Arc,
  4};
  5
  6use anyhow::{Context as _, Result};
  7use collections::HashMap;
  8use gpui::{App, AsyncApp, Entity, SharedString};
  9use project::Project;
 10use schemars::JsonSchema;
 11use serde::{Deserialize, Serialize};
 12use settings::{Settings, SettingsSources, SettingsStore};
 13use util::{ResultExt, paths};
 14
 15pub fn init(cx: &mut App) {
 16    AllAgentServersSettings::register(cx);
 17}
 18
 19#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
 20pub struct AllAgentServersSettings {
 21    gemini: Option<AgentServerSettings>,
 22}
 23
 24#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
 25pub struct AgentServerSettings {
 26    #[serde(flatten)]
 27    command: AgentServerCommand,
 28}
 29
 30#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
 31pub struct AgentServerCommand {
 32    #[serde(rename = "command")]
 33    pub path: PathBuf,
 34    #[serde(default)]
 35    pub args: Vec<String>,
 36    pub env: Option<HashMap<String, String>>,
 37}
 38
 39pub struct Gemini;
 40
 41pub struct AgentServerVersion {
 42    pub current_version: SharedString,
 43    pub supported: bool,
 44}
 45
 46pub trait AgentServer: Send {
 47    fn command(
 48        &self,
 49        project: &Entity<Project>,
 50        cx: &mut AsyncApp,
 51    ) -> impl Future<Output = Result<AgentServerCommand>>;
 52
 53    fn version(
 54        &self,
 55        command: &AgentServerCommand,
 56    ) -> impl Future<Output = Result<AgentServerVersion>> + Send;
 57}
 58
 59const GEMINI_ACP_ARG: &str = "--acp";
 60
 61impl AgentServer for Gemini {
 62    async fn command(
 63        &self,
 64        project: &Entity<Project>,
 65        cx: &mut AsyncApp,
 66    ) -> Result<AgentServerCommand> {
 67        let custom_command = cx.read_global(|settings: &SettingsStore, _| {
 68            let settings = settings.get::<AllAgentServersSettings>(None);
 69            settings
 70                .gemini
 71                .as_ref()
 72                .map(|gemini_settings| AgentServerCommand {
 73                    path: gemini_settings.command.path.clone(),
 74                    args: gemini_settings
 75                        .command
 76                        .args
 77                        .iter()
 78                        .cloned()
 79                        .chain(std::iter::once(GEMINI_ACP_ARG.into()))
 80                        .collect(),
 81                    env: gemini_settings.command.env.clone(),
 82                })
 83        })?;
 84
 85        if let Some(custom_command) = custom_command {
 86            return Ok(custom_command);
 87        }
 88
 89        if let Some(path) = find_bin_in_path("gemini", project, cx).await {
 90            return Ok(AgentServerCommand {
 91                path,
 92                args: vec![GEMINI_ACP_ARG.into()],
 93                env: None,
 94            });
 95        }
 96
 97        let (fs, node_runtime) = project.update(cx, |project, _| {
 98            (project.fs().clone(), project.node_runtime().cloned())
 99        })?;
100        let node_runtime = node_runtime.context("gemini not found on path")?;
101
102        let directory = ::paths::agent_servers_dir().join("gemini");
103        fs.create_dir(&directory).await?;
104        node_runtime
105            .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
106            .await?;
107        let path = directory.join("node_modules/.bin/gemini");
108
109        Ok(AgentServerCommand {
110            path,
111            args: vec![GEMINI_ACP_ARG.into()],
112            env: None,
113        })
114    }
115
116    async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
117        let version_fut = util::command::new_smol_command(&command.path)
118            .args(command.args.iter())
119            .arg("--version")
120            .kill_on_drop(true)
121            .output();
122
123        let help_fut = util::command::new_smol_command(&command.path)
124            .args(command.args.iter())
125            .arg("--help")
126            .kill_on_drop(true)
127            .output();
128
129        let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
130
131        let current_version = String::from_utf8(version_output?.stdout)?.into();
132        let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
133
134        Ok(AgentServerVersion {
135            current_version,
136            supported,
137        })
138    }
139}
140
141async fn find_bin_in_path(
142    bin_name: &'static str,
143    project: &Entity<Project>,
144    cx: &mut AsyncApp,
145) -> Option<PathBuf> {
146    let (env_task, root_dir) = project
147        .update(cx, |project, cx| {
148            let worktree = project.visible_worktrees(cx).next();
149            match worktree {
150                Some(worktree) => {
151                    let env_task = project.environment().update(cx, |env, cx| {
152                        env.get_worktree_environment(worktree.clone(), cx)
153                    });
154
155                    let path = worktree.read(cx).abs_path();
156                    (env_task, path)
157                }
158                None => {
159                    let path: Arc<Path> = paths::home_dir().as_path().into();
160                    let env_task = project.environment().update(cx, |env, cx| {
161                        env.get_directory_environment(path.clone(), cx)
162                    });
163                    (env_task, path)
164                }
165            }
166        })
167        .log_err()?;
168
169    cx.background_executor()
170        .spawn(async move {
171            let which_result = if cfg!(windows) {
172                which::which(bin_name)
173            } else {
174                let env = env_task.await.unwrap_or_default();
175                let shell_path = env.get("PATH").cloned();
176                which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
177            };
178
179            if let Err(which::Error::CannotFindBinaryPath) = which_result {
180                return None;
181            }
182
183            which_result.log_err()
184        })
185        .await
186}
187
188impl std::fmt::Debug for AgentServerCommand {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        let filtered_env = self.env.as_ref().map(|env| {
191            env.iter()
192                .map(|(k, v)| {
193                    (
194                        k,
195                        if util::redact::should_redact(k) {
196                            "[REDACTED]"
197                        } else {
198                            v
199                        },
200                    )
201                })
202                .collect::<Vec<_>>()
203        });
204
205        f.debug_struct("AgentServerCommand")
206            .field("path", &self.path)
207            .field("args", &self.args)
208            .field("env", &filtered_env)
209            .finish()
210    }
211}
212
213impl settings::Settings for AllAgentServersSettings {
214    const KEY: Option<&'static str> = Some("agent_servers");
215
216    type FileContent = Self;
217
218    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
219        let mut settings = AllAgentServersSettings::default();
220
221        for value in sources.defaults_and_customizations() {
222            if value.gemini.is_some() {
223                settings.gemini = value.gemini.clone();
224            }
225        }
226
227        Ok(settings)
228    }
229
230    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
231}