diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 08723529b42516728942362d56772be91464f513..107d6a7baafef1e61e63d48b1ca8fe645a219de9 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -3475,6 +3475,7 @@ fn setup_context_server( name.into(), project::project_settings::ContextServerSettings::Stdio { enabled: true, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: Vec::new(), diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index d10354981c2065d91bf310b1e308a240345bdd2d..21ad590f10d6547e3266a13ab2f2df6c6ab84103 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -358,52 +358,49 @@ impl AgentConnection for AcpConnection { let default_config_options = self.default_config_options.clone(); let cwd = cwd.to_path_buf(); let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = if project.read(cx).is_local() { - context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - match &*configuration { - project::context_server_store::ContextServerConfiguration::Custom { - command, - .. - } - | project::context_server_store::ContextServerConfiguration::Extension { - command, - .. - } => Some(acp::McpServer::Stdio( - acp::McpServerStdio::new(id.0.to_string(), &command.path) - .args(command.args.clone()) - .env(if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable::new(name, value)) - .collect() - } else { - vec![] - }), - )), - project::context_server_store::ContextServerConfiguration::Http { - url, - headers, - timeout: _, - } => Some(acp::McpServer::Http( - acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers( - headers - .iter() - .map(|(name, value)| acp::HttpHeader::new(name, value)) - .collect(), - ), - )), + let is_local = project.read(cx).is_local(); + let mcp_servers = context_server_store + .configured_server_ids() + .iter() + .filter_map(|id| { + let configuration = context_server_store.configuration_for_server(id)?; + match &*configuration { + project::context_server_store::ContextServerConfiguration::Custom { + command, + remote, + .. } - }) - .collect() - } else { - // In SSH projects, the external agent is running on the remote - // machine, and currently we only run MCP servers on the local - // machine. So don't pass any MCP servers to the agent in that case. - Vec::new() - }; + | project::context_server_store::ContextServerConfiguration::Extension { + command, + remote, + .. + } if is_local || *remote => Some(acp::McpServer::Stdio( + acp::McpServerStdio::new(id.0.to_string(), &command.path) + .args(command.args.clone()) + .env(if let Some(env) = command.env.as_ref() { + env.iter() + .map(|(name, value)| acp::EnvVariable::new(name, value)) + .collect() + } else { + vec![] + }), + )), + project::context_server_store::ContextServerConfiguration::Http { + url, + headers, + timeout: _, + } => Some(acp::McpServer::Http( + acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers( + headers + .iter() + .map(|(name, value)| acp::HttpHeader::new(name, value)) + .collect(), + ), + )), + _ => None, + } + }) + .collect(); cx.spawn(async move |cx| { let response = conn diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 4e10dc0478625e2b534460b29d2efbc9a925cdce..b08a9df6cb848a5258839ba3a615d2b1b8669375 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -920,6 +920,7 @@ impl AgentConfiguration { .or_insert_with(|| { settings::ContextServerSettingsContent::Extension { enabled: is_enabled, + remote: false, settings: serde_json::json!({}), } }) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index f37da34b8a735efaa9c07eab822e35a819691edf..3069d55718e4b11f87a2ce73703a593d1f7acf4c 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -182,6 +182,7 @@ impl ConfigurationSource { id, ContextServerSettings::Stdio { enabled: true, + remote: false, command, }, ) @@ -209,6 +210,7 @@ impl ConfigurationSource { id.clone(), ContextServerSettings::Extension { enabled: true, + remote: false, settings, }, )) @@ -404,6 +406,7 @@ impl ConfigureContextServerModal { ContextServerSettings::Stdio { enabled: _, command, + .. } => Some(ConfigurationTarget::Existing { id: server_id, command, @@ -413,6 +416,7 @@ impl ConfigureContextServerModal { url, headers, timeout: _, + .. } => Some(ConfigurationTarget::ExistingHttp { id: server_id, url, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index faa2d7395042bd05f8e47df2288094eb0bb7a53a..9fd1cd79363dc91108a25ba5235a7ee7900fe9c4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -977,6 +977,7 @@ impl ExtensionImports for WasmState { project::project_settings::ContextServerSettings::Stdio { enabled: _, command, + .. } => Ok(serde_json::to_string(&settings::ContextServerSettings { command: Some(settings::CommandSettings { path: command.path.to_str().map(|path| path.to_string()), @@ -988,6 +989,7 @@ impl ExtensionImports for WasmState { project::project_settings::ContextServerSettings::Extension { enabled: _, settings, + .. } => Ok(serde_json::to_string(&settings::ContextServerSettings { command: None, settings: Some(settings), diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 05a8ee243fb9ccf77cbc6e4c6273cc0efeaaab44..df41220cd4c2899325c090c9d65f856e1ff0a7a3 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -1,6 +1,7 @@ pub mod extension; pub mod registry; +use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -10,6 +11,8 @@ use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use futures::{FutureExt as _, future::join_all}; use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions}; use registry::ContextServerDescriptorRegistry; +use remote::RemoteClient; +use rpc::{AnyProtoClient, TypedEnvelope, proto}; use settings::{Settings as _, SettingsStore}; use util::{ResultExt as _, rel_path::RelPath}; @@ -99,10 +102,12 @@ impl ContextServerState { pub enum ContextServerConfiguration { Custom { command: ContextServerCommand, + remote: bool, }, Extension { command: ContextServerCommand, settings: serde_json::Value, + remote: bool, }, Http { url: url::Url, @@ -114,12 +119,20 @@ pub enum ContextServerConfiguration { impl ContextServerConfiguration { pub fn command(&self) -> Option<&ContextServerCommand> { match self { - ContextServerConfiguration::Custom { command } => Some(command), + ContextServerConfiguration::Custom { command, .. } => Some(command), ContextServerConfiguration::Extension { command, .. } => Some(command), ContextServerConfiguration::Http { .. } => None, } } + pub fn remote(&self) -> bool { + match self { + ContextServerConfiguration::Custom { remote, .. } => *remote, + ContextServerConfiguration::Extension { remote, .. } => *remote, + ContextServerConfiguration::Http { .. } => false, + } + } + pub async fn from_settings( settings: ContextServerSettings, id: ContextServerId, @@ -131,18 +144,22 @@ impl ContextServerConfiguration { ContextServerSettings::Stdio { enabled: _, command, - } => Some(ContextServerConfiguration::Custom { command }), + remote, + } => Some(ContextServerConfiguration::Custom { command, remote }), ContextServerSettings::Extension { enabled: _, settings, + remote, } => { let descriptor = cx.update(|cx| registry.read(cx).context_server_descriptor(&id.0))?; match descriptor.command(worktree_store, cx).await { - Ok(command) => { - Some(ContextServerConfiguration::Extension { command, settings }) - } + Ok(command) => Some(ContextServerConfiguration::Extension { + command, + settings, + remote, + }), Err(e) => { log::error!( "Failed to create context server configuration from settings: {e:#}" @@ -171,11 +188,23 @@ impl ContextServerConfiguration { pub type ContextServerFactory = Box) -> Arc>; +enum ContextServerStoreState { + Local { + downstream_client: Option<(u64, AnyProtoClient)>, + is_headless: bool, + }, + Remote { + project_id: u64, + upstream_client: Entity, + }, +} + pub struct ContextServerStore { + state: ContextServerStoreState, context_server_settings: HashMap, ContextServerSettings>, servers: HashMap, worktree_store: Entity, - project: WeakEntity, + project: Option>, registry: Entity, update_servers_task: Option>>, context_server_factory: Option, @@ -193,9 +222,31 @@ pub enum Event { impl EventEmitter for ContextServerStore {} impl ContextServerStore { - pub fn new( + pub fn local( + worktree_store: Entity, + weak_project: Option>, + headless: bool, + cx: &mut Context, + ) -> Self { + Self::new_internal( + !headless, + None, + ContextServerDescriptorRegistry::default_global(cx), + worktree_store, + weak_project, + ContextServerStoreState::Local { + downstream_client: None, + is_headless: headless, + }, + cx, + ) + } + + pub fn remote( + project_id: u64, + upstream_client: Entity, worktree_store: Entity, - weak_project: WeakEntity, + weak_project: Option>, cx: &mut Context, ) -> Self { Self::new_internal( @@ -204,10 +255,31 @@ impl ContextServerStore { ContextServerDescriptorRegistry::default_global(cx), worktree_store, weak_project, + ContextServerStoreState::Remote { + project_id, + upstream_client, + }, cx, ) } + pub fn init_headless(session: &AnyProtoClient) { + session.add_entity_request_handler(Self::handle_get_context_server_command); + } + + pub fn shared(&mut self, project_id: u64, client: AnyProtoClient) { + if let ContextServerStoreState::Local { + downstream_client, .. + } = &mut self.state + { + *downstream_client = Some((project_id, client)); + } + } + + pub fn is_remote_project(&self) -> bool { + matches!(self.state, ContextServerStoreState::Remote { .. }) + } + /// Returns all configured context server ids, excluding the ones that are disabled pub fn configured_server_ids(&self) -> Vec { self.context_server_settings @@ -221,10 +293,21 @@ impl ContextServerStore { pub fn test( registry: Entity, worktree_store: Entity, - weak_project: WeakEntity, + weak_project: Option>, cx: &mut Context, ) -> Self { - Self::new_internal(false, None, registry, worktree_store, weak_project, cx) + Self::new_internal( + false, + None, + registry, + worktree_store, + weak_project, + ContextServerStoreState::Local { + downstream_client: None, + is_headless: false, + }, + cx, + ) } #[cfg(any(test, feature = "test-support"))] @@ -232,7 +315,7 @@ impl ContextServerStore { context_server_factory: Option, registry: Entity, worktree_store: Entity, - weak_project: WeakEntity, + weak_project: Option>, cx: &mut Context, ) -> Self { Self::new_internal( @@ -241,6 +324,10 @@ impl ContextServerStore { registry, worktree_store, weak_project, + ContextServerStoreState::Local { + downstream_client: None, + is_headless: false, + }, cx, ) } @@ -264,6 +351,7 @@ impl ContextServerStore { env: None, timeout: None, }, + remote: false, }); self.run_server(server, configuration, cx); } @@ -273,29 +361,30 @@ impl ContextServerStore { context_server_factory: Option, registry: Entity, worktree_store: Entity, - weak_project: WeakEntity, + weak_project: Option>, + state: ContextServerStoreState, cx: &mut Context, ) -> Self { - let subscriptions = if maintain_server_loop { - vec![ - cx.observe(®istry, |this, _registry, cx| { - this.available_context_servers_changed(cx); - }), - cx.observe_global::(|this, cx| { - let settings = - &Self::resolve_project_settings(&this.worktree_store, cx).context_servers; - if &this.context_server_settings == settings { - return; - } - this.context_server_settings = settings.clone(); - this.available_context_servers_changed(cx); - }), - ] - } else { - Vec::new() - }; + let mut subscriptions = vec![cx.observe_global::(move |this, cx| { + let settings = + &Self::resolve_project_settings(&this.worktree_store, cx).context_servers; + if &this.context_server_settings == settings { + return; + } + this.context_server_settings = settings.clone(); + if maintain_server_loop { + this.available_context_servers_changed(cx); + } + })]; + + if maintain_server_loop { + subscriptions.push(cx.observe(®istry, |this, _registry, cx| { + this.available_context_servers_changed(cx); + })); + } let mut this = Self { + state, _subscriptions: subscriptions, context_server_settings: Self::resolve_project_settings(&worktree_store, cx) .context_servers @@ -509,66 +598,191 @@ impl ContextServerStore { Ok(()) } - fn create_context_server( - &self, + async fn create_context_server( + this: WeakEntity, id: ContextServerId, configuration: Arc, - cx: &mut Context, - ) -> Result> { - let global_timeout = - Self::resolve_project_settings(&self.worktree_store, cx).context_server_timeout; + cx: &mut AsyncApp, + ) -> Result<(Arc, Arc)> { + let remote = configuration.remote(); + let needs_remote_command = match configuration.as_ref() { + ContextServerConfiguration::Custom { .. } + | ContextServerConfiguration::Extension { .. } => remote, + ContextServerConfiguration::Http { .. } => false, + }; - if let Some(factory) = self.context_server_factory.as_ref() { - return Ok(factory(id, configuration)); - } + let (remote_state, is_remote_project) = this.update(cx, |this, _| { + let remote_state = match &this.state { + ContextServerStoreState::Remote { + project_id, + upstream_client, + } if needs_remote_command => Some((*project_id, upstream_client.clone())), + _ => None, + }; + (remote_state, this.is_remote_project()) + })?; - match configuration.as_ref() { - ContextServerConfiguration::Http { - url, - headers, - timeout, - } => Ok(Arc::new(ContextServer::http( - id, - url, - headers.clone(), - cx.http_client(), - cx.background_executor().clone(), - Some(Duration::from_secs( - timeout.unwrap_or(global_timeout).min(MAX_TIMEOUT_SECS), - )), - )?)), - _ => { - let root_path = self - .project - .read_with(cx, |project, cx| project.active_project_directory(cx)) - .ok() - .flatten() - .or_else(|| { - self.worktree_store.read_with(cx, |store, cx| { - store.visible_worktrees(cx).fold(None, |acc, item| { - if acc.is_none() { - item.read(cx).root_dir() - } else { - acc - } - }) + let root_path: Option> = this.update(cx, |this, cx| { + this.project + .as_ref() + .and_then(|project| { + project + .read_with(cx, |project, cx| project.active_project_directory(cx)) + .ok() + .flatten() + }) + .or_else(|| { + this.worktree_store.read_with(cx, |store, cx| { + store.visible_worktrees(cx).fold(None, |acc, item| { + if acc.is_none() { + item.read(cx).root_dir() + } else { + acc + } }) - }); - - let mut command = configuration - .command() - .context("Missing command configuration for stdio context server")? - .clone(); - command.timeout = Some( - command - .timeout - .unwrap_or(global_timeout) - .min(MAX_TIMEOUT_SECS), - ); + }) + }) + })?; - Ok(Arc::new(ContextServer::stdio(id, command, root_path))) + let configuration = if let Some((project_id, upstream_client)) = remote_state { + let root_dir = root_path.as_ref().map(|p| p.display().to_string()); + + let response = upstream_client + .update(cx, |client, _| { + client + .proto_client() + .request(proto::GetContextServerCommand { + project_id, + server_id: id.0.to_string(), + root_dir: root_dir.clone(), + }) + }) + .await?; + + let remote_command = upstream_client.update(cx, |client, _| { + client.build_command( + Some(response.path), + &response.args, + &response.env.into_iter().collect(), + root_dir, + None, + ) + })?; + + let command = ContextServerCommand { + path: remote_command.program.into(), + args: remote_command.args, + env: Some(remote_command.env.into_iter().collect()), + timeout: None, + }; + + Arc::new(ContextServerConfiguration::Custom { command, remote }) + } else { + configuration + }; + + let server: Arc = this.update(cx, |this, cx| { + let global_timeout = + Self::resolve_project_settings(&this.worktree_store, cx).context_server_timeout; + + if let Some(factory) = this.context_server_factory.as_ref() { + return anyhow::Ok(factory(id.clone(), configuration.clone())); } - } + + match configuration.as_ref() { + ContextServerConfiguration::Http { + url, + headers, + timeout, + } => anyhow::Ok(Arc::new(ContextServer::http( + id, + url, + headers.clone(), + cx.http_client(), + cx.background_executor().clone(), + Some(Duration::from_secs( + timeout.unwrap_or(global_timeout).min(MAX_TIMEOUT_SECS), + )), + )?)), + _ => { + let mut command = configuration + .command() + .context("Missing command configuration for stdio context server")? + .clone(); + command.timeout = Some( + command + .timeout + .unwrap_or(global_timeout) + .min(MAX_TIMEOUT_SECS), + ); + + // Don't pass remote paths as working directory for locally-spawned processes + let working_directory = if is_remote_project { None } else { root_path }; + anyhow::Ok(Arc::new(ContextServer::stdio( + id, + command, + working_directory, + ))) + } + } + })??; + + Ok((server, configuration)) + } + + async fn handle_get_context_server_command( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let server_id = ContextServerId(envelope.payload.server_id.into()); + + let (settings, registry, worktree_store) = this.update(&mut cx, |this, inner_cx| { + let ContextServerStoreState::Local { + is_headless: true, .. + } = &this.state + else { + anyhow::bail!("unexpected GetContextServerCommand request in a non-local project"); + }; + + let settings = this + .context_server_settings + .get(&server_id.0) + .cloned() + .or_else(|| { + this.registry + .read(inner_cx) + .context_server_descriptor(&server_id.0) + .map(|_| ContextServerSettings::default_extension()) + }) + .with_context(|| format!("context server `{}` not found", server_id))?; + + anyhow::Ok((settings, this.registry.clone(), this.worktree_store.clone())) + })?; + + let configuration = ContextServerConfiguration::from_settings( + settings, + server_id.clone(), + registry, + worktree_store, + &cx, + ) + .await + .with_context(|| format!("failed to build configuration for `{}`", server_id))?; + + let command = configuration + .command() + .context("context server has no command (HTTP servers don't need RPC)")?; + + Ok(proto::ContextServerCommand { + path: command.path.display().to_string(), + args: command.args.clone(), + env: command + .env + .clone() + .map(|env| env.into_iter().collect()) + .unwrap_or_default(), + }) } fn resolve_project_settings<'a>( @@ -651,7 +865,7 @@ impl ContextServerStore { worktree_store.clone(), cx, ) - .map(|config| (id, config)) + .map(move |config| (id, config)) })) .await .into_iter() @@ -662,7 +876,7 @@ impl ContextServerStore { let mut servers_to_remove = HashSet::default(); let mut servers_to_stop = HashSet::default(); - this.update(cx, |this, cx| { + this.update(cx, |this, _cx| { for server_id in this.servers.keys() { // All servers that are not in desired_servers should be removed from the store. // This can happen if the user removed a server from the context server settings. @@ -681,8 +895,7 @@ impl ContextServerStore { let existing_config = state.as_ref().map(|state| state.configuration()); if existing_config.as_deref() != Some(&config) || is_stopped { let config = Arc::new(config); - let server = this.create_context_server(id.clone(), config.clone(), cx)?; - servers_to_start.push((server, config)); + servers_to_start.push((id.clone(), config)); if this.servers.contains_key(&id) { servers_to_stop.insert(id); } @@ -692,18 +905,25 @@ impl ContextServerStore { anyhow::Ok(()) })??; - this.update(cx, |this, cx| { + this.update(cx, |this, inner_cx| { for id in servers_to_stop { - this.stop_server(&id, cx)?; + this.stop_server(&id, inner_cx)?; } for id in servers_to_remove { - this.remove_server(&id, cx)?; - } - for (server, config) in servers_to_start { - this.run_server(server, config, cx); + this.remove_server(&id, inner_cx)?; } anyhow::Ok(()) - })? + })??; + + for (id, config) in servers_to_start { + let (server, config) = + Self::create_context_server(this.clone(), id, config, cx).await?; + this.update(cx, |this, cx| { + this.run_server(server, config, cx); + })?; + } + + Ok(()) } } @@ -733,7 +953,7 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); @@ -807,7 +1027,7 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); @@ -862,7 +1082,7 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); @@ -942,6 +1162,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": true }), @@ -979,6 +1200,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -998,6 +1220,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -1025,6 +1248,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -1034,6 +1258,7 @@ mod tests { server_2_id.0.clone(), settings::ContextServerSettingsContent::Stdio { enabled: true, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: vec!["arg".to_string()], @@ -1066,6 +1291,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -1075,6 +1301,7 @@ mod tests { server_2_id.0.clone(), settings::ContextServerSettingsContent::Stdio { enabled: true, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: vec!["anotherArg".to_string()], @@ -1102,6 +1329,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -1125,6 +1353,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Extension { enabled: true, + remote: false, settings: json!({ "somevalue": false }), @@ -1169,6 +1398,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Stdio { enabled: true, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: vec!["arg".to_string()], @@ -1205,6 +1435,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Stdio { enabled: false, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: vec!["arg".to_string()], @@ -1234,6 +1465,7 @@ mod tests { server_1_id.0.clone(), settings::ContextServerSettingsContent::Stdio { enabled: true, + remote: false, command: ContextServerCommand { path: "somebinary".into(), args: vec!["arg".to_string()], @@ -1362,23 +1594,23 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); - let result = store.update(cx, |store, cx| { - store.create_context_server( - ContextServerId("test-server".into()), - Arc::new(ContextServerConfiguration::Http { - url: url::Url::parse("http://localhost:8080") - .expect("Failed to parse test URL"), - headers: Default::default(), - timeout: None, - }), - cx, - ) - }); + let mut async_cx = cx.to_async(); + let result = ContextServerStore::create_context_server( + store.downgrade(), + ContextServerId("test-server".into()), + Arc::new(ContextServerConfiguration::Http { + url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"), + headers: Default::default(), + timeout: None, + }), + &mut async_cx, + ) + .await; assert!( result.is_ok(), @@ -1420,23 +1652,23 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); - let result = store.update(cx, |store, cx| { - store.create_context_server( - ContextServerId("test-server".into()), - Arc::new(ContextServerConfiguration::Http { - url: url::Url::parse("http://localhost:8080") - .expect("Failed to parse test URL"), - headers: Default::default(), - timeout: Some(120), - }), - cx, - ) - }); + let mut async_cx = cx.to_async(); + let result = ContextServerStore::create_context_server( + store.downgrade(), + ContextServerId("test-server".into()), + Arc::new(ContextServerConfiguration::Http { + url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"), + headers: Default::default(), + timeout: Some(120), + }), + &mut async_cx, + ) + .await; assert!( result.is_ok(), @@ -1453,25 +1685,27 @@ mod tests { ContextServerStore::test( registry.clone(), project.read(cx).worktree_store(), - project.downgrade(), + Some(project.downgrade()), cx, ) }); - let result = store.update(cx, |store, cx| { - store.create_context_server( - ContextServerId("stdio-server".into()), - Arc::new(ContextServerConfiguration::Custom { - command: ContextServerCommand { - path: "/usr/bin/node".into(), - args: vec!["server.js".into()], - env: None, - timeout: Some(180000), - }, - }), - cx, - ) - }); + let mut async_cx = cx.to_async(); + let result = ContextServerStore::create_context_server( + store.downgrade(), + ContextServerId("stdio-server".into()), + Arc::new(ContextServerConfiguration::Custom { + command: ContextServerCommand { + path: "/usr/bin/node".into(), + args: vec!["server.js".into()], + env: None, + timeout: Some(180000), + }, + remote: false, + }), + &mut async_cx, + ) + .await; assert!( result.is_ok(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 94d37e1aae3a11302431cd2435da2835ad2e9cfd..f58ec9fad80a565ad33627d4e25df31219466212 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1102,8 +1102,14 @@ impl Project { .detach(); let weak_self = cx.weak_entity(); - let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self.clone(), cx)); + let context_server_store = cx.new(|cx| { + ContextServerStore::local( + worktree_store.clone(), + Some(weak_self.clone()), + false, + cx, + ) + }); let environment = cx.new(|cx| { ProjectEnvironment::new(env, worktree_store.downgrade(), None, false, cx) @@ -1310,8 +1316,6 @@ impl Project { } let weak_self = cx.weak_entity(); - let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self.clone(), cx)); let buffer_store = cx.new(|cx| { BufferStore::remote( @@ -1339,6 +1343,7 @@ impl Project { cx, ) }); + let task_store = cx.new(|cx| { TaskStore::remote( buffer_store.downgrade(), @@ -1363,6 +1368,16 @@ impl Project { cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); + let context_server_store = cx.new(|cx| { + ContextServerStore::remote( + rpc::proto::REMOTE_SERVER_PROJECT_ID, + remote.clone(), + worktree_store.clone(), + Some(weak_self.clone()), + cx, + ) + }); + let environment = cx.new(|cx| { ProjectEnvironment::new( None, @@ -1677,8 +1692,9 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); let weak_self = cx.weak_entity(); - let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx)); + let context_server_store = cx.new(|cx| { + ContextServerStore::local(worktree_store.clone(), Some(weak_self), false, cx) + }); let mut worktrees = Vec::new(); for worktree in response.payload.worktrees { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 06dda24860ae00d942ea975351acfcf166e9d8c8..50fe994f20fda651fbeb4e3e3c14484bef9b511a 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -131,7 +131,9 @@ pub enum ContextServerSettings { /// Whether the context server is enabled. #[serde(default = "default_true")] enabled: bool, - + /// If true, run this server on the remote server when using remote development. + #[serde(default)] + remote: bool, #[serde(flatten)] command: ContextServerCommand, }, @@ -151,6 +153,9 @@ pub enum ContextServerSettings { /// Whether the context server is enabled. #[serde(default = "default_true")] enabled: bool, + /// If true, run this server on the remote server when using remote development. + #[serde(default)] + remote: bool, /// The settings for this context server specified by the extension. /// /// Consult the documentation for the context server to see what settings @@ -162,12 +167,24 @@ pub enum ContextServerSettings { impl From for ContextServerSettings { fn from(value: settings::ContextServerSettingsContent) -> Self { match value { - settings::ContextServerSettingsContent::Stdio { enabled, command } => { - ContextServerSettings::Stdio { enabled, command } - } - settings::ContextServerSettingsContent::Extension { enabled, settings } => { - ContextServerSettings::Extension { enabled, settings } - } + settings::ContextServerSettingsContent::Stdio { + enabled, + remote, + command, + } => ContextServerSettings::Stdio { + enabled, + remote, + command, + }, + settings::ContextServerSettingsContent::Extension { + enabled, + remote, + settings, + } => ContextServerSettings::Extension { + enabled, + remote, + settings, + }, settings::ContextServerSettingsContent::Http { enabled, url, @@ -185,12 +202,24 @@ impl From for ContextServerSettings { impl Into for ContextServerSettings { fn into(self) -> settings::ContextServerSettingsContent { match self { - ContextServerSettings::Stdio { enabled, command } => { - settings::ContextServerSettingsContent::Stdio { enabled, command } - } - ContextServerSettings::Extension { enabled, settings } => { - settings::ContextServerSettingsContent::Extension { enabled, settings } - } + ContextServerSettings::Stdio { + enabled, + remote, + command, + } => settings::ContextServerSettingsContent::Stdio { + enabled, + remote, + command, + }, + ContextServerSettings::Extension { + enabled, + remote, + settings, + } => settings::ContextServerSettingsContent::Extension { + enabled, + remote, + settings, + }, ContextServerSettings::Http { enabled, url, @@ -210,6 +239,7 @@ impl ContextServerSettings { pub fn default_extension() -> Self { Self::Extension { enabled: true, + remote: false, settings: serde_json::json!({}), } } diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto index 17ef6efaa525479e08e494430613090814066394..b2a8a371c4422e80ad5edd677f2b75288f69ebd4 100644 --- a/crates/proto/proto/ai.proto +++ b/crates/proto/proto/ai.proto @@ -172,6 +172,18 @@ message GetAgentServerCommand { optional string root_dir = 3; } +message GetContextServerCommand { + uint64 project_id = 1; + string server_id = 2; + optional string root_dir = 3; +} + +message ContextServerCommand { + string path = 1; + repeated string args = 2; + map env = 3; +} + message AgentServerCommand { string path = 1; repeated string args = 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index e5cfd0c02d4d40bf109cc2dd6357e9fbbdcc8adf..8fb1ada28caeb466e711f517415277c62b1a20a8 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -453,7 +453,9 @@ message Envelope { GetSharedAgentThreadResponse get_shared_agent_thread_response = 408; FindSearchCandidatesChunk find_search_candidates_chunk = 409; - FindSearchCandidatesCancelled find_search_candidates_cancelled = 410; // current max + FindSearchCandidatesCancelled find_search_candidates_cancelled = 410; + GetContextServerCommand get_context_server_command = 411; + ContextServerCommand context_server_command = 412; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 00f9d0e3ecfeeb438eca7fd89c087f5b4ac2b641..0e71a76d43abae2e8be10695798d050391816805 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -334,6 +334,8 @@ messages!( (DirectoryEnvironment, Background), (GetAgentServerCommand, Background), (AgentServerCommand, Background), + (GetContextServerCommand, Background), + (ContextServerCommand, Background), (ExternalAgentsUpdated, Background), (ExternalExtensionAgentsUpdated, Background), (ExternalAgentLoadingStatusUpdated, Background), @@ -531,6 +533,7 @@ request_messages!( (GetDirectoryEnvironment, DirectoryEnvironment), (GetProcesses, GetProcessesResponse), (GetAgentServerCommand, AgentServerCommand), + (GetContextServerCommand, ContextServerCommand), (RemoteStarted, Ack), (GitGetWorktrees, GitWorktreesResponse), (GitCreateWorktree, Ack), @@ -704,6 +707,7 @@ entity_messages!( GetBlobContent, GitClone, GetAgentServerCommand, + GetContextServerCommand, ExternalAgentsUpdated, ExternalExtensionAgentsUpdated, ExternalAgentLoadingStatusUpdated, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 87807fab71776a4bdc2a681be193574112cc15d6..a4a26c4a46707f9f4d4b4329441f0c6bfbe6b0dc 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -16,6 +16,7 @@ use project::{ ToolchainStore, WorktreeId, agent_server_store::AgentServerStore, buffer_store::{BufferStore, BufferStoreEvent}, + context_server_store::ContextServerStore, debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, git_store::GitStore, image_store::ImageId, @@ -54,6 +55,7 @@ pub struct HeadlessProject { pub dap_store: Entity, pub breakpoint_store: Entity, pub agent_server_store: Entity, + pub context_server_store: Entity, pub settings_observer: Entity, pub next_entry_id: Arc, pub languages: Arc, @@ -232,6 +234,13 @@ impl HeadlessProject { agent_server_store }); + let context_server_store = cx.new(|cx| { + let mut context_server_store = + ContextServerStore::local(worktree_store.clone(), None, true, cx); + context_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone()); + context_server_store + }); + cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); language_extension::init( language_extension::LspAccess::ViaLspStore(lsp_store.clone()), @@ -267,6 +276,7 @@ impl HeadlessProject { session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer); session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store); session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &context_server_store); session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); @@ -311,6 +321,7 @@ impl HeadlessProject { BreakpointStore::init(&session); GitStore::init(&session); AgentServerStore::init_headless(&session); + ContextServerStore::init_headless(&session); HeadlessProject { next_entry_id: Default::default(), @@ -324,6 +335,7 @@ impl HeadlessProject { dap_store, breakpoint_store, agent_server_store, + context_server_store, languages, extensions, git_store, diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index d4f986a69417dca1fe5e6ce8a98028535e1709ab..87b4e5f550ba6a9ebf94e2c175da27d344fc95d6 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -227,7 +227,13 @@ pub enum ContextServerSettingsContent { /// Whether the context server is enabled. #[serde(default = "default_true")] enabled: bool, - + /// Whether to run the context server on the remote server when using remote development. + /// + /// If this is false, the context server will always run on the local machine. + /// + /// Default: false + #[serde(default)] + remote: bool, #[serde(flatten)] command: ContextServerCommand, }, @@ -247,6 +253,13 @@ pub enum ContextServerSettingsContent { /// Whether the context server is enabled. #[serde(default = "default_true")] enabled: bool, + /// Whether to run the context server on the remote server when using remote development. + /// + /// If this is false, the context server will always run on the local machine. + /// + /// Default: false + #[serde(default)] + remote: bool, /// The settings for this context server specified by the extension. /// /// Consult the documentation for the context server to see what settings diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 5e2719f7191cd21b767847ae1c00b49c44c459f8..aabffd8cba9401d92546b7209fda9b497c9ac5f2 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -576,6 +576,7 @@ impl VsCodeSettings { k.clone().into(), ContextServerSettingsContent::Stdio { enabled: true, + remote: false, command: serde_json::from_value::(v.clone()) .ok() .map(|cmd| ContextServerCommand {