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