From 2edf9c4f4a41c4982dff9a93006ddd147163dc40 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 29 Apr 2026 13:33:34 +0200 Subject: [PATCH] Fix the thread completion notification not dismissing when focusing the panel (#55188) Before: https://github.com/user-attachments/assets/2ea0f1ed-5189-4b27-8b1e-3f47d7684a28 After: https://github.com/user-attachments/assets/535d47b6-d6dc-41cd-a14e-d2f5d6b5a3a5 Release Notes: - Fixed the thread completion notification not dismissing when focusing the panel --- crates/agent_ui/src/conversation_view.rs | 158 ++++++++++++++++++++--- 1 file changed, 142 insertions(+), 16 deletions(-) diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 3b2381f1c2..c6ce183863 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -88,7 +88,7 @@ use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore}; use crate::ui::{AgentNotification, AgentNotificationEvent}; use crate::{ - Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce, + Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AgentPanelEvent, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, RejectAll, RejectOnce, RemoveFirstQueuedMessage, @@ -2656,23 +2656,61 @@ impl ConversationView { self.notifications.push(screen_window); - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); + let dismiss_if_visible = { + let pop_up_weak = pop_up.downgrade(); + move |this: &ConversationView, + window: &mut Window, + cx: &mut Context| { + if this.agent_status_visible(window, cx) + && let Some(pop_up) = pop_up_weak.upgrade() + { + pop_up.update(cx, |notification, cx| { + notification.dismiss(cx); + }); + } + } + }; - cx.observe_window_activation(window, move |this, window, cx| { - if this.agent_status_visible(window, cx) - && let Some(pop_up) = pop_up_weak.upgrade() - { - pop_up.update(cx, |notification, cx| { - notification.dismiss(cx); - }); + let subscriptions = self + .notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new); + + subscriptions.push({ + let dismiss_if_visible = dismiss_if_visible.clone(); + cx.observe_window_activation(window, move |this, window, cx| { + dismiss_if_visible(this, window, cx); + }) + }); + + if let Some(multi_workspace) = window.root::().flatten() { + let dismiss_if_visible = dismiss_if_visible.clone(); + subscriptions.push(cx.observe_in( + &multi_workspace, + window, + move |this, _, window, cx| { + dismiss_if_visible(this, window, cx); + }, + )); + } + + if let Some(panel) = self + .workspace + .upgrade() + .and_then(|workspace| workspace.read(cx).panel::(cx)) + { + subscriptions.push(cx.subscribe_in( + &panel, + window, + move |this, _, event: &AgentPanelEvent, window, cx| match event { + AgentPanelEvent::ActiveViewChanged | AgentPanelEvent::ThreadFocused => { + dismiss_if_visible(this, window, cx); } - }) - }); + AgentPanelEvent::RetainedThreadChanged + | AgentPanelEvent::ThreadInteracted { .. } => {} + }, + )); + } } } @@ -3889,6 +3927,94 @@ pub(crate) mod tests { ); } + #[gpui::test] + async fn test_notification_dismissed_when_sidebar_opens(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + ::set_global(fs.clone(), cx); + }); + + let project = Project::test(fs, [], cx).await; + let multi_workspace_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace = multi_workspace_handle + .read_with(cx, |mw, _cx| mw.workspace().clone()) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx); + + let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); + let connection_store = + cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); + + let conversation_view = cx.update(|window, cx| { + cx.new(|cx| { + ConversationView::new( + Rc::new(StubAgentServer::default_response()), + connection_store, + Agent::Custom { id: "Test".into() }, + None, + None, + None, + None, + None, + workspace.downgrade(), + project.clone(), + Some(thread_store), + None, + "agent_panel", + window, + cx, + ) + }) + }); + + cx.run_until_parked(); + + let message_editor = message_editor(&conversation_view, cx); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); + + cx.run_until_parked(); + + assert_eq!( + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .count(), + 1, + "Expected a notification while the thread is not visible" + ); + + multi_workspace_handle + .update(cx, |mw, _window, cx| { + mw.open_sidebar(cx); + }) + .unwrap(); + + cx.run_until_parked(); + + assert_eq!( + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .count(), + 0, + "Notification should auto-dismiss when the sidebar opens and makes the thread visible" + ); + } + #[gpui::test] async fn test_notification_when_workspace_is_background_in_multi_workspace( cx: &mut TestAppContext,