diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 6d9c77f2968d7b39302391829d2d14b6d4493a91..1b253cfbdaa0b4c46d34dfb732f9c47341505511 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,4 +1,5 @@ mod claude; +mod codex; mod gemini; mod settings; mod stdio_agent_server; diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs new file mode 100644 index 0000000000000000000000000000000000000000..d20c93ef2a284696ab9c4230a0017cb0f9f63a9c --- /dev/null +++ b/crates/agent_servers/src/codex.rs @@ -0,0 +1,121 @@ +use crate::stdio_agent_server::StdioAgentServer; +use crate::{AgentServerCommand, AgentServerVersion}; +use anyhow::{Context as _, Result}; +use gpui::{AsyncApp, Entity}; +use project::Project; +use settings::SettingsStore; + +use crate::AllAgentServersSettings; + +#[derive(Clone)] +pub struct Codex; + +const ACP_ARG: &str = "experimental-acp"; + +impl StdioAgentServer for Codex { + fn name(&self) -> &'static str { + "Codex" + } + + fn empty_state_headline(&self) -> &'static str { + "Welcome to Codex" + } + + fn empty_state_message(&self) -> &'static str { + "" + } + + fn supports_always_allow(&self) -> bool { + true + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiOpenAi + } + + async fn command( + &self, + project: &Entity, + cx: &mut AsyncApp, + ) -> Result { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).codex.clone() + })?; + + if let Some(command) = + AgentServerCommand::resolve("codex", &[ACP_ARG], settings, &project, cx).await + { + return Ok(command); + }; + + let (fs, node_runtime) = project.update(cx, |project, _| { + (project.fs().clone(), project.node_runtime().cloned()) + })?; + let node_runtime = node_runtime.context("codex not found on path")?; + + let directory = ::paths::agent_servers_dir().join("codex"); + fs.create_dir(&directory).await?; + node_runtime + .npm_install_packages(&directory, &[("@openai/codex", "latest")]) + .await?; + let path = directory.join("node_modules/.bin/codex"); + + Ok(AgentServerCommand { + path, + args: vec![ACP_ARG.into()], + env: None, + }) + } + + async fn version(&self, command: &AgentServerCommand) -> Result { + let version_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output(); + + let help_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--help") + .kill_on_drop(true) + .output(); + + let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; + + let current_version = String::from_utf8(version_output?.stdout)?; + let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); + + if supported { + Ok(AgentServerVersion::Supported) + } else { + Ok(AgentServerVersion::Unsupported { + error_message: format!( + "Your installed version of Codex {} doesn't support the Agentic Coding Protocol (ACP).", + current_version + ).into(), + upgrade_message: "Upgrade Codex to Latest".into(), + upgrade_command: "npm install -g @openai/codex@latest".into(), + }) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::AgentServerCommand; + use std::path::Path; + + crate::common_e2e_tests!(Codex); + + pub fn local_command() -> AgentServerCommand { + let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../codex/codex-rs/target/debug/codex"); + + AgentServerCommand { + path: cli_path, + args: vec![], + env: None, + } + } +} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 923c6cdd6f1112233b45386c8a6a4db0ab380d65..ec5c5444880f995ffa2846b556439e4dc3394932 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -307,6 +307,9 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { claude: Some(AgentServerSettings { command: crate::claude::tests::local_command(), }), + codex: Some(AgentServerSettings { + command: crate::codex::tests::local_command(), + }), gemini: Some(AgentServerSettings { command: crate::gemini::tests::local_command(), }), diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 29dcf5eb8c9b3e1738fd5c24b18e309d897b0c6f..acdb790b2d0c1c5ffbc30ef7a241999c2ec60659 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -13,6 +13,7 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + pub codex: Option, } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]