From b9d9602074268741d9e8c75182dc6eabcedac50f Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 1 Oct 2025 23:52:06 -0400 Subject: [PATCH] Add codex acp (#39327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behind a feature flag for now. Screenshot 2025-10-01 at 9 34 16 PM Release Notes: - N/A --- Cargo.lock | 3 + crates/agent_servers/src/acp.rs | 4 + crates/agent_servers/src/agent_servers.rs | 2 + crates/agent_servers/src/codex.rs | 80 ++++++ crates/agent_servers/src/e2e_tests.rs | 7 + crates/agent_ui/src/acp/thread_view.rs | 37 ++- crates/agent_ui/src/agent_configuration.rs | 6 +- crates/agent_ui/src/agent_panel.rs | 42 +++- crates/agent_ui/src/agent_ui.rs | 3 + crates/feature_flags/src/flags.rs | 6 + crates/project/Cargo.toml | 3 + crates/project/src/agent_server_store.rs | 235 +++++++++++++++++- crates/settings/src/settings_content/agent.rs | 1 + 13 files changed, 410 insertions(+), 19 deletions(-) create mode 100644 crates/agent_servers/src/codex.rs diff --git a/Cargo.lock b/Cargo.lock index d4ba0368fa97bad2226f240b769b25f2490df9b4..8b3229a4b327b9e53aebd16e9a9961c705262515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12082,6 +12082,8 @@ dependencies = [ "aho-corasick", "anyhow", "askpass", + "async-compression", + "async-tar", "async-trait", "base64 0.22.1", "buffer_diff", @@ -12094,6 +12096,7 @@ dependencies = [ "dap_adapters", "extension", "fancy-regex 0.14.0", + "feature_flags", "fs", "futures 0.3.31", "fuzzy", diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b14c0467c58d3f41e32e602996560e2cc672d76a..52760a5b65ba335660e31a693c304c7c61acdec9 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -380,6 +380,10 @@ impl AgentConnection for AcpConnection { match result { Ok(response) => Ok(response), Err(err) => { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + return Err(anyhow!(acp::Error::auth_required())); + } + if err.code != ErrorCode::INTERNAL_ERROR.code { anyhow::bail!(err) } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index af95fdfa4cde66ffbc04f7234112ba5243e7d951..b44c2123fb5052e2487464d813936cd1edf9821a 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,5 +1,6 @@ mod acp; mod claude; +mod codex; mod custom; mod gemini; @@ -8,6 +9,7 @@ pub mod e2e_tests; pub use claude::*; use client::ProxySettings; +pub use codex::*; use collections::HashMap; pub use custom::*; use fs::Fs; diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a19cfd03214972e9c7cd62aee713f3689d525df --- /dev/null +++ b/crates/agent_servers/src/codex.rs @@ -0,0 +1,80 @@ +use std::rc::Rc; +use std::{any::Any, path::Path}; + +use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; +use acp_thread::AgentConnection; +use anyhow::{Context as _, Result}; +use gpui::{App, SharedString, Task}; +use project::agent_server_store::CODEX_NAME; + +#[derive(Clone)] +pub struct Codex; + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once"); +} + +impl AgentServer for Codex { + fn telemetry_id(&self) -> &'static str { + "codex" + } + + fn name(&self) -> SharedString { + "Codex".into() + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiOpenAi + } + + fn connect( + &self, + root_dir: Option<&Path>, + delegate: AgentServerDelegate, + cx: &mut App, + ) -> Task, Option)>> { + let name = self.name(); + let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); + let is_remote = delegate.project.read(cx).is_via_remote_server(); + let store = delegate.store.downgrade(); + let extra_env = load_proxy_env(cx); + let default_mode = self.default_mode(cx); + + cx.spawn(async move |cx| { + let (command, root_dir, login) = store + .update(cx, |store, cx| { + let agent = store + .get_external_agent(&CODEX_NAME.into()) + .context("Codex is not registered")?; + anyhow::Ok(agent.get_command( + root_dir.as_deref(), + extra_env, + delegate.status_tx, + // For now, report that there are no updates. + // (A future PR will use the GitHub Releases API to fetch them.) + delegate.new_version_available, + &mut cx.to_async(), + )) + })?? + .await?; + + let connection = crate::acp::connect( + name, + command, + root_dir.as_ref(), + default_mode, + is_remote, + cx, + ) + .await?; + Ok((connection, login)) + }) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 1ee2e099f0ae355267b5f0a5aaddb3371f427240..60480caa541ba1c39dba62ed709c157fd67fede0 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -483,6 +483,13 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { default_mode: None, }), gemini: Some(crate::gemini::tests::local_command().into()), + codex: Some(BuiltinAgentServerSettings { + path: Some("codex-acp".into()), + args: None, + env: None, + ignore_system_version: None, + default_mode: None, + }), custom: collections::HashMap::default(), }, cx, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a743da30f49f971b1a1bc7cccaccac736d2e709f..87f6a88be42ea91e1181c2c0de503bedf7d44aa9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -577,6 +577,31 @@ impl AcpThreadView { AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); + // Proactively surface Authentication Required if the agent advertises auth methods. + if let Some(acp_conn) = thread + .read(cx) + .connection() + .clone() + .downcast::() + { + let methods = acp_conn.auth_methods(); + if !methods.is_empty() { + // Immediately transition to auth-required UI, but defer to avoid re-entrant update. + let err = AuthRequired { + description: None, + provider_id: None, + }; + let this_weak = cx.weak_entity(); + let agent = agent.clone(); + let connection = thread.read(cx).connection().clone(); + window.defer(cx, move |window, cx| { + Self::handle_auth_required( + this_weak, err, agent, connection, window, cx, + ); + }); + } + } + this.model_selector = thread .read(cx) .connection() @@ -1012,11 +1037,13 @@ impl AcpThreadView { }; let connection = thread.read(cx).connection().clone(); - if !connection - .auth_methods() - .iter() - .any(|method| method.id.0.as_ref() == "claude-login") - { + let auth_methods = connection.auth_methods(); + let has_supported_auth = auth_methods.iter().any(|method| { + let id = method.id.0.as_ref(); + id == "claude-login" || id == "spawn-gemini-cli" + }); + let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some(); + if !can_login { return; }; let this = cx.weak_entity(); diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 3fd78c44ec5a249c6acf4ddd9ac548988a51612c..54fbfc536c2eae0581a4c3d9949913724537a7a7 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -26,7 +26,7 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ - agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME}, + agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME}, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, }; use settings::{Settings, SettingsStore, update_settings_file}; @@ -1014,7 +1014,9 @@ impl AgentConfiguration { .agent_server_store .read(cx) .external_agents() - .filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME) + .filter(|name| { + name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME + }) .cloned() .collect::>(); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index ca6a5fb2f6c216e7886394da069c93e5029a5ed0..69d4dbe6d193f3f18c240420234f825618db48be 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -7,7 +7,7 @@ use acp_thread::AcpThread; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use project::agent_server_store::{ - AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME, + AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME, }; use serde::{Deserialize, Serialize}; use settings::{ @@ -75,6 +75,7 @@ use zed_actions::{ assistant::{OpenRulesLibrary, ToggleFocus}, }; +use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _}; const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize, Debug)] @@ -216,6 +217,7 @@ pub enum AgentType { TextThread, Gemini, ClaudeCode, + Codex, NativeAgent, Custom { name: SharedString, @@ -230,6 +232,7 @@ impl AgentType { Self::NativeAgent => "Agent 2".into(), Self::Gemini => "Gemini CLI".into(), Self::ClaudeCode => "Claude Code".into(), + Self::Codex => "Codex".into(), Self::Custom { name, .. } => name.into(), } } @@ -239,6 +242,7 @@ impl AgentType { Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), + Self::Codex => Some(IconName::AiOpenAi), Self::Custom { .. } => Some(IconName::Terminal), } } @@ -249,6 +253,7 @@ impl From for AgentType { match value { ExternalAgent::Gemini => Self::Gemini, ExternalAgent::ClaudeCode => Self::ClaudeCode, + ExternalAgent::Codex => Self::Codex, ExternalAgent::Custom { name, command } => Self::Custom { name, command }, ExternalAgent::NativeAgent => Self::NativeAgent, } @@ -1427,6 +1432,11 @@ impl AgentPanel { cx, ) } + AgentType::Codex => { + self.selected_agent = AgentType::Codex; + self.serialize(cx); + self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx) + } AgentType::Custom { name, command } => self.external_thread( Some(crate::ExternalAgent::Custom { name, command }), None, @@ -1991,12 +2001,40 @@ impl AgentPanel { } }), ) + .when(cx.has_flag::(), |this| { + this.item( + ContextMenuEntry::new("New Codex Thread") + .icon(IconName::AiOpenAi) + .disabled(is_via_collab) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.new_agent_thread( + AgentType::Codex, + window, + cx, + ); + }); + } + }); + } + } + }), + ) + }) .map(|mut menu| { let agent_names = agent_server_store .read(cx) .external_agents() .filter(|name| { - name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME + name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME }) .cloned() .collect::>(); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index b64334f403bed8cfcf86e80e4fe4589ba920b06d..2c439a725456976f090ddc4cb754664c4953d626 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -167,6 +167,7 @@ enum ExternalAgent { #[default] Gemini, ClaudeCode, + Codex, NativeAgent, Custom { name: SharedString, @@ -188,6 +189,7 @@ impl ExternalAgent { Self::NativeAgent => "zed", Self::Gemini => "gemini-cli", Self::ClaudeCode => "claude-code", + Self::Codex => "codex", Self::Custom { .. } => "custom", } } @@ -200,6 +202,7 @@ impl ExternalAgent { match self { Self::Gemini => Rc::new(agent_servers::Gemini), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + Self::Codex => Rc::new(agent_servers::Codex), Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), Self::Custom { name, command: _ } => { Rc::new(agent_servers::CustomAgentServer::new(name.clone())) diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 47b6f1230ac747c2633327d1be923d33388cf179..981abc0fa6aa096d53aa4ee5f74d37567f0eb5e1 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -17,3 +17,9 @@ pub struct PanicFeatureFlag; impl FeatureFlag for PanicFeatureFlag { const NAME: &'static str = "panic"; } + +pub struct CodexAcpFeatureFlag; + +impl FeatureFlag for CodexAcpFeatureFlag { + const NAME: &'static str = "codex-acp"; +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 39dc0621732bfd42b3a24735ad803915fbf2885c..f7a037bf1ccc9a86aab827d07ccad6be4592a348 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -30,6 +30,8 @@ test-support = [ aho-corasick.workspace = true anyhow.workspace = true askpass.workspace = true +async-compression.workspace = true +async-tar.workspace = true async-trait.workspace = true base64.workspace = true buffer_diff.workspace = true @@ -90,6 +92,7 @@ which.workspace = true worktree.workspace = true zeroize.workspace = true zlog.workspace = true +feature_flags.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index fc2400bc9afa903dc452bd383a5d0851852da4d8..7a34c34f759d017b16b644a4f38efca7524f2451 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -8,6 +8,7 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; +use client::Client; use collections::HashMap; use fs::{Fs, RemoveOptions, RenameOptions}; use futures::StreamExt as _; @@ -182,6 +183,32 @@ impl AgentServerStore { .unwrap_or(true), }), ); + self.external_agents + .extend(new_settings.custom.iter().map(|(name, settings)| { + ( + ExternalAgentServerName(name.clone()), + Box::new(LocalCustomAgent { + command: settings.command.clone(), + project_environment: project_environment.clone(), + }) as Box, + ) + })); + + use feature_flags::FeatureFlagAppExt as _; + if cx.has_flag::() || new_settings.codex.is_some() { + self.external_agents.insert( + CODEX_NAME.into(), + Box::new(LocalCodex { + fs: fs.clone(), + project_environment: project_environment.clone(), + custom_command: new_settings + .codex + .clone() + .and_then(|settings| settings.custom_command()), + }), + ); + } + self.external_agents.insert( CLAUDE_CODE_NAME.into(), Box::new(LocalClaudeCode { @@ -194,16 +221,6 @@ impl AgentServerStore { .and_then(|settings| settings.custom_command()), }), ); - self.external_agents - .extend(new_settings.custom.iter().map(|(name, settings)| { - ( - ExternalAgentServerName(name.clone()), - Box::new(LocalCustomAgent { - command: settings.command.clone(), - project_environment: project_environment.clone(), - }) as Box, - ) - })); *old_settings = Some(new_settings.clone()); @@ -214,6 +231,7 @@ impl AgentServerStore { names: self .external_agents .keys() + .filter(|name| name.0 != CODEX_NAME) .map(|name| name.to_string()) .collect(), }) @@ -950,6 +968,164 @@ impl ExternalAgentServer for LocalClaudeCode { } } +struct LocalCodex { + fs: Arc, + project_environment: Entity, + custom_command: Option, +} + +impl ExternalAgentServer for LocalCodex { + fn get_command( + &mut self, + root_dir: Option<&str>, + extra_env: HashMap, + _status_tx: Option>, + _new_version_available_tx: Option>>, + cx: &mut AsyncApp, + ) -> Task)>> { + let fs = self.fs.clone(); + let project_environment = self.project_environment.downgrade(); + let custom_command = self.custom_command.clone(); + let root_dir: Arc = root_dir + .map(|root_dir| Path::new(root_dir)) + .unwrap_or(paths::home_dir()) + .into(); + + cx.spawn(async move |cx| { + let mut env = project_environment + .update(cx, |project_environment, cx| { + project_environment.get_directory_environment(root_dir.clone(), cx) + })? + .await + .unwrap_or_default(); + + let mut command = if let Some(mut custom_command) = custom_command { + env.extend(custom_command.env.unwrap_or_default()); + custom_command.env = Some(env); + custom_command + } else { + let dir = paths::data_dir().join("external_agents").join(CODEX_NAME); + fs.create_dir(&dir).await?; + + // Find or install the latest Codex release (no update checks for now). + let http = cx.update(|cx| Client::global(cx).http_client())?; + let release = ::http_client::github::latest_github_release( + "zed-industries/codex-acp", + true, + false, + http.clone(), + ) + .await + .context("fetching Codex latest release")?; + + let version_dir = dir.join(&release.tag_name); + if !fs.is_dir(&version_dir).await { + // Assemble release download URL from prefix, tag, and filename based on target triple. + // If unsupported, silently skip download. + let tag = release.tag_name.clone(); // e.g. "v0.1.0" + let version_number = tag.trim_start_matches('v'); + if let Some(asset_url) = codex_release_url(version_number) { + let http = http.clone(); + let mut response = http + .get(&asset_url, Default::default(), true) + .await + .with_context(|| { + format!("downloading Codex binary from {}", asset_url) + })?; + anyhow::ensure!( + response.status().is_success(), + "failed to download Codex release: {}", + response.status() + ); + + // Extract archive into the version directory. + if asset_url.ends_with(".zip") { + let reader = futures::io::BufReader::new(response.body_mut()); + util::archive::extract_zip(&version_dir, reader) + .await + .context("extracting Codex binary from zip")?; + } else { + // Decompress and extract the tar.gz into the version directory. + let reader = futures::io::BufReader::new(response.body_mut()); + let decoder = + async_compression::futures::bufread::GzipDecoder::new(reader); + let archive = async_tar::Archive::new(decoder); + archive + .unpack(&version_dir) + .await + .context("extracting Codex binary from tar.gz")?; + } + } + } + + let bin_name = if cfg!(windows) { + "codex-acp.exe" + } else { + "codex-acp" + }; + let bin_path = version_dir.join(bin_name); + anyhow::ensure!( + fs.is_file(&bin_path).await, + "Missing Codex binary at {} after installation", + bin_path.to_string_lossy() + ); + + let mut cmd = AgentServerCommand { + path: bin_path, + args: Vec::new(), + env: None, + }; + cmd.env = Some(env); + cmd + }; + + command.env.get_or_insert_default().extend(extra_env); + Ok((command, root_dir.to_string_lossy().into_owned(), None)) + }) + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +/// Assemble Codex release URL for the current OS/arch and the given version number. +/// Returns None if the current target is unsupported. +/// Example output: +/// https://github.com/zed-industries/codex-acp/releases/download/v{version}/codex-acp-{version}-{arch}-{platform}.{ext} +fn codex_release_url(version: &str) -> Option { + let arch = if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else { + return None; + }; + + let platform = if cfg!(target_os = "macos") { + "apple-darwin" + } else if cfg!(target_os = "windows") { + "pc-windows-msvc" + } else if cfg!(target_os = "linux") { + "unknown-linux-gnu" + } else { + return None; + }; + + // Only Windows x86_64 uses .zip in release assets + let ext = if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") { + "zip" + } else { + "tar.gz" + }; + + let prefix = "https://github.com/zed-industries/codex-acp/releases/download"; + + Some(format!( + "{prefix}/v{version}/codex-acp-{version}-{arch}-{platform}.{ext}" + )) +} + struct LocalCustomAgent { project_environment: Entity, command: AgentServerCommand, @@ -989,13 +1165,51 @@ impl ExternalAgentServer for LocalCustomAgent { } } +#[cfg(test)] +mod tests { + #[test] + fn assembles_codex_release_url_for_current_target() { + let version_number = "0.1.0"; + + // This test fails the build if we are building a version of Zed + // which does not have a known build of codex-acp, to prevent us + // from accidentally doing a release on a new target without + // realizing that codex-acp support will not work on that target! + // + // Additionally, it verifies that our logic for assembling URLs + // correctly resolves to a known-good URL on each of our targets. + let allowed = [ + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-apple-darwin.tar.gz", + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-pc-windows-msvc.tar.gz", + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-unknown-linux-gnu.tar.gz", + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-apple-darwin.tar.gz", + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-pc-windows-msvc.zip", + "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-unknown-linux-gnu.tar.gz", + ]; + + if let Some(url) = super::codex_release_url(version_number) { + assert!( + allowed.contains(&url.as_str()), + "Assembled URL {} not in allowed list", + url + ); + } else { + panic!( + "This target does not have a known codex-acp release! We should fix this by building a release of codex-acp for this target, as otherwise codex-acp will not be usable with this Zed build." + ); + } + } +} + pub const GEMINI_NAME: &'static str = "gemini"; pub const CLAUDE_CODE_NAME: &'static str = "claude"; +pub const CODEX_NAME: &'static str = "codex"; #[derive(Default, Clone, JsonSchema, Debug, PartialEq)] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + pub codex: Option, pub custom: HashMap, } #[derive(Default, Clone, JsonSchema, Debug, PartialEq)] @@ -1070,6 +1284,7 @@ impl settings::Settings for AllAgentServersSettings { Self { gemini: agent_settings.gemini.map(Into::into), claude: agent_settings.claude.map(Into::into), + codex: agent_settings.codex.map(Into::into), custom: agent_settings .custom .into_iter() diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 88b27615aab5b2ddef1a55678897fff0beb114a1..9644cbb3bd455f42052d0c4c45d958d9a492d712 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -282,6 +282,7 @@ impl From<&str> for LanguageModelProviderSetting { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + pub codex: Option, /// Custom agent servers configured by the user #[serde(flatten)]