From d066ff0ae5139cff216ffa3fb9503aaa8a85c962 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:08:28 -0300 Subject: [PATCH] sidebar: Add some UI adjustments (#54025) - 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 --- 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 + .../ui/src/components/icon/icon_decoration.rs | 5 - 15 files changed, 341 insertions(+), 237 deletions(-) create mode 100644 assets/icons/history.svg delete mode 100644 assets/icons/knockouts/archive_bg.svg delete mode 100644 assets/icons/knockouts/archive_fg.svg diff --git a/assets/icons/history.svg b/assets/icons/history.svg new file mode 100644 index 0000000000000000000000000000000000000000..f9b803f2bd64be8b287838e398b99032bc643f57 --- /dev/null +++ b/assets/icons/history.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/knockouts/archive_bg.svg b/assets/icons/knockouts/archive_bg.svg deleted file mode 100644 index 1954d14b1ee16adf605e2cfe31309838d2448f7a..0000000000000000000000000000000000000000 --- a/assets/icons/knockouts/archive_bg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/knockouts/archive_fg.svg b/assets/icons/knockouts/archive_fg.svg deleted file mode 100644 index 74d1238c5399105ba83046c63f4505b0d1fec877..0000000000000000000000000000000000000000 --- a/assets/icons/knockouts/archive_fg.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 15912773167c560f8e8158ed9237159a4b341e8c..d6df01ba1e18f98e0091cf6169cf6c7b7ad3cde6 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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 diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs index 8facafecd9518eafcbf2a9e0486674e0abcd9ebc..1cebd175be46eaabd420853a3997ae1fd6ce7a50 100644 --- a/crates/agent_ui/src/thread_history_view.rs +++ b/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 }); diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index 99ab121027ae2e9a61a0a85b6333425ba02354cc..cb1234484410a5672c3bf9137ae2b790e181ff5f 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/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), diff --git a/crates/agent_ui/src/thread_worktree_picker.rs b/crates/agent_ui/src/thread_worktree_picker.rs index c77da77d0d353e27d8e45d896e9657a853633e05..93d04fd131d4241d15c2fbb0af96b5d69d3920af 100644 --- a/crates/agent_ui/src/thread_worktree_picker.rs +++ b/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) diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index aa082a0c23e524c142426bde171a7ed28aa32cf7..351e83bdff7336b2817bdc43da7e5f601539de7b 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/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 { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 3215d761e0ace95d9010bbcb9ac7bb0b711c2c82..1e0f97bf6acf48a64b8464e521d1c19c421561ff 100644 --- a/crates/git/src/repository.rs +++ b/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 diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 89d84cb7fb86cbf6359c8f3336a5ac9ae9fc1a9a..f9069d2920eedcd3ec75c1af781d90c818d49ba3 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/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( diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 3c6e09cf60451ea36d9ab44471137d96d76987da..9fc8d4220bf1d28750928309b20bc167445312eb 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -153,6 +153,7 @@ pub enum IconName { GitWorktree, Github, Hash, + History, HistoryRerun, Image, Inception, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index d561bd4e3b475b0bc527a0bd61a76929aae7e92b..210fb699f0bde93a12ef045f342c455fdd856227 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -369,6 +369,7 @@ pub struct Sidebar { view: SidebarView, restoring_tasks: HashMap>, recent_projects_popover_handle: PopoverMenuHandle, + project_header_menu_handles: HashMap>, project_header_menu_ix: Option, _subscriptions: Vec, /// 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, ) -> 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| { diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 159e5bb48422a5102ee5ad3e9edde54007be62ec..9d34f8a09593623923e6497dcd7c92ba0d1354ef 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/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, icon_visible: bool, custom_icon_from_external_svg: Option, - icon_decoration: Option, title: SharedString, title_label_color: Option, title_generating: bool, @@ -63,7 +58,6 @@ pub struct ThreadItem { on_click: Option>, on_hover: Box, action_slot: Option, - tooltip: Option AnyView + 'static>>, base_bg: Option, } @@ -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) -> 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 = 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()) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 2fcfd73b93d7c47018819fd9ec4426e9f1b38147..d9552552f4d948e9c25576410103d391de00043f 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/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) -> Self { self.items.push(ContextMenuItem::Label(label.into())); self diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs index 4515b8fa44f50d21ba5ca4274689324fbbde2bb8..1c40890c4343415d88ea735346868082637f155c 100644 --- a/crates/ui/src/components/icon/icon_decoration.rs +++ b/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, } } }