acp: Require gemini version 0.2.0 (#36960)

Conrad Irwin created

Release Notes:

- N/A

Change summary

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(-)

Detailed changes

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),

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,

crates/agent_servers/src/acp.rs 🔗

@@ -56,7 +56,7 @@ impl AcpConnection {
         root_dir: &Path,
         cx: &mut AsyncApp,
     ) -> Result<Self> {
-        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 {

crates/agent_servers/src/agent_servers.rs 🔗

@@ -46,6 +46,8 @@ pub trait AgentServer: Send {
     ) -> Task<Result<Rc<dyn AgentConnection>>>;
 
     fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+
+    fn install_command(&self) -> Option<&'static str>;
 }
 
 impl dyn AgentServer {

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 }

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<Self>) -> Rc<dyn std::any::Any> {
         self
     }

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::<AcpConnection>()
+                        && !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"
     }
 }
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -2825,19 +2825,14 @@ impl AcpThreadView {
         cx: &mut Context<Self>,
     ) -> 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<Self>) {
+    fn install_agent(&self, window: &mut Window, cx: &mut Context<Self>) {
         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<Self>,
     ) -> 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,