fix(sidebar): reveal collapsed workspaces without clearing filters

This commit is contained in:
Neil
2026-09-07 17:06:43 -07:00
parent 9f044031fc
commit 1f0026ea7c
3 changed files with 115 additions and 47 deletions
@@ -236,7 +236,8 @@ const WorktreeList = React.memo(function WorktreeList({
useSidebarRevealRequests({
groupBy,
renderedSidebarRowKeys: rowModel.renderedSidebarRowKeys,
renderedWorktreeIdentities: selection.renderedWorktreeIdentities,
visibleWorktrees,
visibleFolderWorkspaces: visibleScope.visibleFolderWorkspacesForRows,
currentSidebarWorktreeId,
currentSidebarExecutionHostId: activeWorkspaceExecutionHostId,
worktreeMap,
@@ -82,7 +82,8 @@ beforeEach(() => {
args = {
groupBy: 'repo',
renderedSidebarRowKeys: new Set(),
renderedWorktreeIdentities: [],
visibleWorktrees: [],
visibleFolderWorkspaces: [],
currentSidebarWorktreeId: worktree.id,
currentSidebarExecutionHostId: 'ssh:dev',
worktreeMap: new Map([[worktree.id, worktree]]),
@@ -133,7 +134,7 @@ describe('revealing a filtered workspace', () => {
args = {
...args,
hasFilters: visible,
renderedWorktreeIdentities: visible ? ['ssh:dev|wt-1'] : []
visibleWorktrees: visible ? args.worktrees : []
}
await render()
await act(async () => requestScrollToCurrentWorkspaceReveal())
@@ -143,6 +144,41 @@ describe('revealing a filtered workspace', () => {
}
)
it.each(['ssh:dev', null] as const)(
'reveals a collapsed workspace that passes filters with active host %s',
async (executionHostId) => {
args = {
...args,
currentSidebarExecutionHostId: executionHostId,
visibleWorktrees: args.worktrees
}
await render()
await act(async () => requestScrollToCurrentWorkspaceReveal())
expect(document.querySelector('[role="dialog"]')).toBeNull()
expect(args.clearFilters).not.toHaveBeenCalled()
expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1)
}
)
it('does not let a visible same-id workspace on another host bypass confirmation', async () => {
args = { ...args, visibleWorktrees: [{ ...args.worktrees[0], hostId: 'local' }] }
await render()
await act(async () => requestScrollToCurrentWorkspaceReveal())
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled()
await click('Keep filters')
})
it('preserves filters when the target becomes included while confirmation is open', async () => {
await render()
await act(async () => requestScrollToCurrentWorkspaceReveal())
args = { ...args, visibleWorktrees: args.worktrees }
await render()
await click('Clear filters and reveal')
expect(args.clearFilters).not.toHaveBeenCalled()
expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1)
})
it('does not apply a stale confirmation after switching workspaces', async () => {
await render()
await act(async () => requestScrollToCurrentWorkspaceReveal())
@@ -153,39 +189,47 @@ describe('revealing a filtered workspace', () => {
expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled()
})
it('confirms filtered folder workspaces and preserves the rename request', async () => {
args = {
...args,
currentSidebarWorktreeId: folderWorkspaceKey('folder-1'),
currentSidebarExecutionHostId: null,
folderWorkspaces: [
{
id: 'folder-1',
projectGroupId: 'project-1',
name: 'Notes',
folderPath: '/notes',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 1,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
]
it.each([true, false])(
'reveals folder workspaces and preserves rename (filtered: %s)',
async (filtered) => {
args = {
...args,
currentSidebarWorktreeId: folderWorkspaceKey('folder-1'),
currentSidebarExecutionHostId: null,
folderWorkspaces: [
{
id: 'folder-1',
projectGroupId: 'project-1',
name: 'Notes',
folderPath: '/notes',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 1,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
]
}
args.visibleFolderWorkspaces = filtered ? [] : args.folderWorkspaces
await render()
await act(async () => requestScrollToCurrentWorkspaceRevealAndRename())
expect(args.clearFilters).not.toHaveBeenCalled()
if (filtered) {
await click('Clear filters and reveal')
expect(args.clearFilters).toHaveBeenCalledTimes(1)
} else {
expect(document.querySelector('[role="dialog"]')).toBeNull()
}
expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), {
behavior: 'smooth',
highlight: true,
beginRename: true,
executionHostId: undefined
})
}
await render()
await act(async () => requestScrollToCurrentWorkspaceRevealAndRename())
expect(args.clearFilters).not.toHaveBeenCalled()
await click('Clear filters and reveal')
expect(args.clearFilters).toHaveBeenCalledTimes(1)
expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), {
behavior: 'smooth',
highlight: true,
beginRename: true,
executionHostId: undefined
})
})
)
})
@@ -10,16 +10,30 @@ import {
import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import { composeWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
import type { WorktreeGroupBy } from '../grouping/row-types'
import { getKnownSidebarWorktreeById } from './folder-reveal'
function workspacePassesFilters(
worktree: Worktree,
worktrees: readonly Worktree[],
folderWorkspaces: readonly FolderWorkspace[]
): boolean {
const identity = getWorktreeHostIdentity(worktree)
return (
worktrees.some((candidate) => getWorktreeHostIdentity(candidate) === identity) ||
folderWorkspaces.some((workspace) => folderWorkspaceKey(workspace.id) === worktree.id)
)
}
// Turns a "show me the current workspace" request into whatever the sidebar must change
// first — grouping mode, active filters — before the viewport can scroll to it.
export function useSidebarRevealRequests(args: {
groupBy: WorktreeGroupBy
renderedSidebarRowKeys: ReadonlySet<string>
renderedWorktreeIdentities: readonly string[]
visibleWorktrees: readonly Worktree[]
visibleFolderWorkspaces: readonly FolderWorkspace[]
currentSidebarWorktreeId: string | null
currentSidebarExecutionHostId: ExecutionHostId | null
worktreeMap: Map<string, Worktree>
@@ -31,7 +45,8 @@ export function useSidebarRevealRequests(args: {
const {
groupBy,
renderedSidebarRowKeys,
renderedWorktreeIdentities,
visibleWorktrees,
visibleFolderWorkspaces,
currentSidebarWorktreeId,
currentSidebarExecutionHostId,
worktreeMap,
@@ -106,11 +121,11 @@ export function useSidebarRevealRequests(args: {
if (!activeWorktree || activeWorktree.isArchived) {
return
}
const currentIdentity = composeWorktreeHostIdentity(
currentSidebarExecutionHostId ?? undefined,
currentSidebarWorktreeId
)
if (hasFilters && !renderedWorktreeIdentities.includes(currentIdentity)) {
// Collapsed groups hide rows without excluding their workspaces from the filter results.
if (
hasFilters &&
!workspacePassesFilters(activeWorktree, visibleWorktrees, visibleFolderWorkspaces)
) {
if (confirmationPending.current) {
return
}
@@ -141,7 +156,14 @@ export function useSidebarRevealRequests(args: {
) {
return
}
if (latest.hasFilters && !latest.renderedWorktreeIdentities.includes(currentIdentity)) {
if (
latest.hasFilters &&
!workspacePassesFilters(
activeWorktree,
latest.visibleWorktrees,
latest.visibleFolderWorkspaces
)
) {
latest.clearFilters()
}
}
@@ -159,7 +181,8 @@ export function useSidebarRevealRequests(args: {
currentSidebarExecutionHostId,
folderWorkspaces,
revealSidebarRow,
renderedWorktreeIdentities,
visibleWorktrees,
visibleFolderWorkspaces,
revealWorktreeInSidebar,
worktreeMap,
worktrees