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