1use agent_client_protocol as acp;
2use collections::HashSet;
3use fs::Fs;
4use settings::{SettingsStore, update_settings_file};
5use std::path::Path;
6use std::rc::Rc;
7use std::sync::Arc;
8use std::{any::Any, path::PathBuf};
9
10use anyhow::{Context as _, Result};
11use gpui::{App, AppContext as _, SharedString, Task};
12use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
13
14use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
15use acp_thread::AgentConnection;
16
17#[derive(Clone)]
18pub struct ClaudeCode;
19
20pub struct AgentServerLoginCommand {
21 pub path: PathBuf,
22 pub arguments: Vec<String>,
23}
24
25impl AgentServer for ClaudeCode {
26 fn name(&self) -> SharedString {
27 "Claude Code".into()
28 }
29
30 fn logo(&self) -> ui::IconName {
31 ui::IconName::AiClaude
32 }
33
34 fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
35 let settings = cx.read_global(|settings: &SettingsStore, _| {
36 settings.get::<AllAgentServersSettings>(None).claude.clone()
37 });
38
39 settings
40 .as_ref()
41 .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
42 }
43
44 fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
45 update_settings_file(fs, cx, |settings, _| {
46 settings
47 .agent_servers
48 .get_or_insert_default()
49 .claude
50 .get_or_insert_default()
51 .default_mode = mode_id.map(|m| m.to_string())
52 });
53 }
54
55 fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
56 let settings = cx.read_global(|settings: &SettingsStore, _| {
57 settings.get::<AllAgentServersSettings>(None).claude.clone()
58 });
59
60 settings
61 .as_ref()
62 .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
63 }
64
65 fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
66 update_settings_file(fs, cx, |settings, _| {
67 settings
68 .agent_servers
69 .get_or_insert_default()
70 .claude
71 .get_or_insert_default()
72 .default_model = model_id.map(|m| m.to_string())
73 });
74 }
75
76 fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
77 let settings = cx.read_global(|settings: &SettingsStore, _| {
78 settings.get::<AllAgentServersSettings>(None).claude.clone()
79 });
80
81 settings
82 .as_ref()
83 .map(|s| {
84 s.favorite_models
85 .iter()
86 .map(|id| acp::ModelId::new(id.clone()))
87 .collect()
88 })
89 .unwrap_or_default()
90 }
91
92 fn toggle_favorite_model(
93 &self,
94 model_id: acp::ModelId,
95 should_be_favorite: bool,
96 fs: Arc<dyn Fs>,
97 cx: &App,
98 ) {
99 update_settings_file(fs, cx, move |settings, _| {
100 let favorite_models = &mut settings
101 .agent_servers
102 .get_or_insert_default()
103 .claude
104 .get_or_insert_default()
105 .favorite_models;
106
107 let model_id_str = model_id.to_string();
108 if should_be_favorite {
109 if !favorite_models.contains(&model_id_str) {
110 favorite_models.push(model_id_str);
111 }
112 } else {
113 favorite_models.retain(|id| id != &model_id_str);
114 }
115 });
116 }
117
118 fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
119 let settings = cx.read_global(|settings: &SettingsStore, _| {
120 settings.get::<AllAgentServersSettings>(None).claude.clone()
121 });
122
123 settings
124 .as_ref()
125 .and_then(|s| s.default_config_options.get(config_id).cloned())
126 }
127
128 fn set_default_config_option(
129 &self,
130 config_id: &str,
131 value_id: Option<&str>,
132 fs: Arc<dyn Fs>,
133 cx: &mut App,
134 ) {
135 let config_id = config_id.to_string();
136 let value_id = value_id.map(|s| s.to_string());
137 update_settings_file(fs, cx, move |settings, _| {
138 let config_options = &mut settings
139 .agent_servers
140 .get_or_insert_default()
141 .claude
142 .get_or_insert_default()
143 .default_config_options;
144
145 if let Some(value) = value_id.clone() {
146 config_options.insert(config_id.clone(), value);
147 } else {
148 config_options.remove(&config_id);
149 }
150 });
151 }
152
153 fn favorite_config_option_value_ids(
154 &self,
155 config_id: &acp::SessionConfigId,
156 cx: &mut App,
157 ) -> HashSet<acp::SessionConfigValueId> {
158 let settings = cx.read_global(|settings: &SettingsStore, _| {
159 settings.get::<AllAgentServersSettings>(None).claude.clone()
160 });
161
162 settings
163 .as_ref()
164 .and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
165 .map(|values| {
166 values
167 .iter()
168 .cloned()
169 .map(acp::SessionConfigValueId::new)
170 .collect()
171 })
172 .unwrap_or_default()
173 }
174
175 fn toggle_favorite_config_option_value(
176 &self,
177 config_id: acp::SessionConfigId,
178 value_id: acp::SessionConfigValueId,
179 should_be_favorite: bool,
180 fs: Arc<dyn Fs>,
181 cx: &App,
182 ) {
183 let config_id = config_id.to_string();
184 let value_id = value_id.to_string();
185
186 update_settings_file(fs, cx, move |settings, _| {
187 let favorites = &mut settings
188 .agent_servers
189 .get_or_insert_default()
190 .claude
191 .get_or_insert_default()
192 .favorite_config_option_values;
193
194 let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
195
196 if should_be_favorite {
197 if !entry.iter().any(|v| v == &value_id) {
198 entry.push(value_id.clone());
199 }
200 } else {
201 entry.retain(|v| v != &value_id);
202 if entry.is_empty() {
203 favorites.remove(&config_id);
204 }
205 }
206 });
207 }
208
209 fn connect(
210 &self,
211 root_dir: Option<&Path>,
212 delegate: AgentServerDelegate,
213 cx: &mut App,
214 ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
215 let name = self.name();
216 let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
217 let is_remote = delegate.project.read(cx).is_via_remote_server();
218 let store = delegate.store.downgrade();
219 let extra_env = load_proxy_env(cx);
220 let default_mode = self.default_mode(cx);
221 let default_model = self.default_model(cx);
222 let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
223 settings
224 .get::<AllAgentServersSettings>(None)
225 .claude
226 .as_ref()
227 .map(|s| s.default_config_options.clone())
228 .unwrap_or_default()
229 });
230
231 cx.spawn(async move |cx| {
232 let (command, root_dir, login) = store
233 .update(cx, |store, cx| {
234 let agent = store
235 .get_external_agent(&CLAUDE_CODE_NAME.into())
236 .context("Claude Code is not registered")?;
237 anyhow::Ok(agent.get_command(
238 root_dir.as_deref(),
239 extra_env,
240 delegate.status_tx,
241 delegate.new_version_available,
242 &mut cx.to_async(),
243 ))
244 })??
245 .await?;
246 let connection = crate::acp::connect(
247 name,
248 command,
249 root_dir.as_ref(),
250 default_mode,
251 default_model,
252 default_config_options,
253 is_remote,
254 cx,
255 )
256 .await?;
257 Ok((connection, login))
258 })
259 }
260
261 fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
262 self
263 }
264}