recent_projects: Make the currently active project visible in the picker (#53302)

Danilo Leal created

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.

Change summary

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 
crates/picker/src/highlighted_match_with_paths.rs     |  23 +
crates/recent_projects/src/recent_projects.rs         | 127 ++++++++----
crates/recent_projects/src/sidebar_recent_projects.rs |  10 
8 files changed, 127 insertions(+), 54 deletions(-)

Detailed changes

assets/icons/folder_open_add.svg πŸ”—

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.24135 9.23279L5.18103 7.41608C5.28319 7.21319 5.43858 7.0419 5.63058 6.92052C5.82258 6.79914 6.04397 6.73224 6.27106 6.72698H13.0117M13.0117 6.72698C13.2031 6.72664 13.392 6.77016 13.564 6.8542C13.736 6.93824 13.8864 7.06056 14.0037 7.21177C14.1211 7.36298 14.2022 7.53907 14.2409 7.72652C14.2796 7.91397 14.2749 8.10779 14.227 8.29311L13.9858 9.23279M13.0117 6.72698V5.47407C13.0117 5.14178 12.8797 4.8231 12.6447 4.58813C12.4098 4.35317 12.0911 4.22116 11.7588 4.22116H8.04392C7.8365 4.22113 7.63233 4.1696 7.44973 4.07119C7.26714 3.97279 7.11183 3.83059 6.99774 3.65736L6.49032 2.90561C6.37507 2.7306 6.21778 2.58728 6.03282 2.48878C5.84786 2.39028 5.64115 2.33975 5.43161 2.3418H2.98844C2.65615 2.3418 2.33747 2.47381 2.1025 2.70877C1.86754 2.94374 1.73553 3.26242 1.73553 3.59471V11.7386C1.73553 12.0709 1.86754 12.3896 2.1025 12.6245C2.33747 12.8595 2.65615 12.9915 2.98844 12.9915H7.3118" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.6 9.88724L11.6 14.1318" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.47626 12.5234L11.6 14.6471L13.7237 12.5234" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/folder_plus.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 7.29524V10.6536" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.3208 8.97442H9.67917" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/open_new_window.svg πŸ”—

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.4381 11.5973V4.40274C14.4381 3.7405 13.8616 3.20366 13.1505 3.20366H2.84956C2.13843 3.20366 1.56195 3.7405 1.56195 4.40274V11.5973C1.56195 12.2595 2.13843 12.7963 2.84956 12.7963H5.69262" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.71237 3.20366V5.75366" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M1.56195 5.75365H14.4381" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.13715 3.20366V5.75366" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.01288 13.0158H10.8129M10.8129 13.0158H12.6129M10.8129 13.0158V11.2158M10.8129 13.0158V14.8158" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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,

crates/picker/src/highlighted_match_with_paths.rs πŸ”—

@@ -5,6 +5,7 @@ pub struct HighlightedMatchWithPaths {
     pub prefix: Option<SharedString>,
     pub match_label: HighlightedMatch,
     pub paths: Vec<HighlightedMatch>,
+    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)

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::<MultiWorkspace>() {
                     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<AnyElement> = 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()

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(),
         )
     }