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