diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index fcd9665c52451d62fe8185abca919148a1666126..b29c4d09d6b7b18bab2ca8a295471f9d057974da 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -567,19 +567,12 @@ impl ThreadMetadataStore { PathList::new(&paths) }; - let main_worktree_paths = { - let project = thread_ref.project().read(cx); - let mut main_paths: Vec> = Vec::new(); - for repo in project.repositories(cx).values() { - let snapshot = repo.read(cx).snapshot(); - if snapshot.is_linked_worktree() { - main_paths.push(snapshot.original_repo_abs_path.clone()); - } - } - main_paths.sort(); - main_paths.dedup(); - PathList::new(&main_paths) - }; + let main_worktree_paths = thread_ref + .project() + .read(cx) + .project_group_key(cx) + .path_list() + .clone(); // Threads without a folder path (e.g. started in an empty // window) are archived by default so they don't get lost, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index d6589361cd9417c2ac6d9025af92f1e096b341b1..9fd829675dca1e8a1d7bc4301407a3336603e440 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -163,6 +163,7 @@ struct WorktreeInfo { name: SharedString, full_path: SharedString, highlight_positions: Vec, + kind: ui::WorktreeKind, } #[derive(Clone)] @@ -307,23 +308,25 @@ fn workspace_path_list(workspace: &Entity, cx: &App) -> PathList { /// Derives worktree display info from a thread's stored path list. /// -/// For each path in the thread's `folder_paths` that is not one of the -/// group's main paths (i.e. it's a git linked worktree), produces a -/// [`WorktreeInfo`] with the short worktree name and full path. +/// For each path in the thread's `folder_paths`, produces a +/// [`WorktreeInfo`] with a short display name, full path, and whether +/// the worktree is the main checkout or a linked git worktree. fn worktree_info_from_thread_paths( folder_paths: &PathList, group_key: &project::ProjectGroupKey, -) -> Vec { +) -> impl Iterator { let main_paths = group_key.path_list().paths(); - folder_paths - .paths() - .iter() - .filter_map(|path| { - if main_paths.iter().any(|mp| mp.as_path() == path.as_path()) { - return None; - } - // Find the main path whose file name matches this linked - // worktree's file name, falling back to the first main path. + folder_paths.paths().iter().filter_map(|path| { + let is_main = main_paths.iter().any(|mp| mp.as_path() == path.as_path()); + if is_main { + let name = path.file_name()?.to_string_lossy().to_string(); + Some(WorktreeInfo { + name: SharedString::from(name), + full_path: SharedString::from(path.display().to_string()), + highlight_positions: Vec::new(), + kind: ui::WorktreeKind::Main, + }) + } else { let main_path = main_paths .iter() .find(|mp| mp.file_name() == path.file_name()) @@ -332,9 +335,10 @@ fn worktree_info_from_thread_paths( name: linked_worktree_short_name(main_path, path).unwrap_or_default(), full_path: SharedString::from(path.display().to_string()), highlight_positions: Vec::new(), + kind: ui::WorktreeKind::Linked, }) - }) - .collect() + } + }) } /// The sidebar re-derives its entire entry list from scratch on every @@ -851,7 +855,8 @@ impl Sidebar { workspace: ThreadEntryWorkspace| -> ThreadEntry { let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id); - let worktrees = worktree_info_from_thread_paths(&row.folder_paths, &group_key); + let worktrees: Vec = + worktree_info_from_thread_paths(&row.folder_paths, &group_key).collect(); ThreadEntry { metadata: row, icon, @@ -1059,7 +1064,9 @@ impl Sidebar { if let Some(ActiveEntry::Draft(draft_ws)) = &self.active_entry { let ws_path_list = workspace_path_list(draft_ws, cx); let worktrees = worktree_info_from_thread_paths(&ws_path_list, &group_key); - entries.push(ListEntry::DraftThread { worktrees }); + entries.push(ListEntry::DraftThread { + worktrees: worktrees.collect(), + }); } } @@ -1073,7 +1080,8 @@ impl Sidebar { && active_workspace.as_ref().is_some_and(|active_ws| { let ws_path_list = workspace_path_list(active_ws, cx); let has_linked_worktrees = - !worktree_info_from_thread_paths(&ws_path_list, &group_key).is_empty(); + worktree_info_from_thread_paths(&ws_path_list, &group_key) + .any(|wt| wt.kind == ui::WorktreeKind::Linked); if !has_linked_worktrees { return false; } @@ -1102,6 +1110,7 @@ impl Sidebar { &workspace_path_list(ws, cx), &group_key, ) + .collect() }) .unwrap_or_default() } else { @@ -2545,6 +2554,7 @@ impl Sidebar { name: wt.name.clone(), full_path: wt.full_path.clone(), highlight_positions: Vec::new(), + kind: wt.kind, }) .collect(), diff_stats: thread.diff_stats, @@ -2817,6 +2827,7 @@ impl Sidebar { name: wt.name.clone(), full_path: wt.full_path.clone(), highlight_positions: wt.highlight_positions.clone(), + kind: wt.kind, }) .collect(), ) @@ -3095,6 +3106,7 @@ impl Sidebar { name: wt.name.clone(), full_path: wt.full_path.clone(), highlight_positions: wt.highlight_positions.clone(), + kind: wt.kind, }) .collect(), ) @@ -3132,6 +3144,7 @@ impl Sidebar { name: wt.name.clone(), full_path: wt.full_path.clone(), highlight_positions: wt.highlight_positions.clone(), + kind: wt.kind, }) .collect(), ) diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 09fd44af35679a69908e1d86d203ea8c3aa5c545..e1462f5307546a50fda9fb55819b3570e5106365 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -191,6 +191,25 @@ fn focus_sidebar(sidebar: &Entity, cx: &mut gpui::VisualTestContext) { cx.run_until_parked(); } +fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String { + let mut seen = Vec::new(); + let mut chips = Vec::new(); + for wt in worktrees { + if wt.kind == ui::WorktreeKind::Main { + continue; + } + if !seen.contains(&wt.name) { + seen.push(wt.name.clone()); + chips.push(format!("{{{}}}", wt.name)); + } + } + if chips.is_empty() { + String::new() + } else { + format!(" {}", chips.join(", ")) + } +} + fn visible_entries_as_strings( sidebar: &Entity, cx: &mut gpui::VisualTestContext, @@ -238,23 +257,8 @@ fn visible_entries_as_strings( } else { "" }; - let worktree = if thread.worktrees.is_empty() { - String::new() - } else { - let mut seen = Vec::new(); - let mut chips = Vec::new(); - for wt in &thread.worktrees { - if !seen.contains(&wt.name) { - seen.push(wt.name.clone()); - chips.push(format!("{{{}}}", wt.name)); - } - } - format!(" {}", chips.join(", ")) - }; - format!( - " {}{}{}{}{}{}", - title, worktree, active, status_str, notified, selected - ) + let worktree = format_linked_worktree_chips(&thread.worktrees); + format!(" {title}{worktree}{active}{status_str}{notified}{selected}") } ListEntry::ViewMore { is_fully_expanded, .. @@ -266,35 +270,11 @@ fn visible_entries_as_strings( } } ListEntry::DraftThread { worktrees, .. } => { - let worktree = if worktrees.is_empty() { - String::new() - } else { - let mut seen = Vec::new(); - let mut chips = Vec::new(); - for wt in worktrees { - if !seen.contains(&wt.name) { - seen.push(wt.name.clone()); - chips.push(format!("{{{}}}", wt.name)); - } - } - format!(" {}", chips.join(", ")) - }; + let worktree = format_linked_worktree_chips(worktrees); format!(" [~ Draft{}]{}", worktree, selected) } ListEntry::NewThread { worktrees, .. } => { - let worktree = if worktrees.is_empty() { - String::new() - } else { - let mut seen = Vec::new(); - let mut chips = Vec::new(); - for wt in worktrees { - if !seen.contains(&wt.name) { - seen.push(wt.name.clone()); - chips.push(format!("{{{}}}", wt.name)); - } - } - format!(" {}", chips.join(", ")) - }; + let worktree = format_linked_worktree_chips(worktrees); format!(" [+ New Thread{}]{}", worktree, selected) } } diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 7658946b6395d6314d90db52716020a922c85ccc..34aa6b4869d44aa4835f4f1d2ed2557f4dd138b4 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -16,11 +16,19 @@ pub enum AgentThreadStatus { Error, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WorktreeKind { + #[default] + Main, + Linked, +} + #[derive(Clone)] pub struct ThreadItemWorktreeInfo { pub name: SharedString, pub full_path: SharedString, pub highlight_positions: Vec, + pub kind: WorktreeKind, } #[derive(IntoElement, RegisterComponent)] @@ -359,7 +367,10 @@ impl RenderOnce for ThreadItem { let has_project_name = self.project_name.is_some(); let has_project_paths = project_paths.is_some(); - let has_worktree = !self.worktrees.is_empty(); + let has_worktree = self + .worktrees + .iter() + .any(|wt| wt.kind == WorktreeKind::Linked); let has_timestamp = !self.timestamp.is_empty(); let timestamp = self.timestamp; @@ -449,6 +460,10 @@ impl RenderOnce for ThreadItem { continue; } + if wt.kind == WorktreeKind::Main { + continue; + } + let chip_index = seen_names.len(); seen_names.push(wt.name.clone()); @@ -624,6 +639,7 @@ impl Component for ThreadItem { name: "link-agent-panel".into(), full_path: "link-agent-panel".into(), highlight_positions: Vec::new(), + kind: WorktreeKind::Linked, }]), ) .into_any_element(), @@ -650,6 +666,7 @@ impl Component for ThreadItem { name: "my-project".into(), full_path: "my-project".into(), highlight_positions: Vec::new(), + kind: WorktreeKind::Linked, }]) .added(42) .removed(17) @@ -729,6 +746,7 @@ impl Component for ThreadItem { name: "my-project-name".into(), full_path: "my-project-name".into(), highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11], + kind: WorktreeKind::Linked, }]), ) .into_any_element(),