agent_servers.rs

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