From 877e8d69e7c7d808d7cf8d05538f74e6b0c75ab9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 Mar 2026 20:33:12 -0700 Subject: [PATCH] Start work on using the multiworkspace's project groups in the sidebar --- crates/sidebar/src/sidebar.rs | 118 +++++++++++------------- crates/workspace/src/multi_workspace.rs | 10 +- crates/workspace/src/workspace.rs | 4 +- 3 files changed, 64 insertions(+), 68 deletions(-) diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 9e75b17a45dafe594cf37a5c2a9a56878892638a..b3cc2f42d9237482fdbbcb9d96e5e82c403f8b80 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -54,8 +54,6 @@ use zed_actions::agents_sidebar::{FocusSidebarFilter, ToggleThreadSwitcher}; use crate::thread_switcher::{ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent}; -use crate::project_group_builder::ProjectGroupBuilder; - mod project_group_builder; #[cfg(test)] @@ -326,7 +324,7 @@ fn workspace_path_list(workspace: &Entity, cx: &App) -> PathList { /// with the short worktree name and full path. fn worktree_info_from_thread_paths( folder_paths: &PathList, - project_groups: &ProjectGroupBuilder, + project_groups: &[workspace::ProjectGroup], ) -> Vec { folder_paths .paths() @@ -696,54 +694,59 @@ impl Sidebar { let Some(multi_workspace) = self.multi_workspace.upgrade() else { return; }; - let mw = multi_workspace.read(cx); - let workspaces = mw.workspaces().collect::>(); - let active_workspace = Some(mw.active_workspace()); - let agent_server_store = workspaces - .first() - .map(|ws| ws.read(cx).project().read(cx).agent_server_store().clone()); + let mw = multi_workspace.read(cx); + let project_groups = mw.project_groups(); + let active_workspace = mw.active_workspace(); + let agent_server_store = active_workspace + .read(cx) + .project() + .read(cx) + .agent_server_store() + .clone(); let query = self.filter_editor.read(cx).text(cx); // Derive active_entry from the active workspace's agent panel. // Draft is checked first because a conversation can have a session_id // before any messages are sent. However, a thread that's still loading - // also appears as a "draft" (no messages yet). - if let Some(active_ws) = &active_workspace { - if let Some(panel) = active_ws.read(cx).panel::(cx) { - if panel.read(cx).active_thread_is_draft(cx) - || panel.read(cx).active_conversation_view().is_none() - { - let conversation_parent_id = panel - .read(cx) - .active_conversation_view() - .and_then(|cv| cv.read(cx).parent_id(cx)); - let preserving_thread = - if let Some(ActiveEntry::Thread { session_id, .. }) = &self.active_entry { - self.active_entry_workspace() == Some(active_ws) - && conversation_parent_id - .as_ref() - .is_some_and(|id| id == session_id) - } else { - false - }; - if !preserving_thread { - self.active_entry = Some(ActiveEntry::Draft(active_ws.clone())); - } - } else if let Some(session_id) = panel + // also appears as a "draft" (no messages yet), so when we already have + // an eager Thread write for this workspace we preserve it. A session_id + // on a non-draft is a positive Thread signal. The remaining case + // (conversation exists, not draft, no session_id) is a genuine + // mid-load — keep the previous value. + if let Some(panel) = active_workspace.read(cx).panel::(cx) { + if panel.read(cx).active_thread_is_draft(cx) + || panel.read(cx).active_conversation_view().is_none() + { + let conversation_parent_id = panel .read(cx) .active_conversation_view() - .and_then(|cv| cv.read(cx).parent_id(cx)) - { - self.active_entry = Some(ActiveEntry::Thread { - session_id, - workspace: active_ws.clone(), - }); + .and_then(|cv| cv.read(cx).parent_id(cx)); + let preserving_thread = + if let Some(ActiveEntry::Thread { session_id, .. }) = &self.active_entry { + self.active_entry_workspace() == Some(&active_workspace) + && conversation_parent_id + .as_ref() + .is_some_and(|id| id == session_id) + } else { + false + }; + if !preserving_thread { + self.active_entry = Some(ActiveEntry::Draft(active_workspace.clone())); } - // else: conversation exists, not a draft, but no session_id - // yet — thread is mid-load. Keep previous value. + } else if let Some(session_id) = panel + .read(cx) + .active_conversation_view() + .and_then(|cv| cv.read(cx).parent_id(cx)) + { + self.active_entry = Some(ActiveEntry::Thread { + session_id, + workspace: active_workspace.clone(), + }); } + // else: conversation exists, not a draft, but no session_id + // yet — thread is mid-load. Keep previous value. } let previous = mem::take(&mut self.contents); @@ -764,14 +767,9 @@ impl Sidebar { let mut current_session_ids: HashSet = HashSet::new(); let mut project_header_indices: Vec = Vec::new(); - // Use ProjectGroupBuilder to canonically group workspaces by their - // main git repository. This replaces the manual absorbed-workspace - // detection that was here before. - let project_groups = ProjectGroupBuilder::from_multiworkspace(mw, cx); - - let has_open_projects = workspaces + let has_open_projects = project_groups .iter() - .any(|ws| !workspace_path_list(ws, cx).paths().is_empty()); + .any(|group| !group.workspaces.is_empty() && !group.key.main_worktree_paths.is_empty()); let resolve_agent_icon = |agent_id: &AgentId| -> (IconName, Option) { let agent = Agent::from(agent_id.clone()); @@ -779,32 +777,26 @@ impl Sidebar { Agent::NativeAgent => IconName::ZedAgent, Agent::Custom { .. } => IconName::Terminal, }; - let icon_from_external_svg = agent_server_store - .as_ref() - .and_then(|store| store.read(cx).agent_icon(&agent_id)); + let icon_from_external_svg = agent_server_store.read(cx).agent_icon(&agent_id); (icon, icon_from_external_svg) }; - for (group_name, group) in project_groups.groups() { - let path_list = group_name.path_list().clone(); + for group in project_groups { + let path_list = group.key.main_worktree_paths.clone(); if path_list.paths().is_empty() { continue; } - let label = group_name.display_name(); + let label = group.key.display_name(); let is_collapsed = self.collapsed_groups.contains(&path_list); let should_load_threads = !is_collapsed || !query.is_empty(); - - let is_active = active_workspace - .as_ref() - .is_some_and(|active| group.workspaces.contains(active)); + let is_active = group.workspaces.contains(&active_workspace); // Pick a representative workspace for the group: prefer the active // workspace if it belongs to this group, otherwise use the main // repo workspace (not a linked worktree). - let representative_workspace = active_workspace - .as_ref() + let representative_workspace = Some(&active_workspace) .filter(|_| is_active) .unwrap_or_else(|| group.main_workspace(cx)); @@ -975,9 +967,9 @@ impl Sidebar { let session_id = &thread.metadata.session_id; let is_thread_workspace_active = match &thread.workspace { - ThreadEntryWorkspace::Open(thread_workspace) => active_workspace - .as_ref() - .is_some_and(|active| active == thread_workspace), + ThreadEntryWorkspace::Open(thread_workspace) => { + &active_workspace == thread_workspace + } ThreadEntryWorkspace::Closed(_) => false, }; @@ -1067,7 +1059,7 @@ impl Sidebar { } else { let is_draft_for_workspace = is_active && matches!(&self.active_entry, Some(ActiveEntry::Draft(_))) - && self.active_entry_workspace() == Some(representative_workspace); + && self.active_entry_workspace() == Some(&representative_workspace); project_header_indices.push(entries.len()); entries.push(ListEntry::ProjectHeader { diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 376e44bd1fabf32429d5c1d99727aab61b1a7c9c..91b51d255bd3a2f9a07d14eae1c8cad224723f6c 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -272,9 +272,9 @@ pub struct MultiWorkspace { /// Represents a group of workspaces with the same project key (main worktree paths and host). /// /// Invariant: a project group always has at least one workspace. -struct ProjectGroup { - key: ProjectGroupKey, - workspaces: Vec>, +pub struct ProjectGroup { + pub key: ProjectGroupKey, + pub workspaces: Vec>, } impl ProjectGroup { @@ -443,6 +443,10 @@ impl MultiWorkspace { .flat_map(|group| group.workspaces.iter().cloned()) } + pub fn project_groups(&self) -> &[ProjectGroup] { + &self.project_groups + } + pub fn open_sidebar(&mut self, cx: &mut Context) { self.sidebar_open = true; let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5b1cdc38642ff83411f91343545d5e43dc1db90a..937e2397b48acefabb63d4015b9d65eec4c4a540 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -31,8 +31,8 @@ pub use crate::notifications::NotificationFrame; pub use dock::Panel; pub use multi_workspace::{ CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, - MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, ProjectGroupKey, Sidebar, SidebarEvent, - SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, + MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, ProjectGroup, ProjectGroupKey, Sidebar, + SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu, }; pub use path_list::{PathList, SerializedPathList};