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                current_thread: thread_rc,
111            });
112
113            Ok(connection)
114        })
115    }
116}
117
118impl Gemini {
119    async fn command(
120        &self,
121        project: &Entity<Project>,
122        cx: &mut AsyncApp,
123    ) -> Result<AgentServerCommand> {
124        let settings = cx.read_global(|settings: &SettingsStore, _| {
125            settings.get::<AllAgentServersSettings>(None).gemini.clone()
126        })?;
127
128        if let Some(command) =
129            AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await
130        {
131            return Ok(command);
132        };
133
134        let (fs, node_runtime) = project.update(cx, |project, _| {
135            (project.fs().clone(), project.node_runtime().cloned())
136        })?;
137        let node_runtime = node_runtime.context("gemini not found on path")?;
138
139        let directory = ::paths::agent_servers_dir().join("gemini");
140        fs.create_dir(&directory).await?;
141        node_runtime
142            .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
143            .await?;
144        let path = directory.join("node_modules/.bin/gemini");
145
146        Ok(AgentServerCommand {
147            path,
148            args: vec![ACP_ARG.into()],
149            env: None,
150        })
151    }
152
153    async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
154        let version_fut = util::command::new_smol_command(&command.path)
155            .args(command.args.iter())
156            .arg("--version")
157            .kill_on_drop(true)
158            .output();
159
160        let help_fut = util::command::new_smol_command(&command.path)
161            .args(command.args.iter())
162            .arg("--help")
163            .kill_on_drop(true)
164            .output();
165
166        let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
167
168        let current_version = String::from_utf8(version_output?.stdout)?;
169        let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
170
171        if supported {
172            Ok(AgentServerVersion::Supported)
173        } else {
174            Ok(AgentServerVersion::Unsupported {
175                error_message: format!(
176                    "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
177                    current_version
178                ).into(),
179                upgrade_message: "Upgrade Gemini to Latest".into(),
180                upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
181            })
182        }
183    }
184}
185
186#[cfg(test)]
187pub(crate) mod tests {
188    use super::*;
189    use crate::AgentServerCommand;
190    use std::path::Path;
191
192    crate::common_e2e_tests!(Gemini, allow_option_id = "0");
193
194    pub fn local_command() -> AgentServerCommand {
195        let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
196            .join("../../../gemini-cli/packages/cli")
197            .to_string_lossy()
198            .to_string();
199
200        AgentServerCommand {
201            path: "node".into(),
202            args: vec![cli_path, ACP_ARG.into()],
203            env: None,
204        }
205    }
206}