From e6e64017eabb20907dbdd75ddfe7e9c536b48756 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 26 Aug 2025 20:01:51 -0600 Subject: [PATCH] acp: Require gemini version 0.2.0 (#36960) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 20 ++-- crates/agent2/src/native_agent_server.rs | 4 + crates/agent_servers/src/acp.rs | 6 +- crates/agent_servers/src/agent_servers.rs | 2 + crates/agent_servers/src/claude.rs | 23 ++--- crates/agent_servers/src/custom.rs | 4 + crates/agent_servers/src/gemini.rs | 106 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 67 +++++++------- 8 files changed, 134 insertions(+), 98 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4ded647a746f18e030529e4c14a00c1ffd2335e3..0da4b43394b76125b2ea9a310ae5bfe9bf0fac9a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -789,15 +789,10 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { - NotInstalled { - error_message: SharedString, - install_message: SharedString, - install_command: String, - }, + NotInstalled, Unsupported { - error_message: SharedString, - upgrade_message: SharedString, - upgrade_command: String, + command: SharedString, + current_version: SharedString, }, Exited { status: ExitStatus, @@ -808,9 +803,12 @@ pub enum LoadError { impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NotInstalled { error_message, .. } - | LoadError::Unsupported { error_message, .. } => { - write!(f, "{error_message}") + LoadError::NotInstalled => write!(f, "not installed"), + LoadError::Unsupported { + command: path, + current_version, + } => { + write!(f, "version {current_version} from {path} is not supported") } LoadError::Exited { status } => write!(f, "Server exited with status {status}"), LoadError::Other(msg) => write!(f, "{}", msg), diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 9ff98ccd18dec4d9a17a1a7161cd3622dacf0d3f..0079dcc5724a77e02cd68be3deaee0735fcf56fa 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -42,6 +42,10 @@ impl AgentServer for NativeAgentServer { ui::IconName::ZedAgent } + fn install_command(&self) -> Option<&'static str> { + None + } + fn connect( &self, _root_dir: &Path, diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4e897374ad079265a85f2f504109abefcbc075f..b4f82a0a238ec65116688a90986f8792d78e05ed 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -56,7 +56,7 @@ impl AcpConnection { root_dir: &Path, cx: &mut AsyncApp, ) -> Result { - let mut child = util::command::new_smol_command(&command.path) + let mut child = util::command::new_smol_command(command.path) .args(command.args.iter().map(|arg| arg.as_str())) .envs(command.env.iter().flatten()) .current_dir(root_dir) @@ -150,6 +150,10 @@ impl AcpConnection { _io_task: io_task, }) } + + pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities { + &self.prompt_capabilities + } } impl AgentConnection for AcpConnection { diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 7c7e124ca71b684cdda7a24e02c82d1b6117a0cc..dc7d75c52d72928cfc3673bc1eb476f08206669f 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -46,6 +46,8 @@ pub trait AgentServer: Send { ) -> Task>>; fn into_any(self: Rc) -> Rc; + + fn install_command(&self) -> Option<&'static str>; } impl dyn AgentServer { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 250e564526d5360be7a84f5cfd9511e5f73a2c1f..3a16b0601a0a85a2a66d170455af6b4cb9f4ae8f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -63,6 +63,10 @@ impl AgentServer for ClaudeCode { ui::IconName::AiClaude } + fn install_command(&self) -> Option<&'static str> { + Some("npm install -g @anthropic-ai/claude-code@latest") + } + fn connect( &self, _root_dir: &Path, @@ -108,11 +112,7 @@ impl AgentConnection for ClaudeAgentConnection { ) .await else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Claude Code binary".into(), - install_message: "Install Claude Code".into(), - install_command: "npm install -g @anthropic-ai/claude-code@latest".into(), - }.into()); + return Err(LoadError::NotInstalled.into()); }; let api_key = @@ -230,17 +230,8 @@ impl AgentConnection for ClaudeAgentConnection { || !help.contains("--session-id")) { LoadError::Unsupported { - error_message: format!( - "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.", - command.path.to_string_lossy(), - version, - ) - .into(), - upgrade_message: "Upgrade Claude Code to latest".into(), - upgrade_command: format!( - "{} update", - command.path.to_string_lossy() - ), + command: command.path.to_string_lossy().to_string().into(), + current_version: version.to_string().into(), } } else { LoadError::Exited { status } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 72823026d7ce353e76485fbe76783b3cc2bbeb56..75928a26a8b4499d7b6ced8a8392191ac3ca2f32 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -57,6 +57,10 @@ impl crate::AgentServer for CustomAgentServer { }) } + fn install_command(&self) -> Option<&'static str> { + None + } + fn into_any(self: Rc) -> Rc { self } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 5d6a70fa64d981b2f25aee13c9d1d3ac7f94468f..33d92060a49ccb79fd12605f2089bfa0a9d65608 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,6 +1,7 @@ use std::rc::Rc; use std::{any::Any, path::Path}; +use crate::acp::AcpConnection; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; @@ -37,6 +38,10 @@ impl AgentServer for Gemini { ui::IconName::AiGemini } + fn install_command(&self) -> Option<&'static str> { + Some("npm install -g @google/gemini-cli@latest") + } + fn connect( &self, root_dir: &Path, @@ -52,48 +57,73 @@ impl AgentServer for Gemini { })?; let Some(mut command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await + AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx) + .await else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Gemini CLI binary".into(), - install_message: "Install Gemini CLI".into(), - install_command: Self::install_command().into(), - }.into()); + return Err(LoadError::NotInstalled.into()); }; - if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { - command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); + if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { + command + .env + .get_or_insert_default() + .insert("GEMINI_API_KEY".to_owned(), api_key.key); } let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; - if result.is_err() { - let version_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--version") - .kill_on_drop(true) - .output(); - - let help_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--help") - .kill_on_drop(true) - .output(); - - let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; - - let current_version = String::from_utf8(version_output?.stdout)?; - let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); - - if !supported { - return Err(LoadError::Unsupported { - error_message: format!( - "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).", - command.path.to_string_lossy(), - current_version - ).into(), - upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: Self::upgrade_command().into(), - }.into()) + match &result { + Ok(connection) => { + if let Some(connection) = connection.clone().downcast::() + && !connection.prompt_capabilities().image + { + let version_output = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output() + .await; + let current_version = + String::from_utf8(version_output?.stdout)?.trim().to_owned(); + if !connection.prompt_capabilities().image { + return Err(LoadError::Unsupported { + current_version: current_version.into(), + command: format!( + "{} {}", + command.path.to_string_lossy(), + command.args.join(" ") + ) + .into(), + } + .into()); + } + } + } + Err(_) => { + let version_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output(); + + let help_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--help") + .kill_on_drop(true) + .output(); + + let (version_output, help_output) = + futures::future::join(version_fut, help_fut).await; + + let current_version = String::from_utf8(version_output?.stdout)?; + let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); + + if !supported { + return Err(LoadError::Unsupported { + current_version: current_version.into(), + command: command.path.to_string_lossy().to_string().into(), + } + .into()); + } } } result @@ -111,11 +141,11 @@ impl Gemini { } pub fn install_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" + "npm install -g @google/gemini-cli@latest" } pub fn upgrade_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" + "npm install -g @google/gemini-cli@latest" } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f01a7958a8cfff7a6d45627a761fcdf62b76fd71..54d3421c3bff63bccf2b2cdeaa9dfb509dd7b96b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2825,19 +2825,14 @@ impl AcpThreadView { cx: &mut Context, ) -> AnyElement { let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled { - error_message: _, - install_message: _, - install_command, - } => { - return self.render_not_installed(install_command.clone(), false, window, cx); + LoadError::NotInstalled => { + return self.render_not_installed(None, window, cx); } LoadError::Unsupported { - error_message: _, - upgrade_message: _, - upgrade_command, + command: path, + current_version, } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); + return self.render_not_installed(Some((path, current_version)), window, cx); } LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), LoadError::Other(msg) => ( @@ -2855,8 +2850,11 @@ impl AcpThreadView { .into_any_element() } - fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { + fn install_agent(&self, window: &mut Window, cx: &mut Context) { telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); + let Some(install_command) = self.agent.install_command().map(|s| s.to_owned()) else { + return; + }; let task = self .workspace .update(cx, |workspace, cx| { @@ -2899,32 +2897,35 @@ impl AcpThreadView { fn render_not_installed( &self, - install_command: String, - is_upgrade: bool, + existing_version: Option<(&SharedString, &SharedString)>, window: &mut Window, cx: &mut Context, ) -> AnyElement { + let install_command = self.agent.install_command().unwrap_or_default(); + self.install_command_markdown.update(cx, |markdown, cx| { if !markdown.source().contains(&install_command) { markdown.replace(format!("```\n{}\n```", install_command), cx); } }); - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; + let (heading_label, description_label, button_label) = + if let Some((path, version)) = existing_version { + ( + format!("Upgrade {} to work with Zed", self.agent.name()), + format!( + "Currently using {}, which is only version {}", + path, version + ), + format!("Upgrade {}", self.agent.name()), + ) + } else { + ( + format!("Get Started with {} in Zed", self.agent.name()), + "Use Google's new coding agent directly in Zed.".to_string(), + format!("Install {}", self.agent.name()), + ) + }; v_flex() .w_full() @@ -2954,12 +2955,10 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - this.install_agent(install_command.clone(), window, cx) - })), + .on_click(cx.listener(|this, _, window, cx| this.install_agent(window, cx))), ) .child( - Label::new(or_label) + Label::new("Or, run the following command in your terminal:") .size(LabelSize::Small) .color(Color::Muted), ) @@ -5403,6 +5402,10 @@ pub(crate) mod tests { "Test".into() } + fn install_command(&self) -> Option<&'static str> { + None + } + fn connect( &self, _root_dir: &Path,