@@ -30,7 +30,8 @@ use std::rc::Rc;
use theme::ActiveTheme;
use ui::{
AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, HighlightedLabel, KeyBinding,
- PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, prelude::*,
+ PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, ThreadItemWorktreeInfo, TintColor, Tooltip,
+ WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use util::path_list::PathList;
@@ -102,6 +103,13 @@ enum ThreadEntryWorkspace {
Closed(PathList),
}
+#[derive(Clone)]
+struct WorktreeInfo {
+ name: SharedString,
+ full_path: SharedString,
+ highlight_positions: Vec<usize>,
+}
+
#[derive(Clone)]
struct ThreadEntry {
agent: Agent,
@@ -114,9 +122,7 @@ struct ThreadEntry {
is_background: bool,
is_title_generating: bool,
highlight_positions: Vec<usize>,
- worktree_name: Option<SharedString>,
- worktree_full_path: Option<SharedString>,
- worktree_highlight_positions: Vec<usize>,
+ worktrees: Vec<WorktreeInfo>,
diff_stats: DiffStats,
}
@@ -229,6 +235,33 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
PathList::new(&workspace.read(cx).root_paths(cx))
}
+/// Derives worktree display info from a thread's stored path list.
+///
+/// For each path in the thread's `folder_paths` that canonicalizes to a
+/// different path (i.e. it's a git worktree), produces a [`WorktreeInfo`]
+/// with the short worktree name and full path.
+fn worktree_info_from_thread_paths(
+ folder_paths: &PathList,
+ project_groups: &ProjectGroupBuilder,
+) -> Vec<WorktreeInfo> {
+ folder_paths
+ .paths()
+ .iter()
+ .filter_map(|path| {
+ let canonical = project_groups.canonicalize_path(path);
+ if canonical != path.as_path() {
+ Some(WorktreeInfo {
+ name: linked_worktree_short_name(canonical, path).unwrap_or_default(),
+ full_path: SharedString::from(path.display().to_string()),
+ highlight_positions: Vec::new(),
+ })
+ } else {
+ None
+ }
+ })
+ .collect()
+}
+
/// The sidebar re-derives its entire entry list from scratch on every
/// change via `update_entries` → `rebuild_contents`. Avoid adding
/// incremental or inter-event coordination state — if something can
@@ -693,39 +726,21 @@ impl Sidebar {
for workspace in &group.workspaces {
let ws_path_list = workspace_path_list(workspace, cx);
- // Determine if this workspace covers a git worktree (its
- // path canonicalizes to the main repo, not itself). If so,
- // threads from it get a worktree chip in the sidebar.
- let worktree_info: Option<(SharedString, SharedString)> =
- ws_path_list.paths().first().and_then(|path| {
- let canonical = project_groups.canonicalize_path(path);
- if canonical != path.as_path() {
- let name =
- linked_worktree_short_name(canonical, path).unwrap_or_default();
- let full_path: SharedString = path.display().to_string().into();
- Some((name, full_path))
- } else {
- None
- }
- });
-
- let workspace_threads: Vec<_> = thread_store
- .read(cx)
- .entries_for_path(&ws_path_list)
- .collect();
- for thread in workspace_threads {
- if !seen_session_ids.insert(thread.session_id.clone()) {
+ for row in thread_store.read(cx).entries_for_path(&ws_path_list) {
+ if !seen_session_ids.insert(row.session_id.clone()) {
continue;
}
- let (agent, icon, icon_from_external_svg) = resolve_agent(&thread);
+ let (agent, icon, icon_from_external_svg) = resolve_agent(&row);
+ let worktrees =
+ worktree_info_from_thread_paths(&row.folder_paths, &project_groups);
threads.push(ThreadEntry {
agent,
session_info: acp_thread::AgentSessionInfo {
- session_id: thread.session_id.clone(),
+ session_id: row.session_id.clone(),
work_dirs: None,
- title: Some(thread.title.clone()),
- updated_at: Some(thread.updated_at),
- created_at: thread.created_at,
+ title: Some(row.title.clone()),
+ updated_at: Some(row.updated_at),
+ created_at: row.created_at,
meta: None,
},
icon,
@@ -736,20 +751,15 @@ impl Sidebar {
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: worktree_info.as_ref().map(|(name, _)| name.clone()),
- worktree_full_path: worktree_info
- .as_ref()
- .map(|(_, path)| path.clone()),
- worktree_highlight_positions: Vec::new(),
+ worktrees,
diff_stats: DiffStats::default(),
});
}
}
- // Load threads from linked git worktrees that don't have an
- // open workspace in this group. Only include worktrees that
- // belong to this group (not shared with another group).
- let linked_worktree_path_lists = group
+ // Load threads from linked git worktrees whose
+ // canonical paths belong to this group.
+ let linked_worktree_queries = group
.workspaces
.iter()
.flat_map(|ws| root_repository_snapshots(ws, cx))
@@ -765,23 +775,14 @@ impl Sidebar {
.collect::<Vec<_>>()
});
- for worktree_path_list in linked_worktree_path_lists {
+ for worktree_path_list in linked_worktree_queries {
for row in thread_store.read(cx).entries_for_path(&worktree_path_list) {
if !seen_session_ids.insert(row.session_id.clone()) {
continue;
}
- let worktree_info = row.folder_paths.paths().first().and_then(|path| {
- let canonical = project_groups.canonicalize_path(path);
- if canonical != path.as_path() {
- let name =
- linked_worktree_short_name(canonical, path).unwrap_or_default();
- let full_path: SharedString = path.display().to_string().into();
- Some((name, full_path))
- } else {
- None
- }
- });
let (agent, icon, icon_from_external_svg) = resolve_agent(&row);
+ let worktrees =
+ worktree_info_from_thread_paths(&row.folder_paths, &project_groups);
threads.push(ThreadEntry {
agent,
session_info: acp_thread::AgentSessionInfo {
@@ -795,14 +796,12 @@ impl Sidebar {
icon,
icon_from_external_svg,
status: AgentThreadStatus::default(),
- workspace: ThreadEntryWorkspace::Closed(row.folder_paths.clone()),
+ workspace: ThreadEntryWorkspace::Closed(worktree_path_list.clone()),
is_live: false,
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: worktree_info.as_ref().map(|(name, _)| name.clone()),
- worktree_full_path: worktree_info.map(|(_, path)| path),
- worktree_highlight_positions: Vec::new(),
+ worktrees,
diff_stats: DiffStats::default(),
});
}
@@ -882,12 +881,13 @@ impl Sidebar {
if let Some(positions) = fuzzy_match_positions(&query, title) {
thread.highlight_positions = positions;
}
- if let Some(worktree_name) = &thread.worktree_name {
- if let Some(positions) = fuzzy_match_positions(&query, worktree_name) {
- thread.worktree_highlight_positions = positions;
+ let mut worktree_matched = false;
+ for worktree in &mut thread.worktrees {
+ if let Some(positions) = fuzzy_match_positions(&query, &worktree.name) {
+ worktree.highlight_positions = positions;
+ worktree_matched = true;
}
}
- let worktree_matched = !thread.worktree_highlight_positions.is_empty();
if workspace_matched
|| !thread.highlight_positions.is_empty()
|| worktree_matched
@@ -2437,14 +2437,17 @@ impl Sidebar {
.when_some(thread.icon_from_external_svg.clone(), |this, svg| {
this.custom_icon_from_external_svg(svg)
})
- .when_some(thread.worktree_name.clone(), |this, name| {
- let this = this.worktree(name);
- match thread.worktree_full_path.clone() {
- Some(path) => this.worktree_full_path(path),
- None => this,
- }
- })
- .worktree_highlight_positions(thread.worktree_highlight_positions.clone())
+ .worktrees(
+ thread
+ .worktrees
+ .iter()
+ .map(|wt| ThreadItemWorktreeInfo {
+ name: wt.name.clone(),
+ full_path: wt.full_path.clone(),
+ highlight_positions: wt.highlight_positions.clone(),
+ })
+ .collect(),
+ )
.when_some(timestamp, |this, ts| this.timestamp(ts))
.highlight_positions(thread.highlight_positions.to_vec())
.title_generating(thread.is_title_generating)
@@ -3400,11 +3403,19 @@ mod tests {
} else {
""
};
- let worktree = thread
- .worktree_name
- .as_ref()
- .map(|name| format!(" {{{}}}", name))
- .unwrap_or_default();
+ 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
@@ -3777,9 +3788,7 @@ mod tests {
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
// Active thread with Running status
@@ -3801,9 +3810,7 @@ mod tests {
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
// Active thread with Error status
@@ -3825,9 +3832,7 @@ mod tests {
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
// Thread with WaitingForConfirmation status, not active
@@ -3849,9 +3854,7 @@ mod tests {
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
// Background thread that completed (should show notification)
@@ -3873,9 +3876,7 @@ mod tests {
is_background: true,
is_title_generating: false,
highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
// View More entry
@@ -5817,6 +5818,227 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext) {
+ // A thread created in a workspace with roots from different git
+ // worktrees should show a chip for each distinct worktree name.
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ // Two main repos.
+ fs.insert_tree(
+ "/project_a",
+ serde_json::json!({
+ ".git": {
+ "worktrees": {
+ "olivetti": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/olivetti",
+ },
+ "selectric": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/selectric",
+ },
+ },
+ },
+ "src": {},
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/project_b",
+ serde_json::json!({
+ ".git": {
+ "worktrees": {
+ "olivetti": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/olivetti",
+ },
+ "selectric": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/selectric",
+ },
+ },
+ },
+ "src": {},
+ }),
+ )
+ .await;
+
+ // Worktree checkouts.
+ for (repo, branch) in &[
+ ("project_a", "olivetti"),
+ ("project_a", "selectric"),
+ ("project_b", "olivetti"),
+ ("project_b", "selectric"),
+ ] {
+ let worktree_path = format!("/worktrees/{repo}/{branch}/{repo}");
+ let gitdir = format!("gitdir: /{repo}/.git/worktrees/{branch}");
+ fs.insert_tree(
+ &worktree_path,
+ serde_json::json!({
+ ".git": gitdir,
+ "src": {},
+ }),
+ )
+ .await;
+ }
+
+ // Register linked worktrees.
+ for repo in &["project_a", "project_b"] {
+ let git_path = format!("/{repo}/.git");
+ fs.with_git_state(std::path::Path::new(&git_path), false, |state| {
+ for branch in &["olivetti", "selectric"] {
+ state.worktrees.push(git::repository::Worktree {
+ path: std::path::PathBuf::from(format!(
+ "/worktrees/{repo}/{branch}/{repo}"
+ )),
+ ref_name: Some(format!("refs/heads/{branch}").into()),
+ sha: "aaa".into(),
+ });
+ }
+ })
+ .unwrap();
+ }
+
+ cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+ // Open a workspace with the worktree checkout paths as roots
+ // (this is the workspace the thread was created in).
+ let project = project::Project::test(
+ fs.clone(),
+ [
+ "/worktrees/project_a/olivetti/project_a".as_ref(),
+ "/worktrees/project_b/selectric/project_b".as_ref(),
+ ],
+ cx,
+ )
+ .await;
+ project.update(cx, |p, cx| p.git_scans_complete(cx)).await;
+
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
+ // Save a thread under the same paths as the workspace roots.
+ let thread_paths = PathList::new(&[
+ std::path::PathBuf::from("/worktrees/project_a/olivetti/project_a"),
+ std::path::PathBuf::from("/worktrees/project_b/selectric/project_b"),
+ ]);
+ save_named_thread_metadata("wt-thread", "Cross Worktree Thread", &thread_paths, cx).await;
+
+ multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
+ cx.run_until_parked();
+
+ // Should show two distinct worktree chips.
+ assert_eq!(
+ visible_entries_as_strings(&sidebar, cx),
+ vec![
+ "v [project_a, project_b]",
+ " Cross Worktree Thread {olivetti}, {selectric}",
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext) {
+ // When a thread's roots span multiple repos but share the same
+ // worktree name (e.g. both in "olivetti"), only one chip should
+ // appear.
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ fs.insert_tree(
+ "/project_a",
+ serde_json::json!({
+ ".git": {
+ "worktrees": {
+ "olivetti": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/olivetti",
+ },
+ },
+ },
+ "src": {},
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/project_b",
+ serde_json::json!({
+ ".git": {
+ "worktrees": {
+ "olivetti": {
+ "commondir": "../../",
+ "HEAD": "ref: refs/heads/olivetti",
+ },
+ },
+ },
+ "src": {},
+ }),
+ )
+ .await;
+
+ for repo in &["project_a", "project_b"] {
+ let worktree_path = format!("/worktrees/{repo}/olivetti/{repo}");
+ let gitdir = format!("gitdir: /{repo}/.git/worktrees/olivetti");
+ fs.insert_tree(
+ &worktree_path,
+ serde_json::json!({
+ ".git": gitdir,
+ "src": {},
+ }),
+ )
+ .await;
+
+ let git_path = format!("/{repo}/.git");
+ fs.with_git_state(std::path::Path::new(&git_path), false, |state| {
+ state.worktrees.push(git::repository::Worktree {
+ path: std::path::PathBuf::from(format!("/worktrees/{repo}/olivetti/{repo}")),
+ ref_name: Some("refs/heads/olivetti".into()),
+ sha: "aaa".into(),
+ });
+ })
+ .unwrap();
+ }
+
+ cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+ let project = project::Project::test(
+ fs.clone(),
+ [
+ "/worktrees/project_a/olivetti/project_a".as_ref(),
+ "/worktrees/project_b/olivetti/project_b".as_ref(),
+ ],
+ cx,
+ )
+ .await;
+ project.update(cx, |p, cx| p.git_scans_complete(cx)).await;
+
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
+ // Thread with roots in both repos' "olivetti" worktrees.
+ let thread_paths = PathList::new(&[
+ std::path::PathBuf::from("/worktrees/project_a/olivetti/project_a"),
+ std::path::PathBuf::from("/worktrees/project_b/olivetti/project_b"),
+ ]);
+ save_named_thread_metadata("wt-thread", "Same Branch Thread", &thread_paths, cx).await;
+
+ multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
+ cx.run_until_parked();
+
+ // Both worktree paths have the name "olivetti", so only one chip.
+ assert_eq!(
+ visible_entries_as_strings(&sidebar, cx),
+ vec![
+ "v [project_a, project_b]",
+ " Same Branch Thread {olivetti}",
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAppContext) {
// When a worktree workspace is absorbed under the main repo, a
@@ -6251,7 +6473,7 @@ mod tests {
.as_ref()
.map(|title| title.as_ref())
== Some("WT Thread")
- && thread.worktree_name.as_ref().map(|name| name.as_ref())
+ && thread.worktrees.first().map(|wt| wt.name.as_ref())
== Some("wt-feature-a") =>
{
saw_expected_thread = true;
@@ -6264,9 +6486,9 @@ mod tests {
.map(|title| title.as_ref())
.unwrap_or("Untitled");
let worktree_name = thread
- .worktree_name
- .as_ref()
- .map(|name| name.as_ref())
+ .worktrees
+ .first()
+ .map(|wt| wt.name.as_ref())
.unwrap_or("<none>");
panic!(
"unexpected sidebar thread while opening linked worktree thread: title=`{title}`, worktree=`{worktree_name}`"
@@ -7070,6 +7292,7 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
+ // Two independent repos, each with their own git history.
fs.insert_tree(
"/project",
serde_json::json!({
@@ -7102,6 +7325,7 @@ mod tests {
)
.await;
+ // Register the linked worktree in the main repo.
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
@@ -7113,11 +7337,13 @@ mod tests {
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+ // Workspace 1: just /project.
let project_only = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
project_only
.update(cx, |p, cx| p.git_scans_complete(cx))
.await;
+ // Workspace 2: /other and /project together (multi-root).
let multi_root =
project::Project::test(fs.clone(), ["/other".as_ref(), "/project".as_ref()], cx).await;
multi_root
@@ -7132,12 +7358,15 @@ mod tests {
});
let sidebar = setup_sidebar(&multi_workspace, cx);
+ // Save a thread under the linked worktree path.
let wt_paths = PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]);
save_named_thread_metadata("wt-thread", "Worktree Thread", &wt_paths, cx).await;
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
cx.run_until_parked();
+ // The thread should appear only under [project] (the dedicated
+ // group for the /project repo), not under [other, project].
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
@@ -18,6 +18,13 @@ pub enum AgentThreadStatus {
Error,
}
+#[derive(Clone)]
+pub struct ThreadItemWorktreeInfo {
+ pub name: SharedString,
+ pub full_path: SharedString,
+ pub highlight_positions: Vec<usize>,
+}
+
#[derive(IntoElement, RegisterComponent)]
pub struct ThreadItem {
id: ElementId,
@@ -37,9 +44,7 @@ pub struct ThreadItem {
hovered: bool,
added: Option<usize>,
removed: Option<usize>,
- worktree: Option<SharedString>,
- worktree_full_path: Option<SharedString>,
- worktree_highlight_positions: Vec<usize>,
+ worktrees: Vec<ThreadItemWorktreeInfo>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
action_slot: Option<AnyElement>,
@@ -66,9 +71,7 @@ impl ThreadItem {
hovered: false,
added: None,
removed: None,
- worktree: None,
- worktree_full_path: None,
- worktree_highlight_positions: Vec::new(),
+ worktrees: Vec::new(),
on_click: None,
on_hover: Box::new(|_, _, _| {}),
action_slot: None,
@@ -146,18 +149,8 @@ impl ThreadItem {
self
}
- pub fn worktree(mut self, worktree: impl Into<SharedString>) -> Self {
- self.worktree = Some(worktree.into());
- self
- }
-
- pub fn worktree_full_path(mut self, worktree_full_path: impl Into<SharedString>) -> Self {
- self.worktree_full_path = Some(worktree_full_path.into());
- self
- }
-
- pub fn worktree_highlight_positions(mut self, positions: Vec<usize>) -> Self {
- self.worktree_highlight_positions = positions;
+ pub fn worktrees(mut self, worktrees: Vec<ThreadItemWorktreeInfo>) -> Self {
+ self.worktrees = worktrees;
self
}
@@ -319,7 +312,7 @@ impl RenderOnce for ThreadItem {
let added_count = self.added.unwrap_or(0);
let removed_count = self.removed.unwrap_or(0);
- let has_worktree = self.worktree.is_some();
+ let has_worktree = !self.worktrees.is_empty();
let has_timestamp = !self.timestamp.is_empty();
let timestamp = self.timestamp;
@@ -376,48 +369,67 @@ impl RenderOnce for ThreadItem {
}),
)
.when(has_worktree || has_diff_stats || has_timestamp, |this| {
- let worktree_full_path = self.worktree_full_path.clone().unwrap_or_default();
- let worktree_label = self.worktree.map(|worktree| {
- let positions = self.worktree_highlight_positions;
- if positions.is_empty() {
- Label::new(worktree)
+ // Collect all full paths for the shared tooltip.
+ let worktree_tooltip: SharedString = self
+ .worktrees
+ .iter()
+ .map(|wt| wt.full_path.as_ref())
+ .collect::<Vec<_>>()
+ .join("\n")
+ .into();
+ let worktree_tooltip_title = if self.worktrees.len() > 1 {
+ "Thread Running in Local Git Worktrees"
+ } else {
+ "Thread Running in a Local Git Worktree"
+ };
+
+ // Deduplicate chips by name — e.g. two paths both named
+ // "olivetti" produce a single chip. Highlight positions
+ // come from the first occurrence.
+ let mut seen_names: Vec<SharedString> = Vec::new();
+ let mut worktree_chips: Vec<AnyElement> = Vec::new();
+ for wt in self.worktrees {
+ if seen_names.contains(&wt.name) {
+ continue;
+ }
+ let chip_index = seen_names.len();
+ seen_names.push(wt.name.clone());
+ let label = if wt.highlight_positions.is_empty() {
+ Label::new(wt.name)
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
} else {
- HighlightedLabel::new(worktree, positions)
+ HighlightedLabel::new(wt.name, wt.highlight_positions)
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
- }
- });
+ };
+ let tooltip_title = worktree_tooltip_title;
+ let tooltip_meta = worktree_tooltip.clone();
+ worktree_chips.push(
+ h_flex()
+ .id(format!("{}-worktree-{chip_index}", self.id.clone()))
+ .gap_0p5()
+ .child(
+ Icon::new(IconName::GitWorktree)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(label)
+ .tooltip(move |_, cx| {
+ Tooltip::with_meta(tooltip_title, None, tooltip_meta.clone(), cx)
+ })
+ .into_any_element(),
+ );
+ }
this.child(
h_flex()
.min_w_0()
.gap_1p5()
.child(icon_container()) // Icon Spacing
- .when_some(worktree_label, |this, label| {
- this.child(
- h_flex()
- .id(format!("{}-worktree", self.id.clone()))
- .gap_0p5()
- .child(
- Icon::new(IconName::GitWorktree)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(label)
- .tooltip(move |_, cx| {
- Tooltip::with_meta(
- "Thread Running in a Local Git Worktree",
- None,
- worktree_full_path.clone(),
- cx,
- )
- }),
- )
- })
+ .children(worktree_chips)
.when(has_worktree && (has_diff_stats || has_timestamp), |this| {
this.child(dot_separator())
})
@@ -526,7 +538,11 @@ impl Component for ThreadItem {
ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock")
.icon(IconName::AiClaude)
.timestamp("2w")
- .worktree("link-agent-panel"),
+ .worktrees(vec![ThreadItemWorktreeInfo {
+ name: "link-agent-panel".into(),
+ full_path: "link-agent-panel".into(),
+ highlight_positions: Vec::new(),
+ }]),
)
.into_any_element(),
),
@@ -548,7 +564,11 @@ impl Component for ThreadItem {
.child(
ThreadItem::new("ti-5b", "Full metadata example")
.icon(IconName::AiClaude)
- .worktree("my-project")
+ .worktrees(vec![ThreadItemWorktreeInfo {
+ name: "my-project".into(),
+ full_path: "my-project".into(),
+ highlight_positions: Vec::new(),
+ }])
.added(42)
.removed(17)
.timestamp("3w"),
@@ -623,8 +643,11 @@ impl Component for ThreadItem {
ThreadItem::new("ti-11", "Search in worktree name")
.icon(IconName::AiClaude)
.timestamp("3mo")
- .worktree("my-project-name")
- .worktree_highlight_positions(vec![3, 4, 5, 6, 7, 8, 9, 10, 11]),
+ .worktrees(vec![ThreadItemWorktreeInfo {
+ name: "my-project-name".into(),
+ full_path: "my-project-name".into(),
+ highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }]),
)
.into_any_element(),
),