Reveal workspaces by adjusting only blocking filters

This commit is contained in:
Neil
2026-09-17 21:46:26 -07:00
parent 8c6ae79e94
commit f404ccfb0d
4 changed files with 132 additions and 16 deletions
@@ -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({
@@ -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<typeof useSidebarWorktreeFilters>
@@ -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
}
}
@@ -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()
@@ -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
]
)