codex.rs

  1use std::rc::Rc;
  2use std::sync::Arc;
  3use std::{any::Any, path::Path};
  4
  5use acp_thread::AgentConnection;
  6use agent_client_protocol as acp;
  7use anyhow::{Context as _, Result};
  8use collections::HashSet;
  9use fs::Fs;
 10use gpui::{App, AppContext as _, SharedString, Task};
 11use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
 12use settings::{SettingsStore, update_settings_file};
 13
 14use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
 15
 16#[derive(Clone)]
 17pub struct Codex;
 18
 19const CODEX_API_KEY_VAR_NAME: &str = "CODEX_API_KEY";
 20const OPEN_AI_API_KEY_VAR_NAME: &str = "OPEN_AI_API_KEY";
 21
 22impl AgentServer for Codex {
 23    fn name(&self) -> SharedString {
 24        "Codex".into()
 25    }
 26
 27    fn logo(&self) -> ui::IconName {
 28        ui::IconName::AiOpenAi
 29    }
 30
 31    fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
 32        let settings = cx.read_global(|settings: &SettingsStore, _| {
 33            settings.get::<AllAgentServersSettings>(None).codex.clone()
 34        });
 35
 36        settings
 37            .as_ref()
 38            .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
 39    }
 40
 41    fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
 42        update_settings_file(fs, cx, |settings, _| {
 43            settings
 44                .agent_servers
 45                .get_or_insert_default()
 46                .codex
 47                .get_or_insert_default()
 48                .default_mode = mode_id.map(|m| m.to_string())
 49        });
 50    }
 51
 52    fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
 53        let settings = cx.read_global(|settings: &SettingsStore, _| {
 54            settings.get::<AllAgentServersSettings>(None).codex.clone()
 55        });
 56
 57        settings
 58            .as_ref()
 59            .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
 60    }
 61
 62    fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
 63        update_settings_file(fs, cx, |settings, _| {
 64            settings
 65                .agent_servers
 66                .get_or_insert_default()
 67                .codex
 68                .get_or_insert_default()
 69                .default_model = model_id.map(|m| m.to_string())
 70        });
 71    }
 72
 73    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
 74        let settings = cx.read_global(|settings: &SettingsStore, _| {
 75            settings.get::<AllAgentServersSettings>(None).codex.clone()
 76        });
 77
 78        settings
 79            .as_ref()
 80            .map(|s| {
 81                s.favorite_models
 82                    .iter()
 83                    .map(|id| acp::ModelId::new(id.clone()))
 84                    .collect()
 85            })
 86            .unwrap_or_default()
 87    }
 88
 89    fn toggle_favorite_model(
 90        &self,
 91        model_id: acp::ModelId,
 92        should_be_favorite: bool,
 93        fs: Arc<dyn Fs>,
 94        cx: &App,
 95    ) {
 96        update_settings_file(fs, cx, move |settings, _| {
 97            let favorite_models = &mut settings
 98                .agent_servers
 99                .get_or_insert_default()
100                .codex
101                .get_or_insert_default()
102                .favorite_models;
103
104            let model_id_str = model_id.to_string();
105            if should_be_favorite {
106                if !favorite_models.contains(&model_id_str) {
107                    favorite_models.push(model_id_str);
108                }
109            } else {
110                favorite_models.retain(|id| id != &model_id_str);
111            }
112        });
113    }
114
115    fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
116        let settings = cx.read_global(|settings: &SettingsStore, _| {
117            settings.get::<AllAgentServersSettings>(None).codex.clone()
118        });
119
120        settings
121            .as_ref()
122            .and_then(|s| s.default_config_options.get(config_id).cloned())
123    }
124
125    fn set_default_config_option(
126        &self,
127        config_id: &str,
128        value_id: Option<&str>,
129        fs: Arc<dyn Fs>,
130        cx: &mut App,
131    ) {
132        let config_id = config_id.to_string();
133        let value_id = value_id.map(|s| s.to_string());
134        update_settings_file(fs, cx, move |settings, _| {
135            let config_options = &mut settings
136                .agent_servers
137                .get_or_insert_default()
138                .codex
139                .get_or_insert_default()
140                .default_config_options;
141
142            if let Some(value) = value_id.clone() {
143                config_options.insert(config_id.clone(), value);
144            } else {
145                config_options.remove(&config_id);
146            }
147        });
148    }
149
150    fn favorite_config_option_value_ids(
151        &self,
152        config_id: &acp::SessionConfigId,
153        cx: &mut App,
154    ) -> HashSet<acp::SessionConfigValueId> {
155        let settings = cx.read_global(|settings: &SettingsStore, _| {
156            settings.get::<AllAgentServersSettings>(None).codex.clone()
157        });
158
159        settings
160            .as_ref()
161            .and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
162            .map(|values| {
163                values
164                    .iter()
165                    .cloned()
166                    .map(acp::SessionConfigValueId::new)
167                    .collect()
168            })
169            .unwrap_or_default()
170    }
171
172    fn toggle_favorite_config_option_value(
173        &self,
174        config_id: acp::SessionConfigId,
175        value_id: acp::SessionConfigValueId,
176        should_be_favorite: bool,
177        fs: Arc<dyn Fs>,
178        cx: &App,
179    ) {
180        let config_id = config_id.to_string();
181        let value_id = value_id.to_string();
182
183        update_settings_file(fs, cx, move |settings, _| {
184            let favorites = &mut settings
185                .agent_servers
186                .get_or_insert_default()
187                .codex
188                .get_or_insert_default()
189                .favorite_config_option_values;
190
191            let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
192
193            if should_be_favorite {
194                if !entry.iter().any(|v| v == &value_id) {
195                    entry.push(value_id.clone());
196                }
197            } else {
198                entry.retain(|v| v != &value_id);
199                if entry.is_empty() {
200                    favorites.remove(&config_id);
201                }
202            }
203        });
204    }
205
206    fn connect(
207        &self,
208        root_dir: Option<&Path>,
209        delegate: AgentServerDelegate,
210        cx: &mut App,
211    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
212        let name = self.name();
213        let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
214        let is_remote = delegate.project.read(cx).is_via_remote_server();
215        let store = delegate.store.downgrade();
216        let mut extra_env = load_proxy_env(cx);
217        let default_mode = self.default_mode(cx);
218        let default_model = self.default_model(cx);
219        let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
220            settings
221                .get::<AllAgentServersSettings>(None)
222                .codex
223                .as_ref()
224                .map(|s| s.default_config_options.clone())
225                .unwrap_or_default()
226        });
227        if let Ok(api_key) = std::env::var(CODEX_API_KEY_VAR_NAME) {
228            extra_env.insert(CODEX_API_KEY_VAR_NAME.into(), api_key);
229        }
230        if let Ok(api_key) = std::env::var(OPEN_AI_API_KEY_VAR_NAME) {
231            extra_env.insert(OPEN_AI_API_KEY_VAR_NAME.into(), api_key);
232        }
233
234        cx.spawn(async move |cx| {
235            let (command, root_dir, login) = store
236                .update(cx, |store, cx| {
237                    let agent = store
238                        .get_external_agent(&CODEX_NAME.into())
239                        .context("Codex is not registered")?;
240                    anyhow::Ok(agent.get_command(
241                        root_dir.as_deref(),
242                        extra_env,
243                        delegate.status_tx,
244                        delegate.new_version_available,
245                        &mut cx.to_async(),
246                    ))
247                })??
248                .await?;
249
250            let connection = crate::acp::connect(
251                name,
252                command,
253                root_dir.as_ref(),
254                default_mode,
255                default_model,
256                default_config_options,
257                is_remote,
258                cx,
259            )
260            .await?;
261            Ok((connection, login))
262        })
263    }
264
265    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
266        self
267    }
268}
269
270#[cfg(test)]
271pub(crate) mod tests {
272    use super::*;
273
274    crate::common_e2e_tests!(async |_, _| Codex, allow_option_id = "proceed_once");
275}