1mod acp;
2mod claude;
3mod custom;
4mod gemini;
5mod settings;
6
7#[cfg(any(test, feature = "test-support"))]
8pub mod e2e_tests;
9
10pub use claude::*;
11pub use custom::*;
12pub use gemini::*;
13pub use settings::*;
14
15use acp_thread::AgentConnection;
16use acp_thread::LoadError;
17use anyhow::Result;
18use anyhow::anyhow;
19use anyhow::bail;
20use collections::HashMap;
21use gpui::AppContext as _;
22use gpui::{App, AsyncApp, Entity, SharedString, Task};
23use node_runtime::VersionStrategy;
24use project::Project;
25use schemars::JsonSchema;
26use semver::Version;
27use serde::{Deserialize, Serialize};
28use std::str::FromStr as _;
29use std::{
30 any::Any,
31 path::{Path, PathBuf},
32 rc::Rc,
33 sync::Arc,
34};
35use util::ResultExt as _;
36
37pub fn init(cx: &mut App) {
38 settings::init(cx);
39}
40
41pub struct AgentServerDelegate {
42 project: Entity<Project>,
43 status_tx: watch::Sender<SharedString>,
44}
45
46impl AgentServerDelegate {
47 pub fn new(project: Entity<Project>, status_tx: watch::Sender<SharedString>) -> Self {
48 Self { project, status_tx }
49 }
50
51 pub fn project(&self) -> &Entity<Project> {
52 &self.project
53 }
54
55 fn get_or_npm_install_builtin_agent(
56 self,
57 binary_name: SharedString,
58 package_name: SharedString,
59 entrypoint_path: PathBuf,
60 settings: Option<BuiltinAgentServerSettings>,
61 minimum_version: Option<Version>,
62 cx: &mut App,
63 ) -> Task<Result<AgentServerCommand>> {
64 if let Some(settings) = &settings
65 && let Some(command) = settings.clone().custom_command()
66 {
67 return Task::ready(Ok(command));
68 }
69
70 let project = self.project;
71 let fs = project.read(cx).fs().clone();
72 let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
73 return Task::ready(Err(anyhow!("Missing node runtime")));
74 };
75 let mut status_tx = self.status_tx;
76
77 cx.spawn(async move |cx| {
78 if let Some(settings) = settings && !settings.ignore_system_version.unwrap_or(true) {
79 if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
80 return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() })
81 }
82 }
83
84 cx.background_spawn(async move {
85 let node_path = node_runtime.binary_path().await?;
86 let dir = paths::data_dir().join("external_agents").join(binary_name.as_str());
87 fs.create_dir(&dir).await?;
88 let local_executable_path = dir.join(entrypoint_path);
89 let command = AgentServerCommand {
90 path: node_path,
91 args: vec![local_executable_path.to_string_lossy().to_string()],
92 env: Default::default(),
93 };
94
95 let installed_version = node_runtime
96 .npm_package_installed_version(&dir, &package_name)
97 .await?
98 .filter(|version| {
99 Version::from_str(&version)
100 .is_ok_and(|version| Some(version) >= minimum_version)
101 });
102
103 status_tx.send("Checking for latest version…".into())?;
104 let latest_version = match node_runtime.npm_package_latest_version(&package_name).await
105 {
106 Ok(latest_version) => latest_version,
107 Err(e) => {
108 if let Some(installed_version) = installed_version {
109 log::error!("{e}");
110 log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}");
111 return Ok(command);
112 } else {
113 bail!(e);
114 }
115 }
116 };
117
118 let should_install = node_runtime
119 .should_install_npm_package(
120 &package_name,
121 &local_executable_path,
122 &dir,
123 VersionStrategy::Latest(&latest_version),
124 )
125 .await;
126
127 if should_install {
128 status_tx.send("Installing latest version…".into())?;
129 node_runtime
130 .npm_install_packages(&dir, &[(&package_name, &latest_version)])
131 .await?;
132 }
133
134 Ok(command)
135 }).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
136 })
137 }
138}
139
140pub trait AgentServer: Send {
141 fn logo(&self) -> ui::IconName;
142 fn name(&self) -> SharedString;
143 fn telemetry_id(&self) -> &'static str;
144
145 fn connect(
146 &self,
147 root_dir: &Path,
148 delegate: AgentServerDelegate,
149 cx: &mut App,
150 ) -> Task<Result<Rc<dyn AgentConnection>>>;
151
152 fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
153}
154
155impl dyn AgentServer {
156 pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
157 self.into_any().downcast().ok()
158 }
159}
160
161impl std::fmt::Debug for AgentServerCommand {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 let filtered_env = self.env.as_ref().map(|env| {
164 env.iter()
165 .map(|(k, v)| {
166 (
167 k,
168 if util::redact::should_redact(k) {
169 "[REDACTED]"
170 } else {
171 v
172 },
173 )
174 })
175 .collect::<Vec<_>>()
176 });
177
178 f.debug_struct("AgentServerCommand")
179 .field("path", &self.path)
180 .field("args", &self.args)
181 .field("env", &filtered_env)
182 .finish()
183 }
184}
185
186#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
187pub struct AgentServerCommand {
188 #[serde(rename = "command")]
189 pub path: PathBuf,
190 #[serde(default)]
191 pub args: Vec<String>,
192 pub env: Option<HashMap<String, String>>,
193}
194
195impl AgentServerCommand {
196 pub async fn resolve(
197 path_bin_name: &'static str,
198 extra_args: &[&'static str],
199 fallback_path: Option<&Path>,
200 settings: Option<BuiltinAgentServerSettings>,
201 project: &Entity<Project>,
202 cx: &mut AsyncApp,
203 ) -> Option<Self> {
204 if let Some(settings) = settings
205 && let Some(command) = settings.custom_command()
206 {
207 Some(command)
208 } else {
209 match find_bin_in_path(path_bin_name.into(), project, cx).await {
210 Some(path) => Some(Self {
211 path,
212 args: extra_args.iter().map(|arg| arg.to_string()).collect(),
213 env: None,
214 }),
215 None => fallback_path.and_then(|path| {
216 if path.exists() {
217 Some(Self {
218 path: path.to_path_buf(),
219 args: extra_args.iter().map(|arg| arg.to_string()).collect(),
220 env: None,
221 })
222 } else {
223 None
224 }
225 }),
226 }
227 }
228 }
229}
230
231async fn find_bin_in_path(
232 bin_name: SharedString,
233 project: &Entity<Project>,
234 cx: &mut AsyncApp,
235) -> Option<PathBuf> {
236 let (env_task, root_dir) = project
237 .update(cx, |project, cx| {
238 let worktree = project.visible_worktrees(cx).next();
239 match worktree {
240 Some(worktree) => {
241 let env_task = project.environment().update(cx, |env, cx| {
242 env.get_worktree_environment(worktree.clone(), cx)
243 });
244
245 let path = worktree.read(cx).abs_path();
246 (env_task, path)
247 }
248 None => {
249 let path: Arc<Path> = paths::home_dir().as_path().into();
250 let env_task = project.environment().update(cx, |env, cx| {
251 env.get_directory_environment(path.clone(), cx)
252 });
253 (env_task, path)
254 }
255 }
256 })
257 .log_err()?;
258
259 cx.background_executor()
260 .spawn(async move {
261 let which_result = if cfg!(windows) {
262 which::which(bin_name.as_str())
263 } else {
264 let env = env_task.await.unwrap_or_default();
265 let shell_path = env.get("PATH").cloned();
266 which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
267 };
268
269 if let Err(which::Error::CannotFindBinaryPath) = which_result {
270 return None;
271 }
272
273 which_result.log_err()
274 })
275 .await
276}