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