sidebar: Add some UI adjustments (#54025)

Danilo Leal created

- Don't ever swap to the ellipsis menu with the close icon button; we
now always have it
- Promote the "focus the last workspace" feature through the ellipsis
menu
- Add a unified tooltip for the thread item to show relevant thread
metadata
- Use a different icon for accessing the now "all threads" view
- Simplifies how we display archived threads
- Bonus: Don't display the "open in new window" button in currently
active worktrees (in dedicated picker)
- Bonus: Use the "main worktree" label for whenever we're mentioning the
original worktree

Release Notes:

- N/A

Change summary

assets/icons/history.svg                         |   4 
assets/icons/knockouts/archive_bg.svg            |  10 
assets/icons/knockouts/archive_fg.svg            |   4 
crates/agent_ui/src/agent_panel.rs               |   2 
crates/agent_ui/src/thread_history_view.rs       |   2 
crates/agent_ui/src/thread_import.rs             |   2 
crates/agent_ui/src/thread_worktree_picker.rs    |   1 
crates/agent_ui/src/threads_archive_view.rs      |  31 -
crates/git/src/repository.rs                     |   2 
crates/git_ui/src/worktree_picker.rs             |  37 +-
crates/icons/src/icons.rs                        |   1 
crates/sidebar/src/sidebar.rs                    | 240 ++++++++++-------
crates/ui/src/components/ai/thread_item.rs       | 226 +++++++++++-----
crates/ui/src/components/context_menu.rs         |  11 
crates/ui/src/components/icon/icon_decoration.rs |   5 
15 files changed, 341 insertions(+), 237 deletions(-)

Detailed changes

assets/icons/history.svg πŸ”—

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.3329 11.0949V3.66708C12.3329 3.33875 12.2025 3.02387 11.9703 2.7917C11.7381 2.55954 11.4233 2.42911 11.0949 2.42911H3.0481" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.52407 13.5709H12.9519C13.2802 13.5709 13.5951 13.4404 13.8273 13.2083C14.0595 12.9761 14.1899 12.6612 14.1899 12.3329V11.7139C14.1899 11.5497 14.1247 11.3923 14.0086 11.2762C13.8925 11.1601 13.7351 11.0949 13.5709 11.0949H7.38103C7.21687 11.0949 7.05943 11.1601 6.94334 11.2762C6.82726 11.3923 6.76205 11.5497 6.76205 11.7139V12.3329C6.76205 12.6612 6.63162 12.9761 6.39945 13.2083C6.16729 13.4404 5.8524 13.5709 5.52407 13.5709ZM5.52407 13.5709C5.19574 13.5709 4.88086 13.4404 4.64869 13.2083C4.41653 12.9761 4.2861 12.6612 4.2861 12.3329V3.66708C4.2861 3.33875 4.15567 3.02387 3.9235 2.7917C3.69134 2.55954 3.37646 2.42911 3.04812 2.42911C2.71979 2.42911 2.40491 2.55954 2.17274 2.7917C1.94058 3.02387 1.81015 3.33875 1.81015 3.66708V4.90506C1.81015 5.06922 1.87536 5.22666 1.99145 5.34275C2.10753 5.45883 2.26497 5.52404 2.42914 5.52404H4.2861" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/knockouts/archive_bg.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_4330_2332)">
-<path d="M8.31127 0.918671C9.28885 0.918671 10.0813 1.71115 10.0813 2.68873C10.0813 3.22911 9.83832 3.71194 9.4566 4.03661V7.99891C9.4566 9.149 8.52427 10.0813 7.37418 10.0813H3.62582C2.47573 10.0813 1.5434 9.149 1.5434 7.99891V4.03661C1.16168 3.71194 0.918671 3.22911 0.918671 2.68873C0.918671 1.71115 1.71115 0.918671 2.68873 0.918671H8.31127Z" fill="#C6CAD0"/>
-</g>
-<defs>
-<clipPath id="clip0_4330_2332">
-<rect width="11" height="11" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/knockouts/archive_fg.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.37637 1.75164C2.21069 1.75164 2.05179 1.81746 1.93463 1.93462C1.81747 2.05178 1.75165 2.21068 1.75165 2.37637V3.00109C1.75165 3.16678 1.81747 3.32568 1.93463 3.44284C2.05179 3.56 2.21069 3.62582 2.37637 3.62582H8.62364C8.78933 3.62582 8.94823 3.56 9.06539 3.44284C9.18255 3.32568 9.24837 3.16678 9.24837 3.00109V2.37637C9.24837 2.21068 9.18255 2.05178 9.06539 1.93462C8.94823 1.81746 8.78933 1.75164 8.62364 1.75164H2.37637Z" fill="#C6CAD0"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2.37634 4.77041H8.62361V7.99891C8.62361 8.33028 8.49197 8.64808 8.25765 8.8824C8.02334 9.11672 7.70553 9.24836 7.37416 9.24836H3.6258C3.29442 9.24836 2.97662 9.11672 2.7423 8.8824C2.50798 8.64808 2.37634 8.33028 2.37634 7.99891L2.37634 4.77041ZM3.80699 6.39988C3.80699 6.23151 3.87388 6.07002 3.99294 5.95096C4.112 5.8319 4.27348 5.76501 4.44186 5.76501H6.55809C6.72647 5.76501 6.88795 5.8319 7.00701 5.95096C7.12607 6.07002 7.19296 6.23151 7.19296 6.39988C7.19296 6.56826 7.12607 6.72974 7.00701 6.8488C6.88795 6.96786 6.72647 7.03475 6.55809 7.03475H4.44186C4.27348 7.03475 4.112 6.96786 3.99294 6.8488C3.87388 6.72974 3.80699 6.56826 3.80699 6.39988Z" fill="#C6CAD0"/>
-</svg>

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -4052,7 +4052,7 @@ impl AgentPanel {
             let current_path = &repo.work_directory_abs_path;
 
             return linked_worktree_short_name(main_path, current_path)
-                .unwrap_or_else(|| "main".into());
+                .unwrap_or_else(|| "main worktree".into());
         }
 
         project

