diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 3bb6c9dbef4e1ee986bacd6f9f187b3f2b8f5f74..db8b2276be282ecb70c380b95463477f98faeee9 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -18,7 +18,7 @@ use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _}; use git::repository::{AskPassDelegate, CommitOptions, ResetMode}; use gpui::{ Action as _, AnyElement, App, AsyncWindowContext, Context, Entity, FocusHandle, Focusable, - KeyContext, ListState, Pixels, PromptLevel, Render, SharedString, WeakEntity, Window, + KeyContext, ListState, Pixels, PromptLevel, Render, SharedString, Task, WeakEntity, Window, WindowHandle, linear_color_stop, linear_gradient, list, prelude::*, px, }; use menu::{ @@ -384,6 +384,7 @@ pub struct Sidebar { project_header_menu_ix: Option, _subscriptions: Vec, _draft_observation: Option, + pending_worktree_archives: HashMap>>, } fn find_main_repo_in_workspaces( @@ -494,6 +495,7 @@ impl Sidebar { project_header_menu_ix: None, _subscriptions: Vec::new(), _draft_observation: None, + pending_worktree_archives: HashMap::default(), } } @@ -2244,6 +2246,12 @@ impl Sidebar { window: &mut Window, cx: &mut Context, ) { + // Cancel any in-flight archive tasks for the paths we're about to + // restore, so a slow archive cannot delete a worktree we are restoring. + for path in &paths { + self.pending_worktree_archives.remove(path); + } + let Some(multi_workspace) = self.multi_workspace.upgrade() else { return; }; @@ -2822,7 +2830,7 @@ impl Sidebar { /// that worktree, create a WIP commit, anchor it with a git ref, and /// delete the worktree. fn maybe_delete_git_worktree_for_archived_thread( - &self, + &mut self, session_id: &acp::SessionId, window: &mut Window, cx: &mut Context, @@ -2890,14 +2898,28 @@ impl Sidebar { let worktree_path_str = worktree_path.to_string_lossy().to_string(); let main_repo_path_str = main_repo_path.to_string_lossy().to_string(); let session_id = session_id.clone(); + let folder_paths_for_recheck = folder_paths.clone(); + let worktree_path_for_key = worktree_path.clone(); - cx.spawn_in(window, async move |_this, cx| { + let task = cx.spawn_in(window, async move |_this, cx| { if !is_last_thread { return anyhow::Ok(()); } let store = cx.update(|_window, cx| ThreadMetadataStore::global(cx))?; + // Re-check inside the async block to close the TOCTOU window: + // another thread on the same worktree may have been un-archived + // (or a new one created) between the synchronous check and here. + let still_last_thread = store.update(cx, |store, _cx| { + !store + .entries_for_path(&folder_paths_for_recheck) + .any(|entry| &entry.session_id != &session_id) + }); + if !still_last_thread { + return anyhow::Ok(()); + } + // Helper: unarchive the thread so it reappears in the sidebar. let unarchive = |cx: &mut AsyncWindowContext| { store.update(cx, |store, cx| { @@ -3191,8 +3213,9 @@ impl Sidebar { .log_err(); anyhow::Ok(()) - }) - .detach_and_log_err(cx); + }); + self.pending_worktree_archives + .insert(worktree_path_for_key, task); } fn remove_selected_thread(