diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index cb42cb92ddcd0799acb4ccb6839a38cd5690d93c..80190858151b2cf79500290a95ee0d0b6a4e8c97 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -2308,22 +2308,31 @@ impl ConversationView { self.show_notification(caption, icon, window, cx); } - fn agent_panel_visible(&self, multi_workspace: &Entity, cx: &App) -> bool { + fn is_visible(&self, multi_workspace: &Entity, cx: &Context) -> bool { let Some(workspace) = self.workspace.upgrade() else { return false; }; - multi_workspace.read(cx).workspace() == &workspace && AgentPanel::is_visible(&workspace, cx) + multi_workspace.read(cx).workspace() == &workspace + && AgentPanel::is_visible(&workspace, cx) + && multi_workspace + .read(cx) + .workspace() + .read(cx) + .panel::(cx) + .map_or(false, |p| { + p.read(cx).active_conversation_view().map(|c| c.entity_id()) + == Some(cx.entity_id()) + }) } - fn agent_status_visible(&self, window: &Window, cx: &App) -> bool { + fn agent_status_visible(&self, window: &Window, cx: &Context) -> bool { if !window.is_window_active() { return false; } if let Some(multi_workspace) = window.root::().flatten() { - multi_workspace.read(cx).sidebar_open() - || self.agent_panel_visible(&multi_workspace, cx) + self.is_visible(&multi_workspace, cx) } else { self.workspace .upgrade() @@ -2332,10 +2341,10 @@ impl ConversationView { } #[cfg(feature = "audio")] - fn play_notification_sound(&self, window: &Window, cx: &mut App) { + fn play_notification_sound(&self, window: &Window, cx: &mut Context) { let visible = window.is_window_active() && if let Some(mw) = window.root::().flatten() { - self.agent_panel_visible(&mw, cx) + self.is_visible(&mw, cx) } else { self.workspace .upgrade() @@ -2366,19 +2375,47 @@ impl ConversationView { return; } + let Some(root_thread) = self.root_thread(cx) else { + return; + }; + let root_thread = root_thread.read(cx).thread.read(cx); + let root_session_id = root_thread.session_id().clone(); + let root_work_dirs = root_thread.work_dirs().cloned(); + let root_title = root_thread.title(); + // TODO: Change this once we have title summarization for external agents. let title = self.agent.agent_id().0; match settings.notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title, window, primary, cx); + self.pop_up( + icon, + caption.into(), + title, + root_session_id, + root_work_dirs, + root_title, + window, + primary, + cx, + ); } } NotifyWhenAgentWaiting::AllScreens => { let caption = caption.into(); for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); + self.pop_up( + icon, + caption.clone(), + title.clone(), + root_session_id.clone(), + root_work_dirs.clone(), + root_title.clone(), + window, + screen, + cx, + ); } } NotifyWhenAgentWaiting::Never => { @@ -2392,6 +2429,9 @@ impl ConversationView { icon: IconName, caption: SharedString, title: SharedString, + root_session_id: acp::SessionId, + root_work_dirs: Option, + root_title: Option, window: &mut Window, screen: Rc, cx: &mut Context, @@ -2421,7 +2461,7 @@ impl ConversationView { .entry(screen_window) .or_insert_with(Vec::new) .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { + move |this, _, event, window, cx| match event { AgentNotificationEvent::Accepted => { let Some(handle) = window.window_handle().downcast::() else { @@ -2431,6 +2471,10 @@ impl ConversationView { cx.activate(true); let workspace_handle = this.workspace.clone(); + let agent = this.connection_key.clone(); + let root_session_id = root_session_id.clone(); + let root_work_dirs = root_work_dirs.clone(); + let root_title = root_title.clone(); cx.defer(move |cx| { handle @@ -2439,6 +2483,22 @@ impl ConversationView { if let Some(workspace) = workspace_handle.upgrade() { multi_workspace.activate(workspace.clone(), window, cx); workspace.update(cx, |workspace, cx| { + workspace.reveal_panel::(window, cx); + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.load_agent_thread( + agent.clone(), + root_session_id.clone(), + root_work_dirs.clone(), + root_title.clone(), + true, + window, + cx, + ); + }); + } workspace.focus_panel::(window, cx); }); } @@ -3426,6 +3486,109 @@ pub(crate) mod tests { ); } + #[gpui::test] + async fn test_notification_when_different_conversation_is_active_in_visible_panel( + 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 panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + workspace.focus_panel::(window, cx); + panel + }); + + cx.run_until_parked(); + + panel.update_in(cx, |panel, window, cx| { + panel.open_external_thread_with_server( + Rc::new(StubAgentServer::default_response()), + window, + cx, + ); + }); + + cx.run_until_parked(); + + panel.read_with(cx, |panel, cx| { + assert!(crate::AgentPanel::is_visible(&workspace, cx)); + assert!(panel.active_conversation_view().is_some()); + }); + + 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, + workspace.downgrade(), + project.clone(), + Some(thread_store), + None, + window, + cx, + ) + }) + }); + + cx.run_until_parked(); + + panel.read_with(cx, |panel, _cx| { + assert_ne!( + panel + .active_conversation_view() + .map(|view| view.entity_id()), + Some(conversation_view.entity_id()), + "The visible panel should still be showing a different conversation" + ); + }); + + 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!( + cx.windows() + .iter() + .any(|window| window.downcast::().is_some()), + "Expected notification when a different conversation is active in the visible panel" + ); + } + #[gpui::test] async fn test_notification_when_workspace_is_background_in_multi_workspace( cx: &mut TestAppContext, @@ -3454,12 +3617,23 @@ pub(crate) mod tests { let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx); - workspace1.update_in(cx, |workspace, window, cx| { + let panel = workspace1.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx)); - workspace.add_panel(panel, window, cx); + workspace.add_panel(panel.clone(), window, cx); // Open the dock and activate the agent panel so it's visible workspace.focus_panel::(window, cx); + panel + }); + + cx.run_until_parked(); + + panel.update_in(cx, |panel, window, cx| { + panel.open_external_thread_with_server( + Rc::new(StubAgentServer::new(RestoredAvailableCommandsConnection)), + window, + cx, + ); }); cx.run_until_parked(); @@ -3476,11 +3650,10 @@ pub(crate) mod tests { let connection_store = cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project1.clone(), cx))); - let agent = StubAgentServer::default_response(); let conversation_view = cx.update(|window, cx| { cx.new(|cx| { ConversationView::new( - Rc::new(agent), + Rc::new(StubAgentServer::new(RestoredAvailableCommandsConnection)), connection_store, Agent::Custom { id: "Test".into() }, None, @@ -3498,6 +3671,13 @@ pub(crate) mod tests { }); cx.run_until_parked(); + let root_session_id = conversation_view + .read_with(cx, |view, cx| { + view.root_thread(cx) + .map(|thread| thread.read(cx).thread.read(cx).session_id().clone()) + }) + .expect("Conversation view should have a root thread"); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); @@ -3555,6 +3735,17 @@ pub(crate) mod tests { ); }) .unwrap(); + + panel.read_with(cx, |panel, cx| { + let active_session_id = panel + .active_agent_thread(cx) + .map(|thread| thread.read(cx).session_id().clone()); + assert_eq!( + active_session_id, + Some(root_session_id), + "Expected accepting the notification to load the notified thread in AgentPanel" + ); + }); } #[gpui::test] diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 18a4161f1df99988177462059870234f81e48b5c..bcd67df50d8366ef24be034ce70de04fe22ed1d4 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -179,7 +179,7 @@ impl Render for AgentNotification { .gap_1() .items_center() .child( - Button::new("open", "View Panel") + Button::new("open", "View") .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() .on_click({