Cargo.lock 🔗
@@ -414,6 +414,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
+ "shlex",
"smol",
"streaming_diff",
"task",
Antonio Scandurra , Bennet Bo Fenner , Agus Zubiaga , Nathan Sobo , Cole Miller , and morgankrey created
Release Notes:
- N/A
---------
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: morgankrey <morgan@zed.dev>
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(-)
@@ -414,6 +414,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
+ "shlex",
"smol",
"streaming_diff",
"task",
@@ -44,11 +44,11 @@ pub fn init(cx: &mut App) {
pub struct AgentServerDelegate {
project: Entity<Project>,
- status_tx: watch::Sender<SharedString>,
+ status_tx: Option<watch::Sender<SharedString>>,
}
impl AgentServerDelegate {
- pub fn new(project: Entity<Project>, status_tx: watch::Sender<SharedString>) -> Self {
+ pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> 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,
@@ -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<String>,
+}
+
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<Result<ClaudeCodeLoginCommand>> {
+ let settings = cx.read_global(|settings: &SettingsStore, _| {
+ settings.get::<AllAgentServersSettings>(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 {
@@ -498,7 +498,7 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
- 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))
@@ -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
@@ -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?;
@@ -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::<TerminalPanel>(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::<Result<Vec<_>>>()?;
+
+ 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))?