Separate out local and remote login/logout

Richard Feldman created

Change summary

crates/agent/src/native_agent_server.rs   | 16 +++++++
crates/agent_servers/src/agent_servers.rs | 18 ++++++-
crates/agent_servers/src/claude.rs        | 12 ++++
crates/agent_servers/src/codex.rs         | 16 +++++++
crates/agent_servers/src/custom.rs        | 16 +++++++
crates/agent_servers/src/gemini.rs        | 26 +++++++----
crates/agent_ui/src/acp/thread_view.rs    | 51 ++++++++++++++++++++++--
7 files changed, 133 insertions(+), 22 deletions(-)

Detailed changes

crates/agent/src/native_agent_server.rs 🔗

@@ -34,6 +34,22 @@ impl AgentServer for NativeAgentServer {
         ui::IconName::ZedAgent
     }
 
+    fn local_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn local_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
     fn connect(
         &self,
         _root_dir: Option<&Path>,

crates/agent_servers/src/agent_servers.rs 🔗

@@ -69,14 +69,24 @@ pub trait AgentServer: Send {
     }
 
     /// Returns the list of slash commands that should trigger Zed's authentication UI
-    /// when the user types them (e.g., "/login").
+    /// when running locally (e.g., "/login").
     /// These commands will be intercepted by Zed to show the auth method selection UI.
-    fn login_commands(&self) -> Vec<&'static str>;
+    fn local_login_commands(&self) -> Vec<&'static str>;
+
+    /// Returns the list of slash commands that should trigger Zed's authentication UI
+    /// when running remotely (e.g., "/login").
+    /// These commands will be intercepted by Zed to show the auth method selection UI.
+    fn remote_login_commands(&self) -> Vec<&'static str>;
+
+    /// Returns the list of logout-related slash commands that should be sent to the agent
+    /// when running locally to let it reset internal state (e.g., "/logout").
+    /// These commands will be added to available_commands and passed through to the agent.
+    fn local_logout_commands(&self) -> Vec<&'static str>;
 
     /// Returns the list of logout-related slash commands that should be sent to the agent
-    /// to let it reset internal state (e.g., "/logout").
+    /// when running remotely to let it reset internal state (e.g., "/logout").
     /// These commands will be added to available_commands and passed through to the agent.
-    fn logout_commands(&self) -> Vec<&'static str>;
+    fn remote_logout_commands(&self) -> Vec<&'static str>;
 
     fn connect(
         &self,

crates/agent_servers/src/claude.rs 🔗

@@ -56,11 +56,19 @@ impl AgentServer for ClaudeCode {
         });
     }
 
-    fn login_commands(&self) -> Vec<&'static str> {
+    fn local_login_commands(&self) -> Vec<&'static str> {
         vec!["login"]
     }
 
-    fn logout_commands(&self) -> Vec<&'static str> {
+    fn remote_login_commands(&self) -> Vec<&'static str> {
+        vec!["login"]
+    }
+
+    fn local_logout_commands(&self) -> Vec<&'static str> {
+        vec!["logout"]
+    }
+
+    fn remote_logout_commands(&self) -> Vec<&'static str> {
         vec!["logout"]
     }
 

crates/agent_servers/src/codex.rs 🔗

@@ -36,6 +36,22 @@ impl AgentServer for Codex {
         ui::IconName::AiOpenAi
     }
 
+    fn local_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn local_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
     fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings.get::<AllAgentServersSettings>(None).codex.clone()

crates/agent_servers/src/custom.rs 🔗

@@ -34,6 +34,22 @@ impl crate::AgentServer for CustomAgentServer {
         IconName::Terminal
     }
 
+    fn local_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_login_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn local_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
     fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings

crates/agent_servers/src/gemini.rs 🔗

@@ -25,10 +25,24 @@ impl AgentServer for Gemini {
         ui::IconName::AiGemini
     }
 
-    fn login_commands(&self) -> Vec<&'static str> {
+    fn local_login_commands(&self) -> Vec<&'static str> {
         vec!["login"]
     }
 
