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}