Fix duplicate thread entries in agent navigation menu (#29672)

Cole Miller created

Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/agent/src/assistant_panel.rs | 11 ++++++---
crates/agent/src/history_store.rs   | 35 +++++++++++++++++++++++-------
2 files changed, 34 insertions(+), 12 deletions(-)

Detailed changes

crates/agent/src/assistant_panel.rs 🔗

@@ -401,11 +401,12 @@ impl AssistantPanel {
                 }
             });
 
+        let thread_id = thread.read(cx).id().clone();
         let history_store = cx.new(|cx| {
             HistoryStore::new(
                 thread_store.clone(),
                 context_store.clone(),
-                [RecentEntry::Thread(thread.clone())],
+                [RecentEntry::Thread(thread_id, thread.clone())],
                 cx,
             )
         });
@@ -467,7 +468,7 @@ impl AssistantPanel {
                                             .update(cx, {
                                                 let entry = entry.clone();
                                                 move |this, cx| match entry {
-                                                    RecentEntry::Thread(thread) => {
+                                                    RecentEntry::Thread(_, thread) => {
                                                         this.open_thread(thread, window, cx)
                                                     }
                                                     RecentEntry::Context(context) => {
@@ -1099,7 +1100,8 @@ impl AssistantPanel {
             ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
                 if let Some(thread) = thread.upgrade() {
                     if thread.read(cx).is_empty() {
-                        store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
+                        let id = thread.read(cx).id().clone();
+                        store.remove_recently_opened_thread(id, cx);
                     }
                 }
             }),
@@ -1109,7 +1111,8 @@ impl AssistantPanel {
         match &new_view {
             ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
                 if let Some(thread) = thread.upgrade() {
-                    store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
+                    let id = thread.read(cx).id().clone();
+                    store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
                 }
             }),
             ActiveView::PromptEditor { context_editor, .. } => {

crates/agent/src/history_store.rs 🔗

@@ -36,16 +36,28 @@ impl HistoryEntry {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug)]
 pub(crate) enum RecentEntry {
-    Thread(Entity<Thread>),
+    Thread(ThreadId, Entity<Thread>),
     Context(Entity<AssistantContext>),
 }
 
+impl PartialEq for RecentEntry {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
+            (Self::Context(l0), Self::Context(r0)) => l0 == r0,
+            _ => false,
+        }
+    }
+}
+
+impl Eq for RecentEntry {}
+
 impl RecentEntry {
     pub(crate) fn summary(&self, cx: &App) -> SharedString {
         match self {
-            RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
+            RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
             RecentEntry::Context(context) => context.read(cx).summary_or_default(),
         }
     }
@@ -93,9 +105,10 @@ impl HistoryStore {
                     .map(|serialized| match serialized {
                         SerializedRecentEntry::Thread(id) => thread_store
                             .update(cx, |thread_store, cx| {
+                                let thread_id = ThreadId::from(id.as_str());
                                 thread_store
-                                    .open_thread(&ThreadId::from(id.as_str()), cx)
-                                    .map_ok(RecentEntry::Thread)
+                                    .open_thread(&thread_id, cx)
+                                    .map_ok(|thread| RecentEntry::Thread(thread_id, thread))
                                     .boxed()
                             })
                             .unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
@@ -170,9 +183,7 @@ impl HistoryStore {
                 RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
                     context.read(cx).path()?.to_str()?.to_owned(),
                 )),
-                RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
-                    thread.read(cx).id().to_string(),
-                )),
+                RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
             })
             .collect::<Vec<_>>();
 
@@ -200,6 +211,14 @@ impl HistoryStore {
         self.save_recently_opened_entries(cx);
     }
 
+    pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
+        self.recently_opened_entries.retain(|entry| match entry {
+            RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
+            _ => true,
+        });
+        self.save_recently_opened_entries(cx);
+    }
+
     pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
         self.recently_opened_entries
             .retain(|old_entry| old_entry != entry);