1mod claude;
2mod codex;
3mod gemini;
4mod settings;
5
6#[cfg(test)]
7mod e2e_tests;
8
9pub use claude::*;
10pub use codex::*;
11pub use gemini::*;
12pub use settings::*;
13
14use acp_thread::AgentConnection;
15use anyhow::Result;
16use collections::HashMap;
17use gpui::{App, AsyncApp, Entity, SharedString, Task};
18use project::Project;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use std::{
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 // these will go away when old_acp is fully removed
41 root_dir: &Path,
42 project: &Entity<Project>,
43 cx: &mut App,
44 ) -> Task<Result<Rc<dyn AgentConnection>>>;
45}
46
47impl std::fmt::Debug for AgentServerCommand {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 let filtered_env = self.env.as_ref().map(|env| {
50 env.iter()
51 .map(|(k, v)| {
52 (
53 k,
54 if util::redact::should_redact(k) {
55 "[REDACTED]"
56 } else {
57 v
58 },
59 )
60 })
61 .collect::<Vec<_>>()
62 });
63
64 f.debug_struct("AgentServerCommand")
65 .field("path", &self.path)
66 .field("args", &self.args)
67 .field("env", &filtered_env)
68 .finish()
69 }
70}
71
72pub enum AgentServerVersion {
73 Supported,
74 Unsupported {
75 error_message: SharedString,
76 upgrade_message: SharedString,
77 upgrade_command: String,
78 },
79}
80
81#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
82pub struct AgentServerCommand {
83 #[serde(rename = "command")]
84 pub path: PathBuf,
85 #[serde(default)]
86 pub args: Vec<String>,
87 pub env: Option<HashMap<String, String>>,
88}
89
90impl AgentServerCommand {
91 pub(crate) async fn resolve(
92 path_bin_name: &'static str,
93 extra_args: &[&'static str],
94 settings: Option<AgentServerSettings>,
95 project: &Entity<Project>,
96 cx: &mut AsyncApp,
97 ) -> Option<Self> {
98 if let Some(agent_settings) = settings {
99 return Some(Self {
100 path: agent_settings.command.path,
101 args: agent_settings
102 .command
103 .args
104 .into_iter()
105 .chain(extra_args.iter().map(|arg| arg.to_string()))
106 .collect(),
107 env: agent_settings.command.env,
108 });
109 } else {
110 find_bin_in_path(path_bin_name, project, cx)
111 .await
112 .map(|path| Self {
113 path,
114 args: extra_args.iter().map(|arg| arg.to_string()).collect(),
115 env: None,
116 })
117 }
118 }
119}
120
121async fn find_bin_in_path(
122 bin_name: &'static str,
123 project: &Entity<Project>,
124 cx: &mut AsyncApp,
125) -> Option<PathBuf> {
126 let (env_task, root_dir) = project
127 .update(cx, |project, cx| {
128 let worktree = project.visible_worktrees(cx).next();
129 match worktree {
130 Some(worktree) => {
131 let env_task = project.environment().update(cx, |env, cx| {
132 env.get_worktree_environment(worktree.clone(), cx)
133 });
134
135 let path = worktree.read(cx).abs_path();
136 (env_task, path)
137 }
138 None => {
139 let path: Arc<Path> = paths::home_dir().as_path().into();
140 let env_task = project.environment().update(cx, |env, cx| {
141 env.get_directory_environment(path.clone(), cx)
142 });
143 (env_task, path)
144 }
145 }
146 })
147 .log_err()?;
148
149 cx.background_executor()
150 .spawn(async move {
151 let which_result = if cfg!(windows) {
152 which::which(bin_name)
153 } else {
154 let env = env_task.await.unwrap_or_default();
155 let shell_path = env.get("PATH").cloned();
156 which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
157 };
158
159 if let Err(which::Error::CannotFindBinaryPath) = which_result {
160 return None;
161 }
162
163 which_result.log_err()
164 })
165 .await
166}