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}