crates/agent_ui/src/thread_history_view.rs πŸ”—

@@ -74,7 +74,7 @@ impl ThreadHistoryView {
     ) -> Self {
         let search_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search threads...", window, cx);
+            editor.set_placeholder_text("Search all threads…", window, cx);
             editor
         });
 

crates/agent_ui/src/thread_import.rs πŸ”—

@@ -391,7 +391,7 @@ impl Render for ThreadImportModal {
                             .headline("Import External Agent Threads")
                             .description(
                                 "Import threads from agents like Claude Agent, Codex, and more, whether started in Zed or another client. \
-                                Choose which agents to include, and their threads will appear in your archive."
+                                Choose which agents to include, and their threads will appear in your list."
                             )
                             .show_dismiss_button(true),
 

crates/agent_ui/src/thread_worktree_picker.rs πŸ”—

@@ -361,7 +361,6 @@ impl PickerDelegate for ThreadWorktreePickerDelegate {
         }
 
         // When the user is typing, fuzzy-match worktree names using display_name
-        // For the main worktree, also match against "main"
         let main_worktree_path = repo_worktrees
             .iter()
             .find(|wt| wt.is_main)

crates/agent_ui/src/threads_archive_view.rs πŸ”—

@@ -30,10 +30,9 @@ use picker::{
 use project::{AgentId, AgentServerStore};
 use settings::Settings as _;
 use theme::ActiveTheme;
-use ui::{AgentThreadStatus, IconDecoration, IconDecorationKind, Tab, ThreadItem};
 use ui::{
-    Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, WithScrollbar,
-    prelude::*, utils::platform_title_bar_height,
+    AgentThreadStatus, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tab,
+    ThreadItem, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height,
 };
 use ui_input::ErasedEditor;
 use util::ResultExt;
@@ -155,7 +154,7 @@ impl ThreadsArchiveView {
 
         let filter_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search threads…", window, cx);
+            editor.set_placeholder_text("Search all threads…", window, cx);
             editor
         });
 
@@ -606,24 +605,13 @@ impl ThreadsArchiveView {
                     &branch_names_for_thread,
                 );
 
-                let color = cx.theme().colors();
-                let knockout_color = color
-                    .title_bar_background
-                    .blend(color.panel_background.opacity(0.25));
-                let archived_decoration =
-                    IconDecoration::new(IconDecorationKind::Archive, knockout_color, cx)
-                        .color(color.icon_disabled)
-                        .position(gpui::Point {
-                            x: px(-3.),
-                            y: px(-3.5),
-                        });
+                let archived_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.85));
 
                 let base = ThreadItem::new(id, thread.display_title())
                     .icon(icon)
                     .when(is_archived, |this| {
-                        this.icon_color(Color::Muted)
-                            .title_label_color(Color::Muted)
-                            .icon_decoration(archived_decoration)
+                        this.icon_color(archived_color)
+                            .title_label_color(archived_color)
                     })
                     .when_some(icon_from_external_svg, |this, svg| {
                         this.custom_icon_from_external_svg(svg)
@@ -661,7 +649,6 @@ impl ThreadsArchiveView {
                                     })
                                 }),
                         )
-                        .tooltip(Tooltip::text("Restoring…"))
                         .into_any_element()
                 } else if is_archived {
                     base.action_slot(
@@ -694,9 +681,6 @@ impl ThreadsArchiveView {
                                 })
                             }),
                     )
