From d30652fa4be1f5d286df663aa464e2a4ee2a4541 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 23 Mar 2026 14:45:15 -0700 Subject: [PATCH] sidebar: Adjust the sidebar UI slightly (#52228) This PR: - Moves the "remove workspace" option into the context menu, so that it's not swapping places with the + button in some circumstances. This matches codexs behavior. Another solution to the problem could be having the add button remain even when the "new thread" option exists. - Adds a "go to workspace" button to the header ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- assets/icons/focus.svg | 7 ++ assets/icons/stop.svg | 2 +- crates/icons/src/icons.rs | 1 + crates/sidebar/src/sidebar.rs | 142 +++++++++++++++++++++------------- 4 files changed, 97 insertions(+), 55 deletions(-) create mode 100644 assets/icons/focus.svg diff --git a/assets/icons/focus.svg b/assets/icons/focus.svg new file mode 100644 index 0000000000000000000000000000000000000000..9003e437cee1afa43e87fa273c9510284bb5ae0b --- /dev/null +++ b/assets/icons/focus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg index cc2bbe9207acf5acd44ff13e93140099d222250b..5ca9cd29edf17981500482b81e47aa53a16e2713 100644 --- a/assets/icons/stop.svg +++ b/assets/icons/stop.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 1aefd7fde212ac068562c9a2fc6b612ca9b06330..321c7fcc6fa3382921df6ffff809c6652adc6bea 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -133,6 +133,7 @@ pub enum IconName { FileTree, Filter, Flame, + Focus, Folder, FolderOpen, FolderPlus, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index ff394ef30c4bb129cf0fabab4ac70c38c62f1350..ed9187ff0d10bc98be5e63f721d8031b9f38b489 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -55,7 +55,7 @@ gpui::actions!( ] ); -const DEFAULT_WIDTH: Pixels = px(320.0); +const DEFAULT_WIDTH: Pixels = px(300.0); const MIN_WIDTH: Pixels = px(200.0); const MAX_WIDTH: Pixels = px(800.0); const DEFAULT_THREADS_SHOWN: usize = 5; @@ -125,6 +125,7 @@ enum ListEntry { highlight_positions: Vec, has_running_threads: bool, waiting_thread_count: usize, + is_active: bool, }, Thread(ThreadEntry), ViewMore { @@ -729,6 +730,13 @@ impl Sidebar { let is_collapsed = self.collapsed_groups.contains(&path_list); let should_load_threads = !is_collapsed || !query.is_empty(); + let is_active = active_ws_index.is_some_and(|active_idx| { + active_idx == ws_index + || absorbed + .get(&active_idx) + .is_some_and(|(main_idx, _)| *main_idx == ws_index) + }); + let mut live_infos = Self::all_thread_infos_for_workspace(workspace, cx); let mut threads: Vec = Vec::new(); @@ -980,6 +988,7 @@ impl Sidebar { highlight_positions: workspace_highlight_positions, has_running_threads, waiting_thread_count, + is_active, }); for thread in matched_threads { @@ -991,12 +1000,7 @@ impl Sidebar { let is_draft_for_workspace = self.agent_panel_visible && self.active_thread_is_draft && self.focused_thread.is_none() - && active_ws_index.is_some_and(|active_idx| { - active_idx == ws_index - || absorbed - .get(&active_idx) - .is_some_and(|(main_idx, _)| *main_idx == ws_index) - }); + && is_active; let show_new_thread_entry = thread_count == 0 || is_draft_for_workspace; @@ -1008,6 +1012,7 @@ impl Sidebar { highlight_positions: Vec::new(), has_running_threads, waiting_thread_count, + is_active, }); if is_collapsed { @@ -1148,6 +1153,7 @@ impl Sidebar { highlight_positions, has_running_threads, waiting_thread_count, + is_active, } => self.render_project_header( ix, false, @@ -1157,6 +1163,7 @@ impl Sidebar { highlight_positions, *has_running_threads, *waiting_thread_count, + *is_active, is_selected, cx, ), @@ -1196,6 +1203,7 @@ impl Sidebar { highlight_positions: &[usize], has_running_threads: bool, waiting_thread_count: usize, + is_active: bool, is_selected: bool, cx: &mut Context, ) -> AnyElement { @@ -1219,16 +1227,12 @@ impl Sidebar { let workspace_for_remove = workspace.clone(); let workspace_for_menu = workspace.clone(); + let workspace_for_open = workspace.clone(); let path_list_for_toggle = path_list.clone(); let path_list_for_collapse = path_list.clone(); let view_more_expanded = self.expanded_groups.contains_key(path_list); - let multi_workspace = self.multi_workspace.upgrade(); - let workspace_count = multi_workspace - .as_ref() - .map_or(0, |mw| mw.read(cx).workspaces().len()); - let label = if highlight_positions.is_empty() { Label::new(label.clone()) .color(Color::Muted) @@ -1249,7 +1253,8 @@ impl Sidebar { .group(&group_name) .h(Tab::content_height(cx)) .w_full() - .px_1p5() + .pl_1p5() + .pr_1() .border_1() .map(|this| { if is_selected { @@ -1274,30 +1279,34 @@ impl Sidebar { ), ) .child(label) - .when(is_collapsed && has_running_threads, |this| { - this.child( - Icon::new(IconName::LoadCircle) - .size(IconSize::XSmall) - .color(Color::Muted) - .with_rotate_animation(2), - ) - }) - .when(is_collapsed && waiting_thread_count > 0, |this| { - let tooltip_text = if waiting_thread_count == 1 { - "1 thread is waiting for confirmation".to_string() - } else { - format!("{waiting_thread_count} threads are waiting for confirmation",) - }; - this.child( - div() - .id(format!("{id_prefix}waiting-indicator-{ix}")) - .child( - Icon::new(IconName::Warning) - .size(IconSize::XSmall) - .color(Color::Warning), + .when(is_collapsed, |this| { + this.when(has_running_threads, |this| { + this.child( + Icon::new(IconName::LoadCircle) + .size(IconSize::XSmall) + .color(Color::Muted) + .with_rotate_animation(2), + ) + }) + .when(waiting_thread_count > 0, |this| { + let tooltip_text = if waiting_thread_count == 1 { + "1 thread is waiting for confirmation".to_string() + } else { + format!( + "{waiting_thread_count} threads are waiting for confirmation", ) - .tooltip(Tooltip::text(tooltip_text)), - ) + }; + this.child( + div() + .id(format!("{id_prefix}waiting-indicator-{ix}")) + .child( + Icon::new(IconName::Warning) + .size(IconSize::XSmall) + .color(Color::Warning), + ) + .tooltip(Tooltip::text(tooltip_text)), + ) + }) }), ) .child({ @@ -1339,23 +1348,33 @@ impl Sidebar { })), ) }) - .when(workspace_count > 1, |this| { - let workspace_for_remove_btn = workspace_for_remove.clone(); + .when(!is_active, |this| { this.child( IconButton::new( SharedString::from(format!( - "{id_prefix}project-header-remove-{ix}", + "{id_prefix}project-header-open-workspace-{ix}", )), - IconName::Close, + IconName::Focus, ) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .tooltip(Tooltip::text("Remove Project")) - .on_click(cx.listener( + .tooltip(Tooltip::text("Activate Workspace")) + .on_click(cx.listener({ move |this, _, window, cx| { - this.remove_workspace(&workspace_for_remove_btn, window, cx); - }, - )), + this.focused_thread = None; + if let Some(multi_workspace) = this.multi_workspace.upgrade() { + multi_workspace.update(cx, |multi_workspace, cx| { + multi_workspace + .activate(workspace_for_open.clone(), cx); + }); + } + if AgentPanel::is_visible(&workspace_for_open, cx) { + workspace_for_open.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + }); + } + } + })), ) }) .when(show_new_thread_button, |this| { @@ -1387,11 +1406,6 @@ impl Sidebar { this.selection = None; this.toggle_collapse(&path_list_for_toggle, window, cx); })) - // TODO: Decide if we really want the header to be activating different workspaces - // .on_click(cx.listener(move |this, _, window, cx| { - // this.selection = None; - // this.activate_workspace(&workspace_for_activate, window, cx); - // })) .into_any_element() } @@ -1502,7 +1516,7 @@ impl Sidebar { let workspace_count = multi_workspace .upgrade() .map_or(0, |mw| mw.read(cx).workspaces().len()); - if workspace_count > 1 { + let menu = if workspace_count > 1 { let workspace_for_move = workspace.clone(); let multi_workspace_for_move = multi_workspace.clone(); menu.entry( @@ -1527,7 +1541,23 @@ impl Sidebar { ) } else { menu - } + }; + + let workspace_for_remove = workspace_for_remove.clone(); + let multi_workspace_for_remove = multi_workspace.clone(); + menu.separator() + .entry("Remove Project", None, move |window, cx| { + if let Some(mw) = multi_workspace_for_remove.upgrade() { + let ws = workspace_for_remove.clone(); + mw.update(cx, |multi_workspace, cx| { + if let Some(index) = + multi_workspace.workspaces().iter().position(|w| *w == ws) + { + multi_workspace.remove_workspace(index, window, cx); + } + }); + } + }) }); let this = this.clone(); @@ -1587,6 +1617,7 @@ impl Sidebar { highlight_positions, has_running_threads, waiting_thread_count, + is_active, } = self.contents.entries.get(header_idx)? else { return None; @@ -1604,6 +1635,7 @@ impl Sidebar { &highlight_positions, *has_running_threads, *waiting_thread_count, + *is_active, is_selected, cx, ); @@ -3114,7 +3146,6 @@ impl Render for Sidebar { .child( h_flex() .gap_1() - .child(self.render_recent_projects_button(cx)) .child( IconButton::new("archive", IconName::Archive) .icon_size(IconSize::Small) @@ -3129,7 +3160,8 @@ impl Render for Sidebar { .on_click(cx.listener(|this, _, window, cx| { this.toggle_archive(&ToggleArchive, window, cx); })), - ), + ) + .child(self.render_recent_projects_button(cx)), ), ) } @@ -3693,6 +3725,7 @@ mod tests { highlight_positions: Vec::new(), has_running_threads: false, waiting_thread_count: 0, + is_active: true, }, ListEntry::Thread(ThreadEntry { agent: Agent::NativeAgent, @@ -3826,6 +3859,7 @@ mod tests { highlight_positions: Vec::new(), has_running_threads: false, waiting_thread_count: 0, + is_active: false, }, ];