acp: Support terminal auth methods (#51999)

Ben Brandt created

## Context

Brings in support for the new terminal auth methods. Currently behind
AcpBeta feature flag while the RFD stabilizes

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

Cargo.lock                                |   1 
crates/acp_thread/src/connection.rs       |  33 +++
crates/agent_servers/Cargo.toml           |   1 
crates/agent_servers/src/acp.rs           | 229 ++++++++++++++++++++++++
crates/agent_servers/src/agent_servers.rs |   2 
crates/agent_ui/src/conversation_view.rs  | 219 +++++++++--------------
crates/project/src/agent_server_store.rs  |   9 
7 files changed, 352 insertions(+), 142 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -271,6 +271,7 @@ dependencies = [
  "collections",
  "credentials_provider",
  "env_logger 0.11.8",
+ "feature_flags",
  "fs",
  "futures 0.3.31",
  "google_ai",

crates/acp_thread/src/connection.rs 🔗

@@ -2,12 +2,13 @@ use crate::AcpThread;
 use agent_client_protocol::{self as acp};
 use anyhow::Result;
 use chrono::{DateTime, Utc};
-use collections::IndexMap;
+use collections::{HashMap, IndexMap};
 use gpui::{Entity, SharedString, Task};
 use language_model::LanguageModelProviderId;
 use project::{AgentId, Project};
 use serde::{Deserialize, Serialize};
 use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc};
+use task::{HideStrategy, SpawnInTerminal, TaskId};
 use ui::{App, IconName};
 use util::path_list::PathList;
 use uuid::Uuid;
@@ -21,6 +22,28 @@ impl UserMessageId {
     }
 }
 
