Add version detection for CC (#36502)
- Render a helpful message when the installed CC version is too old - Show the full path for agent binaries when the version is not recent enough (helps in cases where multiple binaries are installed in different places) - Add UI for the case where a server binary is not installed at all - Refresh thread view after installing/updating server binary Release Notes: - N/A
This commit is contained in:
parent
7c7043947b
commit
3996587c0b
8 changed files with 195 additions and 85 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -285,6 +285,7 @@ dependencies = [
|
|||
"project",
|
||||
"rand 0.8.5",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
|
|
|
@ -707,7 +707,7 @@ pub enum AcpThreadEvent {
|
|||
Retry(RetryStatus),
|
||||
Stopped,
|
||||
Error,
|
||||
ServerExited(ExitStatus),
|
||||
LoadError(LoadError),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
|
@ -721,20 +721,30 @@ pub enum ThreadStatus {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadError {
|
||||
NotInstalled {
|
||||
error_message: SharedString,
|
||||
install_message: SharedString,
|
||||
install_command: String,
|
||||
},
|
||||
Unsupported {
|
||||
error_message: SharedString,
|
||||
upgrade_message: SharedString,
|
||||
upgrade_command: String,
|
||||
},
|
||||
Exited(i32),
|
||||
Exited {
|
||||
status: ExitStatus,
|
||||
},
|
||||
Other(SharedString),
|
||||
}
|
||||
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message),
|
||||
LoadError::Exited(status) => write!(f, "Server exited with status {}", status),
|
||||
LoadError::NotInstalled { error_message, .. }
|
||||
| LoadError::Unsupported { error_message, .. } => {
|
||||
write!(f, "{error_message}")
|
||||
}
|
||||
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
|
||||
LoadError::Other(msg) => write!(f, "{}", msg),
|
||||
}
|
||||
}
|
||||
|
@ -1683,8 +1693,8 @@ impl AcpThread {
|
|||
self.entries.iter().map(|e| e.to_markdown(cx)).collect()
|
||||
}
|
||||
|
||||
pub fn emit_server_exited(&mut self, status: ExitStatus, cx: &mut Context<Self>) {
|
||||
cx.emit(AcpThreadEvent::ServerExited(status));
|
||||
pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) {
|
||||
cx.emit(AcpThreadEvent::LoadError(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ paths.workspace = true
|
|||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
|
|
@ -14,7 +14,7 @@ use anyhow::{Context as _, Result};
|
|||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::{AgentServerCommand, acp::UnsupportedVersion};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: &'static str,
|
||||
|
@ -87,7 +87,9 @@ impl AcpConnection {
|
|||
for session in sessions.borrow().values() {
|
||||
session
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.emit_server_exited(status, cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_load_error(LoadError::Exited { status }, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@ use smol::process::Child;
|
|||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use util::command::new_smol_command;
|
||||
use uuid::Uuid;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
|
@ -36,7 +37,7 @@ use util::{ResultExt, debug_panic};
|
|||
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
|
||||
use crate::claude::tools::ClaudeTool;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
|
@ -103,7 +104,11 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
)
|
||||
.await
|
||||
else {
|
||||
anyhow::bail!("Failed to find claude binary");
|
||||
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());
|
||||
};
|
||||
|
||||
let api_key =
|
||||
|
@ -211,9 +216,32 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
if let Some(status) = child.status().await.log_err()
|
||||
&& let Some(thread) = thread_rx.recv().await.ok()
|
||||
{
|
||||
let version = claude_version(command.path.clone(), cx).await.log_err();
|
||||
let help = claude_help(command.path.clone(), cx).await.log_err();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_server_exited(status, cx);
|
||||
let error = if let Some(version) = version
|
||||
&& let Some(help) = help
|
||||
&& (!help.contains("--input-format")
|
||||
|| !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()
|
||||
),
|
||||
}
|
||||
} else {
|
||||
LoadError::Exited { status }
|
||||
};
|
||||
thread.emit_load_error(error, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
@ -383,6 +411,27 @@ fn spawn_claude(
|
|||
Ok(child)
|
||||
}
|
||||
|
||||
fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<semver::Version>> {
|
||||
cx.background_spawn(async move {
|
||||
let output = new_smol_command(path).arg("--version").output().await?;
|
||||
let output = String::from_utf8(output.stdout)?;
|
||||
let version = output
|
||||
.trim()
|
||||
.strip_suffix(" (Claude Code)")
|
||||
.context("parsing Claude version")?;
|
||||
let version = semver::Version::parse(version)?;
|
||||
anyhow::Ok(version)
|
||||
})
|
||||
}
|
||||
|
||||
fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<String>> {
|
||||
cx.background_spawn(async move {
|
||||
let output = new_smol_command(path).arg("--help").output().await?;
|
||||
let output = String::from_utf8(output.stdout)?;
|
||||
anyhow::Ok(output)
|
||||
})
|
||||
}
|
||||
|
||||
struct ClaudeAgentSession {
|
||||
outgoing_tx: UnboundedSender<SdkMessage>,
|
||||
turn_state: Rc<RefCell<TurnState>>,
|
||||
|
|
|
@ -50,7 +50,11 @@ impl AgentServer for Gemini {
|
|||
let Some(command) =
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
|
||||
else {
|
||||
anyhow::bail!("Failed to find gemini binary");
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: "npm install -g @google/gemini-cli@latest".into()
|
||||
}.into());
|
||||
};
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
|
@ -75,10 +79,11 @@ impl AgentServer for Gemini {
|
|||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
error_message: format!(
|
||||
"Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
|
||||
"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 to Latest".into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
|
||||
}.into())
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ use rope::Point;
|
|||
use settings::{Settings as _, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
|
||||
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
|
@ -149,9 +149,6 @@ enum ThreadState {
|
|||
configuration_view: Option<AnyView>,
|
||||
_subscription: Option<Subscription>,
|
||||
},
|
||||
ServerExited {
|
||||
status: ExitStatus,
|
||||
},
|
||||
}
|
||||
|
||||
impl AcpThreadView {
|
||||
|
@ -451,8 +448,7 @@ impl AcpThreadView {
|
|||
ThreadState::Ready { thread, .. } => Some(thread),
|
||||
ThreadState::Unauthenticated { .. }
|
||||
| ThreadState::Loading { .. }
|
||||
| ThreadState::LoadError(..)
|
||||
| ThreadState::ServerExited { .. } => None,
|
||||
| ThreadState::LoadError { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,7 +458,6 @@ impl AcpThreadView {
|
|||
ThreadState::Loading { .. } => "Loading…".into(),
|
||||
ThreadState::LoadError(_) => "Failed to load".into(),
|
||||
ThreadState::Unauthenticated { .. } => "Authentication Required".into(),
|
||||
ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -830,9 +825,9 @@ impl AcpThreadView {
|
|||
cx,
|
||||
);
|
||||
}
|
||||
AcpThreadEvent::ServerExited(status) => {
|
||||
AcpThreadEvent::LoadError(error) => {
|
||||
self.thread_retry_status.take();
|
||||
self.thread_state = ThreadState::ServerExited { status: *status };
|
||||
self.thread_state = ThreadState::LoadError(error.clone());
|
||||
}
|
||||
AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {}
|
||||
}
|
||||
|
@ -2154,28 +2149,6 @@ impl AcpThreadView {
|
|||
))
|
||||
}
|
||||
|
||||
fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.render_error_agent_logo())
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_4()
|
||||
.mb_2()
|
||||
.gap_0p5()
|
||||
.text_center()
|
||||
.items_center()
|
||||
.child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium))
|
||||
.child(
|
||||
Label::new(format!("Exit status: {}", status.code().unwrap_or(-127)))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
|
||||
let mut container = v_flex()
|
||||
.items_center()
|
||||
|
@ -2204,15 +2177,18 @@ impl AcpThreadView {
|
|||
{
|
||||
let upgrade_message = upgrade_message.clone();
|
||||
let upgrade_command = upgrade_command.clone();
|
||||
container = container.child(Button::new("upgrade", upgrade_message).on_click(
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
container = container.child(
|
||||
Button::new("upgrade", upgrade_message)
|
||||
.tooltip(Tooltip::text(upgrade_command.clone()))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
let task = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
let spawn_in_terminal = task::SpawnInTerminal {
|
||||
id: task::TaskId("install".to_string()),
|
||||
id: task::TaskId("upgrade".to_string()),
|
||||
full_label: upgrade_command.clone(),
|
||||
label: upgrade_command.clone(),
|
||||
command: Some(upgrade_command.clone()),
|
||||
|
@ -2230,13 +2206,73 @@ impl AcpThreadView {
|
|||
show_command: true,
|
||||
show_rerun: false,
|
||||
};
|
||||
workspace
|
||||
.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||
.detach();
|
||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
));
|
||||
let Some(task) = task else { return };
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(Ok(_)) = task.await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
})),
|
||||
);
|
||||
} else if let LoadError::NotInstalled {
|
||||
install_message,
|
||||
install_command,
|
||||
..
|
||||
} = e
|
||||
{
|
||||
let install_message = install_message.clone();
|
||||
let install_command = install_command.clone();
|
||||
container = container.child(
|
||||
Button::new("install", install_message)
|
||||
.tooltip(Tooltip::text(install_command.clone()))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
let task = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
let spawn_in_terminal = task::SpawnInTerminal {
|
||||
id: task::TaskId("install".to_string()),
|
||||
full_label: install_command.clone(),
|
||||
label: install_command.clone(),
|
||||
command: Some(install_command.clone()),
|
||||
args: Vec::new(),
|
||||
command_label: install_command.clone(),
|
||||
cwd,
|
||||
env: Default::default(),
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: Default::default(),
|
||||
reveal_target: Default::default(),
|
||||
hide: Default::default(),
|
||||
shell,
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
show_rerun: false,
|
||||
};
|
||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||
})
|
||||
.ok();
|
||||
let Some(task) = task else { return };
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(Ok(_)) = task.await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
container.into_any()
|
||||
|
@ -3705,6 +3741,18 @@ impl AcpThreadView {
|
|||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_state = Self::initial_state(
|
||||
self.agent.clone(),
|
||||
None,
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AcpThreadView {
|
||||
|
@ -3743,12 +3791,6 @@ impl Render for AcpThreadView {
|
|||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.render_load_error(e, cx)),
|
||||
ThreadState::ServerExited { status } => v_flex()
|
||||
.p_2()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.render_server_exited(*status, cx)),
|
||||
ThreadState::Ready { thread, .. } => {
|
||||
let thread_clone = thread.clone();
|
||||
|
||||
|
|
|
@ -1522,7 +1522,7 @@ impl AgentDiff {
|
|||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => {
|
||||
AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
AcpThreadEvent::TitleUpdated
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue