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