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