agent_servers.rs

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