+    fn remote_login_commands(&self) -> Vec<&'static str> {
+        // When remote, OAuth doesn't work, so login is handled via the
+        // auth_commands mapping (oauth-personal -> spawn-gemini-cli)
+        vec![]
+    }
+
+    fn local_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
+    fn remote_logout_commands(&self) -> Vec<&'static str> {
+        vec![]
+    }
+
     fn connect(
         &self,
         root_dir: Option<&Path>,
@@ -57,7 +71,7 @@ impl AgentServer for Gemini {
             {
                 extra_env.insert("GEMINI_API_KEY".into(), api_key);
             }
-            let (command, root_dir, mut auth_commands) = store
+            let (command, root_dir, auth_commands) = store
                 .update(cx, |store, cx| {
                     let agent = store
                         .get_external_agent(&GEMINI_NAME.into())
@@ -72,14 +86,6 @@ impl AgentServer for Gemini {
                 })??
                 .await?;
 
-            // When remote, OAuth doesn't work, so we need to use the terminal-based login
-            // for oauth-personal. Map it to the same terminal command as spawn-gemini-cli.
-            if is_remote {
-                if let Some(spawn_gemini_cli) = auth_commands.get("spawn-gemini-cli").cloned() {
-                    auth_commands.insert("oauth-personal".to_string(), spawn_gemini_cli);
-                }
-            }
-
             let connection = crate::acp::connect(
                 name,
                 command,

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -1055,13 +1055,24 @@ impl AcpThreadView {
 
         // Check if this is a login or logout command
         let command_name = text.strip_prefix('/');
+        let is_remote = self.project.read(cx).is_via_remote_server();
+        let login_commands = if is_remote {
+            self.agent.remote_login_commands()
+        } else {
+            self.agent.local_login_commands()
+        };
+        let logout_commands = if is_remote {
+            self.agent.remote_logout_commands()
+        } else {
+            self.agent.local_logout_commands()
+        };
         let is_login_command = if let Some(cmd) = command_name {
-            self.agent.login_commands().contains(&cmd)
+            login_commands.contains(&cmd)
         } else {
             false
         };
         let is_logout_command = if let Some(cmd) = command_name {
-            self.agent.logout_commands().contains(&cmd)
+            logout_commands.contains(&cmd)
         } else {
             false
         };
@@ -1438,10 +1449,22 @@ impl AcpThreadView {
             AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
                 let mut available_commands = available_commands.clone();
 
+                let is_remote = self.project.read(cx).is_via_remote_server();
+                let login_commands = if is_remote {
+                    self.agent.remote_login_commands()
+                } else {
+                    self.agent.local_login_commands()
+                };
+                let logout_commands = if is_remote {
+                    self.agent.remote_logout_commands()
+                } else {
+                    self.agent.local_logout_commands()
+                };
+
                 // Add login commands from the agent
-                for command_name in self.agent.login_commands() {
+                for command_name in login_commands {
                     available_commands.push(acp::AvailableCommand {
-                        name: command_name.to_owned(),
+                        name: command_name.to_string(),
                         description: "Authenticate".to_owned(),
                         input: None,
                         meta: None,
@@ -1449,9 +1472,9 @@ impl AcpThreadView {
                 }
 
                 // Add logout commands from the agent
-                for command_name in self.agent.logout_commands() {
+                for command_name in logout_commands {
                     available_commands.push(acp::AvailableCommand {
-                        name: command_name.to_owned(),
+                        name: command_name.to_string(),
                         description: "Authenticate".to_owned(),
                         input: None,
                         meta: None,
@@ -6027,6 +6050,22 @@ pub(crate) mod tests {
             "Test".into()
         }
 
+        fn local_login_commands(&self) -> Vec<&'static str> {
+            vec![]
+        }
+
+        fn remote_login_commands(&self) -> Vec<&'static str> {
+            vec![]
+        }
+
+        fn local_logout_commands(&self) -> Vec<&'static str> {
+            vec![]
+        }
+
+        fn remote_logout_commands(&self) -> Vec<&'static str> {
+            vec![]
+        }
+
         fn connect(
             &self,
             _root_dir: Option<&Path>,