From 11fb57a6d96f2133c492c6da18b6a976cb2429b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Aug 2025 16:16:02 +0200 Subject: [PATCH] acp: Use the custom claude installation to perform login (#37169) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner Co-authored-by: Agus Zubiaga Co-authored-by: Nathan Sobo Co-authored-by: Cole Miller Co-authored-by: morgankrey --- Cargo.lock | 1 + crates/agent_servers/src/agent_servers.rs | 10 ++-- crates/agent_servers/src/claude.rs | 40 ++++++++++++- crates/agent_servers/src/e2e_tests.rs | 2 +- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 69 +++++++++++++++-------- 7 files changed, 94 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e493c99a2fc0f9514503b7cee8ef41cca582c387..aa1bcab9a68294baa4264916ef5a35adbeb20802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "shlex", "smol", "streaming_diff", "task", diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 83b3be76ce709c9b8c4d9f13ca55632a79e7b677..c1fc7b91ae862a25eac8da998f4b848327a3dd3e 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -44,11 +44,11 @@ pub fn init(cx: &mut App) { pub struct AgentServerDelegate { project: Entity, - status_tx: watch::Sender, + status_tx: Option>, } impl AgentServerDelegate { - pub fn new(project: Entity, status_tx: watch::Sender) -> Self { + pub fn new(project: Entity, status_tx: Option>) -> Self { Self { project, status_tx } } @@ -72,7 +72,7 @@ impl AgentServerDelegate { "External agents are not yet available in remote projects." ))); }; - let mut status_tx = self.status_tx; + let status_tx = self.status_tx; cx.spawn(async move |cx| { if !ignore_system_version { @@ -165,7 +165,9 @@ impl AgentServerDelegate { .detach(); file_name } else { - status_tx.send("Installing…".into()).ok(); + if let Some(mut status_tx) = status_tx { + status_tx.send("Installing…".into()).ok(); + } let dir = dir.clone(); cx.background_spawn(Self::download_latest_version( fs, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index db8853695ec798a8b146666292cd29f2c1fc145c..0a4f152e8afd991fed90af12aa5bbff909c8aa2d 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,8 +1,8 @@ use language_models::provider::anthropic::AnthropicLanguageModelProvider; use settings::SettingsStore; -use std::any::Any; use std::path::Path; use std::rc::Rc; +use std::{any::Any, path::PathBuf}; use anyhow::Result; use gpui::{App, AppContext as _, SharedString, Task}; @@ -13,9 +13,47 @@ use acp_thread::AgentConnection; #[derive(Clone)] pub struct ClaudeCode; +pub struct ClaudeCodeLoginCommand { + pub path: PathBuf, + pub arguments: Vec, +} + impl ClaudeCode { const BINARY_NAME: &'static str = "claude-code-acp"; const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp"; + + pub fn login_command( + delegate: AgentServerDelegate, + cx: &mut App, + ) -> Task> { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + }); + + cx.spawn(async move |cx| { + let mut command = if let Some(settings) = settings { + settings.command + } else { + cx.update(|cx| { + delegate.get_or_npm_install_builtin_agent( + Self::BINARY_NAME.into(), + Self::PACKAGE_NAME.into(), + "node_modules/@anthropic-ai/claude-code/cli.js".into(), + true, + None, + cx, + ) + })? + .await? + }; + command.args.push("/login".into()); + + Ok(ClaudeCodeLoginCommand { + path: command.path, + arguments: command.args, + }) + }) + } } impl AgentServer for ClaudeCode { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 5d2becf0ccc4b30cfeca27f4eb5ee08c2d0bb7d1..7988b86081351b29c8a19b676498db26d0b83fc3 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -498,7 +498,7 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { - let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0); + let delegate = AgentServerDelegate::new(project.clone(), None); let connection = cx .update(|cx| server.connect(current_dir.as_ref(), delegate, cx)) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6b0979ee696571841a7ec620ca48de2880f66492..6c8b9528800041d8920d935a8f75867d03719a9d 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -80,6 +80,7 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true +shlex.workspace = true smol.workspace = true streaming_diff.workspace = true task.workspace = true diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index bd5e4faf7aedba4644206794a1c7a837517c52d6..b9e85e0ee34b3dccd0dcd4a22c1fbaa05031e2d9 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -645,7 +645,7 @@ impl MessageEditor { self.project.read(cx).fs().clone(), self.history_store.clone(), )); - let delegate = AgentServerDelegate::new(self.project.clone(), watch::channel("".into()).0); + let delegate = AgentServerDelegate::new(self.project.clone(), None); let connection = server.connect(Path::new(""), delegate, cx); cx.spawn(async move |_, cx| { let agent = connection.await?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c718540c217425c8987f4282d5990579d529779e..eff9ceedd433ea8beb833108fb9fea1eb3f706da 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context as _, Result, anyhow, bail}; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; @@ -423,7 +423,7 @@ impl AcpThreadView { .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); let (tx, mut rx) = watch::channel("Loading…".into()); - let delegate = AgentServerDelegate::new(project.clone(), tx); + let delegate = AgentServerDelegate::new(project.clone(), Some(tx)); let connect_task = agent.connect(&root_dir, delegate, cx); let load_task = cx.spawn_in(window, async move |this, cx| { @@ -1386,31 +1386,52 @@ impl AcpThreadView { let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { return Task::ready(Ok(())); }; - let project = workspace.read(cx).project().read(cx); + let project_entity = workspace.read(cx).project(); + let project = project_entity.read(cx); let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let terminal = terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task( - &SpawnInTerminal { - id: task::TaskId("claude-login".into()), - full_label: "claude /login".to_owned(), - label: "claude /login".to_owned(), - command: Some("claude".to_owned()), - args: vec!["/login".to_owned()], - command_label: "claude /login".to_owned(), - cwd, - use_new_terminal: true, - allow_concurrent_runs: true, - hide: task::HideStrategy::Always, - shell, - ..Default::default() - }, - window, - cx, - ) - }); - cx.spawn(async move |cx| { + let delegate = AgentServerDelegate::new(project_entity.clone(), None); + let command = ClaudeCode::login_command(delegate, cx); + + window.spawn(cx, async move |cx| { + let login_command = command.await?; + let command = login_command + .path + .to_str() + .with_context(|| format!("invalid login command: {:?}", login_command.path))?; + let command = shlex::try_quote(command)?; + let args = login_command + .arguments + .iter() + .map(|arg| { + Ok(shlex::try_quote(arg) + .context("Failed to quote argument")? + .to_string()) + }) + .collect::>>()?; + + let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { + terminal_panel.spawn_task( + &SpawnInTerminal { + id: task::TaskId("claude-login".into()), + full_label: "claude /login".to_owned(), + label: "claude /login".to_owned(), + command: Some(command.into()), + args, + command_label: "claude /login".to_owned(), + cwd, + use_new_terminal: true, + allow_concurrent_runs: true, + hide: task::HideStrategy::Always, + shell, + ..Default::default() + }, + window, + cx, + ) + })?; + let terminal = terminal.await?; let mut exit_status = terminal .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?