From e1a83a5fe6268d3beed56402d71b841abd663b1c Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 21 Oct 2025 21:49:52 -0400 Subject: [PATCH] Support multiple agent connections --- crates/agent/src/native_agent_server.rs | 5 +- crates/agent_servers/src/agent_servers.rs | 7 +- crates/agent_servers/src/claude.rs | 12 ++- crates/agent_servers/src/codex.rs | 12 ++- crates/agent_servers/src/custom.rs | 12 ++- crates/agent_servers/src/gemini.rs | 12 ++- crates/agent_ui/src/acp/thread_view.rs | 25 +++--- crates/project/src/agent_server_store.rs | 84 +++++++++++++++---- .../remote_server/src/remote_editing_tests.rs | 2 +- 9 files changed, 129 insertions(+), 42 deletions(-) diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index 0dde0ff98552d4292a4391d2aec4f36419228a25..b5c86c5a655807104eac6756163637f7613fcdae 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/crates/agent/src/native_agent_server.rs @@ -2,6 +2,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::{AgentServer, AgentServerDelegate}; use anyhow::Result; +use collections::HashMap; use fs::Fs; use gpui::{App, Entity, SharedString, Task}; use prompt_store::PromptStore; @@ -41,7 +42,7 @@ impl AgentServer for NativeAgentServer { ) -> Task< Result<( Rc, - Option, + HashMap, )>, > { log::debug!( @@ -67,7 +68,7 @@ impl AgentServer for NativeAgentServer { Ok(( Rc::new(connection) as Rc, - None, + HashMap::default(), )) }) } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index b44c2123fb5052e2487464d813936cd1edf9821a..84980409b91f74fa641520d62b84c498f315f3ed 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -73,7 +73,12 @@ pub trait AgentServer: Send { root_dir: Option<&Path>, delegate: AgentServerDelegate, cx: &mut App, - ) -> Task, Option)>>; + ) -> Task< + Result<( + Rc, + HashMap, + )>, + >; fn into_any(self: Rc) -> Rc; } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index b84a386679cee825be22d895634a6971b537fa89..4c939220e074f08f9025a038bac7c5f26f3a07e6 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use std::{any::Any, path::PathBuf}; use anyhow::{Context as _, Result}; +use collections::HashMap; use gpui::{App, AppContext as _, SharedString, Task}; use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME}; @@ -60,7 +61,12 @@ impl AgentServer for ClaudeCode { root_dir: Option<&Path>, delegate: AgentServerDelegate, cx: &mut App, - ) -> Task, Option)>> { + ) -> Task< + Result<( + Rc, + HashMap, + )>, + > { 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(); @@ -69,7 +75,7 @@ impl AgentServer for ClaudeCode { let default_mode = self.default_mode(cx); cx.spawn(async move |cx| { - let (command, root_dir, login) = store + let (command, root_dir, auth_commands) = store .update(cx, |store, cx| { let agent = store .get_external_agent(&CLAUDE_CODE_NAME.into()) @@ -92,7 +98,7 @@ impl AgentServer for ClaudeCode { cx, ) .await?; - Ok((connection, login)) + Ok((connection, auth_commands)) }) } diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs index 3b2b4171de8c0fa17e076761ab36ab03b0f2ac5f..76600ba35bae721d8045484b5ae232add60b6282 100644 --- a/crates/agent_servers/src/codex.rs +++ b/crates/agent_servers/src/codex.rs @@ -5,6 +5,7 @@ use std::{any::Any, path::Path}; use acp_thread::AgentConnection; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; +use collections::HashMap; use fs::Fs; use gpui::{App, AppContext as _, SharedString, Task}; use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME}; @@ -61,7 +62,12 @@ impl AgentServer for Codex { root_dir: Option<&Path>, delegate: AgentServerDelegate, cx: &mut App, - ) -> Task, Option)>> { + ) -> Task< + Result<( + Rc, + HashMap, + )>, + > { 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(); @@ -70,7 +76,7 @@ impl AgentServer for Codex { let default_mode = self.default_mode(cx); cx.spawn(async move |cx| { - let (command, root_dir, login) = store + let (command, root_dir, auth_commands) = store .update(cx, |store, cx| { let agent = store .get_external_agent(&CODEX_NAME.into()) @@ -96,7 +102,7 @@ impl AgentServer for Codex { cx, ) .await?; - Ok((connection, login)) + Ok((connection, auth_commands)) }) } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 406a18965111a44bc4e78469b20aaf199cbda037..0a72db9ae3db48c8060a7c19dda17e582770d9ff 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -2,6 +2,7 @@ use crate::{AgentServerDelegate, load_proxy_env}; use acp_thread::AgentConnection; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; +use collections::HashMap; use fs::Fs; use gpui::{App, AppContext as _, SharedString, Task}; use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName}; @@ -65,7 +66,12 @@ impl crate::AgentServer for CustomAgentServer { root_dir: Option<&Path>, delegate: AgentServerDelegate, cx: &mut App, - ) -> Task, Option)>> { + ) -> Task< + Result<( + Rc, + HashMap, + )>, + > { 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(); @@ -74,7 +80,7 @@ impl crate::AgentServer for CustomAgentServer { let extra_env = load_proxy_env(cx); cx.spawn(async move |cx| { - let (command, root_dir, login) = store + let (command, root_dir, auth_commands) = store .update(cx, |store, cx| { let agent = store .get_external_agent(&ExternalAgentServerName(name.clone())) @@ -99,7 +105,7 @@ impl crate::AgentServer for CustomAgentServer { cx, ) .await?; - Ok((connection, login)) + Ok((connection, auth_commands)) }) } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 8004f5caec4a7bd2e3e6b1d9a885f4943fa21147..a577b0590c83fd06f399e655c062a3664d733d1a 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,6 +4,7 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; use acp_thread::AgentConnection; use anyhow::{Context as _, Result}; +use collections::HashMap; use gpui::{App, SharedString, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use project::agent_server_store::GEMINI_NAME; @@ -29,7 +30,12 @@ impl AgentServer for Gemini { root_dir: Option<&Path>, delegate: AgentServerDelegate, cx: &mut App, - ) -> Task, Option)>> { + ) -> Task< + Result<( + Rc, + HashMap, + )>, + > { 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(); @@ -47,7 +53,7 @@ impl AgentServer for Gemini { { extra_env.insert("GEMINI_API_KEY".into(), api_key); } - let (command, root_dir, login) = store + let (command, root_dir, auth_commands) = store .update(cx, |store, cx| { let agent = store .get_external_agent(&GEMINI_NAME.into()) @@ -71,7 +77,7 @@ impl AgentServer for Gemini { cx, ) .await?; - Ok((connection, login)) + Ok((connection, auth_commands)) }) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cb2e8be2701c2152ef889f7bdc9925f8014f9519..04678ac6a795509395553517037d40f3d7310e83 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -263,7 +263,7 @@ pub struct AcpThreadView { workspace: WeakEntity, project: Entity, thread_state: ThreadState, - login: Option, + auth_commands: HashMap, history_store: Entity, hovered_recent_history_item: Option, entry_view_state: Entity, @@ -416,7 +416,7 @@ impl AcpThreadView { window, cx, ), - login: None, + auth_commands: HashMap::default(), message_editor, model_selector: None, profile_selector: None, @@ -509,8 +509,9 @@ impl AcpThreadView { let connect_task = agent.connect(root_dir.as_deref(), delegate, cx); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { - Ok((connection, login)) => { - this.update(cx, |this, _| this.login = login).ok(); + Ok((connection, auth_commands)) => { + this.update(cx, |this, _| this.auth_commands = auth_commands) + .ok(); connection } Err(err) => { @@ -1057,7 +1058,7 @@ impl AcpThreadView { }; let connection = thread.read(cx).connection().clone(); - let can_login = !connection.auth_methods().is_empty() || self.login.is_some(); + let can_login = !connection.auth_methods().is_empty() || !self.auth_commands.is_empty(); // Does the agent have a specific logout command? Prefer that in case they need to reset internal state. let logout_supported = text == "/logout" && self @@ -1561,10 +1562,7 @@ impl AcpThreadView { self.thread_error.take(); configuration_view.take(); pending_auth_method.replace(method.clone()); - let authenticate = if (method.0.as_ref() == "claude-login" - || method.0.as_ref() == "spawn-gemini-cli") - && let Some(login) = self.login.clone() - { + let authenticate = if let Some(login) = self.auth_commands.get(method.0.as_ref()).cloned() { if let Some(workspace) = self.workspace.upgrade() { Self::spawn_external_agent_login(login, workspace, false, window, cx) } else { @@ -6033,8 +6031,13 @@ pub(crate) mod tests { _root_dir: Option<&Path>, _delegate: AgentServerDelegate, _cx: &mut App, - ) -> Task, Option)>> { - Task::ready(Ok((Rc::new(self.connection.clone()), None))) + ) -> Task< + gpui::Result<( + Rc, + HashMap, + )>, + > { + Task::ready(Ok((Rc::new(self.connection.clone()), HashMap::default()))) } fn into_any(self: Rc) -> Rc { diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index e7f4e9ed22b00278da0f8295eede8f1cc5133489..2bc0d7fc126da1c9f24a5d0b7448711388a9afe4 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -95,7 +95,13 @@ pub trait ExternalAgentServer { status_tx: Option>, new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>>; + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + >; fn as_any_mut(&mut self) -> &mut dyn Any; } @@ -392,7 +398,7 @@ impl AgentServerStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let (command, root_dir, login) = this + let (command, root_dir, _login) = this .update(&mut cx, |this, cx| { let AgentServerStoreState::Local { downstream_client, .. @@ -466,7 +472,7 @@ impl AgentServerStore { .map(|env| env.into_iter().collect()) .unwrap_or_default(), root_dir: root_dir, - login: login.map(|login| login.to_proto()), + login: None, }) } @@ -774,7 +780,13 @@ impl ExternalAgentServer for RemoteExternalAgentServer { status_tx: Option>, new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>> { + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + > { let project_id = self.project_id; let name = self.name.to_string(); let upstream_client = self.upstream_client.downgrade(); @@ -811,9 +823,7 @@ impl ExternalAgentServer for RemoteExternalAgentServer { env: Some(command.env), }, root_dir, - response - .login - .map(|login| task::SpawnInTerminal::from_proto(login)), + HashMap::default(), )) }) } @@ -839,7 +849,13 @@ impl ExternalAgentServer for LocalGemini { status_tx: Option>, new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>> { + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + > { let fs = self.fs.clone(); let node_runtime = self.node_runtime.clone(); let project_environment = self.project_environment.downgrade(); @@ -906,12 +922,15 @@ impl ExternalAgentServer for LocalGemini { ..Default::default() }; + let mut auth_commands = HashMap::default(); + auth_commands.insert("spawn-gemini-cli".to_string(), login); + command.env.get_or_insert_default().extend(extra_env); command.args.push("--experimental-acp".into()); Ok(( command, root_dir.to_string_lossy().into_owned(), - Some(login), + auth_commands, )) }) } @@ -936,7 +955,13 @@ impl ExternalAgentServer for LocalClaudeCode { status_tx: Option>, new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>> { + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + > { let fs = self.fs.clone(); let node_runtime = self.node_runtime.clone(); let project_environment = self.project_environment.downgrade(); @@ -999,8 +1024,17 @@ impl ExternalAgentServer for LocalClaudeCode { (command, login) }; + let mut auth_commands = HashMap::default(); + if let Some(login) = login { + auth_commands.insert("claude-login".to_string(), login); + } + command.env.get_or_insert_default().extend(extra_env); - Ok((command, root_dir.to_string_lossy().into_owned(), login)) + Ok(( + command, + root_dir.to_string_lossy().into_owned(), + auth_commands, + )) }) } @@ -1025,7 +1059,13 @@ impl ExternalAgentServer for LocalCodex { _status_tx: Option>, _new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>> { + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + > { let fs = self.fs.clone(); let project_environment = self.project_environment.downgrade(); let http = self.http_client.clone(); @@ -1116,7 +1156,11 @@ impl ExternalAgentServer for LocalCodex { }; command.env.get_or_insert_default().extend(extra_env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) + Ok(( + command, + root_dir.to_string_lossy().into_owned(), + HashMap::default(), + )) }) } @@ -1173,7 +1217,13 @@ impl ExternalAgentServer for LocalCustomAgent { _status_tx: Option>, _new_version_available_tx: Option>>, cx: &mut AsyncApp, - ) -> Task)>> { + ) -> Task< + Result<( + AgentServerCommand, + String, + HashMap, + )>, + > { let mut command = self.command.clone(); let root_dir: Arc = root_dir .map(|root_dir| Path::new(root_dir)) @@ -1194,7 +1244,11 @@ impl ExternalAgentServer for LocalCustomAgent { env.extend(command.env.unwrap_or_default()); env.extend(extra_env); command.env = Some(env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) + Ok(( + command, + root_dir.to_string_lossy().into_owned(), + HashMap::default(), + )) }) } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 4010d033c09473cb475ae40b977af70fca390b82..9e16b09866157db635e26f14daaf8989df3f260f 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1825,7 +1825,7 @@ async fn test_remote_external_agent_server( } ); assert_eq!(&PathBuf::from(root), paths::home_dir()); - assert!(login.is_none()); + assert!(login.is_empty()); } pub async fn init_test(