diff --git a/crates/agent_ui/src/thread_worktree_archive.rs b/crates/agent_ui/src/thread_worktree_archive.rs index 9cce9dde03af2db4be61654c907d4a77f5d868df..eedc052d143413ac6a046d2d866d8131b0bfca5a 100644 --- a/crates/agent_ui/src/thread_worktree_archive.rs +++ b/crates/agent_ui/src/thread_worktree_archive.rs @@ -968,10 +968,10 @@ pub async fn restore_worktree_via_git( let commit_exists = main_repo .update(cx, |repo, _cx| { - repo.resolve_commit(row.original_commit_hash.clone()) + repo.commit_exists(row.original_commit_hash.clone()) }) .await - .map_err(|_| anyhow!("resolve_commit was canceled"))? + .map_err(|_| anyhow!("commit_exists check was canceled"))? .context("failed to check if original commit exists")?; if !commit_exists { diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 73e5f98e70af9556d590358d0aff3a62ce5ecc50..87d2f596e32ce468c97c3a4fe2e173078fbf3b7c 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -17,8 +17,8 @@ use chrono::{DateTime, Utc}; use editor::Editor; use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _}; use gpui::{ - Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, KeyContext, ListState, - Pixels, Render, SharedString, WeakEntity, Window, WindowHandle, linear_color_stop, + Action as _, AnyElement, App, ClickEvent, Context, Entity, FocusHandle, Focusable, KeyContext, + ListState, Pixels, Render, SharedString, WeakEntity, Window, WindowHandle, linear_color_stop, linear_gradient, list, prelude::*, px, }; use menu::{ @@ -373,6 +373,7 @@ pub struct Sidebar { recent_projects_popover_handle: PopoverMenuHandle, project_header_menu_ix: Option, _subscriptions: Vec, + pending_worktree_restores: HashSet, _draft_observation: Option, } @@ -464,6 +465,7 @@ impl Sidebar { recent_projects_popover_handle: PopoverMenuHandle::default(), project_header_menu_ix: None, _subscriptions: Vec::new(), + pending_worktree_restores: HashSet::default(), _draft_observation: None, } } @@ -2211,9 +2213,6 @@ impl Sidebar { cx.spawn_in(window, async move |this, cx| { let archived_worktrees = task.await?; - // No archived worktrees means the thread wasn't associated with a - // linked worktree that got deleted, so we just need to find (or - // open) a workspace that matches the thread's folder paths. if archived_worktrees.is_empty() { this.update_in(cx, |this, window, cx| { if let Some(workspace) = @@ -2236,23 +2235,26 @@ impl Sidebar { return anyhow::Ok(()); } - // Restore each archived worktree back to disk via git. If the - // worktree already exists (e.g. a previous unarchive of a different - // thread on the same worktree already restored it), it's reused - // as-is. We track (old_path, restored_path) pairs so we can update - // the thread's folder_paths afterward. + this.update_in(cx, |this, _window, cx| { + this.pending_worktree_restores.insert(session_id.clone()); + cx.notify(); + })?; + let mut path_replacements: Vec<(PathBuf, PathBuf)> = Vec::new(); for row in &archived_worktrees { match thread_worktree_archive::restore_worktree_via_git(row, &mut *cx).await { Ok(restored_path) => { - // The worktree is on disk now; clean up the DB record - // and git ref we created during archival. thread_worktree_archive::cleanup_archived_worktree_record(row, &mut *cx) .await; path_replacements.push((row.worktree_path.clone(), restored_path)); } Err(error) => { log::error!("Failed to restore worktree: {error:#}"); + this.update_in(cx, |this, _window, cx| { + this.pending_worktree_restores.remove(&session_id); + cx.notify(); + }) + .ok(); this.update_in(cx, |this, _window, cx| { if let Some(multi_workspace) = this.multi_workspace.upgrade() { let workspace = multi_workspace.read(cx).workspace().clone(); @@ -2275,18 +2277,19 @@ impl Sidebar { } } + this.update_in(cx, |this, _window, cx| { + this.pending_worktree_restores.remove(&session_id); + cx.notify(); + }) + .ok(); + if !path_replacements.is_empty() { - // Update the thread's stored folder_paths: swap each old - // worktree path for the restored path (which may differ if - // the worktree was restored to a new location). cx.update(|_window, cx| { store.update(cx, |store, cx| { store.complete_worktree_restore(&session_id, &path_replacements, cx); }); })?; - // Re-read the metadata (now with updated paths) and open - // the workspace so the user lands in the restored worktree. let updated_metadata = cx.update(|_window, cx| store.read(cx).entry(&session_id).cloned())?; @@ -2923,6 +2926,23 @@ impl Sidebar { }) .collect(), ) + .pending_worktree_restore( + self.pending_worktree_restores + .contains(&thread.metadata.session_id), + ) + .when( + self.pending_worktree_restores + .contains(&thread.metadata.session_id), + |this| { + let session_id = thread.metadata.session_id.clone(); + this.on_cancel_restore(cx.listener( + move |this, _event: &ClickEvent, _window, cx| { + this.pending_worktree_restores.remove(&session_id); + cx.notify(); + }, + )) + }, + ) .timestamp(timestamp) .highlight_positions(thread.highlight_positions.to_vec()) .title_generating(thread.is_title_generating) diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 7658946b6395d6314d90db52716020a922c85ccc..a68da04059312b616f7bae14f2b5183db9713443 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -46,6 +46,8 @@ pub struct ThreadItem { project_paths: Option>, project_name: Option, worktrees: Vec, + pending_worktree_restore: bool, + on_cancel_restore: Option>, on_click: Option>, on_hover: Box, action_slot: Option, @@ -78,6 +80,8 @@ impl ThreadItem { project_paths: None, project_name: None, worktrees: Vec::new(), + pending_worktree_restore: false, + on_cancel_restore: None, on_click: None, on_hover: Box::new(|_, _, _| {}), action_slot: None, @@ -171,6 +175,19 @@ impl ThreadItem { self } + pub fn pending_worktree_restore(mut self, pending: bool) -> Self { + self.pending_worktree_restore = pending; + self + } + + pub fn on_cancel_restore( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_cancel_restore = Some(Box::new(handler)); + self + } + pub fn hovered(mut self, hovered: bool) -> Self { self.hovered = hovered; self @@ -211,7 +228,7 @@ impl ThreadItem { } impl RenderOnce for ThreadItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { let color = cx.theme().colors(); let sidebar_base_bg = color .title_bar_background @@ -359,7 +376,7 @@ impl RenderOnce for ThreadItem { let has_project_name = self.project_name.is_some(); let has_project_paths = project_paths.is_some(); - let has_worktree = !self.worktrees.is_empty(); + let has_worktree = !self.worktrees.is_empty() || self.pending_worktree_restore; let has_timestamp = !self.timestamp.is_empty(); let timestamp = self.timestamp; @@ -488,6 +505,44 @@ impl RenderOnce for ThreadItem { ); } + if self.pending_worktree_restore { + let on_cancel = self.on_cancel_restore.take(); + let restore_element = h_flex() + .id(format!("{}-worktree-restore", self.id.clone())) + .gap_1() + .child( + Icon::new(IconName::LoadCircle) + .size(IconSize::XSmall) + .color(Color::Muted) + .with_rotate_animation(2), + ) + .child( + Label::new("Restoring worktree\u{2026}") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .when_some(on_cancel, |this, on_cancel| { + this.child( + IconButton::new( + format!("{}-cancel-restore", self.id.clone()), + IconName::Close, + ) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Cancel Restore")) + .on_click( + move |event, window, cx| { + cx.stop_propagation(); + on_cancel(event, window, cx); + }, + ), + ) + }) + .tooltip(Tooltip::text("Restoring the Git worktree for this thread")) + .into_any_element(); + worktree_labels.push(restore_element); + } + this.child( h_flex() .min_w_0()