From 833a015dc65f5b1658af1ae8cbb58ebe313cdf66 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:44:19 -0300 Subject: [PATCH] recent_projects: Make the currently active project visible in the picker (#53302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the recent projects picker in the context of multi-workspace: - The currently active project now appears in the "This Window" section with a checkmark indicator. Clicking it simply dismisses the picker, since there's nothing to switch to. This feels like a better UX because it gives you visual confirmation of where you are. - The remove button is hidden for the current project entry, both in the row and the footer, to prevent accidentally removing the workspace you're actively using. - The "Add to Workspace" button now uses a more descriptive icon (`FolderOpenAdd`) and shows a meta tooltip clarifying that it adds the project as a multi-root folder project. The primary click/enter behavior remains unchanged—it opens the selected project in the current window's multi-workspace. The "Open in New Window" action continues to be available via the icon button or shift+enter. Release Notes: - Improved the recent projects picker to show the currently active project in the "This Window" section with a checkmark indicator. --- assets/icons/folder_open_add.svg | 5 + assets/icons/folder_plus.svg | 5 - assets/icons/open_new_window.svg | 7 + crates/agent_ui/src/threads_archive_view.rs | 1 + crates/icons/src/icons.rs | 3 +- .../src/highlighted_match_with_paths.rs | 23 +++- crates/recent_projects/src/recent_projects.rs | 127 ++++++++++++------ .../src/sidebar_recent_projects.rs | 10 +- 8 files changed, 127 insertions(+), 54 deletions(-) create mode 100644 assets/icons/folder_open_add.svg delete mode 100644 assets/icons/folder_plus.svg create mode 100644 assets/icons/open_new_window.svg diff --git a/assets/icons/folder_open_add.svg b/assets/icons/folder_open_add.svg new file mode 100644 index 0000000000000000000000000000000000000000..d5ebbdaa8b080037a2faee0ee0fc3606eec9c6ca --- /dev/null +++ b/assets/icons/folder_open_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/folder_plus.svg b/assets/icons/folder_plus.svg deleted file mode 100644 index a543448ed6197043291369bee640e23b6ad729b9..0000000000000000000000000000000000000000 --- a/assets/icons/folder_plus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/open_new_window.svg b/assets/icons/open_new_window.svg new file mode 100644 index 0000000000000000000000000000000000000000..c81d49f9ff9edfbc965055568efc72e0214efb41 --- /dev/null +++ b/assets/icons/open_new_window.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 13b2aa1a37cd506c338d13db78bce751882e426a..7cb8410e5017438b0e8adde673887c13397d9abf 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -1236,6 +1236,7 @@ impl PickerDelegate for ProjectPickerDelegate { }, match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), paths: Vec::new(), + active: false, }; Some( diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index e29b7d3593025556771d62dc0124786672c540de..bdc3890432414e0a78f69a226bb9174510453331 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -134,7 +134,7 @@ pub enum IconName { Flame, Folder, FolderOpen, - FolderPlus, + FolderOpenAdd, FolderSearch, Font, FontSize, @@ -184,6 +184,7 @@ pub enum IconName { NewThread, Notepad, OpenFolder, + OpenNewWindow, Option, PageDown, PageUp, diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs index 74271047621b26be573dc2eebfffe9e9e0f1a138..7c88213437feea17e6b431dff9c97b0b8557872a 100644 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -5,6 +5,7 @@ pub struct HighlightedMatchWithPaths { pub prefix: Option, pub match_label: HighlightedMatch, pub paths: Vec, + pub active: bool, } #[derive(Debug, Clone, IntoElement)] @@ -63,18 +64,30 @@ impl HighlightedMatchWithPaths { .color(Color::Muted) })) } + + pub fn is_active(mut self, active: bool) -> Self { + self.active = active; + self + } } impl RenderOnce for HighlightedMatchWithPaths { fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement { v_flex() .child( - h_flex().gap_1().child(self.match_label.clone()).when_some( - self.prefix.as_ref(), - |this, prefix| { + h_flex() + .gap_1() + .child(self.match_label.clone()) + .when_some(self.prefix.as_ref(), |this, prefix| { this.child(Label::new(format!("({})", prefix)).color(Color::Muted)) - }, - ), + }) + .when(self.active, |this| { + this.child( + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Accent), + ) + }), ) .when(!self.paths.is_empty(), |this| { self.render_paths_children(this) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index e3bfc0dc08c95c0ce57b818e50965433a6c6bc98..57754dadec20146cb1f21039266de88a0bd5da9f 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -720,6 +720,9 @@ impl RecentProjects { picker.delegate.workspaces.get(hit.candidate_id) { let workspace_id = *workspace_id; + if picker.delegate.is_current_workspace(workspace_id, cx) { + return; + } picker .delegate .remove_sibling_workspace(workspace_id, window, cx); @@ -939,7 +942,7 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _, _, _))| self.is_sibling_workspace(*id, cx)) + .filter(|(_, (id, _, _, _))| self.sibling_workspace_ids.contains(id)) .map(|(id, (_, _, paths, _))| { let combined_string = paths .ordered_paths() @@ -1028,7 +1031,7 @@ impl PickerDelegate for RecentProjectsDelegate { if is_empty_query { for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() { - if self.is_sibling_workspace(*workspace_id, cx) { + if self.sibling_workspace_ids.contains(workspace_id) { entries.push(ProjectPickerEntry::OpenProject(StringMatch { candidate_id: id, score: 0.0, @@ -1106,6 +1109,11 @@ impl PickerDelegate for RecentProjectsDelegate { }; let workspace_id = *workspace_id; + if self.is_current_workspace(workspace_id, cx) { + cx.emit(DismissEvent); + return; + } + if let Some(handle) = window.window_handle().downcast::() { cx.defer(move |cx| { handle @@ -1349,6 +1357,7 @@ impl PickerDelegate for RecentProjectsDelegate { ProjectPickerEntry::OpenProject(hit) => { let (workspace_id, location, paths, _) = self.workspaces.get(hit.candidate_id)?; let workspace_id = *workspace_id; + let is_current = self.is_current_workspace(workspace_id, cx); let ordered_paths: Vec<_> = paths .ordered_paths() .map(|p| p.compact().to_string_lossy().to_string()) @@ -1388,6 +1397,7 @@ impl PickerDelegate for RecentProjectsDelegate { prefix, match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), paths, + active: is_current, }; let icon = icon_for_remote_connection(match location { @@ -1397,20 +1407,24 @@ impl PickerDelegate for RecentProjectsDelegate { let secondary_actions = h_flex() .gap_1() - .child( - IconButton::new("remove_open_project", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Remove Project from Window")) - .on_click(cx.listener(move |picker, _, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - picker - .delegate - .remove_sibling_workspace(workspace_id, window, cx); - let query = picker.query(cx); - picker.update_matches(query, window, cx); - })), - ) + .when(!is_current, |this| { + this.child( + IconButton::new("remove_open_project", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Remove Project from Window")) + .on_click(cx.listener(move |picker, _, window, cx| { + cx.stop_propagation(); + window.prevent_default(); + picker.delegate.remove_sibling_workspace( + workspace_id, + window, + cx, + ); + let query = picker.query(cx); + picker.update_matches(query, window, cx); + })), + ) + }) .into_any_element(); Some( @@ -1483,6 +1497,7 @@ impl PickerDelegate for RecentProjectsDelegate { prefix, match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), paths, + active: false, }; let focus_handle = self.focus_handle.clone(); @@ -1491,9 +1506,16 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_px() .when(is_local, |this| { this.child( - IconButton::new("add_to_workspace", IconName::FolderPlus) + IconButton::new("add_to_workspace", IconName::FolderOpenAdd) .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Add Project to this Workspace")) + .tooltip(move |_, cx| { + Tooltip::with_meta( + "Add Project to this Workspace", + None, + "As a multi-root folder project", + cx, + ) + }) .on_click({ let paths_to_add = paths_to_add.clone(); cx.listener(move |picker, _event, window, cx| { @@ -1509,8 +1531,8 @@ impl PickerDelegate for RecentProjectsDelegate { ) }) .child( - IconButton::new("open_new_window", IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + IconButton::new("open_new_window", IconName::OpenNewWindow) + .icon_size(IconSize::Small) .tooltip({ move |_, cx| { Tooltip::for_action_in( @@ -1565,7 +1587,14 @@ impl PickerDelegate for RecentProjectsDelegate { } highlighted.render(window, cx) }) - .tooltip(Tooltip::text(tooltip_path)), + .tooltip(move |_, cx| { + Tooltip::with_meta( + "Open Project in This Window", + None, + tooltip_path.clone(), + cx, + ) + }), ) .end_slot(secondary_actions) .show_end_slot_on_hover() @@ -1625,27 +1654,41 @@ impl PickerDelegate for RecentProjectsDelegate { let selected_entry = self.filtered_entries.get(self.selected_index); + let is_current_workspace_entry = + if let Some(ProjectPickerEntry::OpenProject(hit)) = selected_entry { + self.workspaces + .get(hit.candidate_id) + .map(|(id, ..)| self.is_current_workspace(*id, cx)) + .unwrap_or(false) + } else { + false + }; + let secondary_footer_actions: Option = match selected_entry { - Some(ProjectPickerEntry::OpenFolder { .. } | ProjectPickerEntry::OpenProject(_)) => { - let label = if matches!(selected_entry, Some(ProjectPickerEntry::OpenFolder { .. })) - { - "Remove Folder" - } else { - "Remove from Window" - }; - Some( - Button::new("remove_selected", label) - .key_binding(KeyBinding::for_action_in( - &RemoveSelected, - &focus_handle, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action(RemoveSelected.boxed_clone(), cx) - }) - .into_any_element(), - ) - } + Some(ProjectPickerEntry::OpenFolder { .. }) => Some( + Button::new("remove_selected", "Remove Folder") + .key_binding(KeyBinding::for_action_in( + &RemoveSelected, + &focus_handle, + cx, + )) + .on_click(|_, window, cx| { + window.dispatch_action(RemoveSelected.boxed_clone(), cx) + }) + .into_any_element(), + ), + Some(ProjectPickerEntry::OpenProject(_)) if !is_current_workspace_entry => Some( + Button::new("remove_selected", "Remove from Window") + .key_binding(KeyBinding::for_action_in( + &RemoveSelected, + &focus_handle, + cx, + )) + .on_click(|_, window, cx| { + window.dispatch_action(RemoveSelected.boxed_clone(), cx) + }) + .into_any_element(), + ), Some(ProjectPickerEntry::RecentProject(_)) => Some( Button::new("delete_recent", "Delete") .key_binding(KeyBinding::for_action_in( @@ -1748,7 +1791,7 @@ impl PickerDelegate for RecentProjectsDelegate { menu.context(focus_handle) .when(show_add_to_workspace, |menu| { menu.action( - "Add to Workspace", + "Add to this Workspace", AddToWorkspace.boxed_clone(), ) .separator() diff --git a/crates/recent_projects/src/sidebar_recent_projects.rs b/crates/recent_projects/src/sidebar_recent_projects.rs index 1fe0d2ae86aefdad45136c496f8049689d77e048..dec269c07eada3a1d6172482cb886f9ed44d784c 100644 --- a/crates/recent_projects/src/sidebar_recent_projects.rs +++ b/crates/recent_projects/src/sidebar_recent_projects.rs @@ -374,6 +374,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate { prefix, match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), paths: Vec::new(), + active: false, }; let icon = icon_for_remote_connection(match location { @@ -395,7 +396,14 @@ impl PickerDelegate for SidebarRecentProjectsDelegate { }) .child(highlighted_match.render(window, cx)), ) - .tooltip(Tooltip::text(tooltip_path)) + .tooltip(move |_, cx| { + Tooltip::with_meta( + "Open Project in This Window", + None, + tooltip_path.clone(), + cx, + ) + }) .into_any_element(), ) }