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 = "--experimental-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}