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}