@@ -16,9 +16,9 @@ use agent_ui::{
use chrono::{DateTime, Utc};
use editor::Editor;
use gpui::{
- Action as _, AnyElement, App, Context, DismissEvent, Entity, EntityId, FocusHandle, Focusable,
- KeyContext, ListState, Pixels, Render, SharedString, Task, WeakEntity, Window, WindowHandle,
- linear_color_stop, linear_gradient, list, prelude::*, px,
+ Action as _, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EntityId, FocusHandle,
+ Focusable, KeyContext, ListState, Modifiers, Pixels, Render, SharedString, Task, WeakEntity,
+ Window, WindowHandle, linear_color_stop, linear_gradient, list, prelude::*, px,
};
use menu::{
Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
@@ -40,7 +40,7 @@ use theme::ActiveTheme;
use ui::{
AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, GradientFade, HighlightedLabel,
KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, ThreadItemWorktreeInfo, TintColor,
- Tooltip, WithScrollbar, prelude::*,
+ Tooltip, WithScrollbar, prelude::*, render_modifiers,
};
use util::ResultExt as _;
use util::path_list::PathList;
@@ -1604,10 +1604,9 @@ impl Sidebar {
let id_prefix = if is_sticky { "sticky-" } else { "" };
let id = SharedString::from(format!("{id_prefix}project-header-{ix}"));
- let disclosure_id = SharedString::from(format!("disclosure-{ix}"));
let group_name = SharedString::from(format!("{id_prefix}header-group-{ix}"));
- let is_collapsed = !has_threads || self.is_group_collapsed(key, cx);
+ let is_collapsed = self.is_group_collapsed(key, cx);
let (disclosure_icon, disclosure_tooltip) = if is_collapsed {
(IconName::ChevronRight, "Expand Project")
} else {
@@ -1639,11 +1638,10 @@ impl Sidebar {
.element_active
.blend(color.element_background.opacity(0.2));
let hover_solid = base_bg.blend(hover_base);
- let real_hover_color = if is_active { base_bg } else { hover_solid };
let group_name_for_gradient = group_name.clone();
let gradient_overlay = move || {
- GradientFade::new(base_bg, real_hover_color, real_hover_color)
+ GradientFade::new(base_bg, hover_solid, hover_solid)
.width(px(64.0))
.right(px(-2.0))
.gradient_stop(0.75)
@@ -1652,13 +1650,14 @@ impl Sidebar {
let is_ellipsis_menu_open = self.project_header_menu_ix == Some(ix);
- h_flex()
+ let header = h_flex()
.id(id)
.group(&group_name)
- .h(Tab::content_height(cx))
+ .cursor_pointer()
.relative()
+ .h(Tab::content_height(cx))
.w_full()
- .pl(px(5.))
+ .pl_2()
.pr_1p5()
.justify_between()
.border_1()
@@ -1669,30 +1668,13 @@ impl Sidebar {
this.border_color(gpui::transparent_black())
}
})
+ .hover(|s| s.bg(hover_solid))
.child(
h_flex()
.relative()
.min_w_0()
.w_full()
- .gap(px(5.))
- .child(
- IconButton::new(disclosure_id, disclosure_icon)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(if has_threads {
- Color::Custom(cx.theme().colors().icon_muted.opacity(0.5))
- } else {
- Color::Custom(cx.theme().colors().icon_disabled)
- })
- .when(has_threads, |this| {
- this.tooltip(Tooltip::text(disclosure_tooltip)).on_click(
- cx.listener(move |this, _, window, cx| {
- this.selection = None;
- this.toggle_collapse(&key_for_toggle, window, cx);
- }),
- )
- }),
- )
+ .gap_1()
.child(label)
.when_some(
self.render_remote_project_icon(ix, host.as_ref()),
@@ -1726,7 +1708,16 @@ impl Sidebar {
.tooltip(Tooltip::text(tooltip_text)),
)
})
- }),
+ })
+ .child(
+ div()
+ .when(!is_focused, |this| this.visible_on_hover(&group_name))
+ .child(
+ Icon::new(disclosure_icon)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ ),
)
.child(gradient_overlay())
.child(
@@ -1738,31 +1729,6 @@ impl Sidebar {
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
- .child(self.render_project_header_ellipsis_menu(ix, id_prefix, key, cx))
- .when(has_threads && view_more_expanded && !is_collapsed, |this| {
- this.child(
- IconButton::new(
- SharedString::from(format!(
- "{id_prefix}project-header-collapse-{ix}",
- )),
- IconName::ListCollapse,
- )
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Collapse Displayed Threads"))
- .on_click(cx.listener({
- let key_for_collapse = key_for_collapse.clone();
- move |this, _, _window, cx| {
- this.selection = None;
- this.set_group_visible_thread_count(
- &key_for_collapse,
- None,
- cx,
- );
- this.update_entries(cx);
- }
- })),
- )
- })
.child({
let key = key.clone();
let focus_handle = self.focus_handle.clone();
@@ -1786,37 +1752,103 @@ impl Sidebar {
move |this, _, window, cx| {
this.set_group_expanded(&key, true, cx);
this.selection = None;
- let workspace = this.multi_workspace.upgrade().and_then(|mw| {
- let mw = mw.read(cx);
- let active = mw.workspace().clone();
- let active_key = active.read(cx).project_group_key(cx);
- if active_key == key {
- Some(active)
- } else {
- mw.workspace_for_paths(
- key.path_list(),
- key.host().as_ref(),
- cx,
- )
- }
- });
- if let Some(workspace) = workspace {
+ if let Some(workspace) = this.workspace_for_group(&key, cx) {
this.create_new_thread(&workspace, window, cx);
} else {
this.open_workspace_and_create_draft(&key, window, cx);
}
},
))
- }),
+ })
+ .when(has_threads && view_more_expanded && !is_collapsed, |this| {
+ this.child(
+ IconButton::new(
+ SharedString::from(format!(
+ "{id_prefix}project-header-collapse-{ix}",
+ )),
+ IconName::ListCollapse,
+ )
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Show Fewer Threads"))
+ .on_click(cx.listener({
+ let key_for_collapse = key_for_collapse.clone();
+ move |this, _, _window, cx| {
+ this.selection = None;
+ this.set_group_visible_thread_count(
+ &key_for_collapse,
+ None,
+ cx,
+ );
+ this.update_entries(cx);
+ }
+ })),
+ )
+ })
+ .child(self.render_project_header_ellipsis_menu(ix, id_prefix, key, cx)),
)
- .cursor_pointer()
- .when(!is_active, |this| this.hover(|s| s.bg(hover_solid)))
- .tooltip(Tooltip::text(disclosure_tooltip))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.selection = None;
- this.toggle_collapse(&key_for_collapse, window, 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()
+ }
}))
- .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);
+ } else {
+ this.open_workspace_for_group(&key, 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()
+ .w_full()
+ .child(header)
+ .child(
+ h_flex()
+ .px_2()
+ .pt_1()
+ .pb_2()
+ .gap(px(7.))
+ .child(Icon::new(IconName::Circle).size(IconSize::Small).color(
+ Color::Custom(cx.theme().colors().icon_placeholder.opacity(0.1)),
+ ))
+ .child(
+ Label::new("No threads yet")
+ .size(LabelSize::Small)
+ .color(Color::Placeholder),
+ ),
+ )
+ .into_any_element()
+ } else {
+ header.into_any_element()
+ }
}
fn render_project_header_ellipsis_menu(
@@ -1831,6 +1863,15 @@ impl Sidebar {
let project_group_key = project_group_key.clone();
PopoverMenu::new(format!("{id_prefix}project-header-menu-{ix}"))
+ .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")),
+ )
.on_open(Rc::new({
let this = this.clone();
move |_window, cx| {
@@ -1962,14 +2003,6 @@ impl Sidebar {
Some(menu)
})
- .trigger(
- IconButton::new(
- SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}")),
- IconName::Ellipsis,
- )
- .selected_style(ButtonStyle::Tinted(TintColor::Accent))
- .icon_size(IconSize::Small),
- )
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.),
@@ -2023,8 +2056,8 @@ impl Sidebar {
*has_running_threads,
*waiting_thread_count,
*is_active,
- *has_threads,
is_selected,
+ *has_threads,
cx,
);
@@ -2240,13 +2273,9 @@ impl Sidebar {
};
match entry {
- ListEntry::ProjectHeader {
- key, has_threads, ..
- } => {
- if *has_threads {
- let key = key.clone();
- self.toggle_collapse(&key, window, cx);
- }
+ ListEntry::ProjectHeader { key, .. } => {
+ let key = key.clone();
+ self.toggle_collapse(&key, window, cx);
}
ListEntry::Thread(thread) => {
let metadata = thread.metadata.clone();
@@ -2766,19 +2795,15 @@ impl Sidebar {
let Some(ix) = self.selection else { return };
match self.contents.entries.get(ix) {
- Some(ListEntry::ProjectHeader {
- key, has_threads, ..
- }) => {
- if *has_threads {
- let key = key.clone();
- if self.is_group_collapsed(&key, cx) {
- self.set_group_expanded(&key, true, cx);
- self.update_entries(cx);
- } else if ix + 1 < self.contents.entries.len() {
- self.selection = Some(ix + 1);
- self.list_state.scroll_to_reveal_item(ix + 1);
- cx.notify();
- }
+ Some(ListEntry::ProjectHeader { key, .. }) => {
+ let key = key.clone();
+ if self.is_group_collapsed(&key, cx) {
+ self.set_group_expanded(&key, true, cx);
+ self.update_entries(cx);
+ } else if ix + 1 < self.contents.entries.len() {
+ self.selection = Some(ix + 1);
+ self.list_state.scroll_to_reveal_item(ix + 1);
+ cx.notify();
}
}
_ => {}
@@ -2794,15 +2819,11 @@ impl Sidebar {
let Some(ix) = self.selection else { return };
match self.contents.entries.get(ix) {
- Some(ListEntry::ProjectHeader {
- key, has_threads, ..
- }) => {
- if *has_threads {
- let key = key.clone();
- if !self.is_group_collapsed(&key, cx) {
- self.set_group_expanded(&key, false, cx);
- self.update_entries(cx);
- }
+ Some(ListEntry::ProjectHeader { key, .. }) => {
+ let key = key.clone();
+ if !self.is_group_collapsed(&key, cx) {
+ self.set_group_expanded(&key, false, cx);
+ self.update_entries(cx);
}
}
Some(ListEntry::Thread(_) | ListEntry::ViewMore { .. }) => {
@@ -2842,20 +2863,16 @@ impl Sidebar {
};
if let Some(header_ix) = header_ix {
- if let Some(ListEntry::ProjectHeader {
- key, has_threads, ..
- }) = self.contents.entries.get(header_ix)
+ if let Some(ListEntry::ProjectHeader { key, .. }) = self.contents.entries.get(header_ix)
{
- if *has_threads {
- let key = key.clone();
- if self.is_group_collapsed(&key, cx) {
- self.set_group_expanded(&key, true, cx);
- } else {
- self.selection = Some(header_ix);
- self.set_group_expanded(&key, false, cx);
- }
- self.update_entries(cx);
+ let key = key.clone();
+ if self.is_group_collapsed(&key, cx) {
+ self.set_group_expanded(&key, true, cx);
+ } else {
+ self.selection = Some(header_ix);
+ self.set_group_expanded(&key, false, cx);
}
+ self.update_entries(cx);
}
}
}
@@ -4025,22 +4042,17 @@ impl Sidebar {
window: &mut Window,
cx: &mut Context<Self>,
) {
- // If there is a keyboard selection, walk backwards through
- // `project_header_indices` to find the header that owns the selected
- // row. Otherwise fall back to the active workspace.
- // Always use the currently active workspace so that drafts
- // are created in the linked worktree the user is focused on,
- // not the main worktree resolved from the project header.
- let workspace = self
- .multi_workspace
- .upgrade()
- .map(|mw| mw.read(cx).workspace().clone());
-
- let Some(workspace) = workspace else {
- return;
- };
-
- self.create_new_thread(&workspace, window, cx);
+ if let Some(key) = self.selected_group_key() {
+ self.set_group_expanded(&key, true, cx);
+ self.selection = None;
+ if let Some(workspace) = self.workspace_for_group(&key, cx) {
+ self.create_new_thread(&workspace, window, cx);
+ } else {
+ self.open_workspace_and_create_draft(&key, window, cx);
+ }
+ } else if let Some(workspace) = self.active_workspace(cx) {
+ self.create_new_thread(&workspace, window, cx);
+ }
}
fn create_new_thread(
@@ -4158,6 +4170,34 @@ impl Sidebar {
Self::truncate_draft_label(&raw)
}
+ fn selected_group_key(&self) -> Option<ProjectGroupKey> {
+ let ix = self.selection?;
+ match self.contents.entries.get(ix) {
+ Some(ListEntry::ProjectHeader { key, .. }) => Some(key.clone()),
+ Some(ListEntry::Thread(_) | ListEntry::ViewMore { .. }) => {
+ (0..ix)
+ .rev()
+ .find_map(|i| match self.contents.entries.get(i) {
+ Some(ListEntry::ProjectHeader { key, .. }) => Some(key.clone()),
+ _ => None,
+ })
+ }
+ _ => None,
+ }
+ }
+
+ fn workspace_for_group(&self, key: &ProjectGroupKey, cx: &App) -> Option<Entity<Workspace>> {
+ let mw = self.multi_workspace.upgrade()?;
+ let mw = mw.read(cx);
+ let active = mw.workspace().clone();
+ let active_key = active.read(cx).project_group_key(cx);
+ if active_key == *key {
+ Some(active)
+ } else {
+ mw.workspace_for_paths(key.path_list(), key.host().as_ref(), cx)
+ }
+ }
+
fn active_project_group_key(&self, cx: &App) -> Option<ProjectGroupKey> {
let multi_workspace = self.multi_workspace.upgrade()?;
let multi_workspace = multi_workspace.read(cx);
@@ -4364,18 +4404,6 @@ impl Sidebar {
self.collapse_thread_group(&active_key, cx);
}
- fn on_new_thread(
- &mut self,
- _: &workspace::NewThread,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some(workspace) = self.active_workspace(cx) else {
- return;
- };
- self.create_new_thread(&workspace, window, cx);
- }
-
fn render_no_results(&self, cx: &mut Context<Self>) -> impl IntoElement {
let has_query = self.has_filter_query(cx);
let message = if has_query {
@@ -4922,7 +4950,6 @@ impl Render for Sidebar {
.on_action(cx.listener(Self::on_previous_thread))
.on_action(cx.listener(Self::on_show_more_threads))
.on_action(cx.listener(Self::on_show_fewer_threads))
- .on_action(cx.listener(Self::on_new_thread))
.on_action(cx.listener(|this, _: &OpenRecent, window, cx| {
this.recent_projects_popover_handle.toggle(window, cx);
}))