gemini.rs

  1use anyhow::anyhow;
  2use std::cell::RefCell;
  3use std::path::Path;
  4use std::rc::Rc;
  5use util::ResultExt as _;
  6
  7use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
  8use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate};
  9use agentic_coding_protocol as acp_old;
 10use anyhow::{Context as _, Result};
 11use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
 12use project::Project;
 13use settings::SettingsStore;
 14use ui::App;
 15
 16use crate::AllAgentServersSettings;
 17
 18#[derive(Clone)]
 19pub struct Gemini;
 20
 21const ACP_ARG: &str = "--experimental-acp";
 22
 23impl AgentServer for Gemini {
 24    fn name(&self) -> &'static str {
 25        "Gemini"
 26    }
 27
 28    fn empty_state_headline(&self) -> &'static str {
 29        "Welcome to Gemini"
 30    }
 31
 32    fn empty_state_message(&self) -> &'static str {
 33        "Ask questions, edit files, run commands.\nBe specific for the best results."
 34    }
 35
 36    fn logo(&self) -> ui::IconName {
 37        ui::IconName::AiGemini
 38    }
 39
 40    fn connect(
 41        &self,
 42        root_dir: &Path,
 43        project: &Entity<Project>,
 44        cx: &mut App,
 45    ) -> Task<Result<Rc<dyn AgentConnection>>> {
 46        let root_dir = root_dir.to_path_buf();
 47        let project = project.clone();
 48        let this = self.clone();
 49        let name = self.name();
 50
 51        cx.spawn(async move |cx| {
 52            let command = this.command(&project, cx).await?;
 53
 54            let mut child = util::command::new_smol_command(&command.path)
 55                .args(command.args.iter())
 56                .current_dir(root_dir)
 57                .stdin(std::process::Stdio::piped())
 58                .stdout(std::process::Stdio::piped())
 59                .stderr(std::process::Stdio::inherit())
 60                .kill_on_drop(true)
 61                .spawn()?;
 62
 63            let stdin = child.stdin.take().unwrap();
 64            let stdout = child.stdout.take().unwrap();
 65
 66            let foreground_executor = cx.foreground_executor().clone();
 67
 68            let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
 69
 70            let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
 71                OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
 72                stdin,
 73                stdout,
 74                move |fut| foreground_executor.spawn(fut).detach(),
 75            );
 76
 77            let io_task = cx.background_spawn(async move {
 78                io_fut.await.log_err();
 79            });
 80
 81            let child_status = cx.background_spawn(async move {
 82                let result = match child.status().await {
 83                    Err(e) => Err(anyhow!(e)),
 84                    Ok(result) if result.success() => Ok(()),
 85                    Ok(result) => {
 86                        if let Some(AgentServerVersion::Unsupported {
 87                            error_message,
 88                            upgrade_message,
 89                            upgrade_command,
 90                        }) = this.version(&command).await.log_err()
 91                        {
 92                            Err(anyhow!(LoadError::Unsupported {
 93                                error_message,
 94                                upgrade_message,
 95                                upgrade_command
 96                            }))
 97                        } else {
 98                            Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
 99                        }
100                    }
101                };
102                drop(io_task);
103                result
104            });
105
106            let connection: Rc<dyn AgentConnection> = Rc::new(OldAcpAgentConnection {
107                name,
108                connection,
109                child_status,
110            });
111
112            Ok(connection)
113        })
114    }
115}
116
117impl Gemini {
118    async fn command(
119        &self,
120        project: &Entity<Project>,
121        cx: &mut AsyncApp,
122    ) -> Result<AgentServerCommand> {
123        let settings = cx.read_global(|settings: &SettingsStore, _| {
124            settings.get::<AllAgentServersSettings>(None).gemini.clone()
125        })?;
126
127        if let Some(command) =
128            AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await
129        {
130            return Ok(command);
131        };
132
133        let (fs, node_runtime) = project.update(cx, |project, _| {
134            (project.fs().clone(), project.node_runtime().cloned())
135        })?;
136        let node_runtime = node_runtime.context("gemini not found on path")?;
137
138        let directory = ::paths::agent_servers_dir().join("gemini");
139        fs.create_dir(&directory).await?;
140        node_runtime
141            .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
142            .await?;
143        let path = directory.join("node_modules/.bin/gemini");
144
145        Ok(AgentServerCommand {
146            path,
147            args: vec![ACP_ARG.into()],
148            env: None,
149        })
150    }
151
152    async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
153        let version_fut = util::command::new_smol_command(&command.path)
154            .args(command.args.iter())
155            .arg("--version")
156            .kill_on_drop(true)
157            .output();
158
159        let help_fut = util::command::new_smol_command(&command.path)
160            .args(command.args.iter())
161            .arg("--help")
162            .kill_on_drop(true)
163            .output();
164
165        let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
166
167        let current_version = String::from_utf8(version_output?.stdout)?;
168        let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
169
170        if supported {
171            Ok(AgentServerVersion::Supported)
172        } else {
173            Ok(AgentServerVersion::Unsupported {
174                error_message: format!(
175                    "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
176                    current_version
177                ).into(),
178                upgrade_message: "Upgrade Gemini to Latest".into(),
179                upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
180            })
181        }
182    }
183}
184
185#[cfg(test)]
186pub(crate) mod tests {
187    use super::*;
188    use crate::AgentServerCommand;
189    use std::path::Path;
190
191    crate::common_e2e_tests!(Gemini, allow_option_id = "0");
192
193    pub fn local_command() -> AgentServerCommand {
194        let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
195            .join("../../../gemini-cli/packages/cli")
196            .to_string_lossy()
197            .to_string();
198
199        AgentServerCommand {
200            path: "node".into(),
201            args: vec![cli_path, ACP_ARG.into()],
202            env: None,
203        }
204    }
205}