Detailed changes
@@ -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),
@@ -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,
@@ -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 {
@@ -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 {
@@ -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 }
@@ -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
}
@@ -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"
}
}
@@ -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,