agent_servers.rs

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