diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 9153ff4e39e..ce6813d229f 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -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, 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 7f78d3d011a..f31b2bc9437 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 @@ -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 - }) - }) + ) }) 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 27a8ff5c3fa..33841f51b6c 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 @@ -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 - renderedWorktreeIdentities: readonly string[] + visibleWorktrees: readonly Worktree[] + visibleFolderWorkspaces: readonly FolderWorkspace[] currentSidebarWorktreeId: string | null currentSidebarExecutionHostId: ExecutionHostId | null worktreeMap: Map @@ -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