1mod acp;
2mod claude;
3mod custom;
4mod gemini;
5mod settings;
6
7#[cfg(any(test, feature = "test-support"))]
8pub mod e2e_tests;
9
10use anyhow::Context as _;
11pub use claude::*;
12pub use custom::*;
13use fs::Fs;
14use fs::RemoveOptions;
15use fs::RenameOptions;
16use futures::StreamExt as _;
17pub use gemini::*;
18use gpui::AppContext;
19use node_runtime::NodeRuntime;
20pub use settings::*;
21
22use acp_thread::AgentConnection;
23use acp_thread::LoadError;
24use anyhow::Result;
25use anyhow::anyhow;
26use collections::HashMap;
27use gpui::{App, AsyncApp, Entity, SharedString, Task};
28use project::Project;
29use schemars::JsonSchema;
30use semver::Version;
31use serde::{Deserialize, Serialize};
32use std::str::FromStr as _;
33use std::{
34 any::Any,
35 path::{Path, PathBuf},
36 rc::Rc,
37 sync::Arc,
38};
39use util::ResultExt as _;
40
41pub fn init(cx: &mut App) {
42 settings::init(cx);
43}
44
45pub struct AgentServerDelegate {
46 project: Entity<Project>,
47 status_tx: watch::Sender<SharedString>,
48}
49
50impl AgentServerDelegate {
51 pub fn new(project: Entity<Project>, status_tx: watch::Sender<SharedString>) -> Self {
52 Self { project, status_tx }
53 }
54
55 pub fn project(&self) -> &Entity<Project> {
56 &self.project
57 }
58
59 fn get_or_npm_install_builtin_agent(
60 self,
61 binary_name: SharedString,
62 package_name: SharedString,
63 entrypoint_path: PathBuf,
64 ignore_system_version: bool,
65 minimum_version: Option<Version>,
66 cx: &mut App,
67 ) -> Task<Result<AgentServerCommand>> {
68 let project = self.project;
69 let fs = project.read(cx).fs().clone();
70 let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
71 return Task::ready(Err(anyhow!(
72 "External agents are not yet available in remote projects."
73 )));
74 };
75 let mut status_tx = self.status_tx;
76
77 cx.spawn(async move |cx| {
78 if !ignore_system_version {
79 if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
80 return Ok(AgentServerCommand {
81 path: bin,
82 args: Vec::new(),
83 env: Default::default(),
84 });
85 }
86 }
87
88 cx.spawn(async move |cx| {
89 let node_path = node_runtime.binary_path().await?;
90 let dir = paths::data_dir()
91 .join("external_agents")
92 .join(binary_name.as_str());
93 fs.create_dir(&dir).await?;
94
95 let mut stream = fs.read_dir(&dir).await?;
96 let mut versions = Vec::new();
97 let mut to_delete = Vec::new();
98 while let Some(entry) = stream.next().await {
99 let Ok(entry) = entry else { continue };
100 let Some(file_name) = entry.file_name() else {
101 continue;
102 };
103
104 if let Some(version) = file_name
105 .to_str()
106 .and_then(|name| semver::Version::from_str(&name).ok())
107 {
108 versions.push((file_name.to_owned(), version));
109 } else {
110 to_delete.push(file_name.to_owned())
111 }
112 }
113
114 versions.sort();
115 let newest_version = if let Some((file_name, version)) = versions.last().cloned()
116 && minimum_version.is_none_or(|minimum_version| version > minimum_version)
117 {
118 versions.pop();
119 Some(file_name)
120 } else {
121 None
122 };
123 to_delete.extend(versions.into_iter().map(|(file_name, _)| file_name));
124
125 cx.background_spawn({
126 let fs = fs.clone();
127 let dir = dir.clone();
128 async move {
129 for file_name in to_delete {
130 fs.remove_dir(
131 &dir.join(file_name),
132 RemoveOptions {
133 recursive: true,
134 ignore_if_not_exists: false,
135 },
136 )
137 .await
138 .ok();
139 }
140 }
141 })
142 .detach();
143
144 let version = if let Some(file_name) = newest_version {
145 cx.background_spawn({
146 let file_name = file_name.clone();
147 let dir = dir.clone();
148 async move {
149 let latest_version =
150 node_runtime.npm_package_latest_version(&package_name).await;
151 if let Ok(latest_version) = latest_version
152 && &latest_version != &file_name.to_string_lossy()
153 {
154 Self::download_latest_version(
155 fs,
156 dir.clone(),
157 node_runtime,
158 package_name,
159 )
160 .await
161 .log_err();
162 }
163 }
164 })
165 .detach();
166 file_name
167 } else {
168 status_tx.send("Installing…".into()).ok();
169 let dir = dir.clone();
170 cx.background_spawn(Self::download_latest_version(
171 fs,
172 dir.clone(),
173 node_runtime,
174 package_name,
175 ))
176 .await?
177 .into()
178 };
179 anyhow::Ok(AgentServerCommand {
180 path: node_path,
181 args: vec![
182 dir.join(version)
183 .join(entrypoint_path)
184 .to_string_lossy()
185 .to_string(),
186 ],
187 env: Default::default(),
188 })
189 })
190 .await
191 .map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
192 })
193 }
194
195 async fn download_latest_version(
196 fs: Arc<dyn Fs>,
197 dir: PathBuf,
198 node_runtime: NodeRuntime,
199 package_name: SharedString,
200 ) -> Result<String> {
201 let tmp_dir = tempfile::tempdir_in(&dir)?;
202
203 node_runtime
204 .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
205 .await?;
206
207 let version = node_runtime
208 .npm_package_installed_version(tmp_dir.path(), &package_name)
209 .await?
210 .context("expected package to be installed")?;
211
212 fs.rename(
213 &tmp_dir.keep(),
214 &dir.join(&version),
215 RenameOptions {
216 ignore_if_exists: true,
217 overwrite: false,
218 },
219 )
220 .await?;
221
222 anyhow::Ok(version)
223 }
224}
225
226pub trait AgentServer: Send {
227 fn logo(&self) -> ui::IconName;
228 fn name(&self) -> SharedString;
229 fn telemetry_id(&self) -> &'static str;
230
231 fn connect(
232 &self,
233 root_dir: &Path,
234 delegate: AgentServerDelegate,
235 cx: &mut App,
236 ) -> Task<Result<Rc<dyn AgentConnection>>>;
237
238 fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
239}
240
241impl dyn AgentServer {
242 pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
243 self.into_any().downcast().ok()
244 }
245}
246
247impl std::fmt::Debug for AgentServerCommand {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 let filtered_env = self.env.as_ref().map(|env| {
250 env.iter()
251 .map(|(k, v)| {
252 (
253 k,
254 if util::redact::should_redact(k) {
255 "[REDACTED]"
256 } else {
257 v
258 },
259 )
260 })
261 .collect::<Vec<_>>()
262 });
263
264 f.debug_struct("AgentServerCommand")
265 .field("path", &self.path)
266 .field("args", &self.args)
267 .field("env", &filtered_env)
268 .finish()
269 }
270}
271
272#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
273pub struct AgentServerCommand {
274 #[serde(rename = "command")]
275 pub path: PathBuf,
276 #[serde(default)]
277 pub args: Vec<String>,
278 pub env: Option<HashMap<String, String>>,
279}
280
281impl AgentServerCommand {
282 pub async fn resolve(
283 path_bin_name: &'static str,
284 extra_args: &[&'static str],
285 fallback_path: Option<&Path>,
286 settings: Option<BuiltinAgentServerSettings>,
287 project: &Entity<Project>,
288 cx: &mut AsyncApp,
289 ) -> Option<Self> {
290 if let Some(settings) = settings
291 && let Some(command) = settings.custom_command()
292 {
293 Some(command)
294 } else {
295 match find_bin_in_path(path_bin_name.into(), project, cx).await {
296 Some(path) => Some(Self {
297 path,
298 args: extra_args.iter().map(|arg| arg.to_string()).collect(),
299 env: None,
300 }),
301 None => fallback_path.and_then(|path| {
302 if path.exists() {
303 Some(Self {
304 path: path.to_path_buf(),
305 args: extra_args.iter().map(|arg| arg.to_string()).collect(),
306 env: None,
307 })
308 } else {
309 None
310 }
311 }),
312 }
313 }
314 }
315}
316
317async fn find_bin_in_path(
318 bin_name: SharedString,
319 project: &Entity<Project>,
320 cx: &mut AsyncApp,
321) -> Option<PathBuf> {
322 let (env_task, root_dir) = project
323 .update(cx, |project, cx| {
324 let worktree = project.visible_worktrees(cx).next();
325 match worktree {
326 Some(worktree) => {
327 let env_task = project.environment().update(cx, |env, cx| {
328 env.get_worktree_environment(worktree.clone(), cx)
329 });
330
331 let path = worktree.read(cx).abs_path();
332 (env_task, path)
333 }
334 None => {
335 let path: Arc<Path> = paths::home_dir().as_path().into();
336 let env_task = project.environment().update(cx, |env, cx| {
337 env.get_directory_environment(path.clone(), cx)
338 });
339 (env_task, path)
340 }
341 }
342 })
343 .log_err()?;
344
345 cx.background_executor()
346 .spawn(async move {
347 let which_result = if cfg!(windows) {
348 which::which(bin_name.as_str())
349 } else {
350 let env = env_task.await.unwrap_or_default();
351 let shell_path = env.get("PATH").cloned();
352 which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
353 };
354
355 if let Err(which::Error::CannotFindBinaryPath) = which_result {
356 return None;
357 }
358
359 which_result.log_err()
360 })
361 .await
362}