diff --git a/assets/icons/threads_sidebar_left_closed.svg b/assets/icons/threads_sidebar_left_closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..feb1015254635ef65f90f2c9ea38efab74d01d60 --- /dev/null +++ b/assets/icons/threads_sidebar_left_closed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/threads_sidebar_left_open.svg b/assets/icons/threads_sidebar_left_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..8057b060a84d7d7ffcf29aff1c0c79a8764edc22 --- /dev/null +++ b/assets/icons/threads_sidebar_left_open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/threads_sidebar_right_closed.svg b/assets/icons/threads_sidebar_right_closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..10fa4b792fd65b5875dcf2cadab1fc12a123ab47 --- /dev/null +++ b/assets/icons/threads_sidebar_right_closed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/threads_sidebar_right_open.svg b/assets/icons/threads_sidebar_right_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..23a01eb3f82a5866157220172c868ed9ded46033 --- /dev/null +++ b/assets/icons/threads_sidebar_right_open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/workspace_nav_closed.svg b/assets/icons/workspace_nav_closed.svg deleted file mode 100644 index ed1fce52d6826a4d10299f331358ff84e4caa973..0000000000000000000000000000000000000000 --- a/assets/icons/workspace_nav_closed.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/workspace_nav_open.svg b/assets/icons/workspace_nav_open.svg deleted file mode 100644 index 464b6aac73c2aeaa9463a805aabc4559377bbfd3..0000000000000000000000000000000000000000 --- a/assets/icons/workspace_nav_open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 1537c05096ec81f1b3f354cac236bfdda52c9f6f..50346bd752cec4432fb5a87e4df7cb4ce09aca83 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -483,9 +483,17 @@ pub fn init(cx: &mut App) { } if let Some(panel) = workspace.panel::(cx) { if let Some(sidebar) = panel.read(cx).sidebar.clone() { + let was_open = sidebar.read(cx).is_open(); sidebar.update(cx, |sidebar, cx| { sidebar.toggle(window, cx); }); + // When closing the sidebar, restore focus to the active pane + // to avoid "zombie focus" on the now-hidden sidebar elements + if was_open { + let active_pane = workspace.active_pane().clone(); + let pane_focus = active_pane.read(cx).focus_handle(cx); + window.focus(&pane_focus, cx); + } } } }) @@ -3623,7 +3631,7 @@ impl AgentPanel { Some((view, width, is_open)) } - fn render_sidebar_toggle(&self, cx: &Context) -> Option { + fn render_sidebar_toggle(&self, docked_right: bool, cx: &Context) -> Option { if !multi_workspace_enabled(cx) { return None; } @@ -3634,20 +3642,41 @@ impl AgentPanel { } let has_notifications = sidebar_read.has_notifications(cx); + let icon = if docked_right { + IconName::ThreadsSidebarRightClosed + } else { + IconName::ThreadsSidebarLeftClosed + }; + Some( - IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed) - .icon_size(IconSize::Small) - .when(has_notifications, |button| { - button - .indicator(Indicator::dot().color(Color::Accent)) - .indicator_border_color(Some(cx.theme().colors().tab_bar_background)) - }) - .tooltip(move |_, cx| { - Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + h_flex() + .h_full() + .px_1() + .map(|this| { + if docked_right { + this.border_l_1() + } else { + this.border_r_1() + } }) + .border_color(cx.theme().colors().border_variant) + .child( + IconButton::new("toggle-workspace-sidebar", icon) + .icon_size(IconSize::Small) + .when(has_notifications, |button| { + button + .indicator(Indicator::dot().color(Color::Accent)) + .indicator_border_color(Some( + cx.theme().colors().tab_bar_background, + )) + }) + .tooltip(move |_, cx| { + Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + }), + ) .into_any_element(), ) } @@ -4104,6 +4133,23 @@ impl AgentPanel { let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config; + let is_sidebar_open = self + .sidebar + .as_ref() + .map(|s| s.read(cx).is_open()) + .unwrap_or(false); + + let base_container = h_flex() + .id("agent-panel-toolbar") + .h(Tab::container_height(cx)) + .max_w_full() + .flex_none() + .justify_between() + .gap_2() + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border); + if use_v2_empty_toolbar { let (chevron_icon, icon_color, label_color) = if self.new_thread_menu_handle.is_deployed() { @@ -4162,34 +4208,26 @@ impl AgentPanel { y: px(1.0), }); - h_flex() - .id("agent-panel-toolbar") - .h(Tab::container_height(cx)) - .max_w_full() - .flex_none() - .justify_between() - .gap_2() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .border_color(cx.theme().colors().border) + base_container .child( h_flex() .size_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) + .gap_1() + .when(is_sidebar_open || docked_right, |this| this.pl_1()) .when(!docked_right, |this| { - this.children(self.render_sidebar_toggle(cx)) + this.children(self.render_sidebar_toggle(false, cx)) }) .child(agent_selector_menu) .child(self.render_start_thread_in_selector(cx)), ) .child( h_flex() + .h_full() .flex_none() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) - .when(show_history_menu, |this| { + .gap_1() + .pl_1() + .pr_1() + .when(show_history_menu && !has_v2_flag, |this| { this.child(self.render_recent_entries_menu( IconName::MenuAltTemp, Corner::TopRight, @@ -4198,7 +4236,7 @@ impl AgentPanel { }) .child(self.render_panel_options_menu(window, cx)) .when(docked_right, |this| { - this.children(self.render_sidebar_toggle(cx)) + this.children(self.render_sidebar_toggle(true, cx)) }), ) .into_any_element() @@ -4222,23 +4260,19 @@ impl AgentPanel { .with_handle(self.new_thread_menu_handle.clone()) .menu(move |window, cx| new_thread_menu_builder(window, cx)); - h_flex() - .id("agent-panel-toolbar") - .h(Tab::container_height(cx)) - .max_w_full() - .flex_none() - .justify_between() - .gap_2() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .border_color(cx.theme().colors().border) + base_container .child( h_flex() .size_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) + .map(|this| { + if is_sidebar_open || docked_right { + this.pl_1().gap_1() + } else { + this.pl_0().gap_0p5() + } + }) .when(!docked_right, |this| { - this.children(self.render_sidebar_toggle(cx)) + this.children(self.render_sidebar_toggle(false, cx)) }) .child(match &self.active_view { ActiveView::History { .. } | ActiveView::Configuration => { @@ -4250,12 +4284,13 @@ impl AgentPanel { ) .child( h_flex() + .h_full() .flex_none() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) + .gap_1() + .pl_1() + .pr_1() .child(new_thread_menu) - .when(show_history_menu, |this| { + .when(show_history_menu && !has_v2_flag, |this| { this.child(self.render_recent_entries_menu( IconName::MenuAltTemp, Corner::TopRight, @@ -4264,7 +4299,7 @@ impl AgentPanel { }) .child(self.render_panel_options_menu(window, cx)) .when(docked_right, |this| { - this.children(self.render_sidebar_toggle(cx)) + this.children(self.render_sidebar_toggle(true, cx)) }), ) .into_any_element() diff --git a/crates/agent_ui/src/sidebar.rs b/crates/agent_ui/src/sidebar.rs index ae3a4f0ccb9df6073ae24a9c482b6c56de0ea968..e36cb750b4a74dc8d749501eed07941cd30c7b6f 100644 --- a/crates/agent_ui/src/sidebar.rs +++ b/crates/agent_ui/src/sidebar.rs @@ -713,6 +713,8 @@ impl Sidebar { let is_group_header_after_first = ix > 0 && matches!(entry, ListEntry::ProjectHeader { .. }); + let docked_right = AgentSettings::get_global(cx).dock == settings::DockPosition::Right; + let rendered = match entry { ListEntry::ProjectHeader { path_list, @@ -728,9 +730,12 @@ impl Sidebar { highlight_positions, *has_threads, is_selected, + docked_right, cx, ), - ListEntry::Thread(thread) => self.render_thread(ix, thread, is_selected, cx), + ListEntry::Thread(thread) => { + self.render_thread(ix, thread, is_selected, docked_right, cx) + } ListEntry::ViewMore { path_list, remaining_count, @@ -770,6 +775,7 @@ impl Sidebar { highlight_positions: &[usize], has_threads: bool, is_selected: bool, + docked_right: bool, cx: &mut Context, ) -> AnyElement { let id = SharedString::from(format!("project-header-{}", ix)); @@ -815,12 +821,13 @@ impl Sidebar { .group_name(group_name) .toggle_state(is_active_workspace) .focused(is_selected) + .docked_right(docked_right) .child( h_flex() .relative() .min_w_0() .w_full() - .p_1() + .py_1() .gap_1p5() .child( Icon::new(disclosure_icon) @@ -969,7 +976,7 @@ impl Sidebar { } fn has_filter_query(&self, cx: &App) -> bool { - self.filter_editor.read(cx).buffer().read(cx).is_empty() + !self.filter_editor.read(cx).text(cx).is_empty() } fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { @@ -1156,6 +1163,7 @@ impl Sidebar { ix: usize, thread: &ThreadEntry, is_selected: bool, + docked_right: bool, cx: &mut Context, ) -> AnyElement { let has_notification = self @@ -1171,6 +1179,7 @@ impl Sidebar { let workspace = thread.workspace.clone(); let id = SharedString::from(format!("thread-entry-{}", ix)); + ThreadItem::new(id, title) .icon(thread.icon) .when_some(thread.icon_from_external_svg.clone(), |this, svg| { @@ -1187,6 +1196,7 @@ impl Sidebar { }) .selected(self.focused_thread.as_ref() == Some(&session_info.session_id)) .focused(is_selected) + .docked_right(docked_right) .on_click(cx.listener(move |this, _, window, cx| { this.selection = None; this.activate_thread(session_info.clone(), &workspace, window, cx); @@ -1301,6 +1311,7 @@ impl Sidebar { div() .w_full() .p_2() + .pt_1p5() .child( Button::new( SharedString::from(format!("new-thread-btn-{}", ix)), @@ -1320,6 +1331,40 @@ impl Sidebar { ) .into_any_element() } + + fn render_sidebar_toggle_button( + &self, + docked_right: bool, + cx: &mut Context, + ) -> impl IntoElement { + let icon = if docked_right { + IconName::ThreadsSidebarRightOpen + } else { + IconName::ThreadsSidebarLeftOpen + }; + + h_flex() + .h_full() + .px_1() + .map(|this| { + if docked_right { + this.pr_1p5().border_l_1() + } else { + this.border_r_1() + } + }) + .border_color(cx.theme().colors().border_variant) + .child( + IconButton::new("sidebar-close-toggle", icon) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action("Close Threads Sidebar", &ToggleWorkspaceSidebar, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + }), + ) + } } impl Sidebar { @@ -1416,37 +1461,19 @@ impl Render for Sidebar { .child({ let docked_right = AgentSettings::get_global(cx).dock == settings::DockPosition::Right; - let render_close_button = || { - IconButton::new("sidebar-close-toggle", IconName::WorkspaceNavOpen) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action( - "Close Threads Sidebar", - &ToggleWorkspaceSidebar, - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); - }) - }; h_flex() - .flex_none() - .px_2p5() .h(Tab::container_height(cx)) - .gap_2() + .flex_none() + .gap_1p5() .border_b_1() .border_color(cx.theme().colors().border) - .when(!docked_right, |this| this.child(render_close_button())) - .child( - Icon::new(IconName::MagnifyingGlass) - .size(IconSize::Small) - .color(Color::Muted), - ) + .when(!docked_right, |this| { + this.child(self.render_sidebar_toggle_button(false, cx)) + }) .child(self.render_filter_input(cx)) .when(has_query, |this| { - this.pr_1().child( + this.when(!docked_right, |this| this.pr_1p5()).child( IconButton::new("clear_filter", IconName::Close) .shape(IconButtonShape::Square) .tooltip(Tooltip::text("Clear Search")) @@ -1456,7 +1483,11 @@ impl Render for Sidebar { })), ) }) - .when(docked_right, |this| this.child(render_close_button())) + .when(docked_right, |this| { + this.pl_2() + .pr_0p5() + .child(self.render_sidebar_toggle_button(true, cx)) + }) }) .child( v_flex() diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 7c06eaef92ece60e8b4a9ad78976b68aee854226..94fed7f03f46e64ef0ac929e60cf6ae848145e72 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -244,6 +244,10 @@ pub enum IconName { ThinkingModeOff, Thread, ThreadFromSummary, + ThreadsSidebarLeftClosed, + ThreadsSidebarLeftOpen, + ThreadsSidebarRightClosed, + ThreadsSidebarRightOpen, ThumbsDown, ThumbsUp, TodoComplete, @@ -272,8 +276,6 @@ pub enum IconName { UserRoundPen, Warning, WholeWord, - WorkspaceNavClosed, - WorkspaceNavOpen, XCircle, XCircleFilled, ZedAgent, diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index edc685159f5c9edc5fa872e9d453d0b81fa9cb16..1ab516b0cbbcb20c98bf61525779d2bd760ef260 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -1,6 +1,6 @@ use crate::{ - DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration, IconDecorationKind, - SpinnerLabel, prelude::*, + CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration, + IconDecorationKind, prelude::*, }; use gpui::{AnyView, ClickEvent, Hsla, SharedString}; @@ -26,6 +26,7 @@ pub struct ThreadItem { selected: bool, focused: bool, hovered: bool, + docked_right: bool, added: Option, removed: Option, worktree: Option, @@ -50,6 +51,7 @@ impl ThreadItem { selected: false, focused: false, hovered: false, + docked_right: false, added: None, removed: None, worktree: None, @@ -107,6 +109,11 @@ impl ThreadItem { self } + pub fn docked_right(mut self, docked_right: bool) -> Self { + self.docked_right = docked_right; + self + } + pub fn worktree(mut self, worktree: impl Into) -> Self { self.worktree = Some(worktree.into()); self @@ -154,12 +161,12 @@ impl ThreadItem { impl RenderOnce for ThreadItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let color = cx.theme().colors(); - // let dot_separator = || { - // Label::new("•") - // .size(LabelSize::Small) - // .color(Color::Muted) - // .alpha(0.5) - // }; + let dot_separator = || { + Label::new("•") + .size(LabelSize::Small) + .color(Color::Muted) + .alpha(0.5) + }; let icon_container = || h_flex().size_4().flex_none().justify_center(); let agent_icon = if let Some(custom_svg) = self.custom_icon_from_external_svg { @@ -194,17 +201,23 @@ impl RenderOnce for ThreadItem { None }; - let icon = if let Some(decoration) = decoration { - icon_container().child(DecoratedIcon::new(agent_icon, Some(decoration))) - } else { - icon_container().child(agent_icon) - }; - let is_running = matches!( self.status, AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation ); - let running_or_action = is_running || (self.hovered && self.action_slot.is_some()); + + let icon = if is_running { + icon_container().child( + Icon::new(IconName::LoadCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_rotate_animation(2), + ) + } else if let Some(decoration) = decoration { + icon_container().child(DecoratedIcon::new(agent_icon, Some(decoration))) + } else { + icon_container().child(agent_icon) + }; let title = self.title; let highlight_positions = self.highlight_positions; @@ -244,13 +257,16 @@ impl RenderOnce for ThreadItem { if has_worktree || has_diff_stats { this.p_2() } else { - this.px_2().py_1() + this.p_1() } }) .when(self.selected, |s| s.bg(color.element_active)) .border_1() .border_color(gpui::transparent_black()) - .when(self.focused, |s| s.border_color(color.panel_focused_border)) + .when(self.focused, |s| { + s.when(self.docked_right, |s| s.border_r_2()) + .border_color(color.border_focused) + }) .hover(|s| s.bg(color.element_hover)) .on_hover(self.on_hover) .child( @@ -270,20 +286,8 @@ impl RenderOnce for ThreadItem { .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)), ) .child(gradient_overlay) - .when(running_or_action, |this| { - this.child( - h_flex() - .gap_1() - .when(is_running, |this| { - this.child( - icon_container() - .child(SpinnerLabel::new().color(Color::Accent)), - ) - }) - .when(self.hovered, |this| { - this.when_some(self.action_slot, |this, slot| this.child(slot)) - }), - ) + .when(self.hovered, |this| { + this.when_some(self.action_slot, |this, slot| this.child(slot)) }), ) .when_some(self.worktree, |this, worktree| { @@ -306,6 +310,7 @@ impl RenderOnce for ThreadItem { .gap_1p5() .child(icon_container()) // Icon Spacing .child(worktree_label) + .child(dot_separator()) .when(has_diff_stats, |this| { this.child(DiffStat::new( diff_stat_id.clone(), diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 01e88e1fe666fa2038b05af055a0e02b195e9bac..d707df82f4d19b0a3f519e9d6ac9ccdb22965e27 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -48,6 +48,7 @@ pub struct ListItem { rounded: bool, overflow_x: bool, focused: Option, + docked_right: bool, } impl ListItem { @@ -78,6 +79,7 @@ impl ListItem { rounded: false, overflow_x: false, focused: None, + docked_right: false, } } @@ -194,6 +196,11 @@ impl ListItem { self.focused = Some(focused); self } + + pub fn docked_right(mut self, docked_right: bool) -> Self { + self.docked_right = docked_right; + self + } } impl Disableable for ListItem { @@ -247,6 +254,7 @@ impl RenderOnce for ListItem { this.when_some(self.focused, |this, focused| { if focused { this.border_1() + .when(self.docked_right, |this| this.border_r_2()) .border_color(cx.theme().colors().border_focused) } else { this.border_1()