diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index c5b326d552..9bbe0db90f 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -237,6 +237,7 @@ struct TerminalEntry { id: TerminalId, title: SharedString, workspace: Entity, + worktrees: Vec, created_at: DateTime, has_notification: bool, highlight_positions: Vec, @@ -543,6 +544,16 @@ fn apply_worktree_label_mode( worktrees } +fn terminal_worktree_info( + workspace: &Entity, + branch_by_path: &HashMap, + cx: &App, +) -> Vec { + 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 = 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::(), + ); 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( workspace: &Entity, + branch_by_path: &HashMap, cx: &App, ) -> impl Iterator { if !cx.has_flag::() { @@ -5589,19 +5627,17 @@ fn terminal_entries_for_workspace( let Some(agent_panel) = workspace.read(cx).panel::(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() } diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 697c260e10..a7fb81c903 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -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| ::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; diff --git a/crates/sidebar/src/thread_switcher.rs b/crates/sidebar/src/thread_switcher.rs index 2e998a2501..ce3b375068 100644 --- a/crates/sidebar/src/thread_switcher.rs +++ b/crates/sidebar/src/thread_switcher.rs @@ -32,6 +32,7 @@ pub(crate) struct ThreadSwitcherTerminalEntry { pub title: SharedString, pub workspace: Entity, pub project_name: Option, + pub worktrees: Vec, pub created_at: chrono::DateTime, pub notified: bool, pub timestamp: SharedString, @@ -118,7 +119,7 @@ impl ThreadSwitcherEntry { fn worktrees(&self) -> Vec { match self { Self::Thread(entry) => entry.worktrees.clone(), - Self::Terminal(_) => Vec::new(), + Self::Terminal(entry) => entry.worktrees.clone(), } }