Provide codex as an option on remote sessions (#39774)

Ben Brandt and Cole Miller created

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/project/src/agent_server_store.rs         | 38 ++++++++++++++---
crates/project/src/project.rs                    |  8 +++
crates/remote_server/src/headless_project.rs     |  9 +++
crates/remote_server/src/remote_editing_tests.rs |  4 
4 files changed, 47 insertions(+), 12 deletions(-)

Detailed changes

crates/project/src/agent_server_store.rs 🔗

@@ -8,7 +8,6 @@ use std::{
 };
 
 use anyhow::{Context as _, Result, bail};
-use client::Client;
 use collections::HashMap;
 use feature_flags::FeatureFlagAppExt as _;
 use fs::{Fs, RemoveOptions, RenameOptions};
@@ -16,7 +15,7 @@ use futures::StreamExt as _;
 use gpui::{
     AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
 };
-use http_client::github::AssetKind;
+use http_client::{HttpClient, github::AssetKind};
 use node_runtime::NodeRuntime;
 use remote::RemoteClient;
 use rpc::{AnyProtoClient, TypedEnvelope, proto};
@@ -114,6 +113,7 @@ enum AgentServerStoreState {
         project_environment: Entity<ProjectEnvironment>,
         downstream_client: Option<(u64, AnyProtoClient)>,
         settings: Option<AllAgentServersSettings>,
+        http_client: Arc<dyn HttpClient>,
         _subscriptions: [Subscription; 1],
     },
     Remote {
@@ -174,6 +174,7 @@ impl AgentServerStore {
             project_environment,
             downstream_client,
             settings: old_settings,
+            http_client,
             ..
         } = &mut self.state
         else {
@@ -227,6 +228,8 @@ impl AgentServerStore {
                         .codex
                         .clone()
                         .and_then(|settings| settings.custom_command()),
+                    http_client: http_client.clone(),
+                    is_remote: downstream_client.is_some(),
                 }),
             );
         }
@@ -253,7 +256,6 @@ impl AgentServerStore {
                     names: self
                         .external_agents
                         .keys()
-                        .filter(|name| name.0 != CODEX_NAME)
                         .map(|name| name.to_string())
                         .collect(),
                 })
@@ -266,6 +268,7 @@ impl AgentServerStore {
         node_runtime: NodeRuntime,
         fs: Arc<dyn Fs>,
         project_environment: Entity<ProjectEnvironment>,
+        http_client: Arc<dyn HttpClient>,
         cx: &mut Context<Self>,
     ) -> Self {
         let subscription = cx.observe_global::<SettingsStore>(|this, cx| {
@@ -283,6 +286,7 @@ impl AgentServerStore {
                 node_runtime,
                 fs,
                 project_environment,
+                http_client,
                 downstream_client: None,
                 settings: None,
                 _subscriptions: [subscription],
@@ -297,12 +301,12 @@ impl AgentServerStore {
     pub(crate) fn remote(
         project_id: u64,
         upstream_client: Entity<RemoteClient>,
-        _cx: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) -> Self {
         // Set up the builtin agents here so they're immediately available in
         // remote projects--we know that the HeadlessProject on the other end
         // will have them.
-        let external_agents = [
+        let mut external_agents = [
             (
                 GEMINI_NAME.into(),
                 Box::new(RemoteExternalAgentServer {
@@ -325,7 +329,21 @@ impl AgentServerStore {
             ),
         ]
         .into_iter()
-        .collect();
+        .collect::<HashMap<ExternalAgentServerName, Box<dyn ExternalAgentServer>>>();
+
+        use feature_flags::FeatureFlagAppExt as _;
+        if cx.has_flag::<feature_flags::CodexAcpFeatureFlag>() {
+            external_agents.insert(
+                CODEX_NAME.into(),
+                Box::new(RemoteExternalAgentServer {
+                    project_id,
+                    upstream_client: upstream_client.clone(),
+                    name: CODEX_NAME.into(),
+                    status_tx: None,
+                    new_version_available_tx: None,
+                }) as Box<dyn ExternalAgentServer>,
+            );
+        }
 
         Self {
             state: AgentServerStoreState::Remote {
@@ -1003,7 +1021,9 @@ impl ExternalAgentServer for LocalClaudeCode {
 struct LocalCodex {
     fs: Arc<dyn Fs>,
     project_environment: Entity<ProjectEnvironment>,
+    http_client: Arc<dyn HttpClient>,
     custom_command: Option<AgentServerCommand>,
+    is_remote: bool,
 }
 
 impl ExternalAgentServer for LocalCodex {
@@ -1017,11 +1037,13 @@ impl ExternalAgentServer for LocalCodex {
     ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
         let fs = self.fs.clone();
         let project_environment = self.project_environment.downgrade();
+        let http = self.http_client.clone();
         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();
+        let is_remote = self.is_remote;
 
         cx.spawn(async move |cx| {
             let mut env = project_environment
@@ -1030,6 +1052,9 @@ impl ExternalAgentServer for LocalCodex {
                 })?
                 .await
                 .unwrap_or_default();
+            if is_remote {
+                env.insert("NO_BROWSER".to_owned(), "1".to_owned());
+            }
 
             let mut command = if let Some(mut custom_command) = custom_command {
                 env.extend(custom_command.env.unwrap_or_default());
@@ -1040,7 +1065,6 @@ impl ExternalAgentServer for LocalCodex {
                 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(
                     CODEX_ACP_REPO,
                     true,

crates/project/src/project.rs 🔗

@@ -1155,7 +1155,13 @@ impl Project {
             });
 
             let agent_server_store = cx.new(|cx| {
-                AgentServerStore::local(node.clone(), fs.clone(), environment.clone(), cx)
+                AgentServerStore::local(
+                    node.clone(),
+                    fs.clone(),
+                    environment.clone(),
+                    client.http_client(),
+                    cx,
+                )
             });
 
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();

crates/remote_server/src/headless_project.rs 🔗

@@ -196,8 +196,13 @@ impl HeadlessProject {
         });
 
         let agent_server_store = cx.new(|cx| {
-            let mut agent_server_store =
-                AgentServerStore::local(node_runtime.clone(), fs.clone(), environment, cx);
+            let mut agent_server_store = AgentServerStore::local(
+                node_runtime.clone(),
+                fs.clone(),
+                environment,
+                http_client.clone(),
+                cx,
+            );
             agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
             agent_server_store
         });

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1792,7 +1792,7 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
+    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude"]);
     server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
         settings_store
             .set_server_settings(
@@ -1822,7 +1822,7 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
+    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]);
     let (command, root, login) = project
         .update(cx, |project, cx| {
             project.agent_server_store().update(cx, |store, cx| {