Avoid showing draft threads in the sidebar or storing them in the database (#54014)

Max Brunsfeld created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/thread_metadata_store.rs | 67 ++++++++++++++-------
1 file changed, 43 insertions(+), 24 deletions(-)

Detailed changes

crates/agent_ui/src/thread_metadata_store.rs 🔗

@@ -25,7 +25,7 @@ pub use project::WorktreePaths;
 use project::{AgentId, linked_worktree_short_name};
 use remote::{RemoteConnectionOptions, same_remote_connection_identity};
 use ui::{App, Context, SharedString, ThreadItemWorktreeInfo, WorktreeKind};
-use util::ResultExt as _;
+use util::{ResultExt as _, debug_panic};
 use workspace::{PathList, SerializedWorkspaceLocation, WorkspaceDb};
 
 use crate::DEFAULT_THREAD_TITLE;
@@ -593,20 +593,7 @@ impl ThreadMetadataStore {
                     this.threads_by_session.clear();
 
                     for row in rows {
-                        if let Some(sid) = &row.session_id {
-                            this.threads_by_session.insert(sid.clone(), row.thread_id);
-                        }
-                        this.threads_by_paths
-                            .entry(row.folder_paths().clone())
-                            .or_default()
-                            .insert(row.thread_id);
-                        if !row.main_worktree_paths().is_empty() {
-                            this.threads_by_main_paths
-                                .entry(row.main_worktree_paths().clone())
-                                .or_default()
-                                .insert(row.thread_id);
-                        }
-                        this.threads.insert(row.thread_id, row);
+                        this.cache_thread_metadata(row);
                     }
 
                     cx.notify();
@@ -631,6 +618,11 @@ impl ThreadMetadataStore {
     }
 
     fn save_internal(&mut self, metadata: ThreadMetadata) {
+        if metadata.session_id.is_none() {
+            debug_panic!("cannot store thread metadata without a session_id");
+            return;
+        };
+
         if let Some(thread) = self.threads.get(&metadata.thread_id) {
             if thread.folder_paths() != metadata.folder_paths() {
                 if let Some(thread_ids) = self.threads_by_paths.get_mut(thread.folder_paths()) {
@@ -649,10 +641,20 @@ impl ThreadMetadataStore {
             }
         }
 
-        if let Some(sid) = &metadata.session_id {
-            self.threads_by_session
-                .insert(sid.clone(), metadata.thread_id);
-        }
+        self.cache_thread_metadata(metadata.clone());
+        self.pending_thread_ops_tx
+            .try_send(DbOperation::Upsert(metadata))
+            .log_err();
+    }
+
+    fn cache_thread_metadata(&mut self, metadata: ThreadMetadata) {
+        let Some(session_id) = metadata.session_id.as_ref() else {
+            debug_panic!("cannot store thread metadata without a session_id");
+            return;
+        };
+
+        self.threads_by_session
+            .insert(session_id.clone(), metadata.thread_id);
 
         self.threads.insert(metadata.thread_id, metadata.clone());
 
@@ -667,10 +669,6 @@ impl ThreadMetadataStore {
                 .or_default()
                 .insert(metadata.thread_id);
         }
-
-        self.pending_thread_ops_tx
-            .try_send(DbOperation::Upsert(metadata))
-            .log_err();
     }
 
     pub fn update_working_directories(
@@ -1254,6 +1252,19 @@ impl Domain for ThreadMetadataDb {
             DROP TABLE sidebar_threads;
             ALTER TABLE sidebar_threads_v2 RENAME TO sidebar_threads;
         ),
+        sql!(
+            DELETE FROM thread_archived_worktrees
+            WHERE thread_id IN (
+                SELECT thread_id FROM sidebar_threads WHERE session_id IS NULL
+            );
+
+            DELETE FROM sidebar_threads WHERE session_id IS NULL;
+
+            DELETE FROM archived_git_worktrees
+            WHERE id NOT IN (
+                SELECT archived_worktree_id FROM thread_archived_worktrees
+            );
+        ),
     ];
 }
 
@@ -1264,6 +1275,7 @@ impl ThreadMetadataDb {
     pub fn list_ids(&self) -> anyhow::Result<Vec<ThreadId>> {
         self.select::<ThreadId>(
             "SELECT thread_id FROM sidebar_threads \
+             WHERE session_id IS NOT NULL \
              ORDER BY updated_at DESC",
         )?()
     }
@@ -1273,12 +1285,18 @@ impl ThreadMetadataDb {
         self.select::<ThreadMetadata>(
             "SELECT thread_id, session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order, archived, main_worktree_paths, main_worktree_paths_order, remote_connection \
              FROM sidebar_threads \
+             WHERE session_id IS NOT NULL \
              ORDER BY updated_at DESC"
         )?()
     }
 
     /// Upsert metadata for a thread.
     pub async fn save(&self, row: ThreadMetadata) -> anyhow::Result<()> {
+        anyhow::ensure!(
+            row.session_id.is_some(),
+            "refusing to persist thread metadata without a session_id"
+        );
+
         let session_id = row.session_id.as_ref().map(|s| s.0.clone());
         let agent_id = if row.agent_id.as_ref() == ZED_AGENT_ID.as_ref() {
             None
@@ -3370,7 +3388,8 @@ mod tests {
             .unwrap()()
         .unwrap();
 
-        // Run all migrations (0-7). sqlez skips 0-6 and runs only migration 7.
+        // Run all current migrations. sqlez skips the already-applied ones and
+        // runs the remaining migrations.
         connection
             .migrate(
                 ThreadMetadataDb::NAME,