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