agent_servers.rs

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