From def91fa36970fb6a394ffb017f2850dbf8798c3d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 29 May 2026 08:47:11 -0700 Subject: [PATCH] Reconcile source control selection before commit --- .../useSourceControlSelection.test.ts | 35 +++++++++++++ .../useSourceControlSelection.ts | 51 ++++++++++++++----- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts b/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts index 3b9a1569486..666e406e1cb 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts @@ -3,6 +3,7 @@ import type { GitStatusEntry } from '../../../../shared/types' import { getSelectionRangeKeys, reconcileSelectionKeys, + reconcileSourceControlSelectionState, type FlatEntry } from './useSourceControlSelection' @@ -36,6 +37,40 @@ describe('reconcileSelectionKeys', () => { }) }) +describe('reconcileSourceControlSelectionState', () => { + it('keeps selected keys and anchor identity when all keys are still visible', () => { + const flatEntries = [ + makeEntry('unstaged::a.ts', 'unstaged', 'a.ts'), + makeEntry('staged::b.ts', 'staged', 'b.ts') + ] + const selectedKeys = new Set(['unstaged::a.ts']) + + const result = reconcileSourceControlSelectionState({ + selectedKeys, + anchorKey: 'unstaged::a.ts', + flatEntries + }) + + expect(result.selectedKeys).toBe(selectedKeys) + expect(result.anchorKey).toBe('unstaged::a.ts') + }) + + it('prunes stale selected keys and clears stale range anchors', () => { + const flatEntries = [makeEntry('staged::b.ts', 'staged', 'b.ts')] + + expect( + reconcileSourceControlSelectionState({ + selectedKeys: new Set(['unstaged::a.ts', 'staged::b.ts']), + anchorKey: 'unstaged::a.ts', + flatEntries + }) + ).toEqual({ + selectedKeys: new Set(['staged::b.ts']), + anchorKey: null + }) + }) +}) + describe('getSelectionRangeKeys', () => { it('selects an inclusive range across visible sections', () => { const flatEntries = [ diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts b/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts index 67c28a6d7b7..e14ee5cdc5d 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts @@ -28,6 +28,30 @@ export function reconcileSelectionKeys( return nextSelected } +export function reconcileSourceControlSelectionState(args: { + selectedKeys: ReadonlySet + anchorKey: string | null + flatEntries: FlatEntry[] +}): { selectedKeys: ReadonlySet; anchorKey: string | null } { + const { anchorKey, flatEntries, selectedKeys } = args + const validKeys = new Set(flatEntries.map((e) => e.key)) + const nextSelected = new Set() + let selectedChanged = false + + for (const key of selectedKeys) { + if (validKeys.has(key)) { + nextSelected.add(key) + } else { + selectedChanged = true + } + } + + return { + selectedKeys: selectedChanged ? nextSelected : selectedKeys, + anchorKey: anchorKey && !validKeys.has(anchorKey) ? null : anchorKey + } +} + export function getSelectionRangeKeys( flatEntries: FlatEntry[], anchorKey: string | null, @@ -87,20 +111,19 @@ export function useSourceControlSelection({ shouldOpenAsSplitRef.current = shouldOpenAsSplit }, [shouldOpenAsSplit]) - // Clear stale selections if entries disappear - useEffect(() => { - const validKeys = new Set(flatEntries.map((e) => e.key)) - const nextSelected = reconcileSelectionKeys(selectedKeys, flatEntries) - const changed = nextSelected.size !== selectedKeys.size - - if (changed) { - setSelectedKeys(nextSelected) - } - - if (anchorKey && !validKeys.has(anchorKey)) { - setAnchorKey(null) - } - }, [flatEntries, selectedKeys, anchorKey]) + const reconciledSelection = reconcileSourceControlSelectionState({ + selectedKeys, + anchorKey, + flatEntries + }) + if (reconciledSelection.selectedKeys !== selectedKeys) { + // Why: visible source-control rows can disappear after filtering, staging, + // or status refresh; prune stale bulk-action keys before children see them. + setSelectedKeys(new Set(reconciledSelection.selectedKeys)) + } + if (reconciledSelection.anchorKey !== anchorKey) { + setAnchorKey(reconciledSelection.anchorKey) + } const handleSelect = useCallback((e: React.MouseEvent, key: string, entry: GitStatusEntry) => { if (shouldOpenAsSplitRef.current?.(e)) {