From ab71d1a2974d125377fbd215766c5ad50e860c59 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 27 Mar 2026 11:11:50 +0100 Subject: [PATCH] agent_ui: Delete metadata for empty released threads (#52563) Keep sidebar metadata only for threads with entries. Important for ACP agents especially that won't persist the thread. 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 is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/thread_metadata_store.rs | 118 ++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 03b3f645c5e0eecd6d71bd2f69c545d7a7d23522..9a99ca9fcd5766e041fa50206d6a536ac4a97854 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -325,8 +325,14 @@ impl SidebarThreadMetadataStore { let weak_store = weak_store.clone(); move |thread, cx| { weak_store - .update(cx, |store, _cx| { - store.session_subscriptions.remove(thread.session_id()); + .update(cx, |store, cx| { + let session_id = thread.session_id().clone(); + store.session_subscriptions.remove(&session_id); + if thread.entries().is_empty() { + // Empty threads can be unloaded without ever being + // durably persisted by the underlying agent. + store.delete(session_id, cx); + } }) .ok(); } @@ -998,6 +1004,114 @@ mod tests { assert_eq!(list[0].session_id.0.as_ref(), "existing-session"); } + #[gpui::test] + async fn test_empty_thread_metadata_deleted_when_thread_released(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); + SidebarThreadMetadataStore::init_global(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None::<&Path>, cx).await; + let connection = Rc::new(StubAgentConnection::new()); + + let thread = cx + .update(|cx| { + connection + .clone() + .new_session(project.clone(), PathList::default(), cx) + }) + .await + .unwrap(); + let session_id = cx.read(|cx| thread.read(cx).session_id().clone()); + + cx.update(|cx| { + thread.update(cx, |thread, cx| { + thread.set_title("Draft Thread".into(), cx).detach(); + }); + }); + cx.run_until_parked(); + + let metadata_ids = cx.update(|cx| { + SidebarThreadMetadataStore::global(cx) + .read(cx) + .entry_ids() + .collect::>() + }); + assert_eq!(metadata_ids, vec![session_id]); + + drop(thread); + cx.update(|_| {}); + cx.run_until_parked(); + cx.run_until_parked(); + + let metadata_ids = cx.update(|cx| { + SidebarThreadMetadataStore::global(cx) + .read(cx) + .entry_ids() + .collect::>() + }); + assert!( + metadata_ids.is_empty(), + "expected empty draft thread metadata to be deleted on release" + ); + } + + #[gpui::test] + async fn test_nonempty_thread_metadata_preserved_when_thread_released(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); + SidebarThreadMetadataStore::init_global(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None::<&Path>, cx).await; + let connection = Rc::new(StubAgentConnection::new()); + + let thread = cx + .update(|cx| { + connection + .clone() + .new_session(project.clone(), PathList::default(), cx) + }) + .await + .unwrap(); + let session_id = cx.read(|cx| thread.read(cx).session_id().clone()); + + cx.update(|cx| { + thread.update(cx, |thread, cx| { + thread.push_user_content_block(None, "Hello".into(), cx); + }); + }); + cx.run_until_parked(); + + let metadata_ids = cx.update(|cx| { + SidebarThreadMetadataStore::global(cx) + .read(cx) + .entry_ids() + .collect::>() + }); + assert_eq!(metadata_ids, vec![session_id.clone()]); + + drop(thread); + cx.update(|_| {}); + cx.run_until_parked(); + + let metadata_ids = cx.update(|cx| { + SidebarThreadMetadataStore::global(cx) + .read(cx) + .entry_ids() + .collect::>() + }); + assert_eq!(metadata_ids, vec![session_id]); + } + #[gpui::test] async fn test_subagent_threads_excluded_from_sidebar_metadata(cx: &mut TestAppContext) { cx.update(|cx| {