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