-                    .tooltip(move |_, cx| {
-                        Tooltip::for_action("Open Archived Thread", &menu::Confirm, cx)
-                    })
                     .on_click({
                         let thread = thread.clone();
                         cx.listener(move |this, _, window, cx| {
@@ -718,7 +702,6 @@ impl ThreadsArchiveView {
                                 })
                             }),
                     )
-                    .tooltip(move |_, cx| Tooltip::for_action("Open Thread", &menu::Confirm, cx))
                     .on_click({
                         let thread = thread.clone();
                         cx.listener(move |this, _, window, cx| {
@@ -895,7 +878,7 @@ impl ThreadsArchiveView {
                     .color(Color::Muted),
             )
             .child(
-                IconButton::new("toggle-archived-only", IconName::ListFilter)
+                IconButton::new("toggle-archived-only", IconName::Archive)
                     .icon_size(IconSize::Small)
                     .toggle_state(self.show_archived_only)
                     .tooltip(Tooltip::text(if self.show_archived_only {

crates/git/src/repository.rs πŸ”—

@@ -295,7 +295,7 @@ impl Worktree {
 
     pub fn directory_name(&self, main_worktree_path: Option<&Path>) -> String {
         if self.is_main {
-            return "main".to_string();
+            return "main worktree".to_string();
         }
 
         let dir_name = self

crates/git_ui/src/worktree_picker.rs πŸ”—

@@ -969,7 +969,7 @@ impl PickerDelegate for WorktreeListDelegate {
                             }
                         })),
                 )
-                .when(!entry.is_new, |this| {
+                .when(!entry.is_new && !is_current, |this| {
                     let focus_handle = self.focus_handle.clone();
                     let open_in_new_window_button =
                         IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
@@ -1007,6 +1007,13 @@ impl PickerDelegate for WorktreeListDelegate {
         let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
         let can_delete = selected_entry
             .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
+        let is_current = selected_entry.is_some_and(|entry| {
+            !entry.is_new
+                && self
+                    .current_worktree_path
+                    .as_ref()
+                    .is_some_and(|current| *current == entry.worktree.path)
+        });
 
         let footer_container = h_flex()
             .w_full()
@@ -1066,20 +1073,22 @@ impl PickerDelegate for WorktreeListDelegate {
                                 }),
                         )
                     })
-                    .child(
-                        Button::new("open-in-new-window", "Open in New Window")
-                            .key_binding(
-                                KeyBinding::for_action_in(
-                                    &menu::SecondaryConfirm,
-                                    &focus_handle,
-                                    cx,
+                    .when(!is_current, |this| {
+                        this.child(
+                            Button::new("open-in-new-window", "Open in New Window")
+                                .key_binding(
+                                    KeyBinding::for_action_in(
+                                        &menu::SecondaryConfirm,
+                                        &focus_handle,
+                                        cx,
+                                    )
+                                    .map(|kb| kb.size(rems_from_px(12.))),
                                 )
-                                .map(|kb| kb.size(rems_from_px(12.))),
-                            )
-                            .on_click(|_, window, cx| {
-                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
-                            }),
-                    )
+                                .on_click(|_, window, cx| {
+                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
+                                }),
+                        )
+                    })
                     .child(
                         Button::new("open-in-window", "Open")
                             .key_binding(

crates/sidebar/src/sidebar.rs πŸ”—

@@ -369,6 +369,7 @@ pub struct Sidebar {
     view: SidebarView,
     restoring_tasks: HashMap<agent_ui::ThreadId, Task<()>>,
     recent_projects_popover_handle: PopoverMenuHandle<SidebarRecentProjects>,
+    project_header_menu_handles: HashMap<usize, PopoverMenuHandle<ContextMenu>>,
     project_header_menu_ix: Option<usize>,
     _subscriptions: Vec<gpui::Subscription>,
     /// For the thread import banners, if there is just one we show "Import
@@ -463,6 +464,7 @@ impl Sidebar {
             view: SidebarView::default(),
             restoring_tasks: HashMap::new(),
             recent_projects_popover_handle: PopoverMenuHandle::default(),
+            project_header_menu_handles: HashMap::new(),
             project_header_menu_ix: None,
             _subscriptions: Vec::new(),
             import_banners_use_verbose_labels: None,
@@ -1331,6 +1333,7 @@ impl Sidebar {
                             panel.active_thread_is_draft(cx)
                                 || panel.active_conversation_view().is_none()
                         });
+                self.project_header_menu_handles.entry(ix).or_default();
                 self.render_project_header(
                     ix,
                     false,
@@ -1407,13 +1410,14 @@ impl Sidebar {
         let group_name = SharedString::from(format!("{id_prefix}header-group-{ix}"));
 
         let is_collapsed = self.is_group_collapsed(key, cx);
-        let (disclosure_icon, disclosure_tooltip) = if is_collapsed {
-            (IconName::ChevronRight, "Expand Project")
+        let disclosure_icon = if is_collapsed {
+            IconName::ChevronRight
         } else {
-            (IconName::ChevronDown, "Collapse Project")
+            IconName::ChevronDown
         };
 
         let key_for_toggle = key.clone();
+        let key_for_focus = key.clone();
 
         let label = if highlight_positions.is_empty() {
             Label::new(label.clone())
@@ -1446,8 +1450,6 @@ impl Sidebar {
                 .group_name(group_name_for_gradient.clone())
         };
 
-        let is_ellipsis_menu_open = self.project_header_menu_ix == Some(ix);
-
         let header = h_flex()
             .id(id)
             .group(&group_name)
@@ -1520,9 +1522,6 @@ impl Sidebar {
             .child(gradient_overlay())
             .child(
                 h_flex()
-                    .when(!is_ellipsis_menu_open && !has_active_draft, |this| {
-                        this.visible_on_hover(&group_name)
-                    })
                     .child(gradient_overlay())
                     .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
                         cx.stop_propagation();
@@ -1539,6 +1538,7 @@ impl Sidebar {
                         )
                         .icon_size(IconSize::Small)
                         .when(has_active_draft, |this| this.icon_color(Color::Accent))
+                        .when(!has_active_draft, |this| this.visible_on_hover(&group_name))
                         .tooltip(move |_, cx| {
                             Tooltip::for_action_in(
                                 "Start New Agent Thread",
@@ -1559,47 +1559,31 @@ impl Sidebar {
                             },
                         ))
                     })
-                    .child(self.render_project_header_ellipsis_menu(ix, id_prefix, key, cx)),
+                    .child(self.render_project_header_ellipsis_menu(
+                        ix,
+                        id_prefix,
+                        key,
+                        is_active,
+                        has_threads,
+                        &group_name,
+                        cx,
+                    )),
             )
-            .tooltip(Tooltip::element({
-                move |_, cx| {
-                    v_flex()
-                        .gap_1()
-                        .child(Label::new(disclosure_tooltip))
-                        .child(
-                            h_flex()
-                                .pt_1()
-                                .border_t_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .child(h_flex().flex_shrink_0().children(render_modifiers(
-                                    &Modifiers::secondary_key(),
-                                    PlatformStyle::platform(),
-                                    None,
-                                    Some(TextSize::Default.rems(cx).into()),
-                                    false,
-                                )))
-                                .child(
-                                    Label::new("-click to activate most recent workspace")
-                                        .color(Color::Muted),
-                                ),
-                        )
-                        .into_any_element()
-                }
-            }))
-            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
-                if event.modifiers().platform {
-                    let key = key_for_toggle.clone();
-                    if let Some(workspace) = this.workspace_for_group(&key, cx) {
-                        this.activate_workspace(&workspace, window, cx);
+            .on_click(
+                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
+                    if event.modifiers().secondary() {
+                        if let Some(workspace) = this.workspace_for_group(&key_for_focus, cx) {
+                            this.activate_workspace(&workspace, window, cx);
+                        } else {
+                            this.open_workspace_for_group(&key_for_focus, window, cx);
+                        }
+                        this.selection = None;
+                        this.active_entry = None;
                     } else {
-                        this.open_workspace_for_group(&key, window, cx);
+                        this.toggle_collapse(&key_for_toggle, window, cx);
                     }
-                    this.selection = None;
-                    this.active_entry = None;
-                } else {
-                    this.toggle_collapse(&key_for_toggle, window, cx);
-                }
-            }));
+                }),
+            );
 
         if !is_collapsed && !has_threads {
             v_flex()
@@ -1631,49 +1615,37 @@ impl Sidebar {
         ix: usize,
         id_prefix: &str,
         project_group_key: &ProjectGroupKey,
+        is_active: bool,
+        has_threads: bool,
+        group_name: &SharedString,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let multi_workspace = self.multi_workspace.clone();
         let project_group_key = project_group_key.clone();
 
-        let show_menu = multi_workspace
+        let show_multi_project_entries = multi_workspace
             .read_with(cx, |mw, _| {
                 project_group_key.host().is_none() && mw.project_group_keys().len() >= 2
             })
             .unwrap_or(false);
 
-        if !show_menu {
-            return IconButton::new(
-                SharedString::from(format!("{id_prefix}-close-project-{ix}")),
-                IconName::Close,
-            )
-            .icon_size(IconSize::Small)
-            .tooltip(Tooltip::text("Remove Project"))
-            .on_click(cx.listener({
-                move |_, _, window, cx| {
-                    multi_workspace
-                        .update(cx, |multi_workspace, cx| {
-                            multi_workspace
-                                .remove_project_group(&project_group_key, window, cx)
-                                .detach_and_log_err(cx);
-                        })
-                        .ok();
-                }
-            }))
-            .into_any_element();
-        }
-
         let this = cx.weak_entity();
 
+        let trigger_id = SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}"));
+        let menu_handle = self
+            .project_header_menu_handles
+            .get(&ix)
+            .cloned()
+            .unwrap_or_default();
+        let is_menu_open = menu_handle.is_deployed();
+
         PopoverMenu::new(format!("{id_prefix}project-header-menu-{ix}"))
+            .with_handle(menu_handle)
             .trigger(
-                IconButton::new(
-                    SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}")),
-                    IconName::Ellipsis,
-                )
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .icon_size(IconSize::Small)
-                .tooltip(Tooltip::text("Toggle Project Menu")),
+                IconButton::new(trigger_id, IconName::Ellipsis)
+                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                    .icon_size(IconSize::Small)
+                    .when(!is_menu_open, |el| el.visible_on_hover(group_name)),
             )
             .on_open(Rc::new({
                 let this = this.clone();
@@ -1688,22 +1660,106 @@ impl Sidebar {
             .menu(move |window, cx| {
                 let multi_workspace = multi_workspace.clone();
                 let project_group_key = project_group_key.clone();
+                let this_for_menu = this.clone();
 
                 let menu =
                     ContextMenu::build_persistent(window, cx, move |menu, _window, menu_cx| {
                         let weak_menu = menu_cx.weak_entity();
 
-                        let menu = menu.entry(
-                            "Open Project in New Window",
-                            Some(Box::new(workspace::MoveProjectToNewWindow)),
-                            {
-                                let project_group_key = project_group_key.clone();
-                                let multi_workspace = multi_workspace.clone();
-                                move |window, cx| {
+                        let menu = menu.when(show_multi_project_entries, |this| {
+                            this.entry(
+                                "Open Project in New Window",
+                                Some(Box::new(workspace::MoveProjectToNewWindow)),
+                                {
+                                    let project_group_key = project_group_key.clone();
+                                    let multi_workspace = multi_workspace.clone();
+                                    move |window, cx| {
+                                        multi_workspace
+                                            .update(cx, |multi_workspace, cx| {
+                                                multi_workspace
+                                                    .open_project_group_in_new_window(
+                                                        &project_group_key,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                    .detach_and_log_err(cx);
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            )
+                        });
+
+                        let menu = menu
+                            .custom_entry(
+                                {
+                                    move |_window, cx| {
+                                        let action = h_flex()
+                                            .opacity(0.6)
+                                            .children(render_modifiers(
+                                                &Modifiers::secondary_key(),
+                                                PlatformStyle::platform(),
+                                                None,
+                                                Some(TextSize::Default.rems(cx).into()),
+                                                false,
+                                            ))
+                                            .child(Label::new("-click").color(Color::Muted));
+
+                                        let label = if has_threads {
+                                            "Focus Last Workspace"
+                                        } else {
+                                            "Focus Workspace"
+                                        };
+
+                                        h_flex()
+                                            .w_full()
+                                            .justify_between()
+                                            .gap_4()
+                                            .child(
+                                                Label::new(label)
+                                                    .when(is_active, |s| s.color(Color::Disabled)),
+                                            )
+                                            .child(action)
+                                            .into_any_element()
+                                    }
+                                },
+                                {
+                                    let project_group_key = project_group_key.clone();
+                                    let this = this_for_menu.clone();
+                                    move |window, cx| {
+                                        if is_active {
+                                            return;
+                                        }
+                                        this.update(cx, |sidebar, cx| {
+                                            if let Some(workspace) =
+                                                sidebar.workspace_for_group(&project_group_key, cx)
+                                            {
+                                                sidebar.activate_workspace(&workspace, window, cx);
+                                            } else {
+                                                sidebar.open_workspace_for_group(
+                                                    &project_group_key,
+                                                    window,
+                                                    cx,
+                                                );
+                                            }
+                                            sidebar.selection = None;
+                                            sidebar.active_entry = None;
+                                        })
+                                        .ok();
+                                    }
+                                },
+                            )
+                            .selectable(!is_active);
+
+                        menu.when(show_multi_project_entries, |menu| {
+                            let project_group_key = project_group_key.clone();
+                            let multi_workspace = multi_workspace.clone();
+                            menu.separator()
+                                .entry("Remove Project", None, move |window, cx| {
                                     multi_workspace
                                         .update(cx, |multi_workspace, cx| {
                                             multi_workspace
-                                                .open_project_group_in_new_window(
+                                                .remove_project_group(
                                                     &project_group_key,
                                                     window,
                                                     cx,
@@ -1711,25 +1767,13 @@ impl Sidebar {
                                                 .detach_and_log_err(cx);
                                         })
                                         .ok();
-                                }
-                            },
-                        );
-
-                        let project_group_key = project_group_key.clone();
-                        let multi_workspace = multi_workspace.clone();
-                        menu.entry("Remove Project", None, move |window, cx| {
-                            multi_workspace
-                                .update(cx, |multi_workspace, cx| {
-                                    multi_workspace
-                                        .remove_project_group(&project_group_key, window, cx)
-                                        .detach_and_log_err(cx);
+                                    weak_menu.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
                                 })
-                                .ok();
-                            weak_menu.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
                         })
                     });
 
                 let this = this.clone();
+
                 window
                     .subscribe(&menu, cx, move |_, _: &gpui::DismissEvent, _window, cx| {
                         this.update(cx, |sidebar, cx| {
@@ -4208,7 +4252,7 @@ impl Sidebar {
                 )
             })
             .child(
-                IconButton::new("archive", IconName::Archive)
+                IconButton::new("history", IconName::History)
                     .icon_size(IconSize::Small)
                     .toggle_state(is_archive)
                     .tooltip(move |_, cx| {

crates/ui/src/components/ai/thread_item.rs πŸ”—

@@ -1,11 +1,7 @@
-use crate::{
-    CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration,
-    Tooltip, prelude::*,
-};
+use crate::{CommonAnimationExt, DiffStat, GradientFade, HighlightedLabel, Tooltip, prelude::*};
 
 use gpui::{
-    Animation, AnimationExt, AnyView, ClickEvent, Hsla, MouseButton, SharedString,
-    pulsating_between,
+    Animation, AnimationExt, ClickEvent, Hsla, MouseButton, SharedString, pulsating_between,
 };
 use itertools::Itertools as _;
 use std::{path::PathBuf, sync::Arc, time::Duration};
@@ -42,7 +38,6 @@ pub struct ThreadItem {
     icon_color: Option<Color>,
     icon_visible: bool,
     custom_icon_from_external_svg: Option<SharedString>,
-    icon_decoration: Option<IconDecoration>,
     title: SharedString,
     title_label_color: Option<Color>,
     title_generating: bool,
@@ -63,7 +58,6 @@ pub struct ThreadItem {
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
     on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
     action_slot: Option<AnyElement>,
-    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
     base_bg: Option<Hsla>,
 }
 
@@ -75,7 +69,6 @@ impl ThreadItem {
             icon_color: None,
             icon_visible: true,
             custom_icon_from_external_svg: None,
-            icon_decoration: None,
             title: title.into(),
             title_label_color: None,
             title_generating: false,
@@ -89,7 +82,6 @@ impl ThreadItem {
             rounded: false,
             added: None,
             removed: None,
-
             project_paths: None,
             project_name: None,
             worktrees: Vec::new(),
@@ -97,7 +89,6 @@ impl ThreadItem {
             on_click: None,
             on_hover: Box::new(|_, _, _| {}),
             action_slot: None,
-            tooltip: None,
             base_bg: None,
         }
     }
@@ -122,11 +113,6 @@ impl ThreadItem {
         self
     }
 
-    pub fn icon_decoration(mut self, decoration: IconDecoration) -> Self {
-        self.icon_decoration = Some(decoration);
-        self
-    }
-
     pub fn custom_icon_from_external_svg(mut self, svg: impl Into<SharedString>) -> Self {
         self.custom_icon_from_external_svg = Some(svg.into());
         self
@@ -225,11 +211,6 @@ impl ThreadItem {
         self
     }
 
-    pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
-        self.tooltip = Some(Box::new(tooltip));
-        self
-    }
-
     pub fn base_bg(mut self, color: Hsla) -> Self {
         self.base_bg = Some(color);
         self
@@ -289,35 +270,26 @@ impl RenderOnce for ThreadItem {
             Icon::new(self.icon).color(icon_color).size(IconSize::Small)
         };
 
-        let (status_icon, icon_tooltip) = if self.status == AgentThreadStatus::Error {
-            (
-                Some(
-                    Icon::new(IconName::Close)
-                        .size(IconSize::Small)
-                        .color(Color::Error),
-                ),
-                Some("Thread has an Error"),
+        let status_icon = if self.status == AgentThreadStatus::Error {
+            Some(
+                Icon::new(IconName::Close)
+                    .size(IconSize::Small)
+                    .color(Color::Error),
             )
         } else if self.status == AgentThreadStatus::WaitingForConfirmation {
-            (
-                Some(
-                    Icon::new(IconName::Warning)
-                        .size(IconSize::XSmall)
-                        .color(Color::Warning),
-                ),
-                Some("Thread is Waiting for Confirmation"),
+            Some(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::XSmall)
+                    .color(Color::Warning),
             )
         } else if self.notified {
-            (
-                Some(
-                    Icon::new(IconName::Circle)
-                        .size(IconSize::Small)
-                        .color(Color::Accent),
-                ),
-                Some("Thread's Generation is Complete"),
+            Some(
+                Icon::new(IconName::Circle)
+                    .size(IconSize::Small)
+                    .color(Color::Accent),
             )
         } else {
-            (None, None)
+            None
         };
 
         let icon = if self.status == AgentThreadStatus::Running {
@@ -330,20 +302,17 @@ impl RenderOnce for ThreadItem {
                 )
                 .into_any_element()
         } else if let Some(status_icon) = status_icon {
-            icon_container()
-                .child(status_icon)
-                .when_some(icon_tooltip, |icon, tooltip| {
-                    icon.tooltip(Tooltip::text(tooltip))
-                })
-                .into_any_element()
-        } else if let Some(decoration) = self.icon_decoration {
-            icon_container()
-                .child(DecoratedIcon::new(agent_icon, Some(decoration)))
-                .into_any_element()
+            icon_container().child(status_icon).into_any_element()
         } else {
             icon_container().child(agent_icon).into_any_element()
         };
 
+        let tooltip_title = self.title.clone();
+        let tooltip_status = self.status;
+        let tooltip_worktrees = self.worktrees.clone();
+        let tooltip_added = self.added;
+        let tooltip_removed = self.removed;
+
         let title = self.title;
         let highlight_positions = self.highlight_positions;
 
@@ -398,13 +367,6 @@ impl RenderOnce for ThreadItem {
             .filter(|wt| !(wt.kind == WorktreeKind::Main && wt.branch_name.is_none()))
             .count();
 
-        let worktree_tooltip_title = match (self.is_remote, visible_worktree_count > 1) {
-            (true, true) => "Thread Running in Remote Git Worktrees",
-            (true, false) => "Thread Running in a Remote Git Worktree",
-            (false, true) => "Thread Running in Local Git Worktrees",
-            (false, false) => "Thread Running in a Local Git Worktree",
-        };
-
         let mut worktree_labels: Vec<AnyElement> = Vec::new();
 
         let slash_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.4));
@@ -414,8 +376,6 @@ impl RenderOnce for ThreadItem {
                 (WorktreeKind::Main, None) => continue,
                 (WorktreeKind::Main, Some(branch)) => {
                     let chip_index = worktree_labels.len();
-                    let tooltip_title = worktree_tooltip_title;
-                    let full_path = wt.full_path.clone();
 
                     worktree_labels.push(
                         h_flex()
@@ -441,16 +401,11 @@ impl RenderOnce for ThreadItem {
                                     .color(Color::Muted)
                                     .truncate(),
                             )
-                            .tooltip(move |_, cx| {
-                                Tooltip::with_meta(tooltip_title, None, full_path.clone(), cx)
-                            })
                             .into_any_element(),
                     );
                 }
                 (WorktreeKind::Linked, branch) => {
                     let chip_index = worktree_labels.len();
-                    let tooltip_title = worktree_tooltip_title;
-                    let full_path = wt.full_path.clone();
 
                     let label = if wt.highlight_positions.is_empty() {
                         Label::new(wt.name)
@@ -491,9 +446,6 @@ impl RenderOnce for ThreadItem {
                                         .truncate(),
                                 )
                             })
-                            .tooltip(move |_, cx| {
-                                Tooltip::with_meta(tooltip_title, None, full_path.clone(), cx)
-                            })
                             .into_any_element(),
                     );
                 }
@@ -502,6 +454,129 @@ impl RenderOnce for ThreadItem {
 
         let has_worktree = !worktree_labels.is_empty();
 
+        let unified_tooltip = {
+            let title = tooltip_title;
+            let status = tooltip_status;
+            let worktrees = tooltip_worktrees;
+            let added = tooltip_added;
+            let removed = tooltip_removed;
+
+            Tooltip::element(move |_window, cx| {
+                v_flex()
+                    .min_w_0()
+                    .gap_1()
+                    .child(Label::new(title.clone()))
+                    .children(worktrees.iter().map(|wt| {
+                        let is_linked = wt.kind == WorktreeKind::Linked;
+
+                        v_flex()
+                            .gap_1()
+                            .when(is_linked, |this| {
+                                this.child(
+                                    v_flex()
+                                        .child(
+                                            h_flex()
+                                                .gap_1()
+                                                .child(
+                                                    Icon::new(IconName::GitWorktree)
+                                                        .size(IconSize::Small)
+                                                        .color(Color::Muted),
+                                                )
+                                                .child(
+                                                    Label::new(wt.name.clone())
+                                                        .size(LabelSize::Small)
+                                                        .color(Color::Muted),
+                                                ),
+                                        )
+                                        .child(
+                                            div()
+                                                .pl(IconSize::Small.rems() + rems(0.25))
+                                                .w(px(280.))
+                                                .whitespace_normal()
+                                                .text_ui_sm(cx)
+                                                .text_color(
+                                                    cx.theme().colors().text_muted.opacity(0.8),
+                                                )
+                                                .child(wt.full_path.clone()),
+                                        ),
+                                )
+                            })
+                            .when_some(wt.branch_name.clone(), |this, branch| {
+                                this.child(
+                                    h_flex()
+                                        .gap_1()
+                                        .child(
+                                            Icon::new(IconName::GitBranch)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .child(
+                                            Label::new(branch)
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted),
+                                        ),
+                                )
+                            })
+                    }))
+                    .when(status == AgentThreadStatus::Error, |this| {
+                        this.child(
+                            h_flex()
+                                .gap_1()
+                                .pt_1()
+                                .border_t_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(
+                                    Icon::new(IconName::Close)
+                                        .size(IconSize::Small)
+                                        .color(Color::Error),
+                                )
+                                .child(Label::new("Error").size(LabelSize::Small)),
+                        )
+                    })
+                    .when(
+                        status == AgentThreadStatus::WaitingForConfirmation,
+                        |this| {
+                            this.child(
+                                h_flex()
+                                    .pt_1()
+                                    .border_t_1()
+                                    .border_color(cx.theme().colors().border_variant)
+                                    .gap_1()
+                                    .child(
+                                        Icon::new(IconName::Warning)
+                                            .size(IconSize::Small)
+                                            .color(Color::Warning),
+                                    )
+                                    .child(
+                                        Label::new("Waiting for Confirmation")
+                                            .size(LabelSize::Small),
+                                    ),
+                            )
+                        },
+                    )
+                    .when(added.is_some() || removed.is_some(), |this| {
+                        this.child(
+                            h_flex()
+                                .pt_1()
+                                .border_t_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .gap_1()
+                                .child(DiffStat::new(
+                                    "diff",
+                                    added.unwrap_or(0),
+                                    removed.unwrap_or(0),
+                                ))
+                                .child(
+                                    Label::new("Unreviewed Changes")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted),
+                                ),
+                        )
+                    })
+                    .into_any_element()
+            })
+        };
+
         v_flex()
             .id(self.id.clone())
             .cursor_pointer()
@@ -518,6 +593,7 @@ impl RenderOnce for ThreadItem {
             .when(self.rounded, |s| s.rounded_sm())
             .hover(|s| s.bg(hover_color))
             .on_hover(self.on_hover)
+            .tooltip(unified_tooltip)
             .child(
                 h_flex()
                     .min_w_0()
@@ -531,8 +607,7 @@ impl RenderOnce for ThreadItem {
                             .flex_1()
                             .gap_1p5()
                             .child(icon)
-                            .child(title_label)
-                            .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
+                            .child(title_label),
                     )
                     .child(gradient_overlay)
                     .when(self.hovered, |this| {
@@ -609,10 +684,7 @@ impl RenderOnce for ThreadItem {
                                 |this| this.child(dot_separator()),
                             )
                             .when(has_diff_stats, |this| {
-                                this.child(
-                                    DiffStat::new(diff_stat_id, added_count, removed_count)
-                                        .tooltip("Unreviewed Changes"),
-                                )
+                                this.child(DiffStat::new(diff_stat_id, added_count, removed_count))
                             })
                             .when(has_diff_stats && has_timestamp, |this| {
                                 this.child(dot_separator())

crates/ui/src/components/context_menu.rs πŸ”—

@@ -680,6 +680,17 @@ impl ContextMenu {
         self
     }
 
+    pub fn selectable(mut self, selectable: bool) -> Self {
+        if let Some(ContextMenuItem::CustomEntry {
+            selectable: entry_selectable,
+            ..
+        }) = self.items.last_mut()
+        {
+            *entry_selectable = selectable;
+        }
+        self
+    }
+
     pub fn label(mut self, label: impl Into<SharedString>) -> Self {
         self.items.push(ContextMenuItem::Label(label.into()));
         self

crates/ui/src/components/icon/icon_decoration.rs πŸ”—

@@ -18,8 +18,6 @@ pub enum KnockoutIconName {
     DotBg,
     TriangleFg,
     TriangleBg,
-    ArchiveFg,
-    ArchiveBg,
 }
 
 impl KnockoutIconName {
@@ -35,7 +33,6 @@ pub enum IconDecorationKind {
     X,
     Dot,
     Triangle,
-    Archive,
 }
 
 impl IconDecorationKind {
@@ -44,7 +41,6 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XFg,
             Self::Dot => KnockoutIconName::DotFg,
             Self::Triangle => KnockoutIconName::TriangleFg,
-            Self::Archive => KnockoutIconName::ArchiveFg,
         }
     }
 
@@ -53,7 +49,6 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XBg,
             Self::Dot => KnockoutIconName::DotBg,
             Self::Triangle => KnockoutIconName::TriangleBg,
-            Self::Archive => KnockoutIconName::ArchiveBg,
         }
     }
 }