1use std::{
2 any::Any,
3 borrow::Borrow,
4 path::{Path, PathBuf},
5 str::FromStr as _,
6 sync::Arc,
7 time::Duration,
8};
9
10use anyhow::{Context as _, Result, bail};
11use collections::HashMap;
12use fs::{Fs, RemoveOptions, RenameOptions};
13use futures::StreamExt as _;
14use gpui::{
15 App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
16};
17use node_runtime::NodeRuntime;
18use remote::RemoteClient;
19use rpc::{
20 AnyProtoClient, TypedEnvelope,
21 proto::{self, ToProto},
22};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25use settings::{SettingsContent, SettingsStore};
26use util::{ResultExt as _, debug_panic};
27
28use crate::ProjectEnvironment;
29
30#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
31pub struct AgentServerCommand {
32 #[serde(rename = "command")]
33 pub path: PathBuf,
34 #[serde(default)]
35 pub args: Vec<String>,
36 pub env: Option<HashMap<String, String>>,
37}
38
39impl std::fmt::Debug for AgentServerCommand {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 let filtered_env = self.env.as_ref().map(|env| {
42 env.iter()
43 .map(|(k, v)| {
44 (
45 k,
46 if util::redact::should_redact(k) {
47 "[REDACTED]"
48 } else {
49 v
50 },
51 )
52 })
53 .collect::<Vec<_>>()
54 });
55
56 f.debug_struct("AgentServerCommand")
57 .field("path", &self.path)
58 .field("args", &self.args)
59 .field("env", &filtered_env)
60 .finish()
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Hash)]
65pub struct ExternalAgentServerName(pub SharedString);
66
67impl std::fmt::Display for ExternalAgentServerName {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73impl From<&'static str> for ExternalAgentServerName {
74 fn from(value: &'static str) -> Self {
75 ExternalAgentServerName(value.into())
76 }
77}
78
79impl From<ExternalAgentServerName> for SharedString {
80 fn from(value: ExternalAgentServerName) -> Self {
81 value.0
82 }
83}
84
85impl Borrow<str> for ExternalAgentServerName {
86 fn borrow(&self) -> &str {
87 &self.0
88 }
89}
90
91pub trait ExternalAgentServer {
92 fn get_command(
93 &mut self,
94 root_dir: Option<&str>,
95 extra_env: HashMap<String, String>,
96 status_tx: Option<watch::Sender<SharedString>>,
97 new_version_available_tx: Option<watch::Sender<Option<String>>>,
98 cx: &mut AsyncApp,
99 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>>;
100
101 fn as_any_mut(&mut self) -> &mut dyn Any;
102}
103
104impl dyn ExternalAgentServer {
105 fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
106 self.as_any_mut().downcast_mut()
107 }
108}
109
110enum AgentServerStoreState {
111 Local {
112 node_runtime: NodeRuntime,
113 fs: Arc<dyn Fs>,
114 project_environment: Entity<ProjectEnvironment>,
115 downstream_client: Option<(u64, AnyProtoClient)>,
116 settings: Option<AllAgentServersSettings>,
117 _subscriptions: [Subscription; 1],
118 },
119 Remote {
120 project_id: u64,
121 upstream_client: Entity<RemoteClient>,
122 },
123 Collab,
124}
125
126pub struct AgentServerStore {
127 state: AgentServerStoreState,
128 external_agents: HashMap<ExternalAgentServerName, Box<dyn ExternalAgentServer>>,
129}
130
131pub struct AgentServersUpdated;
132
133impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
134
135impl AgentServerStore {
136 pub fn init_remote(session: &AnyProtoClient) {
137 session.add_entity_message_handler(Self::handle_external_agents_updated);
138 session.add_entity_message_handler(Self::handle_loading_status_updated);
139 session.add_entity_message_handler(Self::handle_new_version_available);
140 }
141
142 pub fn init_headless(session: &AnyProtoClient) {
143 session.add_entity_request_handler(Self::handle_get_agent_server_command);
144 }
145
146 fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
147 let AgentServerStoreState::Local {
148 node_runtime,
149 fs,
150 project_environment,
151 downstream_client,
152 settings: old_settings,
153 ..
154 } = &mut self.state
155 else {
156 debug_panic!(
157 "should not be subscribed to agent server settings changes in non-local project"
158 );
159 return;
160 };
161
162 let new_settings = cx
163 .global::<SettingsStore>()
164 .get::<AllAgentServersSettings>(None)
165 .clone();
166 if Some(&new_settings) == old_settings.as_ref() {
167 return;
168 }
169
170 self.external_agents.clear();
171 self.external_agents.insert(
172 GEMINI_NAME.into(),
173 Box::new(LocalGemini {
174 fs: fs.clone(),
175 node_runtime: node_runtime.clone(),
176 project_environment: project_environment.clone(),
177 custom_command: new_settings
178 .gemini
179 .clone()
180 .and_then(|settings| settings.custom_command()),
181 ignore_system_version: new_settings
182 .gemini
183 .as_ref()
184 .and_then(|settings| settings.ignore_system_version)
185 .unwrap_or(true),
186 }),
187 );
188 self.external_agents.insert(
189 CLAUDE_CODE_NAME.into(),
190 Box::new(LocalClaudeCode {
191 fs: fs.clone(),
192 node_runtime: node_runtime.clone(),
193 project_environment: project_environment.clone(),
194 custom_command: new_settings
195 .claude
196 .clone()
197 .and_then(|settings| settings.custom_command()),
198 }),
199 );
200 self.external_agents
201 .extend(new_settings.custom.iter().map(|(name, settings)| {
202 (
203 ExternalAgentServerName(name.clone()),
204 Box::new(LocalCustomAgent {
205 command: settings.command.clone(),
206 project_environment: project_environment.clone(),
207 }) as Box<dyn ExternalAgentServer>,
208 )
209 }));
210
211 *old_settings = Some(new_settings.clone());
212
213 if let Some((project_id, downstream_client)) = downstream_client {
214 downstream_client
215 .send(proto::ExternalAgentsUpdated {
216 project_id: *project_id,
217 names: self
218 .external_agents
219 .keys()
220 .map(|name| name.to_string())
221 .collect(),
222 })
223 .log_err();
224 }
225 cx.emit(AgentServersUpdated);
226 }
227
228 pub fn local(
229 node_runtime: NodeRuntime,
230 fs: Arc<dyn Fs>,
231 project_environment: Entity<ProjectEnvironment>,
232 cx: &mut Context<Self>,
233 ) -> Self {
234 let subscription = cx.observe_global::<SettingsStore>(|this, cx| {
235 this.agent_servers_settings_changed(cx);
236 });
237 let mut this = Self {
238 state: AgentServerStoreState::Local {
239 node_runtime,
240 fs,
241 project_environment,
242 downstream_client: None,
243 settings: None,
244 _subscriptions: [subscription],
245 },
246 external_agents: Default::default(),
247 };
248 this.agent_servers_settings_changed(cx);
249 this
250 }
251
252 pub(crate) fn remote(
253 project_id: u64,
254 upstream_client: Entity<RemoteClient>,
255 _cx: &mut Context<Self>,
256 ) -> Self {
257 // Set up the builtin agents here so they're immediately available in
258 // remote projects--we know that the HeadlessProject on the other end
259 // will have them.
260 let external_agents = [
261 (
262 GEMINI_NAME.into(),
263 Box::new(RemoteExternalAgentServer {
264 project_id,
265 upstream_client: upstream_client.clone(),
266 name: GEMINI_NAME.into(),
267 status_tx: None,
268 new_version_available_tx: None,
269 }) as Box<dyn ExternalAgentServer>,
270 ),
271 (
272 CLAUDE_CODE_NAME.into(),
273 Box::new(RemoteExternalAgentServer {
274 project_id,
275 upstream_client: upstream_client.clone(),
276 name: CLAUDE_CODE_NAME.into(),
277 status_tx: None,
278 new_version_available_tx: None,
279 }) as Box<dyn ExternalAgentServer>,
280 ),
281 ]
282 .into_iter()
283 .collect();
284
285 Self {
286 state: AgentServerStoreState::Remote {
287 project_id,
288 upstream_client,
289 },
290 external_agents,
291 }
292 }
293
294 pub(crate) fn collab(_cx: &mut Context<Self>) -> Self {
295 Self {
296 state: AgentServerStoreState::Collab,
297 external_agents: Default::default(),
298 }
299 }
300
301 pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
302 match &mut self.state {
303 AgentServerStoreState::Local {
304 downstream_client, ..
305 } => {
306 *downstream_client = Some((project_id, client.clone()));
307 // Send the current list of external agents downstream, but only after a delay,
308 // to avoid having the message arrive before the downstream project's agent server store
309 // sets up its handlers.
310 cx.spawn(async move |this, cx| {
311 cx.background_executor().timer(Duration::from_secs(1)).await;
312 let names = this.update(cx, |this, _| {
313 this.external_agents
314 .keys()
315 .map(|name| name.to_string())
316 .collect()
317 })?;
318 client
319 .send(proto::ExternalAgentsUpdated { project_id, names })
320 .log_err();
321 anyhow::Ok(())
322 })
323 .detach();
324 }
325 AgentServerStoreState::Remote { .. } => {
326 debug_panic!(
327 "external agents over collab not implemented, remote project should not be shared"
328 );
329 }
330 AgentServerStoreState::Collab => {
331 debug_panic!("external agents over collab not implemented, should not be shared");
332 }
333 }
334 }
335
336 pub fn get_external_agent(
337 &mut self,
338 name: &ExternalAgentServerName,
339 ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
340 self.external_agents
341 .get_mut(name)
342 .map(|agent| agent.as_mut())
343 }
344
345 pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
346 self.external_agents.keys()
347 }
348
349 async fn handle_get_agent_server_command(
350 this: Entity<Self>,
351 envelope: TypedEnvelope<proto::GetAgentServerCommand>,
352 mut cx: AsyncApp,
353 ) -> Result<proto::AgentServerCommand> {
354 let (command, root_dir, login) = this
355 .update(&mut cx, |this, cx| {
356 let AgentServerStoreState::Local {
357 downstream_client, ..
358 } = &this.state
359 else {
360 debug_panic!("should not receive GetAgentServerCommand in a non-local project");
361 bail!("unexpected GetAgentServerCommand request in a non-local project");
362 };
363 let agent = this
364 .external_agents
365 .get_mut(&*envelope.payload.name)
366 .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
367 let (status_tx, new_version_available_tx) = downstream_client
368 .clone()
369 .map(|(project_id, downstream_client)| {
370 let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
371 let (new_version_available_tx, mut new_version_available_rx) =
372 watch::channel(None);
373 cx.spawn({
374 let downstream_client = downstream_client.clone();
375 let name = envelope.payload.name.clone();
376 async move |_, _| {
377 while let Some(status) = status_rx.recv().await.ok() {
378 downstream_client.send(
379 proto::ExternalAgentLoadingStatusUpdated {
380 project_id,
381 name: name.clone(),
382 status: status.to_string(),
383 },
384 )?;
385 }
386 anyhow::Ok(())
387 }
388 })
389 .detach_and_log_err(cx);
390 cx.spawn({
391 let name = envelope.payload.name.clone();
392 async move |_, _| {
393 if let Some(version) =
394 new_version_available_rx.recv().await.ok().flatten()
395 {
396 downstream_client.send(
397 proto::NewExternalAgentVersionAvailable {
398 project_id,
399 name: name.clone(),
400 version,
401 },
402 )?;
403 }
404 anyhow::Ok(())
405 }
406 })
407 .detach_and_log_err(cx);
408 (status_tx, new_version_available_tx)
409 })
410 .unzip();
411 anyhow::Ok(agent.get_command(
412 envelope.payload.root_dir.as_deref(),
413 HashMap::default(),
414 status_tx,
415 new_version_available_tx,
416 &mut cx.to_async(),
417 ))
418 })??
419 .await?;
420 Ok(proto::AgentServerCommand {
421 path: command.path.to_string_lossy().to_string(),
422 args: command.args,
423 env: command
424 .env
425 .map(|env| env.into_iter().collect())
426 .unwrap_or_default(),
427 root_dir: root_dir,
428 login: login.map(|login| login.to_proto()),
429 })
430 }
431
432 async fn handle_external_agents_updated(
433 this: Entity<Self>,
434 envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
435 mut cx: AsyncApp,
436 ) -> Result<()> {
437 this.update(&mut cx, |this, cx| {
438 let AgentServerStoreState::Remote {
439 project_id,
440 upstream_client,
441 } = &this.state
442 else {
443 debug_panic!(
444 "handle_external_agents_updated should not be called for a non-remote project"
445 );
446 bail!("unexpected ExternalAgentsUpdated message")
447 };
448
449 let mut status_txs = this
450 .external_agents
451 .iter_mut()
452 .filter_map(|(name, agent)| {
453 Some((
454 name.clone(),
455 agent
456 .downcast_mut::<RemoteExternalAgentServer>()?
457 .status_tx
458 .take(),
459 ))
460 })
461 .collect::<HashMap<_, _>>();
462 let mut new_version_available_txs = this
463 .external_agents
464 .iter_mut()
465 .filter_map(|(name, agent)| {
466 Some((
467 name.clone(),
468 agent
469 .downcast_mut::<RemoteExternalAgentServer>()?
470 .new_version_available_tx
471 .take(),
472 ))
473 })
474 .collect::<HashMap<_, _>>();
475
476 this.external_agents = envelope
477 .payload
478 .names
479 .into_iter()
480 .map(|name| {
481 let agent = RemoteExternalAgentServer {
482 project_id: *project_id,
483 upstream_client: upstream_client.clone(),
484 name: ExternalAgentServerName(name.clone().into()),
485 status_tx: status_txs.remove(&*name).flatten(),
486 new_version_available_tx: new_version_available_txs
487 .remove(&*name)
488 .flatten(),
489 };
490 (
491 ExternalAgentServerName(name.into()),
492 Box::new(agent) as Box<dyn ExternalAgentServer>,
493 )
494 })
495 .collect();
496 cx.emit(AgentServersUpdated);
497 Ok(())
498 })?
499 }
500
501 async fn handle_loading_status_updated(
502 this: Entity<Self>,
503 envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
504 mut cx: AsyncApp,
505 ) -> Result<()> {
506 this.update(&mut cx, |this, _| {
507 if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
508 && let Some(agent) = agent.downcast_mut::<RemoteExternalAgentServer>()
509 && let Some(status_tx) = &mut agent.status_tx
510 {
511 status_tx.send(envelope.payload.status.into()).ok();
512 }
513 })
514 }
515
516 async fn handle_new_version_available(
517 this: Entity<Self>,
518 envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
519 mut cx: AsyncApp,
520 ) -> Result<()> {
521 this.update(&mut cx, |this, _| {
522 if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
523 && let Some(agent) = agent.downcast_mut::<RemoteExternalAgentServer>()
524 && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
525 {
526 new_version_available_tx
527 .send(Some(envelope.payload.version))
528 .ok();
529 }
530 })
531 }
532}
533
534fn get_or_npm_install_builtin_agent(
535 binary_name: SharedString,
536 package_name: SharedString,
537 entrypoint_path: PathBuf,
538 minimum_version: Option<semver::Version>,
539 status_tx: Option<watch::Sender<SharedString>>,
540 new_version_available: Option<watch::Sender<Option<String>>>,
541 fs: Arc<dyn Fs>,
542 node_runtime: NodeRuntime,
543 cx: &mut AsyncApp,
544) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
545 cx.spawn(async move |cx| {
546 let node_path = node_runtime.binary_path().await?;
547 let dir = paths::data_dir()
548 .join("external_agents")
549 .join(binary_name.as_str());
550 fs.create_dir(&dir).await?;
551
552 let mut stream = fs.read_dir(&dir).await?;
553 let mut versions = Vec::new();
554 let mut to_delete = Vec::new();
555 while let Some(entry) = stream.next().await {
556 let Ok(entry) = entry else { continue };
557 let Some(file_name) = entry.file_name() else {
558 continue;
559 };
560
561 if let Some(name) = file_name.to_str()
562 && let Some(version) = semver::Version::from_str(name).ok()
563 && fs
564 .is_file(&dir.join(file_name).join(&entrypoint_path))
565 .await
566 {
567 versions.push((version, file_name.to_owned()));
568 } else {
569 to_delete.push(file_name.to_owned())
570 }
571 }
572
573 versions.sort();
574 let newest_version = if let Some((version, file_name)) = versions.last().cloned()
575 && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
576 {
577 versions.pop();
578 Some(file_name)
579 } else {
580 None
581 };
582 log::debug!("existing version of {package_name}: {newest_version:?}");
583 to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
584
585 cx.background_spawn({
586 let fs = fs.clone();
587 let dir = dir.clone();
588 async move {
589 for file_name in to_delete {
590 fs.remove_dir(
591 &dir.join(file_name),
592 RemoveOptions {
593 recursive: true,
594 ignore_if_not_exists: false,
595 },
596 )
597 .await
598 .ok();
599 }
600 }
601 })
602 .detach();
603
604 let version = if let Some(file_name) = newest_version {
605 cx.background_spawn({
606 let file_name = file_name.clone();
607 let dir = dir.clone();
608 let fs = fs.clone();
609 async move {
610 let latest_version =
611 node_runtime.npm_package_latest_version(&package_name).await;
612 if let Ok(latest_version) = latest_version
613 && &latest_version != &file_name.to_string_lossy()
614 {
615 download_latest_version(
616 fs,
617 dir.clone(),
618 node_runtime,
619 package_name.clone(),
620 )
621 .await
622 .log_err();
623 if let Some(mut new_version_available) = new_version_available {
624 new_version_available.send(Some(latest_version)).ok();
625 }
626 }
627 }
628 })
629 .detach();
630 file_name
631 } else {
632 if let Some(mut status_tx) = status_tx {
633 status_tx.send("Installing…".into()).ok();
634 }
635 let dir = dir.clone();
636 cx.background_spawn(download_latest_version(
637 fs.clone(),
638 dir.clone(),
639 node_runtime,
640 package_name.clone(),
641 ))
642 .await?
643 .into()
644 };
645
646 let agent_server_path = dir.join(version).join(entrypoint_path);
647 let agent_server_path_exists = fs.is_file(&agent_server_path).await;
648 anyhow::ensure!(
649 agent_server_path_exists,
650 "Missing entrypoint path {} after installation",
651 agent_server_path.to_string_lossy()
652 );
653
654 anyhow::Ok(AgentServerCommand {
655 path: node_path,
656 args: vec![agent_server_path.to_string_lossy().to_string()],
657 env: None,
658 })
659 })
660}
661
662fn find_bin_in_path(
663 bin_name: SharedString,
664 root_dir: PathBuf,
665 env: HashMap<String, String>,
666 cx: &mut AsyncApp,
667) -> Task<Option<PathBuf>> {
668 cx.background_executor().spawn(async move {
669 let which_result = if cfg!(windows) {
670 which::which(bin_name.as_str())
671 } else {
672 let shell_path = env.get("PATH").cloned();
673 which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
674 };
675
676 if let Err(which::Error::CannotFindBinaryPath) = which_result {
677 return None;
678 }
679
680 which_result.log_err()
681 })
682}
683
684async fn download_latest_version(
685 fs: Arc<dyn Fs>,
686 dir: PathBuf,
687 node_runtime: NodeRuntime,
688 package_name: SharedString,
689) -> Result<String> {
690 log::debug!("downloading latest version of {package_name}");
691
692 let tmp_dir = tempfile::tempdir_in(&dir)?;
693
694 node_runtime
695 .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
696 .await?;
697
698 let version = node_runtime
699 .npm_package_installed_version(tmp_dir.path(), &package_name)
700 .await?
701 .context("expected package to be installed")?;
702
703 fs.rename(
704 &tmp_dir.keep(),
705 &dir.join(&version),
706 RenameOptions {
707 ignore_if_exists: true,
708 overwrite: false,
709 },
710 )
711 .await?;
712
713 anyhow::Ok(version)
714}
715
716struct RemoteExternalAgentServer {
717 project_id: u64,
718 upstream_client: Entity<RemoteClient>,
719 name: ExternalAgentServerName,
720 status_tx: Option<watch::Sender<SharedString>>,
721 new_version_available_tx: Option<watch::Sender<Option<String>>>,
722}
723
724impl ExternalAgentServer for RemoteExternalAgentServer {
725 fn get_command(
726 &mut self,
727 root_dir: Option<&str>,
728 extra_env: HashMap<String, String>,
729 status_tx: Option<watch::Sender<SharedString>>,
730 new_version_available_tx: Option<watch::Sender<Option<String>>>,
731 cx: &mut AsyncApp,
732 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
733 let project_id = self.project_id;
734 let name = self.name.to_string();
735 let upstream_client = self.upstream_client.downgrade();
736 let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
737 self.status_tx = status_tx;
738 self.new_version_available_tx = new_version_available_tx;
739 cx.spawn(async move |cx| {
740 let mut response = upstream_client
741 .update(cx, |upstream_client, _| {
742 upstream_client
743 .proto_client()
744 .request(proto::GetAgentServerCommand {
745 project_id,
746 name,
747 root_dir: root_dir.clone(),
748 })
749 })?
750 .await?;
751 let root_dir = response.root_dir;
752 response.env.extend(extra_env);
753 let command = upstream_client.update(cx, |client, _| {
754 client.build_command(
755 Some(response.path),
756 &response.args,
757 &response.env.into_iter().collect(),
758 Some(root_dir.clone()),
759 None,
760 )
761 })??;
762 Ok((
763 AgentServerCommand {
764 path: command.program.into(),
765 args: command.args,
766 env: Some(command.env),
767 },
768 root_dir,
769 response
770 .login
771 .map(|login| task::SpawnInTerminal::from_proto(login)),
772 ))
773 })
774 }
775
776 fn as_any_mut(&mut self) -> &mut dyn Any {
777 self
778 }
779}
780
781struct LocalGemini {
782 fs: Arc<dyn Fs>,
783 node_runtime: NodeRuntime,
784 project_environment: Entity<ProjectEnvironment>,
785 custom_command: Option<AgentServerCommand>,
786 ignore_system_version: bool,
787}
788
789impl ExternalAgentServer for LocalGemini {
790 fn get_command(
791 &mut self,
792 root_dir: Option<&str>,
793 extra_env: HashMap<String, String>,
794 status_tx: Option<watch::Sender<SharedString>>,
795 new_version_available_tx: Option<watch::Sender<Option<String>>>,
796 cx: &mut AsyncApp,
797 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
798 let fs = self.fs.clone();
799 let node_runtime = self.node_runtime.clone();
800 let project_environment = self.project_environment.downgrade();
801 let custom_command = self.custom_command.clone();
802 let ignore_system_version = self.ignore_system_version;
803 let root_dir: Arc<Path> = root_dir
804 .map(|root_dir| Path::new(root_dir))
805 .unwrap_or(paths::home_dir())
806 .into();
807
808 cx.spawn(async move |cx| {
809 let mut env = project_environment
810 .update(cx, |project_environment, cx| {
811 project_environment.get_directory_environment(root_dir.clone(), cx)
812 })?
813 .await
814 .unwrap_or_default();
815
816 let mut command = if let Some(mut custom_command) = custom_command {
817 env.extend(custom_command.env.unwrap_or_default());
818 custom_command.env = Some(env);
819 custom_command
820 } else if !ignore_system_version
821 && let Some(bin) =
822 find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
823 {
824 AgentServerCommand {
825 path: bin,
826 args: Vec::new(),
827 env: Some(env),
828 }
829 } else {
830 let mut command = get_or_npm_install_builtin_agent(
831 GEMINI_NAME.into(),
832 "@google/gemini-cli".into(),
833 "node_modules/@google/gemini-cli/dist/index.js".into(),
834 Some("0.2.1".parse().unwrap()),
835 status_tx,
836 new_version_available_tx,
837 fs,
838 node_runtime,
839 cx,
840 )
841 .await?;
842 command.env = Some(env);
843 command
844 };
845
846 // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
847 let login = task::SpawnInTerminal {
848 command: Some(command.path.clone().to_proto()),
849 args: command.args.clone(),
850 env: command.env.clone().unwrap_or_default(),
851 label: "gemini /auth".into(),
852 ..Default::default()
853 };
854
855 command.env.get_or_insert_default().extend(extra_env);
856 command.args.push("--experimental-acp".into());
857 Ok((command, root_dir.to_proto(), Some(login)))
858 })
859 }
860
861 fn as_any_mut(&mut self) -> &mut dyn Any {
862 self
863 }
864}
865
866struct LocalClaudeCode {
867 fs: Arc<dyn Fs>,
868 node_runtime: NodeRuntime,
869 project_environment: Entity<ProjectEnvironment>,
870 custom_command: Option<AgentServerCommand>,
871}
872
873impl ExternalAgentServer for LocalClaudeCode {
874 fn get_command(
875 &mut self,
876 root_dir: Option<&str>,
877 extra_env: HashMap<String, String>,
878 status_tx: Option<watch::Sender<SharedString>>,
879 new_version_available_tx: Option<watch::Sender<Option<String>>>,
880 cx: &mut AsyncApp,
881 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
882 let fs = self.fs.clone();
883 let node_runtime = self.node_runtime.clone();
884 let project_environment = self.project_environment.downgrade();
885 let custom_command = self.custom_command.clone();
886 let root_dir: Arc<Path> = root_dir
887 .map(|root_dir| Path::new(root_dir))
888 .unwrap_or(paths::home_dir())
889 .into();
890
891 cx.spawn(async move |cx| {
892 let mut env = project_environment
893 .update(cx, |project_environment, cx| {
894 project_environment.get_directory_environment(root_dir.clone(), cx)
895 })?
896 .await
897 .unwrap_or_default();
898 env.insert("ANTHROPIC_API_KEY".into(), "".into());
899
900 let (mut command, login) = if let Some(mut custom_command) = custom_command {
901 env.extend(custom_command.env.unwrap_or_default());
902 custom_command.env = Some(env);
903 (custom_command, None)
904 } else {
905 let mut command = get_or_npm_install_builtin_agent(
906 "claude-code-acp".into(),
907 "@zed-industries/claude-code-acp".into(),
908 "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
909 Some("0.2.5".parse().unwrap()),
910 status_tx,
911 new_version_available_tx,
912 fs,
913 node_runtime,
914 cx,
915 )
916 .await?;
917 command.env = Some(env);
918 let login = command
919 .args
920 .first()
921 .and_then(|path| {
922 path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
923 })
924 .map(|path_prefix| task::SpawnInTerminal {
925 command: Some(command.path.clone().to_proto()),
926 args: vec![
927 Path::new(path_prefix)
928 .join("@anthropic-ai/claude-code/cli.js")
929 .to_string_lossy()
930 .to_string(),
931 "/login".into(),
932 ],
933 env: command.env.clone().unwrap_or_default(),
934 label: "claude /login".into(),
935 ..Default::default()
936 });
937 (command, login)
938 };
939
940 command.env.get_or_insert_default().extend(extra_env);
941 Ok((command, root_dir.to_proto(), login))
942 })
943 }
944
945 fn as_any_mut(&mut self) -> &mut dyn Any {
946 self
947 }
948}
949
950struct LocalCustomAgent {
951 project_environment: Entity<ProjectEnvironment>,
952 command: AgentServerCommand,
953}
954
955impl ExternalAgentServer for LocalCustomAgent {
956 fn get_command(
957 &mut self,
958 root_dir: Option<&str>,
959 extra_env: HashMap<String, String>,
960 _status_tx: Option<watch::Sender<SharedString>>,
961 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
962 cx: &mut AsyncApp,
963 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
964 let mut command = self.command.clone();
965 let root_dir: Arc<Path> = root_dir
966 .map(|root_dir| Path::new(root_dir))
967 .unwrap_or(paths::home_dir())
968 .into();
969 let project_environment = self.project_environment.downgrade();
970 cx.spawn(async move |cx| {
971 let mut env = project_environment
972 .update(cx, |project_environment, cx| {
973 project_environment.get_directory_environment(root_dir.clone(), cx)
974 })?
975 .await
976 .unwrap_or_default();
977 env.extend(command.env.unwrap_or_default());
978 env.extend(extra_env);
979 command.env = Some(env);
980 Ok((command, root_dir.to_proto(), None))
981 })
982 }
983
984 fn as_any_mut(&mut self) -> &mut dyn Any {
985 self
986 }
987}
988
989pub const GEMINI_NAME: &'static str = "gemini";
990pub const CLAUDE_CODE_NAME: &'static str = "claude";
991
992#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
993pub struct AllAgentServersSettings {
994 pub gemini: Option<BuiltinAgentServerSettings>,
995 pub claude: Option<BuiltinAgentServerSettings>,
996 pub custom: HashMap<SharedString, CustomAgentServerSettings>,
997}
998#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
999pub struct BuiltinAgentServerSettings {
1000 pub path: Option<PathBuf>,
1001 pub args: Option<Vec<String>>,
1002 pub env: Option<HashMap<String, String>>,
1003 pub ignore_system_version: Option<bool>,
1004 pub default_mode: Option<String>,
1005}
1006
1007impl BuiltinAgentServerSettings {
1008 pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
1009 self.path.map(|path| AgentServerCommand {
1010 path,
1011 args: self.args.unwrap_or_default(),
1012 env: self.env,
1013 })
1014 }
1015}
1016
1017impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
1018 fn from(value: settings::BuiltinAgentServerSettings) -> Self {
1019 BuiltinAgentServerSettings {
1020 path: value.path,
1021 args: value.args,
1022 env: value.env,
1023 ignore_system_version: value.ignore_system_version,
1024 default_mode: value.default_mode,
1025 }
1026 }
1027}
1028
1029impl From<AgentServerCommand> for BuiltinAgentServerSettings {
1030 fn from(value: AgentServerCommand) -> Self {
1031 BuiltinAgentServerSettings {
1032 path: Some(value.path),
1033 args: Some(value.args),
1034 env: value.env,
1035 ..Default::default()
1036 }
1037 }
1038}
1039
1040#[derive(Clone, JsonSchema, Debug, PartialEq)]
1041pub struct CustomAgentServerSettings {
1042 pub command: AgentServerCommand,
1043 /// The default mode to use for this agent.
1044 ///
1045 /// Note: Not only all agents support modes.
1046 ///
1047 /// Default: None
1048 pub default_mode: Option<String>,
1049}
1050
1051impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1052 fn from(value: settings::CustomAgentServerSettings) -> Self {
1053 CustomAgentServerSettings {
1054 command: AgentServerCommand {
1055 path: value.path,
1056 args: value.args,
1057 env: value.env,
1058 },
1059 default_mode: value.default_mode,
1060 }
1061 }
1062}
1063
1064impl settings::Settings for AllAgentServersSettings {
1065 fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
1066 let agent_settings = content.agent_servers.clone().unwrap();
1067 Self {
1068 gemini: agent_settings.gemini.map(Into::into),
1069 claude: agent_settings.claude.map(Into::into),
1070 custom: agent_settings
1071 .custom
1072 .into_iter()
1073 .map(|(k, v)| (k, v.into()))
1074 .collect(),
1075 }
1076 }
1077
1078 fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
1079}