Detailed changes
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.1" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 7 2)" fill="#C6CAD0"/>
+<path d="M7 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.8" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 7 2)" fill="#C6CAD0"/>
+<path d="M7 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.1" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 14 2)" fill="#C6CAD0"/>
+<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.8" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 14 2)" fill="#C6CAD0"/>
+<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>
@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.2" width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
-<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
-</svg>
@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
-<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
-</svg>
@@ -483,9 +483,17 @@ pub fn init(cx: &mut App) {
}
if let Some(panel) = workspace.panel::<AgentPanel>(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<Self>) -> Option<AnyElement> {
+ fn render_sidebar_toggle(&self, docked_right: bool, cx: &Context<Self>) -> Option<AnyElement> {
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()
@@ -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<Self>,
) -> 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<Self>) {
@@ -1156,6 +1163,7 @@ impl Sidebar {
ix: usize,
thread: &ThreadEntry,
is_selected: bool,
+ docked_right: bool,
cx: &mut Context<Self>,
) -> 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<Self>,
+ ) -> 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()
@@ -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,
@@ -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<usize>,
removed: Option<usize>,
worktree: Option<SharedString>,
@@ -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<SharedString>) -> 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(),
@@ -48,6 +48,7 @@ pub struct ListItem {
rounded: bool,
overflow_x: bool,
focused: Option<bool>,
+ 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()