From 4215da0a2157a6ea0e87f4e51e269219e6fd8e1c Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 30 Jul 2026 10:10:06 -0500 Subject: [PATCH] agent_ui: Route MCP configuration to active workspace (#61882) # Objective - Prevent MCP extension configuration dialogs from appearing when switching to background workspaces. - Closes FR-145. ## Solution - Route configuration requests through the active application window and workspace, while keeping uninstall settings cleanup global. ## Testing - Added a regression test covering active and retained background workspaces. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Agent: Fixed MCP extension configuration dialogs appearing in other workspaces. --- .../src/context_server_configuration.rs | 174 +++++++++++++----- 1 file changed, 125 insertions(+), 49 deletions(-) diff --git a/crates/agent_ui/src/context_server_configuration.rs b/crates/agent_ui/src/context_server_configuration.rs index 3a1a869517..3614aef369 100644 --- a/crates/agent_ui/src/context_server_configuration.rs +++ b/crates/agent_ui/src/context_server_configuration.rs @@ -8,57 +8,47 @@ use language::LanguageRegistry; use settings::update_settings_file; use ui::prelude::*; use util::ResultExt; -use workspace::Workspace; +use workspace::{MultiWorkspace, Workspace}; use crate::agent_configuration::ConfigureContextServerModal; pub(crate) fn init(language_registry: Arc, fs: Arc, cx: &mut App) { - cx.observe_new(move |_: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; + let Some(extension_events) = extension::ExtensionEvents::try_global(cx) else { + log::info!( + "No extension events global found. Skipping context server configuration wizard" + ); + return; + }; - if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() { - cx.subscribe_in(extension_events, window, { - let language_registry = language_registry.clone(); - let fs = fs.clone(); - move |_, _, event, window, cx| match event { - extension::Event::ExtensionInstalled(manifest) => { - show_configure_mcp_modal( - language_registry.clone(), - manifest, - cx.weak_entity(), - window, - cx, - ); - } - extension::Event::ExtensionUninstalled(manifest) => { - remove_context_server_settings( - manifest.context_servers.keys().cloned().collect(), - fs.clone(), - cx, - ); - } - extension::Event::ConfigureExtensionRequested(manifest) => { - if !manifest.context_servers.is_empty() { - show_configure_mcp_modal( - language_registry.clone(), - manifest, - cx.weak_entity(), - window, - cx, - ); - } - } - _ => {} - } - }) - .detach(); - } else { - log::info!( - "No extension events global found. Skipping context server configuration wizard" + cx.subscribe(&extension_events, move |_, event, cx| match event { + extension::Event::ExtensionUninstalled(manifest) => { + remove_context_server_settings( + manifest.context_servers.keys().cloned().collect(), + fs.clone(), + cx, ); } + extension::Event::ExtensionInstalled(manifest) + | extension::Event::ConfigureExtensionRequested(manifest) => { + let Some(multi_workspace) = cx + .active_window() + .and_then(|window| window.downcast::()) + else { + return; + }; + multi_workspace + .update(cx, |multi_workspace, window, cx| { + show_configure_mcp_modal( + language_registry.clone(), + manifest, + multi_workspace.workspace().downgrade(), + window, + cx, + ); + }) + .log_err(); + } + extension::Event::ExtensionsInstalledChanged => {} }) .detach(); } @@ -81,12 +71,8 @@ fn show_configure_mcp_modal( manifest: &Arc, workspace: WeakEntity, window: &mut Window, - cx: &mut Context<'_, Workspace>, + cx: &mut Context<'_, MultiWorkspace>, ) { - if !window.is_window_active() { - return; - } - let ids = manifest.context_servers.keys().cloned().collect::>(); if ids.is_empty() { return; @@ -114,3 +100,93 @@ fn show_configure_mcp_modal( }) .detach(); } + +#[cfg(test)] +mod tests { + use super::*; + use context_server::ContextServerCommand; + use gpui::TestAppContext; + use project::{ + FakeFs, Project, + project_settings::{ContextServerSettings, ProjectSettings}, + }; + use settings::Settings as _; + + #[gpui::test] + async fn test_configure_extension_only_opens_modal_in_active_workspace( + cx: &mut TestAppContext, + ) { + crate::test_support::init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + cx.update(|cx| { + extension::init(cx); + let mut settings = ProjectSettings::get_global(cx).clone(); + settings.context_servers.insert( + "slack".into(), + ContextServerSettings::Stdio { + enabled: true, + remote: false, + command: ContextServerCommand { + path: "slack-mcp-server".into(), + args: Vec::new(), + env: None, + timeout: None, + }, + }, + ); + ProjectSettings::override_global(settings, cx); + init(language_registry, fs.clone(), cx); + }); + + let active_project = Project::test(fs.clone(), [], cx).await; + let background_project = Project::test(fs, [], cx).await; + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(active_project, window, cx)); + let (active_workspace, background_workspace) = multi_workspace + .update(cx, |multi_workspace, window, cx| { + let active_workspace = multi_workspace.workspace().clone(); + let background_workspace = + cx.new(|cx| Workspace::test_new(background_project, window, cx)); + multi_workspace.add(background_workspace.clone(), window, cx); + (active_workspace, background_workspace) + }) + .expect("multi-workspace window should exist"); + cx.run_until_parked(); + + let manifest = Arc::new( + serde_json::from_value::(serde_json::json!({ + "id": "slack", + "name": "Slack", + "version": "1.0.0", + "schema_version": 1, + "context_servers": { + "slack": {} + } + })) + .expect("test extension manifest should deserialize"), + ); + cx.update(|cx| { + let extension_events = extension::ExtensionEvents::try_global(cx) + .expect("extension events should be initialized"); + extension_events.update(cx, |extension_events, cx| { + extension_events.emit(extension::Event::ConfigureExtensionRequested(manifest), cx); + }); + }); + cx.run_until_parked(); + + assert!( + active_workspace.read_with(cx, |workspace, cx| workspace + .active_modal::(cx) + .is_some()), + "the active workspace should show the configuration modal" + ); + assert!( + background_workspace.read_with(cx, |workspace, cx| workspace + .active_modal::(cx) + .is_none()), + "a background workspace should not receive a hidden configuration modal" + ); + } +}