agent_servers.rs

  1mod acp;
  2mod claude;
  3mod gemini;
  4mod settings;
  5
  6#[cfg(any(test, feature = "test-support"))]
  7pub mod e2e_tests;
  8
  9pub use claude::*;
 10pub use gemini::*;
 11pub use settings::*;
 12
 13use acp_thread::AgentConnection;
 14use anyhow::Result;
 15use collections::HashMap;
 16use gpui::{App, AsyncApp, Entity, SharedString, Task};
 17use project::Project;
 18use schemars::JsonSchema;
 19use serde::{Deserialize, Serialize};
 20use std::{
 21    any::Any,
 22    path::{Path, PathBuf},
 23    rc::Rc,
 24    sync::Arc,
 25};
 26use util::ResultExt as _;
 27
 28pub fn init(cx: &mut App) {
 29    settings::init(cx);
 30}
 31
 32pub trait AgentServer: Send {
 33    fn logo(&self) -> ui::IconName;
 34    fn name(&self) -> &'static str;
 35    fn empty_state_headline(&self) -> &'static str;
 36    fn empty_state_message(&self) -> &'static str;
 37
 38    fn connect(
 39        &self,
 40        root_dir: &Path,
 41        project: &Entity<Project>,
 42        cx: &mut App,
 43    ) -> Task<Result<Rc<dyn AgentConnection>>>;
 44
 45    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
 46}
 47
 48impl dyn AgentServer {
 49    pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
 50        self.into_any().downcast().ok()
 51    }
 52}
 53
 54impl std::fmt::Debug for AgentServerCommand {
 55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 56        let filtered_env = self.env.as_ref().map(|env| {
 57            env.iter()
 58                .map(|(k, v)| {
 59                    (
 60                        k,
 61                        if util::redact::should_redact(k) {
 62                            "[REDACTED]"
 63                        } else {
 64                            v
 65                        },
 66                    )
 67                })
 68                .collect::<Vec<_>>()
 69        });
 70
 71        f.debug_struct("AgentServerCommand")
 72            .field("path", &self.path)
 73            .field("args", &self.args)
 74            .field("env", &filtered_env)
 75            .finish()
 76    }
 77}
 78
 79pub enum AgentServerVersion {
 80    Supported,
 81    Unsupported {
 82        error_message: SharedString,
 83        upgrade_message: SharedString,
 84        upgrade_command: String,
 85    },
 86}
 87
 88#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
 89pub struct AgentServerCommand {
 90    #[serde(rename = "command")]
 91    pub path: PathBuf,
 92    #[serde(default)]
 93    pub args: Vec<String>,
 94    pub env: Option<HashMap<String, String>>,
 95}
 96
 97impl AgentServerCommand {
 98    pub(crate) async fn resolve(
 99        path_bin_name: &'static str,
100        extra_args: &[&'static str],
101        fallback_path: Option<&Path>,
102        settings: Option<AgentServerSettings>,
103        project: &Entity<Project>,
104        cx: &mut AsyncApp,
105    ) -> Option<Self> {
106        if let Some(agent_settings) = settings {
107            Some(Self {
108                path: agent_settings.command.path,
109                args: agent_settings
110                    .command
111                    .args
112                    .into_iter()
113                    .chain(extra_args.iter().map(|arg| arg.to_string()))
114                    .collect(),
115                env: agent_settings.command.env,
116            })
117        } else {
118            match find_bin_in_path(path_bin_name, project, cx).await {
119                Some(path) => Some(Self {
120                    path,
121                    args: extra_args.iter().map(|arg| arg.to_string()).collect(),
122                    env: None,
123                }),
124                None => fallback_path.and_then(|path| {
125                    if path.exists() {
126                        Some(Self {
127                            path: path.to_path_buf(),
128                            args: extra_args.iter().map(|arg| arg.to_string()).collect(),
129                            env: None,
130                        })
131                    } else {
132                        None
133                    }
134                }),
135            }
136        }
137    }
138}
139
140async fn find_bin_in_path(
141    bin_name: &'static str,
142    project: &Entity<Project>,
143    cx: &mut AsyncApp,
144) -> Option<PathBuf> {
145    let (env_task, root_dir) = project
146        .update(cx, |project, cx| {
147            let worktree = project.visible_worktrees(cx).next();
148            match worktree {
149                Some(worktree) => {
150                    let env_task = project.environment().update(cx, |env, cx| {
151                        env.get_worktree_environment(worktree.clone(), cx)
152                    });
153
154                    let path = worktree.read(cx).abs_path();
155                    (env_task, path)
156                }
157                None => {
158                    let path: Arc<Path> = paths::home_dir().as_path().into();
159                    let env_task = project.environment().update(cx, |env, cx| {
160                        env.get_directory_environment(path.clone(), cx)
161                    });
162                    (env_task, path)
163                }
164            }
165        })
166        .log_err()?;
167
168    cx.background_executor()
169        .spawn(async move {
170            let which_result = if cfg!(windows) {
171                which::which(bin_name)
172            } else {
173                let env = env_task.await.unwrap_or_default();
174                let shell_path = env.get("PATH").cloned();
175                which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
176            };
177
178            if let Err(which::Error::CannotFindBinaryPath) = which_result {
179                return None;
180            }
181
182            which_result.log_err()
183        })
184        .await
185}