agent_servers.rs

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