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, SettingsKey, SettingsStore, SettingsUi};
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 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 cx.spawn(async move |this, cx| {
249 cx.background_executor().timer(Duration::from_secs(1)).await;
250 this.update(cx, |this, cx| {
251 this.agent_servers_settings_changed(cx);
252 })
253 .ok();
254 })
255 .detach();
256 this
257 }
258
259 pub(crate) fn remote(
260 project_id: u64,
261 upstream_client: Entity<RemoteClient>,
262 _cx: &mut Context<Self>,
263 ) -> Self {
264 // Set up the builtin agents here so they're immediately available in
265 // remote projects--we know that the HeadlessProject on the other end
266 // will have them.
267 let external_agents = [
268 (
269 GEMINI_NAME.into(),
270 Box::new(RemoteExternalAgentServer {
271 project_id,
272 upstream_client: upstream_client.clone(),
273 name: GEMINI_NAME.into(),
274 status_tx: None,
275 new_version_available_tx: None,
276 }) as Box<dyn ExternalAgentServer>,
277 ),
278 (
279 CLAUDE_CODE_NAME.into(),
280 Box::new(RemoteExternalAgentServer {
281 project_id,
282 upstream_client: upstream_client.clone(),
283 name: CLAUDE_CODE_NAME.into(),
284 status_tx: None,
285 new_version_available_tx: None,
286 }) as Box<dyn ExternalAgentServer>,
287 ),
288 ]
289 .into_iter()
290 .collect();
291
292 Self {
293 state: AgentServerStoreState::Remote {
294 project_id,
295 upstream_client,
296 },
297 external_agents,
298 }
299 }
300
301 pub(crate) fn collab(_cx: &mut Context<Self>) -> Self {
302 Self {
303 state: AgentServerStoreState::Collab,
304 external_agents: Default::default(),
305 }
306 }
307
308 pub fn shared(&mut self, project_id: u64, client: AnyProtoClient) {
309 match &mut self.state {
310 AgentServerStoreState::Local {
311 downstream_client, ..
312 } => {
313 client
314 .send(proto::ExternalAgentsUpdated {
315 project_id,
316 names: self
317 .external_agents
318 .keys()
319 .map(|name| name.to_string())
320 .collect(),
321 })
322 .log_err();
323 *downstream_client = Some((project_id, client));
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
724// new method: status_updated
725// does nothing in the all-local case
726// for RemoteExternalAgentServer, sends on the stored tx
727// etc.
728
729impl ExternalAgentServer for RemoteExternalAgentServer {
730 fn get_command(
731 &mut self,
732 root_dir: Option<&str>,
733 extra_env: HashMap<String, String>,
734 status_tx: Option<watch::Sender<SharedString>>,
735 new_version_available_tx: Option<watch::Sender<Option<String>>>,
736 cx: &mut AsyncApp,
737 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
738 let project_id = self.project_id;
739 let name = self.name.to_string();
740 let upstream_client = self.upstream_client.downgrade();
741 let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
742 self.status_tx = status_tx;
743 self.new_version_available_tx = new_version_available_tx;
744 cx.spawn(async move |cx| {
745 let mut response = upstream_client
746 .update(cx, |upstream_client, _| {
747 upstream_client
748 .proto_client()
749 .request(proto::GetAgentServerCommand {
750 project_id,
751 name,
752 root_dir: root_dir.clone(),
753 })
754 })?
755 .await?;
756 let root_dir = response.root_dir;
757 response.env.extend(extra_env);
758 let command = upstream_client.update(cx, |client, _| {
759 client.build_command(
760 Some(response.path),
761 &response.args,
762 &response.env.into_iter().collect(),
763 Some(root_dir.clone()),
764 None,
765 )
766 })??;
767 Ok((
768 AgentServerCommand {
769 path: command.program.into(),
770 args: command.args,
771 env: Some(command.env),
772 },
773 root_dir,
774 response
775 .login
776 .map(|login| task::SpawnInTerminal::from_proto(login)),
777 ))
778 })
779 }
780
781 fn as_any_mut(&mut self) -> &mut dyn Any {
782 self
783 }
784}
785
786struct LocalGemini {
787 fs: Arc<dyn Fs>,
788 node_runtime: NodeRuntime,
789 project_environment: Entity<ProjectEnvironment>,
790 custom_command: Option<AgentServerCommand>,
791 ignore_system_version: bool,
792}
793
794impl ExternalAgentServer for LocalGemini {
795 fn get_command(
796 &mut self,
797 root_dir: Option<&str>,
798 extra_env: HashMap<String, String>,
799 status_tx: Option<watch::Sender<SharedString>>,
800 new_version_available_tx: Option<watch::Sender<Option<String>>>,
801 cx: &mut AsyncApp,
802 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
803 let fs = self.fs.clone();
804 let node_runtime = self.node_runtime.clone();
805 let project_environment = self.project_environment.downgrade();
806 let custom_command = self.custom_command.clone();
807 let ignore_system_version = self.ignore_system_version;
808 let root_dir: Arc<Path> = root_dir
809 .map(|root_dir| Path::new(root_dir))
810 .unwrap_or(paths::home_dir())
811 .into();
812
813 cx.spawn(async move |cx| {
814 let mut env = project_environment
815 .update(cx, |project_environment, cx| {
816 project_environment.get_directory_environment(root_dir.clone(), cx)
817 })?
818 .await
819 .unwrap_or_default();
820
821 let mut command = if let Some(mut custom_command) = custom_command {
822 env.extend(custom_command.env.unwrap_or_default());
823 custom_command.env = Some(env);
824 custom_command
825 } else if !ignore_system_version
826 && let Some(bin) =
827 find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
828 {
829 AgentServerCommand {
830 path: bin,
831 args: Vec::new(),
832 env: Some(env),
833 }
834 } else {
835 let mut command = get_or_npm_install_builtin_agent(
836 GEMINI_NAME.into(),
837 "@google/gemini-cli".into(),
838 "node_modules/@google/gemini-cli/dist/index.js".into(),
839 Some("0.2.1".parse().unwrap()),
840 status_tx,
841 new_version_available_tx,
842 fs,
843 node_runtime,
844 cx,
845 )
846 .await?;
847 command.env = Some(env);
848 command
849 };
850
851 // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
852 let login = task::SpawnInTerminal {
853 command: Some(command.path.clone().to_proto()),
854 args: command.args.clone(),
855 env: command.env.clone().unwrap_or_default(),
856 label: "gemini /auth".into(),
857 ..Default::default()
858 };
859
860 command.env.get_or_insert_default().extend(extra_env);
861 command.args.push("--experimental-acp".into());
862 Ok((command, root_dir.to_proto(), Some(login)))
863 })
864 }
865
866 fn as_any_mut(&mut self) -> &mut dyn Any {
867 self
868 }
869}
870
871struct LocalClaudeCode {
872 fs: Arc<dyn Fs>,
873 node_runtime: NodeRuntime,
874 project_environment: Entity<ProjectEnvironment>,
875 custom_command: Option<AgentServerCommand>,
876}
877
878impl ExternalAgentServer for LocalClaudeCode {
879 fn get_command(
880 &mut self,
881 root_dir: Option<&str>,
882 extra_env: HashMap<String, String>,
883 status_tx: Option<watch::Sender<SharedString>>,
884 new_version_available_tx: Option<watch::Sender<Option<String>>>,
885 cx: &mut AsyncApp,
886 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
887 let fs = self.fs.clone();
888 let node_runtime = self.node_runtime.clone();
889 let project_environment = self.project_environment.downgrade();
890 let custom_command = self.custom_command.clone();
891 let root_dir: Arc<Path> = root_dir
892 .map(|root_dir| Path::new(root_dir))
893 .unwrap_or(paths::home_dir())
894 .into();
895
896 cx.spawn(async move |cx| {
897 let mut env = project_environment
898 .update(cx, |project_environment, cx| {
899 project_environment.get_directory_environment(root_dir.clone(), cx)
900 })?
901 .await
902 .unwrap_or_default();
903 env.insert("ANTHROPIC_API_KEY".into(), "".into());
904
905 let (mut command, login) = if let Some(mut custom_command) = custom_command {
906 env.extend(custom_command.env.unwrap_or_default());
907 custom_command.env = Some(env);
908 (custom_command, None)
909 } else {
910 let mut command = get_or_npm_install_builtin_agent(
911 "claude-code-acp".into(),
912 "@zed-industries/claude-code-acp".into(),
913 "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
914 Some("0.2.5".parse().unwrap()),
915 status_tx,
916 new_version_available_tx,
917 fs,
918 node_runtime,
919 cx,
920 )
921 .await?;
922 command.env = Some(env);
923 let login = command
924 .args
925 .first()
926 .and_then(|path| {
927 path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
928 })
929 .map(|path_prefix| task::SpawnInTerminal {
930 command: Some(command.path.clone().to_proto()),
931 args: vec![
932 Path::new(path_prefix)
933 .join("@anthropic-ai/claude-code/cli.js")
934 .to_string_lossy()
935 .to_string(),
936 "/login".into(),
937 ],
938 env: command.env.clone().unwrap_or_default(),
939 label: "claude /login".into(),
940 ..Default::default()
941 });
942 (command, login)
943 };
944
945 command.env.get_or_insert_default().extend(extra_env);
946 Ok((command, root_dir.to_proto(), login))
947 })
948 }
949
950 fn as_any_mut(&mut self) -> &mut dyn Any {
951 self
952 }
953}
954
955struct LocalCustomAgent {
956 project_environment: Entity<ProjectEnvironment>,
957 command: AgentServerCommand,
958}
959
960impl ExternalAgentServer for LocalCustomAgent {
961 fn get_command(
962 &mut self,
963 root_dir: Option<&str>,
964 extra_env: HashMap<String, String>,
965 _status_tx: Option<watch::Sender<SharedString>>,
966 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
967 cx: &mut AsyncApp,
968 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
969 let mut command = self.command.clone();
970 let root_dir: Arc<Path> = root_dir
971 .map(|root_dir| Path::new(root_dir))
972 .unwrap_or(paths::home_dir())
973 .into();
974 let project_environment = self.project_environment.downgrade();
975 cx.spawn(async move |cx| {
976 let mut env = project_environment
977 .update(cx, |project_environment, cx| {
978 project_environment.get_directory_environment(root_dir.clone(), cx)
979 })?
980 .await
981 .unwrap_or_default();
982 env.extend(command.env.unwrap_or_default());
983 env.extend(extra_env);
984 command.env = Some(env);
985 Ok((command, root_dir.to_proto(), None))
986 })
987 }
988
989 fn as_any_mut(&mut self) -> &mut dyn Any {
990 self
991 }
992}
993
994pub const GEMINI_NAME: &'static str = "gemini";
995pub const CLAUDE_CODE_NAME: &'static str = "claude";
996
997#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)]
998#[settings_key(key = "agent_servers")]
999pub struct AllAgentServersSettings {
1000 pub gemini: Option<BuiltinAgentServerSettings>,
1001 pub claude: Option<BuiltinAgentServerSettings>,
1002 pub custom: HashMap<SharedString, CustomAgentServerSettings>,
1003}
1004#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
1005pub struct BuiltinAgentServerSettings {
1006 pub path: Option<PathBuf>,
1007 pub args: Option<Vec<String>>,
1008 pub env: Option<HashMap<String, String>>,
1009 pub ignore_system_version: Option<bool>,
1010 pub default_mode: Option<String>,
1011}
1012
1013impl BuiltinAgentServerSettings {
1014 pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
1015 self.path.map(|path| AgentServerCommand {
1016 path,
1017 args: self.args.unwrap_or_default(),
1018 env: self.env,
1019 })
1020 }
1021}
1022
1023impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
1024 fn from(value: settings::BuiltinAgentServerSettings) -> Self {
1025 BuiltinAgentServerSettings {
1026 path: value.path,
1027 args: value.args,
1028 env: value.env,
1029 ignore_system_version: value.ignore_system_version,
1030 default_mode: value.default_mode,
1031 }
1032 }
1033}
1034
1035impl From<AgentServerCommand> for BuiltinAgentServerSettings {
1036 fn from(value: AgentServerCommand) -> Self {
1037 BuiltinAgentServerSettings {
1038 path: Some(value.path),
1039 args: Some(value.args),
1040 env: value.env,
1041 ..Default::default()
1042 }
1043 }
1044}
1045
1046#[derive(Clone, JsonSchema, Debug, PartialEq)]
1047pub struct CustomAgentServerSettings {
1048 pub command: AgentServerCommand,
1049 /// The default mode to use for this agent.
1050 ///
1051 /// Note: Not only all agents support modes.
1052 ///
1053 /// Default: None
1054 pub default_mode: Option<String>,
1055}
1056
1057impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1058 fn from(value: settings::CustomAgentServerSettings) -> Self {
1059 CustomAgentServerSettings {
1060 command: AgentServerCommand {
1061 path: value.path,
1062 args: value.args,
1063 env: value.env,
1064 },
1065 default_mode: value.default_mode,
1066 }
1067 }
1068}
1069
1070impl settings::Settings for AllAgentServersSettings {
1071 fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
1072 let agent_settings = content.agent_servers.clone().unwrap();
1073 Self {
1074 gemini: agent_settings.gemini.map(Into::into),
1075 claude: agent_settings.claude.map(Into::into),
1076 custom: agent_settings
1077 .custom
1078 .into_iter()
1079 .map(|(k, v)| (k.clone(), v.into()))
1080 .collect(),
1081 }
1082 }
1083
1084 fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
1085 let Some(content) = &content.agent_servers else {
1086 return;
1087 };
1088 if let Some(gemini) = content.gemini.clone() {
1089 self.gemini = Some(gemini.into())
1090 };
1091 if let Some(claude) = content.claude.clone() {
1092 self.claude = Some(claude.into());
1093 }
1094 for (name, config) in content.custom.clone() {
1095 self.custom.insert(name, config.into());
1096 }
1097 }
1098
1099 fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
1100}