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}