sidebar: Show worktree labels for agent terminals (#56412)

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: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
Ben BrandtandBennet Bo Fenner authored and GitHub committed 2026-05-11 13:18:58 +00:00
1 parent 552a5c0ae7
commit f48d72686b
3 files changed
+122 -18

No files matched your search

+52 -16
View File
@@ -237,6 +237,7 @@ struct TerminalEntry {
id: TerminalId,
title: SharedString,
workspace: Entity<Workspace>,
worktrees: Vec<ThreadItemWorktreeInfo>,
created_at: DateTime<Utc>,
has_notification: bool,
highlight_positions: Vec<usize>,
@@ -543,6 +544,16 @@ fn apply_worktree_label_mode(
worktrees
}
fn terminal_worktree_info<S: std::hash::BuildHasher>(
workspace: &Entity<Workspace>,
branch_by_path: &HashMap<PathBuf, SharedString, S>,
cx: &App,
) -> Vec<ThreadItemWorktreeInfo> {
let project = workspace.read(cx).project().clone();
let worktree_paths = project.read(cx).worktree_paths(cx);
worktree_info_from_thread_paths(&worktree_paths, branch_by_path)
}
/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
/// an SSH connection. Suitable for passing to
/// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`
@@ -1196,7 +1207,9 @@ impl Sidebar {
let group_workspaces = &group.workspaces;
let terminals: Vec<TerminalEntry> = group_workspaces
.iter()
.flat_map(|workspace| terminal_entries_for_workspace(workspace, cx))
.flat_map(|workspace| {
terminal_entries_for_workspace(workspace, &branch_by_path, cx)
})
.collect();
current_terminal_ids.extend(terminals.iter().map(|terminal| terminal.id));
notified_terminals.extend(
@@ -1483,7 +1496,17 @@ impl Sidebar {
terminal.highlight_positions = positions;
terminal_matched = true;
}
if workspace_matched || terminal_matched {
let mut worktree_matched = false;
for worktree in &mut terminal.worktrees {
let Some(name) = worktree.worktree_name.as_ref() else {
continue;
};
if let Some(positions) = fuzzy_match_positions(&query, name) {
worktree.highlight_positions = positions;
worktree_matched = true;
}
}
if workspace_matched || terminal_matched || worktree_matched {
matched_terminals.push(terminal);
}
}
@@ -4036,6 +4059,15 @@ impl Sidebar {
title: terminal.title.clone(),
workspace: terminal.workspace.clone(),
project_name: current_header_label.clone(),
worktrees: terminal
.worktrees
.iter()
.cloned()
.map(|mut wt| {
wt.highlight_positions = Vec::new();
wt
})
.collect(),
created_at: terminal.created_at,
notified: self.contents.is_terminal_notified(terminal.id),
timestamp,
@@ -4459,10 +4491,15 @@ impl Sidebar {
let terminal_id = terminal.id;
let workspace = terminal.workspace.clone();
let focus_handle = self.focus_handle.clone();
let worktrees = apply_worktree_label_mode(
terminal.worktrees.clone(),
cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
);
ThreadItem::new(id, terminal.title.clone())
.base_bg(sidebar_bg)
.icon(IconName::Terminal)
.worktrees(worktrees)
.timestamp(timestamp)
.notified(terminal.has_notification)
.highlight_positions(terminal.highlight_positions.clone())
@@ -5579,8 +5616,9 @@ impl Render for Sidebar {
}
}
fn terminal_entries_for_workspace(
fn terminal_entries_for_workspace<S: std::hash::BuildHasher>(
workspace: &Entity<Workspace>,
branch_by_path: &HashMap<PathBuf, SharedString, S>,
cx: &App,
) -> impl Iterator<Item = TerminalEntry> {
if !cx.has_flag::<AgentPanelTerminalFeatureFlag>() {
@@ -5589,19 +5627,17 @@ fn terminal_entries_for_workspace(
let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return None.into_iter().flatten();
};
let terminals =
agent_panel
.read(cx)
.terminals(cx)
.into_iter()
.map(|terminal: AgentPanelTerminalInfo| TerminalEntry {
id: terminal.id,
title: terminal.title,
workspace: workspace.clone(),
created_at: terminal.created_at,
has_notification: terminal.has_notification,
highlight_positions: Vec::new(),
});
let terminals = agent_panel.read(cx).terminals(cx).into_iter().map(
move |terminal: AgentPanelTerminalInfo| TerminalEntry {
id: terminal.id,
title: terminal.title,
workspace: workspace.clone(),
worktrees: terminal_worktree_info(workspace, branch_by_path, cx),
created_at: terminal.created_at,
has_notification: terminal.has_notification,
highlight_positions: Vec::new(),
},
);
Some(terminals).into_iter().flatten()
}
+68 -1
View File
@@ -530,7 +530,8 @@ fn visible_entries_as_strings(
}
ListEntry::Terminal(terminal) => {
let title = &terminal.title;
format!(" {title}{selected}")
let worktree = format_linked_worktree_chips(&terminal.worktrees);
format!(" {title}{worktree}{selected}")
}
}
})
@@ -1469,6 +1470,72 @@ async fn test_agent_panel_terminals_appear_in_sidebar_and_search(cx: &mut TestAp
);
}
#[gpui::test]
async fn test_agent_panel_terminal_shows_project_and_linked_worktree(cx: &mut TestAppContext) {
agent_ui::test_support::init_test(cx);
cx.update(|cx| {
cx.set_global(agent_ui::MaxIdleRetainedThreads(1));
ThreadStore::init_global(cx);
ThreadMetadataStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
prompt_store::init(cx);
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/project", serde_json::json!({ ".git": {}, "src": {} }))
.await;
fs.add_linked_worktree_for_repo(
Path::new("/project/.git"),
false,
git::repository::Worktree {
path: PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
is_main: false,
is_bare: false,
},
)
.await;
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
let worktree_project = project::Project::test(fs.clone(), ["/wt-feature-a".as_ref()], cx).await;
main_project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;
worktree_project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;
enable_agent_panel_terminal(cx);
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
let sidebar = setup_sidebar(&multi_workspace, cx);
let worktree_workspace = multi_workspace.update_in(cx, |multi_workspace, window, cx| {
multi_workspace.test_add_workspace(worktree_project.clone(), window, cx)
});
let panel = add_agent_panel(&worktree_workspace, cx);
panel
.update_in(cx, |panel, window, cx| {
panel.insert_test_terminal("Dev Server", true, window, cx)
})
.expect("test terminal should be inserted");
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec!["v [project]", " Dev Server {wt-feature-a}"]
);
type_in_search(&sidebar, "wt-feature-a", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec!["v [project]", " Dev Server {wt-feature-a} <== selected"]
);
}
#[gpui::test]
async fn test_agent_panel_terminal_notifications_update_sidebar(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
+2 -1
View File
@@ -32,6 +32,7 @@ pub(crate) struct ThreadSwitcherTerminalEntry {
pub title: SharedString,
pub workspace: Entity<Workspace>,
pub project_name: Option<SharedString>,
pub worktrees: Vec<ThreadItemWorktreeInfo>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub notified: bool,
pub timestamp: SharedString,
@@ -118,7 +119,7 @@ impl ThreadSwitcherEntry {
fn worktrees(&self) -> Vec<ThreadItemWorktreeInfo> {
match self {
Self::Thread(entry) => entry.worktrees.clone(),
Self::Terminal(_) => Vec::new(),
Self::Terminal(entry) => entry.worktrees.clone(),
}
}