agent_servers.rs

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