mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Add codex acp (#39327)
Behind a feature flag for now. <img width="576" height="234" alt="Screenshot 2025-10-01 at 9 34 16 PM" src="https://github.com/user-attachments/assets/f4e717cf-3fba-4256-af69-e3ffb5174717" /> Release Notes: - N/A
This commit is contained in:
parent
cc19f66ee1
commit
b9d9602074
13 changed files with 410 additions and 19 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
80
crates/agent_servers/src/codex.rs
Normal file
80
crates/agent_servers/src/codex.rs
Normal file
|
|
@ -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<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
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<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -483,6 +483,13 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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::<agent_servers::AcpConnection>()
|
||||
{
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ExternalAgent> 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::<CodexAcpFeatureFlag>(), |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::<AgentPanel>(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::<Vec<_>>();
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<dyn ExternalAgentServer>,
|
||||
)
|
||||
}));
|
||||
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
if cx.has_flag::<feature_flags::CodexAcpFeatureFlag>() || 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<dyn ExternalAgentServer>,
|
||||
)
|
||||
}));
|
||||
|
||||
*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<dyn Fs>,
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
custom_command: Option<AgentServerCommand>,
|
||||
}
|
||||
|
||||
impl ExternalAgentServer for LocalCodex {
|
||||
fn get_command(
|
||||
&mut self,
|
||||
root_dir: Option<&str>,
|
||||
extra_env: HashMap<String, String>,
|
||||
_status_tx: Option<watch::Sender<SharedString>>,
|
||||
_new_version_available_tx: Option<watch::Sender<Option<String>>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
|
||||
let fs = self.fs.clone();
|
||||
let project_environment = self.project_environment.downgrade();
|
||||
let custom_command = self.custom_command.clone();
|
||||
let root_dir: Arc<Path> = 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<String> {
|
||||
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<ProjectEnvironment>,
|
||||
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<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<BuiltinAgentServerSettings>,
|
||||
pub codex: Option<BuiltinAgentServerSettings>,
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
}
|
||||
#[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()
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@ impl From<&str> for LanguageModelProviderSetting {
|
|||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<BuiltinAgentServerSettings>,
|
||||
pub codex: Option<BuiltinAgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue