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().into_owned(),
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 let download_result = 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 && download_result.is_some()
622 {
623 new_version_available.send(Some(latest_version)).ok();
624 }
625 }
626 }
627 })
628 .detach();
629 file_name
630 } else {
631 if let Some(mut status_tx) = status_tx {
632 status_tx.send("Installing…".into()).ok();
633 }
634 let dir = dir.clone();
635 cx.background_spawn(download_latest_version(
636 fs.clone(),
637 dir.clone(),
638 node_runtime,
639 package_name.clone(),
640 ))
641 .await?
642 .into()
643 };
644
645 let agent_server_path = dir.join(version).join(entrypoint_path);
646 let agent_server_path_exists = fs.is_file(&agent_server_path).await;
647 anyhow::ensure!(
648 agent_server_path_exists,
649 "Missing entrypoint path {} after installation",
650 agent_server_path.to_string_lossy()
651 );
652
653 anyhow::Ok(AgentServerCommand {
654 path: node_path,
655 args: vec![agent_server_path.to_string_lossy().into_owned()],
656 env: None,
657 })
658 })
659}
660
661fn find_bin_in_path(
662 bin_name: SharedString,
663 root_dir: PathBuf,
664 env: HashMap<String, String>,
665 cx: &mut AsyncApp,
666) -> Task<Option<PathBuf>> {
667 cx.background_executor().spawn(async move {
668 let which_result = if cfg!(windows) {
669 which::which(bin_name.as_str())
670 } else {
671 let shell_path = env.get("PATH").cloned();
672 which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
673 };
674
675 if let Err(which::Error::CannotFindBinaryPath) = which_result {
676 return None;
677 }
678
679 which_result.log_err()
680 })
681}
682
683async fn download_latest_version(
684 fs: Arc<dyn Fs>,
685 dir: PathBuf,
686 node_runtime: NodeRuntime,
687 package_name: SharedString,
688) -> Result<String> {
689 log::debug!("downloading latest version of {package_name}");
690
691 let tmp_dir = tempfile::tempdir_in(&dir)?;
692
693 node_runtime
694 .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
695 .await?;
696
697 let version = node_runtime
698 .npm_package_installed_version(tmp_dir.path(), &package_name)
699 .await?
700 .context("expected package to be installed")?;
701
702 fs.rename(
703 &tmp_dir.keep(),
704 &dir.join(&version),
705 RenameOptions {
706 ignore_if_exists: true,
707 overwrite: true,
708 },
709 )
710 .await?;
711
712 anyhow::Ok(version)
713}
714
715struct RemoteExternalAgentServer {
716 project_id: u64,
717 upstream_client: Entity<RemoteClient>,
718 name: ExternalAgentServerName,
719 status_tx: Option<watch::Sender<SharedString>>,
720 new_version_available_tx: Option<watch::Sender<Option<String>>>,
721}
722
723impl ExternalAgentServer for RemoteExternalAgentServer {
724 fn get_command(
725 &mut self,
726 root_dir: Option<&str>,
727 extra_env: HashMap<String, String>,
728 status_tx: Option<watch::Sender<SharedString>>,
729 new_version_available_tx: Option<watch::Sender<Option<String>>>,
730 cx: &mut AsyncApp,
731 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
732 let project_id = self.project_id;
733 let name = self.name.to_string();
734 let upstream_client = self.upstream_client.downgrade();
735 let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
736 self.status_tx = status_tx;
737 self.new_version_available_tx = new_version_available_tx;
738 cx.spawn(async move |cx| {
739 let mut response = upstream_client
740 .update(cx, |upstream_client, _| {
741 upstream_client
742 .proto_client()
743 .request(proto::GetAgentServerCommand {
744 project_id,
745 name,
746 root_dir: root_dir.clone(),
747 })
748 })?
749 .await?;
750 let root_dir = response.root_dir;
751 response.env.extend(extra_env);
752 let command = upstream_client.update(cx, |client, _| {
753 client.build_command(
754 Some(response.path),
755 &response.args,
756 &response.env.into_iter().collect(),
757 Some(root_dir.clone()),
758 None,
759 )
760 })??;
761 Ok((
762 AgentServerCommand {
763 path: command.program.into(),
764 args: command.args,
765 env: Some(command.env),
766 },
767 root_dir,
768 response
769 .login
770 .map(|login| task::SpawnInTerminal::from_proto(login)),
771 ))
772 })
773 }
774
775 fn as_any_mut(&mut self) -> &mut dyn Any {
776 self
777 }
778}
779
780struct LocalGemini {
781 fs: Arc<dyn Fs>,
782 node_runtime: NodeRuntime,
783 project_environment: Entity<ProjectEnvironment>,
784 custom_command: Option<AgentServerCommand>,
785 ignore_system_version: bool,
786}
787
788impl ExternalAgentServer for LocalGemini {
789 fn get_command(
790 &mut self,
791 root_dir: Option<&str>,
792 extra_env: HashMap<String, String>,
793 status_tx: Option<watch::Sender<SharedString>>,
794 new_version_available_tx: Option<watch::Sender<Option<String>>>,
795 cx: &mut AsyncApp,
796 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
797 let fs = self.fs.clone();
798 let node_runtime = self.node_runtime.clone();
799 let project_environment = self.project_environment.downgrade();
800 let custom_command = self.custom_command.clone();
801 let ignore_system_version = self.ignore_system_version;
802 let root_dir: Arc<Path> = root_dir
803 .map(|root_dir| Path::new(root_dir))
804 .unwrap_or(paths::home_dir())
805 .into();
806
807 cx.spawn(async move |cx| {
808 let mut env = project_environment
809 .update(cx, |project_environment, cx| {
810 project_environment.get_directory_environment(root_dir.clone(), cx)
811 })?
812 .await
813 .unwrap_or_default();
814
815 let mut command = if let Some(mut custom_command) = custom_command {
816 env.extend(custom_command.env.unwrap_or_default());
817 custom_command.env = Some(env);
818 custom_command
819 } else if !ignore_system_version
820 && let Some(bin) =
821 find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
822 {
823 AgentServerCommand {
824 path: bin,
825 args: Vec::new(),
826 env: Some(env),
827 }
828 } else {
829 let mut command = get_or_npm_install_builtin_agent(
830 GEMINI_NAME.into(),
831 "@google/gemini-cli".into(),
832 "node_modules/@google/gemini-cli/dist/index.js".into(),
833 Some("0.2.1".parse().unwrap()),
834 status_tx,
835 new_version_available_tx,
836 fs,
837 node_runtime,
838 cx,
839 )
840 .await?;
841 command.env = Some(env);
842 command
843 };
844
845 // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
846 let login = task::SpawnInTerminal {
847 command: Some(command.path.to_string_lossy().into_owned()),
848 args: command.args.clone(),
849 env: command.env.clone().unwrap_or_default(),
850 label: "gemini /auth".into(),
851 ..Default::default()
852 };
853
854 command.env.get_or_insert_default().extend(extra_env);
855 command.args.push("--experimental-acp".into());
856 Ok((
857 command,
858 root_dir.to_string_lossy().into_owned(),
859 Some(login),
860 ))
861 })
862 }
863
864 fn as_any_mut(&mut self) -> &mut dyn Any {
865 self
866 }
867}
868
869struct LocalClaudeCode {
870 fs: Arc<dyn Fs>,
871 node_runtime: NodeRuntime,
872 project_environment: Entity<ProjectEnvironment>,
873 custom_command: Option<AgentServerCommand>,
874}
875
876impl ExternalAgentServer for LocalClaudeCode {
877 fn get_command(
878 &mut self,
879 root_dir: Option<&str>,
880 extra_env: HashMap<String, String>,
881 status_tx: Option<watch::Sender<SharedString>>,
882 new_version_available_tx: Option<watch::Sender<Option<String>>>,
883 cx: &mut AsyncApp,
884 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
885 let fs = self.fs.clone();
886 let node_runtime = self.node_runtime.clone();
887 let project_environment = self.project_environment.downgrade();
888 let custom_command = self.custom_command.clone();
889 let root_dir: Arc<Path> = root_dir
890 .map(|root_dir| Path::new(root_dir))
891 .unwrap_or(paths::home_dir())
892 .into();
893
894 cx.spawn(async move |cx| {
895 let mut env = project_environment
896 .update(cx, |project_environment, cx| {
897 project_environment.get_directory_environment(root_dir.clone(), cx)
898 })?
899 .await
900 .unwrap_or_default();
901 env.insert("ANTHROPIC_API_KEY".into(), "".into());
902
903 let (mut command, login) = if let Some(mut custom_command) = custom_command {
904 env.extend(custom_command.env.unwrap_or_default());
905 custom_command.env = Some(env);
906 (custom_command, None)
907 } else {
908 let mut command = get_or_npm_install_builtin_agent(
909 "claude-code-acp".into(),
910 "@zed-industries/claude-code-acp".into(),
911 "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
912 Some("0.5.2".parse().unwrap()),
913 status_tx,
914 new_version_available_tx,
915 fs,
916 node_runtime,
917 cx,
918 )
919 .await?;
920 command.env = Some(env);
921 let login = command
922 .args
923 .first()
924 .and_then(|path| {
925 path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
926 })
927 .map(|path_prefix| task::SpawnInTerminal {
928 command: Some(command.path.to_string_lossy().into_owned()),
929 args: vec![
930 Path::new(path_prefix)
931 .join("@anthropic-ai/claude-agent-sdk/cli.js")
932 .to_string_lossy()
933 .to_string(),
934 "/login".into(),
935 ],
936 env: command.env.clone().unwrap_or_default(),
937 label: "claude /login".into(),
938 ..Default::default()
939 });
940 (command, login)
941 };
942
943 command.env.get_or_insert_default().extend(extra_env);
944 Ok((command, root_dir.to_string_lossy().into_owned(), login))
945 })
946 }
947
948 fn as_any_mut(&mut self) -> &mut dyn Any {
949 self
950 }
951}
952
953struct LocalCustomAgent {
954 project_environment: Entity<ProjectEnvironment>,
955 command: AgentServerCommand,
956}
957
958impl ExternalAgentServer for LocalCustomAgent {
959 fn get_command(
960 &mut self,
961 root_dir: Option<&str>,
962 extra_env: HashMap<String, String>,
963 _status_tx: Option<watch::Sender<SharedString>>,
964 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
965 cx: &mut AsyncApp,
966 ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
967 let mut command = self.command.clone();
968 let root_dir: Arc<Path> = root_dir
969 .map(|root_dir| Path::new(root_dir))
970 .unwrap_or(paths::home_dir())
971 .into();
972 let project_environment = self.project_environment.downgrade();
973 cx.spawn(async move |cx| {
974 let mut env = project_environment
975 .update(cx, |project_environment, cx| {
976 project_environment.get_directory_environment(root_dir.clone(), cx)
977 })?
978 .await
979 .unwrap_or_default();
980 env.extend(command.env.unwrap_or_default());
981 env.extend(extra_env);
982 command.env = Some(env);
983 Ok((command, root_dir.to_string_lossy().into_owned(), None))
984 })
985 }
986
987 fn as_any_mut(&mut self) -> &mut dyn Any {
988 self
989 }
990}
991
992pub const GEMINI_NAME: &'static str = "gemini";
993pub const CLAUDE_CODE_NAME: &'static str = "claude";
994
995#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
996pub struct AllAgentServersSettings {
997 pub gemini: Option<BuiltinAgentServerSettings>,
998 pub claude: Option<BuiltinAgentServerSettings>,
999 pub custom: HashMap<SharedString, CustomAgentServerSettings>,
1000}
1001#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
1002pub struct BuiltinAgentServerSettings {
1003 pub path: Option<PathBuf>,
1004 pub args: Option<Vec<String>>,
1005 pub env: Option<HashMap<String, String>>,
1006 pub ignore_system_version: Option<bool>,
1007 pub default_mode: Option<String>,
1008}
1009
1010impl BuiltinAgentServerSettings {
1011 pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
1012 self.path.map(|path| AgentServerCommand {
1013 path,
1014 args: self.args.unwrap_or_default(),
1015 env: self.env,
1016 })
1017 }
1018}
1019
1020impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
1021 fn from(value: settings::BuiltinAgentServerSettings) -> Self {
1022 BuiltinAgentServerSettings {
1023 path: value.path,
1024 args: value.args,
1025 env: value.env,
1026 ignore_system_version: value.ignore_system_version,
1027 default_mode: value.default_mode,
1028 }
1029 }
1030}
1031
1032impl From<AgentServerCommand> for BuiltinAgentServerSettings {
1033 fn from(value: AgentServerCommand) -> Self {
1034 BuiltinAgentServerSettings {
1035 path: Some(value.path),
1036 args: Some(value.args),
1037 env: value.env,
1038 ..Default::default()
1039 }
1040 }
1041}
1042
1043#[derive(Clone, JsonSchema, Debug, PartialEq)]
1044pub struct CustomAgentServerSettings {
1045 pub command: AgentServerCommand,
1046 /// The default mode to use for this agent.
1047 ///
1048 /// Note: Not only all agents support modes.
1049 ///
1050 /// Default: None
1051 pub default_mode: Option<String>,
1052}
1053
1054impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1055 fn from(value: settings::CustomAgentServerSettings) -> Self {
1056 CustomAgentServerSettings {
1057 command: AgentServerCommand {
1058 path: value.path,
1059 args: value.args,
1060 env: value.env,
1061 },
1062 default_mode: value.default_mode,
1063 }
1064 }
1065}
1066
1067impl settings::Settings for AllAgentServersSettings {
1068 fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
1069 let agent_settings = content.agent_servers.clone().unwrap();
1070 Self {
1071 gemini: agent_settings.gemini.map(Into::into),
1072 claude: agent_settings.claude.map(Into::into),
1073 custom: agent_settings
1074 .custom
1075 .into_iter()
1076 .map(|(k, v)| (k, v.into()))
1077 .collect(),
1078 }
1079 }
1080
1081 fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
1082}