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