sidebar: Fix subagent threads showing up (#51739)

Bennet Bo Fenner created

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A

Change summary

crates/agent_ui/src/thread_metadata_store.rs | 103 ++++++++++++++++++++++
1 file changed, 103 insertions(+)

Detailed changes

crates/agent_ui/src/thread_metadata_store.rs 🔗

@@ -159,6 +159,11 @@ impl ThreadMetadataStore {
         let weak_store = cx.weak_entity();
 
         cx.observe_new::<acp_thread::AcpThread>(move |thread, _window, cx| {
+            // Don't track subagent threads in the sidebar.
+            if thread.parent_session_id().is_some() {
+                return;
+            }
+
             let thread_entity = cx.entity();
 
             cx.on_release({
@@ -195,6 +200,11 @@ impl ThreadMetadataStore {
         event: &acp_thread::AcpThreadEvent,
         cx: &mut Context<Self>,
     ) {
+        // Don't track subagent threads in the sidebar.
+        if thread.read(cx).parent_session_id().is_some() {
+            return;
+        }
+
         match event {
             acp_thread::AcpThreadEvent::NewEntry
             | acp_thread::AcpThreadEvent::EntryUpdated(_)
@@ -365,8 +375,17 @@ impl Column for ThreadMetadata {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use acp_thread::{AgentConnection, StubAgentConnection};
+    use action_log::ActionLog;
     use agent::DbThread;
+    use agent_client_protocol as acp;
+    use feature_flags::FeatureFlagAppExt;
     use gpui::TestAppContext;
+    use project::FakeFs;
+    use project::Project;
+    use std::path::Path;
+    use std::rc::Rc;
+    use util::path_list::PathList;
 
     fn make_db_thread(title: &str, updated_at: DateTime<Utc>) -> DbThread {
         DbThread {
@@ -525,4 +544,88 @@ mod tests {
         assert_eq!(list.len(), 1);
         assert_eq!(list[0].session_id.0.as_ref(), "existing-session");
     }
+
+    #[gpui::test]
+    async fn test_subagent_threads_excluded_from_sidebar_metadata(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = settings::SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            cx.update_flags(true, vec!["agent-v2".to_string()]);
+            ThreadStore::init_global(cx);
+            ThreadMetadataStore::init_global(cx);
+        });
+
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, None::<&Path>, cx).await;
+        let connection = Rc::new(StubAgentConnection::new());
+
+        // Create a regular (non-subagent) AcpThread.
+        let regular_thread = cx
+            .update(|cx| {
+                connection
+                    .clone()
+                    .new_session(project.clone(), PathList::default(), cx)
+            })
+            .await
+            .unwrap();
+
+        let regular_session_id = cx.read(|cx| regular_thread.read(cx).session_id().clone());
+
+        // Set a title on the regular thread to trigger a save via handle_thread_update.
+        cx.update(|cx| {
+            regular_thread.update(cx, |thread, cx| {
+                thread.set_title("Regular Thread".into(), cx).detach();
+            });
+        });
+        cx.run_until_parked();
+
+        // Create a subagent AcpThread
+        let subagent_session_id = acp::SessionId::new("subagent-session");
+        let subagent_thread = cx.update(|cx| {
+            let action_log = cx.new(|_| ActionLog::new(project.clone()));
+            cx.new(|cx| {
+                acp_thread::AcpThread::new(
+                    Some(regular_session_id.clone()),
+                    "Subagent Thread",
+                    None,
+                    connection.clone(),
+                    project.clone(),
+                    action_log,
+                    subagent_session_id.clone(),
+                    watch::Receiver::constant(acp::PromptCapabilities::new()),
+                    cx,
+                )
+            })
+        });
+
+        // Set a title on the subagent thread to trigger handle_thread_update.
+        cx.update(|cx| {
+            subagent_thread.update(cx, |thread, cx| {
+                thread
+                    .set_title("Subagent Thread Title".into(), cx)
+                    .detach();
+            });
+        });
+        cx.run_until_parked();
+
+        // List all metadata from the store.
+        let metadata_list = cx.update(|cx| {
+            let store = ThreadMetadataStore::global(cx);
+            store.read(cx).list(cx)
+        });
+
+        let list = metadata_list.await.unwrap();
+
+        // The subagent thread should NOT appear in the sidebar metadata.
+        // Only the regular thread should be listed.
+        assert_eq!(
+            list.len(),
+            1,
+            "Expected only the regular thread in sidebar metadata, \
+             but found {} entries (subagent threads are leaking into the sidebar)",
+            list.len(),
+        );
+        assert_eq!(list[0].session_id, regular_session_id);
+        assert_eq!(list[0].title.as_ref(), "Regular Thread");
+    }
 }