+pub fn build_terminal_auth_task(
+    id: String,
+    label: String,
+    command: String,
+    args: Vec<String>,
+    env: HashMap<String, String>,
+) -> SpawnInTerminal {
+    SpawnInTerminal {
+        id: TaskId(id),
+        full_label: label.clone(),
+        label: label.clone(),
+        command: Some(command),
+        args,
+        command_label: label,
+        env,
+        use_new_terminal: true,
+        allow_concurrent_runs: true,
+        hide: HideStrategy::Always,
+        ..Default::default()
+    }
+}
+
 pub trait AgentConnection {
     fn agent_id(&self) -> AgentId;
 
@@ -90,6 +113,14 @@ pub trait AgentConnection {
 
     fn auth_methods(&self) -> &[acp::AuthMethod];
 
+    fn terminal_auth_task(
+        &self,
+        _method: &acp::AuthMethodId,
+        _cx: &App,
+    ) -> Option<SpawnInTerminal> {
+        None
+    }
+
     fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
 
     fn prompt(

crates/agent_servers/Cargo.toml 🔗

@@ -30,6 +30,7 @@ env_logger = { workspace = true, optional = true }
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+feature_flags.workspace = true
 gpui_tokio = { workspace = true, optional = true }
 credentials_provider.workspace = true
 google_ai.workspace = true

crates/agent_servers/src/acp.rs 🔗

@@ -7,13 +7,14 @@ use action_log::ActionLog;
 use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
 use anyhow::anyhow;
 use collections::HashMap;
+use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
 use futures::AsyncBufReadExt as _;
 use futures::io::BufReader;
 use project::agent_server_store::AgentServerCommand;
 use project::{AgentId, Project};
 use serde::Deserialize;
 use settings::Settings as _;
-use task::ShellBuilder;
+use task::{ShellBuilder, SpawnInTerminal};
 use util::ResultExt as _;
 use util::path_list::PathList;
 use util::process::Child;
@@ -33,6 +34,8 @@ use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings
 
 use crate::GEMINI_ID;
 
+pub const GEMINI_TERMINAL_AUTH_METHOD_ID: &str = "spawn-gemini-cli";
+
 #[derive(Debug, Error)]
 #[error("Unsupported version")]
 pub struct UnsupportedVersion;
@@ -44,6 +47,7 @@ pub struct AcpConnection {
     connection: Rc<acp::ClientSideConnection>,
     sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
     auth_methods: Vec<acp::AuthMethod>,
+    command: AgentServerCommand,
     agent_capabilities: acp::AgentCapabilities,
     default_mode: Option<acp::SessionModeId>,
     default_model: Option<acp::ModelId>,
@@ -286,6 +290,7 @@ impl AcpConnection {
                                 .read_text_file(true)
                                 .write_text_file(true))
                             .terminal(true)
+                            .auth(acp::AuthCapabilities::new().terminal(true))
                             // Experimental: Allow for rendering terminal output from the agents
                             .meta(acp::Meta::from_iter([
                                 ("terminal_output".into(), true.into()),
@@ -335,7 +340,7 @@ impl AcpConnection {
             });
             let meta = acp::Meta::from_iter([("terminal-auth".to_string(), value)]);
             vec![acp::AuthMethod::Agent(
-                acp::AuthMethodAgent::new("spawn-gemini-cli", "Login")
+                acp::AuthMethodAgent::new(GEMINI_TERMINAL_AUTH_METHOD_ID, "Login")
                     .description("Login with your Google or Vertex AI account")
                     .meta(meta),
             )]
@@ -345,6 +350,7 @@ impl AcpConnection {
         Ok(Self {
             id: agent_id,
             auth_methods,
+            command,
             connection,
             display_name,
             telemetry_id,
@@ -468,6 +474,64 @@ impl Drop for AcpConnection {
     }
 }
 
+fn terminal_auth_task_id(agent_id: &AgentId, method_id: &acp::AuthMethodId) -> String {
+    format!("external-agent-{}-{}-login", agent_id.0, method_id.0)
+}
+
+fn terminal_auth_task(
+    command: &AgentServerCommand,
+    agent_id: &AgentId,
+    method: &acp::AuthMethodTerminal,
+) -> SpawnInTerminal {
+    let mut args = command.args.clone();
+    args.extend(method.args.clone());
+
+    let mut env = command.env.clone().unwrap_or_default();
+    env.extend(method.env.clone());
+
+    acp_thread::build_terminal_auth_task(
+        terminal_auth_task_id(agent_id, &method.id),
+        method.name.clone(),
+        command.path.to_string_lossy().into_owned(),
+        args,
+        env,
+    )
+}
+
+/// Used to support the _meta method prior to stabilization
+fn meta_terminal_auth_task(
+    agent_id: &AgentId,
+    method_id: &acp::AuthMethodId,
+    method: &acp::AuthMethod,
+) -> Option<SpawnInTerminal> {
+    #[derive(Deserialize)]
+    struct MetaTerminalAuth {
+        label: String,
+        command: String,
+        #[serde(default)]
+        args: Vec<String>,
+        #[serde(default)]
+        env: HashMap<String, String>,
+    }
+
+    let meta = match method {
+        acp::AuthMethod::EnvVar(env_var) => env_var.meta.as_ref(),
+        acp::AuthMethod::Terminal(terminal) => terminal.meta.as_ref(),
+        acp::AuthMethod::Agent(agent) => agent.meta.as_ref(),
+        _ => None,
+    }?;
+    let terminal_auth =
+        serde_json::from_value::<MetaTerminalAuth>(meta.get("terminal-auth")?.clone()).ok()?;
+
+    Some(acp_thread::build_terminal_auth_task(
+        terminal_auth_task_id(agent_id, method_id),
+        terminal_auth.label.clone(),
+        terminal_auth.command,
+        terminal_auth.args,
+        terminal_auth.env,
+    ))
+}
+
 impl AgentConnection for AcpConnection {
     fn agent_id(&self) -> AgentId {
         self.id.clone()
@@ -813,6 +877,24 @@ impl AgentConnection for AcpConnection {
         &self.auth_methods
     }
 
+    fn terminal_auth_task(
+        &self,
+        method_id: &acp::AuthMethodId,
+        cx: &App,
+    ) -> Option<SpawnInTerminal> {
+        let method = self
+            .auth_methods
+            .iter()
+            .find(|method| method.id() == method_id)?;
+
+        match method {
+            acp::AuthMethod::Terminal(terminal) if cx.has_flag::<AcpBetaFeatureFlag>() => {
+                Some(terminal_auth_task(&self.command, &self.id, terminal))
+            }
+            _ => meta_terminal_auth_task(&self.id, method_id, method),
+        }
+    }
+
     fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
         let conn = self.connection.clone();
         cx.foreground_executor().spawn(async move {
@@ -979,6 +1061,149 @@ fn map_acp_error(err: acp::Error) -> anyhow::Error {
     }
 }
 
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn terminal_auth_task_reuses_command_and_merges_args_and_env() {
+        let command = AgentServerCommand {
+            path: "/path/to/agent".into(),
+            args: vec!["--acp".into(), "--verbose".into()],
+            env: Some(HashMap::from_iter([
+                ("BASE".into(), "1".into()),
+                ("SHARED".into(), "base".into()),
+            ])),
+        };
+        let method = acp::AuthMethodTerminal::new("login", "Login")
+            .args(vec!["/auth".into()])
+            .env(std::collections::HashMap::from_iter([
+                ("EXTRA".into(), "2".into()),
+                ("SHARED".into(), "override".into()),
+            ]));
+
+        let terminal_auth_task = terminal_auth_task(&command, &AgentId::new("test-agent"), &method);
+
+        assert_eq!(
+            terminal_auth_task.command.as_deref(),
+            Some("/path/to/agent")
+        );
+        assert_eq!(terminal_auth_task.args, vec!["--acp", "--verbose", "/auth"]);
+        assert_eq!(
+            terminal_auth_task.env,
+            HashMap::from_iter([
+                ("BASE".into(), "1".into()),
+                ("SHARED".into(), "override".into()),
+                ("EXTRA".into(), "2".into()),
+            ])
+        );
+        assert_eq!(terminal_auth_task.label, "Login");
+        assert_eq!(terminal_auth_task.command_label, "Login");
+    }
+
+    #[test]
+    fn legacy_terminal_auth_task_parses_meta_and_retries_session() {
+        let method_id = acp::AuthMethodId::new("legacy-login");
+        let method = acp::AuthMethod::Agent(
+            acp::AuthMethodAgent::new(method_id.clone(), "Login").meta(acp::Meta::from_iter([(
+                "terminal-auth".to_string(),
+                serde_json::json!({
+                    "label": "legacy /auth",
+                    "command": "legacy-agent",
+                    "args": ["auth", "--interactive"],
+                    "env": {
+                        "AUTH_MODE": "interactive",
+                    },
+                }),
+            )])),
+        );
+
+        let terminal_auth_task =
+            meta_terminal_auth_task(&AgentId::new("test-agent"), &method_id, &method)
+                .expect("expected legacy terminal auth task");
+
+        assert_eq!(
+            terminal_auth_task.id.0,
+            "external-agent-test-agent-legacy-login-login"
+        );
+        assert_eq!(terminal_auth_task.command.as_deref(), Some("legacy-agent"));
+        assert_eq!(terminal_auth_task.args, vec!["auth", "--interactive"]);
+        assert_eq!(
+            terminal_auth_task.env,
+            HashMap::from_iter([("AUTH_MODE".into(), "interactive".into())])
+        );
+        assert_eq!(terminal_auth_task.label, "legacy /auth");
+    }
+
+    #[test]
+    fn legacy_terminal_auth_task_returns_none_for_invalid_meta() {
+        let method_id = acp::AuthMethodId::new("legacy-login");
+        let method = acp::AuthMethod::Agent(
+            acp::AuthMethodAgent::new(method_id.clone(), "Login").meta(acp::Meta::from_iter([(
+                "terminal-auth".to_string(),
+                serde_json::json!({
+                    "label": "legacy /auth",
+                }),
+            )])),
+        );
+
+        assert!(
+            meta_terminal_auth_task(&AgentId::new("test-agent"), &method_id, &method).is_none()
+        );
+    }
+
+    #[test]
+    fn first_class_terminal_auth_takes_precedence_over_legacy_meta() {
+        let method_id = acp::AuthMethodId::new("login");
+        let method = acp::AuthMethod::Terminal(
+            acp::AuthMethodTerminal::new(method_id, "Login")
+                .args(vec!["/auth".into()])
+                .env(std::collections::HashMap::from_iter([(
+                    "AUTH_MODE".into(),
+                    "first-class".into(),
+                )]))
+                .meta(acp::Meta::from_iter([(
+                    "terminal-auth".to_string(),
+                    serde_json::json!({
+                        "label": "legacy /auth",
+                        "command": "legacy-agent",
+                        "args": ["legacy-auth"],
+                        "env": {
+                            "AUTH_MODE": "legacy",
+                        },
+                    }),
+                )])),
+        );
+
+        let command = AgentServerCommand {
+            path: "/path/to/agent".into(),
+            args: vec!["--acp".into()],
+            env: Some(HashMap::from_iter([("BASE".into(), "1".into())])),
+        };
+
+        let terminal_auth_task = match &method {
+            acp::AuthMethod::Terminal(terminal) => {
+                terminal_auth_task(&command, &AgentId::new("test-agent"), terminal)
+            }
+            _ => unreachable!(),
+        };
+
+        assert_eq!(
+            terminal_auth_task.command.as_deref(),
+            Some("/path/to/agent")
+        );
+        assert_eq!(terminal_auth_task.args, vec!["--acp", "/auth"]);
+        assert_eq!(
+            terminal_auth_task.env,
+            HashMap::from_iter([
+                ("BASE".into(), "1".into()),
+                ("AUTH_MODE".into(), "first-class".into()),
+            ])
+        );
+        assert_eq!(terminal_auth_task.label, "Login");
+    }
+}
+
 fn mcp_servers_for_project(project: &Entity<Project>, cx: &App) -> Vec<acp::McpServer> {
     let context_server_store = project.read(cx).context_server_store().read(cx);
     let is_local = project.read(cx).is_local();

crates/agent_servers/src/agent_servers.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{App, AppContext, Entity, Task};
 use settings::SettingsStore;
 use std::{any::Any, rc::Rc, sync::Arc};
 
-pub use acp::AcpConnection;
+pub use acp::{AcpConnection, GEMINI_TERMINAL_AUTH_METHOD_ID};
 
 pub struct AgentServerDelegate {
     store: Entity<AgentServerStore>,

crates/agent_ui/src/conversation_view.rs 🔗

@@ -8,9 +8,9 @@ use acp_thread::{AgentConnection, Plan};
 use action_log::{ActionLog, ActionLogTelemetry, DiffStats};
 use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
 use agent_client_protocol as acp;
-use agent_servers::AgentServer;
 #[cfg(test)]
 use agent_servers::AgentServerDelegate;
+use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID};
 use agent_settings::{AgentProfileId, AgentSettings};
 use anyhow::{Result, anyhow};
 use arrayvec::ArrayVec;
@@ -1475,6 +1475,9 @@ impl ConversationView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
         let Some(connected) = self.as_connected_mut() else {
             return;
         };
@@ -1491,119 +1494,65 @@ impl ConversationView {
 
         let agent_telemetry_id = connection.telemetry_id();
 
-        // Check for the experimental "terminal-auth" _meta field
-        let auth_method = connection.auth_methods().iter().find(|m| m.id() == &method);
+        if let Some(login) = connection.terminal_auth_task(&method, cx) {
+            configuration_view.take();
+            pending_auth_method.replace(method.clone());
 
-        if let Some(terminal_auth) = auth_method
-            .and_then(|a| match a {
-                acp::AuthMethod::EnvVar(env_var) => env_var.meta.as_ref(),
-                acp::AuthMethod::Terminal(terminal) => terminal.meta.as_ref(),
-                acp::AuthMethod::Agent(agent) => agent.meta.as_ref(),
-                _ => None,
-            })
-            .and_then(|m| m.get("terminal-auth"))
-        {
-            // Extract terminal auth details from meta
-            if let (Some(command), Some(label)) = (
-                terminal_auth.get("command").and_then(|v| v.as_str()),
-                terminal_auth.get("label").and_then(|v| v.as_str()),
-            ) {
-                let args = terminal_auth
-                    .get("args")
-                    .and_then(|v| v.as_array())
-                    .map(|arr| {
-                        arr.iter()
-                            .filter_map(|v| v.as_str().map(String::from))
-                            .collect()
-                    })
-                    .unwrap_or_default();
-
-                let env = terminal_auth
-                    .get("env")
-                    .and_then(|v| v.as_object())
-                    .map(|obj| {
-                        obj.iter()
-                            .filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
-                            .collect::<HashMap<String, String>>()
-                    })
-                    .unwrap_or_default();
-
-                // Build SpawnInTerminal from _meta
-                let login = task::SpawnInTerminal {
-                    id: task::TaskId(format!("external-agent-{}-login", label)),
-                    full_label: label.to_string(),
-                    label: label.to_string(),
-                    command: Some(command.to_string()),
-                    args,
-                    command_label: label.to_string(),
-                    env,
-                    use_new_terminal: true,
-                    allow_concurrent_runs: true,
-                    hide: task::HideStrategy::Always,
-                    ..Default::default()
-                };
-
-                configuration_view.take();
-                pending_auth_method.replace(method.clone());
-
-                if let Some(workspace) = self.workspace.upgrade() {
-                    let project = self.project.clone();
-                    let authenticate = Self::spawn_external_agent_login(
-                        login,
-                        workspace,
-                        project,
-                        method.clone(),
-                        false,
-                        window,
-                        cx,
-                    );
-                    cx.notify();
-                    self.auth_task = Some(cx.spawn_in(window, {
-                        async move |this, cx| {
-                            let result = authenticate.await;
-
-                            match &result {
-                                Ok(_) => telemetry::event!(
-                                    "Authenticate Agent Succeeded",
-                                    agent = agent_telemetry_id
-                                ),
-                                Err(_) => {
-                                    telemetry::event!(
-                                        "Authenticate Agent Failed",
-                                        agent = agent_telemetry_id,
-                                    )
-                                }
-                            }
+            let project = self.project.clone();
+            let authenticate = Self::spawn_external_agent_login(
+                login,
+                workspace,
+                project,
+                method.clone(),
+                false,
+                window,
+                cx,
+            );
+            cx.notify();
+            self.auth_task = Some(cx.spawn_in(window, {
+                async move |this, cx| {
+                    let result = authenticate.await;
+
+                    match &result {
+                        Ok(_) => telemetry::event!(
+                            "Authenticate Agent Succeeded",
+                            agent = agent_telemetry_id
+                        ),
+                        Err(_) => {
+                            telemetry::event!(
+                                "Authenticate Agent Failed",
+                                agent = agent_telemetry_id,
+                            )
+                        }
+                    }
 
-                            this.update_in(cx, |this, window, cx| {
-                                if let Err(err) = result {
-                                    if let Some(ConnectedServerState {
-                                        auth_state:
-                                            AuthState::Unauthenticated {
-                                                pending_auth_method,
-                                                ..
-                                            },
+                    this.update_in(cx, |this, window, cx| {
+                        if let Err(err) = result {
+                            if let Some(ConnectedServerState {
+                                auth_state:
+                                    AuthState::Unauthenticated {
+                                        pending_auth_method,
                                         ..
-                                    }) = this.as_connected_mut()
-                                    {
-                                        pending_auth_method.take();
-                                    }
-                                    if let Some(active) = this.active_thread() {
-                                        active.update(cx, |active, cx| {
-                                            active.handle_thread_error(err, cx);
-                                        })
-                                    }
-                                } else {
-                                    this.reset(window, cx);
-                                }
-                                this.auth_task.take()
-                            })
-                            .ok();
+                                    },
+                                ..
+                            }) = this.as_connected_mut()
+                            {
+                                pending_auth_method.take();
+                            }
+                            if let Some(active) = this.active_thread() {
+                                active.update(cx, |active, cx| {
+                                    active.handle_thread_error(err, cx);
+                                })
+                            }
+                        } else {
+                            this.reset(window, cx);
                         }
-                    }));
+                        this.auth_task.take()
+                    })
+                    .ok();
                 }
-                return;
-            }
+            }));
+            return;
         }
 
         configuration_view.take();
@@ -1726,7 +1675,7 @@ impl ConversationView {
         cx: &mut App,
     ) -> Task<Result<()>> {
         let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
-            return Task::ready(Ok(()));
+            return Task::ready(Err(anyhow!("Terminal panel is unavailable")));
         };
 
         window.spawn(cx, async move |cx| {
@@ -1734,17 +1683,14 @@ impl ConversationView {
             if let Some(cmd) = &task.command {
                 // Have "node" command use Zed's managed Node runtime by default
                 if cmd == "node" {
-                    let resolved_node_runtime = project
-                        .update(cx, |project, cx| {
-                            let agent_server_store = project.agent_server_store().clone();
-                            agent_server_store.update(cx, |store, cx| {
-                                store.node_runtime().map(|node_runtime| {
-                                    cx.background_spawn(async move {
-                                        node_runtime.binary_path().await
-                                    })
-                                })
+                    let resolved_node_runtime = project.update(cx, |project, cx| {
+                        let agent_server_store = project.agent_server_store().clone();
+                        agent_server_store.update(cx, |store, cx| {
+                            store.node_runtime().map(|node_runtime| {
+                                cx.background_spawn(async move { node_runtime.binary_path().await })
                             })
-                        });
+                        })
+                    });
 
                     if let Some(resolve_task) = resolved_node_runtime {
                         if let Ok(node_path) = resolve_task.await {
@@ -1756,14 +1702,8 @@ impl ConversationView {
             task.shell = task::Shell::WithArguments {
                 program: task.command.take().expect("login command should be set"),
                 args: std::mem::take(&mut task.args),
-                title_override: None
+                title_override: None,
             };
-            task.full_label = task.label.clone();
-            task.id = task::TaskId(format!("external-agent-{}-login", task.label));
-            task.command_label = task.label.clone();
-            task.use_new_terminal = true;
-            task.allow_concurrent_runs = true;
-            task.hide = task::HideStrategy::Always;
 
             let terminal = terminal_panel
                 .update_in(cx, |terminal_panel, window, cx| {
@@ -1772,7 +1712,7 @@ impl ConversationView {
                 .await?;
 
             let success_patterns = match method.0.as_ref() {
-                "claude-login" | "spawn-gemini-cli" => vec![
+                "claude-login" | GEMINI_TERMINAL_AUTH_METHOD_ID => vec![
                     "Login successful".to_string(),
                     "Type your message".to_string(),
                 ],
@@ -1806,7 +1746,9 @@ impl ConversationView {
                                 cx.background_executor().timer(Duration::from_secs(1)).await;
                                 let content =
                                     terminal.update(cx, |terminal, _cx| terminal.get_content())?;
-                                if success_patterns.iter().any(|pattern| content.contains(pattern))
+                                if success_patterns
+                                    .iter()
+                                    .any(|pattern| content.contains(pattern))
                                 {
                                     return anyhow::Ok(());
                                 }
@@ -1823,8 +1765,23 @@ impl ConversationView {
                         }
                     }
                     _ = exit_status => {
-                        if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server()) && login.label.contains("gemini") {
-                            return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), method, true, window, cx))?.await
+                        if !previous_attempt
+                            && project.read_with(cx, |project, _| project.is_via_remote_server())
+                            && method.0.as_ref() == GEMINI_TERMINAL_AUTH_METHOD_ID
+                        {
+                            return cx
+                                .update(|window, cx| {
+                                    Self::spawn_external_agent_login(
+                                        login,
+                                        workspace,
+                                        project.clone(),
+                                        method,
+                                        true,
+                                        window,
+                                        cx,
+                                    )
+                                })?
+                                .await;
                         }
                         return Err(anyhow!("exited before logging in"));
                     }

crates/project/src/agent_server_store.rs 🔗

@@ -1374,13 +1374,8 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
                 .await
                 .unwrap_or_default();
 
-            let mut exec_args = Vec::new();
-            exec_args.push("--yes".to_string());
-            exec_args.push(package.to_string());
-            if !args.is_empty() {
-                exec_args.push("--".to_string());
-                exec_args.extend(args);
-            }
+            let mut exec_args = vec!["--yes".to_string(), "--".to_string(), package.to_string()];
+            exec_args.extend(args);
 
             let npm_command = node_runtime
                 .npm_command(