diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index ce6813d229f..d2f13e524ce 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -104,7 +104,8 @@ const WorktreeList = React.memo(function WorktreeList({ ) const agentSendTargetWorktreeId = useAgentSendTargetWorktreeId() - const { filterState, hasFilters, clearFilters } = useSidebarWorktreeFilters() + const { filterState, hasFilters, clearFilters, revealWorkspaceFilters } = + useSidebarWorktreeFilters() const sortedIds = useSidebarWorktreeSortOrder({ allWorktrees, repoMap, sortBy }) const manualOrderCatalog = useMemo( () => buildWorktreeManualOrderCatalog({ worktrees: allWorktrees, folderWorkspaces }), @@ -244,7 +245,8 @@ const WorktreeList = React.memo(function WorktreeList({ worktrees: allWorktrees, folderWorkspaces, hasFilters, - clearFilters + clearFilters, + revealWorkspaceFilters }) const filtersHideAllRows = shouldFiltersHideAllRows({ diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts b/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts index f04e57194af..37bc57be8de 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts @@ -1,7 +1,30 @@ import { useCallback, useMemo } from 'react' import { useAppStore } from '@/store' import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../../../shared/constants' -import { computeClearFilterActions, sidebarHasActiveFilters } from '../../visible-worktrees' +import { + computeClearFilterActions, + sidebarHasActiveFilters, + isAutomationGeneratedWorkspace, + isCliCreatedWorkspace, + isDetachedHeadWorkspace, + isSleepingSweepExemptWorkspace +} from '../../visible-worktrees' +import type { Worktree } from '../../../../../../shared/worktree/types' +import { + getWorktreeExecutionHostId, + getSettingsFocusedExecutionHostId +} from '../../../../../../shared/execution-host' +import { isDefaultBranchWorkspace } from '../../default-branch-workspace' +import { + getPairedDeviceIdsByEnvironment, + isWorkspaceFromOtherDevice +} from '../../workspace-creator-visibility' +import { getAgentStatusEpochNow } from '@/lib/agent-status-epoch-clock' +import { getWorktreeIdsWithLiveAgent, isInactiveWorkspace } from '@/lib/worktree-activity-state' +import { + getVisibleWorktreeBrowserActivityTabs, + getVisibleWorktreeTerminalActivityTabs +} from '../../visible-worktree-activity-inputs' export type SidebarWorktreeFilters = ReturnType @@ -32,6 +55,70 @@ export function useSidebarWorktreeFilters() { const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds) const setVisibleWorkspaceHostIds = useAppStore((s) => s.setVisibleWorkspaceHostIds) + const revealWorkspaceFilters = useCallback((worktree: Worktree) => { + const state = useAppStore.getState() + const repo = state.repos.find((candidate) => candidate.id === worktree.repoId) + const targetHostId = getWorktreeExecutionHostId( + worktree, + repo, + getSettingsFocusedExecutionHostId(state.settings) + ) + + if (state.filterRepoIds.length > 0 && !state.filterRepoIds.includes(worktree.repoId)) { + state.setFilterRepoIds([...state.filterRepoIds, worktree.repoId]) + } + const visibleHostIds = state.visibleWorkspaceHostIds + const scopedHostIds = + visibleHostIds ?? (state.workspaceHostScope === 'all' ? null : [state.workspaceHostScope]) + if (scopedHostIds && !scopedHostIds.includes(targetHostId)) { + state.setVisibleWorkspaceHostIds([...scopedHostIds, targetHostId]) + } + if (state.hideDefaultBranchWorkspace && isDefaultBranchWorkspace(worktree)) { + state.setHideDefaultBranchWorkspace(false) + } + if (state.hideAutomationGeneratedWorkspaces && isAutomationGeneratedWorkspace(worktree)) { + state.setHideAutomationGeneratedWorkspaces(false) + } + if (state.hideCliCreatedWorkspaces && isCliCreatedWorkspace(worktree)) { + state.setHideCliCreatedWorkspaces(false) + } + if (state.hideDetachedHeadWorkspaces && isDetachedHeadWorkspace(worktree)) { + state.setHideDetachedHeadWorkspaces(false) + } + if (state.hideWorkspacesFromOtherDevices) { + const pairedDeviceIds = getPairedDeviceIdsByEnvironment( + state.runtimeEnvironments, + state.runtimeStatusByEnvironmentId + ) + if (isWorkspaceFromOtherDevice(worktree, pairedDeviceIds)) { + state.setHideWorkspacesFromOtherDevices(false) + } + } + if (!state.showSleepingWorkspaces) { + const tabsByWorktree = getVisibleWorktreeTerminalActivityTabs(state.tabsByWorktree) + const browserTabsByWorktree = getVisibleWorktreeBrowserActivityTabs( + state.browserTabsByWorktree + ) + const liveAgentWorktrees = getWorktreeIdsWithLiveAgent( + state.agentStatusByPaneKey, + tabsByWorktree, + getAgentStatusEpochNow(state.agentStatusEpoch) + ) + if ( + !isSleepingSweepExemptWorkspace(worktree, state.alwaysShowDefaultBranchWorkspace) && + isInactiveWorkspace( + worktree.id, + tabsByWorktree, + state.ptyIdsByTabId, + browserTabsByWorktree, + liveAgentWorktrees + ) + ) { + state.setShowSleepingWorkspaces(true) + } + } + }, []) + // Why: count hideDefaultBranchWorkspace as a filter so the Clear Filters escape hatch stays reachable when it alone empties the list. const filterState = useMemo( () => ({ @@ -102,5 +189,10 @@ export function useSidebarWorktreeFilters() { filterState ]) - return { filterState, hasFilters: sidebarHasActiveFilters(filterState), clearFilters } + return { + filterState, + hasFilters: sidebarHasActiveFilters(filterState), + clearFilters, + revealWorkspaceFilters + } } diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx index f31b2bc9437..b246b8cec15 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx @@ -103,7 +103,9 @@ describe('revealing a filtered workspace', () => { it('explains the filter reset and leaves filters intact when dismissed', async () => { await render() await act(async () => requestScrollToCurrentWorkspaceReveal()) - expect(document.body.textContent).toContain('Revealing it will clear your sidebar filters.') + expect(document.body.textContent).toContain( + 'Revealing it will adjust only the filters hiding it.' + ) expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() await click('Keep filters') @@ -111,13 +113,23 @@ describe('revealing a filtered workspace', () => { expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() }) + it('delegates to the minimal filter revealer when provided', async () => { + const revealWorkspaceFilters = vi.fn() + args = { ...args, revealWorkspaceFilters } + await render() + await act(async () => requestScrollToCurrentWorkspaceReveal()) + await click('Adjust filters and reveal') + expect(revealWorkspaceFilters).toHaveBeenCalledWith(args.worktrees[0]) + expect(args.clearFilters).not.toHaveBeenCalled() + }) + it('clears filters and reveals on the original execution host only after confirmation', async () => { await render() await act(async () => { requestScrollToCurrentWorkspaceReveal() requestScrollToCurrentWorkspaceReveal() }) - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).toHaveBeenCalledTimes(1) expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'smooth', @@ -174,7 +186,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceReveal()) args = { ...args, visibleWorktrees: args.worktrees } await render() - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1) }) @@ -184,7 +196,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceReveal()) args = { ...args, currentSidebarWorktreeId: 'wt-2' } await render() - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() }) @@ -219,7 +231,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceRevealAndRename()) expect(args.clearFilters).not.toHaveBeenCalled() if (filtered) { - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).toHaveBeenCalledTimes(1) } else { expect(document.querySelector('[role="dialog"]')).toBeNull() diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts index 33841f51b6c..bfab6f8fb7c 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts @@ -41,6 +41,7 @@ export function useSidebarRevealRequests(args: { folderWorkspaces: readonly FolderWorkspace[] hasFilters: boolean clearFilters: () => void + revealWorkspaceFilters?: (worktree: Worktree) => void }): void { const { groupBy, @@ -53,7 +54,8 @@ export function useSidebarRevealRequests(args: { worktrees, folderWorkspaces, hasFilters, - clearFilters + clearFilters, + revealWorkspaceFilters = clearFilters } = args const setGroupBy = useAppStore((s) => s.setGroupBy) const pendingRevealSidebarRow = useAppStore((s) => s.pendingRevealSidebarRow) @@ -80,7 +82,12 @@ export function useSidebarRevealRequests(args: { return } if (!renderedSidebarRowKeys.has(rowKey) && hasFilters) { - clearFilters() + const target = worktreeMap.get(rowKey) + if (target) { + revealWorkspaceFilters(target) + } else { + clearFilters() + } } }, [ clearFilters, @@ -88,7 +95,9 @@ export function useSidebarRevealRequests(args: { hasFilters, pendingRevealSidebarRow, renderedSidebarRowKeys, - setGroupBy + setGroupBy, + worktreeMap, + revealWorkspaceFilters ]) const handleRevealCurrentWorkspaceRequest = useCallback( @@ -139,9 +148,9 @@ export function useSidebarRevealRequests(args: { title: translate('sidebar.revealFiltered.title', 'Reveal hidden workspace?'), description: translate( 'sidebar.revealFiltered.description', - 'The active workspace is hidden in the sidebar. Revealing it will clear your sidebar filters.' + 'The active workspace is hidden in the sidebar. Revealing it will adjust only the filters hiding it.' ), - confirmLabel: translate('sidebar.revealFiltered.confirm', 'Clear filters and reveal'), + confirmLabel: translate('sidebar.revealFiltered.confirm', 'Adjust filters and reveal'), cancelLabel: translate('sidebar.revealFiltered.cancel', 'Keep filters') }) } finally { @@ -164,7 +173,7 @@ export function useSidebarRevealRequests(args: { latest.visibleFolderWorkspaces ) ) { - latest.clearFilters() + revealWorkspaceFilters(activeWorktree) } } revealWorktreeInSidebar(currentSidebarWorktreeId, { @@ -185,7 +194,8 @@ export function useSidebarRevealRequests(args: { visibleFolderWorkspaces, revealWorktreeInSidebar, worktreeMap, - worktrees + worktrees, + revealWorkspaceFilters ] )