1use settings::SettingsStore;
2use std::path::Path;
3use std::rc::Rc;
4use std::{any::Any, path::PathBuf};
5
6use anyhow::{Result, bail};
7use gpui::{App, AppContext as _, SharedString, Task};
8
9use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
10use acp_thread::AgentConnection;
11
12#[derive(Clone)]
13pub struct ClaudeCode;
14
15pub struct ClaudeCodeLoginCommand {
16 pub path: PathBuf,
17 pub arguments: Vec<String>,
18}
19
20impl ClaudeCode {
21 const BINARY_NAME: &'static str = "claude-code-acp";
22 const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp";
23
24 pub fn login_command(
25 delegate: AgentServerDelegate,
26 cx: &mut App,
27 ) -> Task<Result<ClaudeCodeLoginCommand>> {
28 let custom_command = cx.read_global(|settings: &SettingsStore, _| {
29 settings
30 .get::<AllAgentServersSettings>(None)
31 .get("claude")
32 .cloned()
33 });
34
35 cx.spawn(async move |cx| {
36 let mut command = if custom_command.is_some() {
37 bail!("Cannot construct login command because a custom command was specified for claude-code-acp in settings")
38 } else {
39 cx.update(|cx| {
40 delegate.get_or_npm_install_builtin_agent(
41 Self::BINARY_NAME.into(),
42 Self::PACKAGE_NAME.into(),
43 "node_modules/@anthropic-ai/claude-code/cli.js".into(),
44 false,
45 Some("0.2.5".parse().unwrap()),
46 cx,
47 )
48 })?
49 .await?
50 };
51 command.args.push("/login".into());
52
53 Ok(ClaudeCodeLoginCommand {
54 path: command.path,
55 arguments: command.args,
56 })
57 })
58 }
59}
60
61impl AgentServer for ClaudeCode {
62 fn telemetry_id(&self) -> &'static str {
63 "claude-code"
64 }
65
66 fn name(&self) -> SharedString {
67 "Claude Code".into()
68 }
69
70 fn logo(&self) -> ui::IconName {
71 ui::IconName::AiClaude
72 }
73
74 fn connect(
75 &self,
76 root_dir: &Path,
77 delegate: AgentServerDelegate,
78 cx: &mut App,
79 ) -> Task<Result<Rc<dyn AgentConnection>>> {
80 let root_dir = root_dir.to_path_buf();
81 let fs = delegate.project().read(cx).fs().clone();
82 let server_name = self.name();
83 let custom_command = cx.read_global(|settings: &SettingsStore, _| {
84 settings
85 .get::<AllAgentServersSettings>(None)
86 .get("claude")
87 .cloned()
88 });
89
90 cx.spawn(async move |cx| {
91 let mut command = if let Some(custom_command) = custom_command {
92 custom_command
93 } else {
94 cx.update(|cx| {
95 delegate.get_or_npm_install_builtin_agent(
96 Self::BINARY_NAME.into(),
97 Self::PACKAGE_NAME.into(),
98 format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
99 false,
100 None,
101 cx,
102 )
103 })?
104 .await?
105 };
106
107 command
108 .env
109 .get_or_insert_default()
110 .insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
111
112 let root_dir_exists = fs.is_dir(&root_dir).await;
113 anyhow::ensure!(
114 root_dir_exists,
115 "Session root {} does not exist or is not a directory",
116 root_dir.to_string_lossy()
117 );
118
119 crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
120 })
121 }
122
123 fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
124 self
125 }
126}