From aec6d440d129d70e1370f34b37255497861fc8a2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:20:52 -0700 Subject: [PATCH] refactor(worktrees): fold removeWorktree cleanup onto one omitRecordKeys helper Drop the single-key omitRecordKey twin and build the removal patch inline from three scoped omitters (worktree / tab / file), keeping every purged field and its why-comment. 273 -> 137 lines. --- .../teardown/record-key-omission.test.ts | 61 ++-- .../worktrees/teardown/record-key-omission.ts | 30 +- .../teardown/remove-worktree-store-cleanup.ts | 339 ++++++++---------- 3 files changed, 172 insertions(+), 258 deletions(-) diff --git a/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts b/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts index a1c945346e0..8062b3f7396 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts @@ -1,43 +1,26 @@ -import { describe, expect, it } from 'vitest' -import { omitRecordKey, omitRecordKeys } from './record-key-omission' +import { describe, expect, it } from "vitest"; +import { omitRecordKeys } from "./record-key-omission"; -describe('omitRecordKey', () => { - it('returns the same record when the key is absent', () => { - const record = { a: 1 } - expect(omitRecordKey(record, 'b')).toBe(record) - }) +describe("omitRecordKeys", () => { + it("returns the same record when none of the keys are present", () => { + const record = { a: 1 }; + expect(omitRecordKeys(record, ["b", "c"])).toBe(record); + expect(omitRecordKeys(record, new Set())).toBe(record); + }); - it('returns a copy without the key when it is present', () => { - const record = { a: 1, b: 2 } - const next = omitRecordKey(record, 'b') - expect(next).not.toBe(record) - expect(next).toEqual({ a: 1 }) - expect(record).toEqual({ a: 1, b: 2 }) - }) + it("copies once and drops every present key", () => { + const record = { a: 1, b: 2, c: 3 }; + const next = omitRecordKeys(record, new Set(["a", "c", "missing"])); + expect(next).not.toBe(record); + expect(next).toEqual({ b: 2 }); + expect(record).toEqual({ a: 1, b: 2, c: 3 }); + }); - it('drops a key whose value is undefined', () => { - const record = { a: undefined } - expect(omitRecordKey(record, 'a')).toEqual({}) - }) + it("drops a key whose value is undefined", () => { + expect(omitRecordKeys({ a: undefined }, ["a"])).toEqual({}); + }); - it('normalizes a missing record to an empty one, as spread-then-delete did', () => { - expect(omitRecordKey(undefined, 'a')).toEqual({}) - expect(omitRecordKeys(undefined, ['a'])).toEqual({}) - }) -}) - -describe('omitRecordKeys', () => { - it('returns the same record when none of the keys are present', () => { - const record = { a: 1 } - expect(omitRecordKeys(record, ['b', 'c'])).toBe(record) - expect(omitRecordKeys(record, new Set())).toBe(record) - }) - - it('copies once and drops every present key', () => { - const record = { a: 1, b: 2, c: 3 } - const next = omitRecordKeys(record, new Set(['a', 'c', 'missing'])) - expect(next).not.toBe(record) - expect(next).toEqual({ b: 2 }) - expect(record).toEqual({ a: 1, b: 2, c: 3 }) - }) -}) + it("normalizes a missing record to an empty one, as spread-then-delete did", () => { + expect(omitRecordKeys(undefined, ["a"])).toEqual({}); + }); +}); diff --git a/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.ts b/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.ts index 1dded7dae5b..4b0095927f6 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/record-key-omission.ts @@ -11,36 +11,20 @@ * to an empty record. Production always initialises them, so the fresh object here * costs nothing at runtime. */ - export function omitRecordKeys( record: Record | undefined, - keys: Iterable + keys: Iterable, ): Record { if (!record) { - return {} + return {}; } - let next: Record | null = null + let next: Record | null = null; for (const key of keys) { if (!(key in record)) { - continue + continue; } - next ??= { ...record } - delete next[key] + next ??= { ...record }; + delete next[key]; } - return next ?? record -} - -export function omitRecordKey( - record: Record | undefined, - key: string -): Record { - if (!record) { - return {} - } - if (!(key in record)) { - return record - } - const next = { ...record } - delete next[key] - return next + return next ?? record; } diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts index 1ca89dab322..54b37c4b16c 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts @@ -1,235 +1,182 @@ -import { worktreeWorkspaceKey } from '../../../../../../shared/workspace-scope' -import type { ExecutionHostId } from '../../../../../../shared/execution-host' -import type { WorktreeSliceSet } from '../listing/worktree-slice-types' -import { removeDeleteStatesForWorktreeIds } from './worktree-delete-state' -import { removeWorktreeVisitEntries } from '@/lib/worktree-visit-recency' -import { forgetAmbiguousOwnerWarnings } from '../listing/worktree-owner-settings' -import { omitRecordKey, omitRecordKeys } from './record-key-omission' +import { worktreeWorkspaceKey } from "../../../../../../shared/workspace-scope"; +import type { ExecutionHostId } from "../../../../../../shared/execution-host"; +import type { WorktreeSliceSet } from "../listing/worktree-slice-types"; +import { removeDeleteStatesForWorktreeIds } from "./worktree-delete-state"; +import { removeWorktreeVisitEntries } from "@/lib/worktree-visit-recency"; +import { forgetAmbiguousOwnerWarnings } from "../listing/worktree-owner-settings"; +import { omitRecordKeys } from "./record-key-omission"; export function applyRemoveWorktreeSuccessState( set: WorktreeSliceSet, worktreeId: string, tabIds: Set, - executionHostId?: ExecutionHostId + executionHostId?: ExecutionHostId, ): void { // Why outside `set`: it is module-scope, not store state. Dropping it also // re-arms the once-per-workspace warning if this id is ever added back. - forgetAmbiguousOwnerWarnings([worktreeId]) + forgetAmbiguousOwnerWarnings([worktreeId]); set((s) => { - const next = { ...s.worktreesByRepo } - for (const repoId of Object.keys(next)) { - next[repoId] = next[repoId].filter((w) => w.id !== worktreeId) + const worktreeIds = [worktreeId]; + const omitByWorktree = (m: Record | undefined) => + omitRecordKeys(m, worktreeIds); + const omitByTabId = (m: Record | undefined) => + omitRecordKeys(m, tabIds); + const nextWorktreesByRepo = { ...s.worktreesByRepo }; + for (const repoId of Object.keys(nextWorktreesByRepo)) { + nextWorktreesByRepo[repoId] = nextWorktreesByRepo[repoId].filter( + (w) => w.id !== worktreeId, + ); } - const nextTabs = omitRecordKey(s.tabsByWorktree, worktreeId) - const nextLayouts = omitRecordKeys(s.terminalLayoutsByTabId, tabIds) - const nextPtyIdsByTabId = omitRecordKeys(s.ptyIdsByTabId, tabIds) - const nextRuntimePaneTitlesByTabId = omitRecordKeys(s.runtimePaneTitlesByTabId, tabIds) - const nextAutomaticAgentResumeClaimsByTabId = omitRecordKeys( - s.automaticAgentResumeClaimsByTabId, - tabIds - ) - const nextNativeChatLaunchPromptByTabId = omitRecordKeys( - s.nativeChatLaunchPromptByTabId, - tabIds - ) - const nextNativeChatLaunchDraftByTabId = omitRecordKeys(s.nativeChatLaunchDraftByTabId, tabIds) - const nextUnverifiedPtyLossTabIds = omitRecordKeys(s.unverifiedPtyLossTabIds, tabIds) - // Why: closeTab deletes these per-tab maps but removeWorktree missed them, leaking a split pane's expand flags. - const nextExpandedPaneByTabId = omitRecordKeys(s.expandedPaneByTabId, tabIds) - const nextCanExpandPaneByTabId = omitRecordKeys(s.canExpandPaneByTabId, tabIds) - const nextDeleteState = removeDeleteStatesForWorktreeIds( - s.deleteStateByWorktreeId, - new Set([worktreeId]) - ) - const nextLineage = omitRecordKey(s.worktreeLineageById, worktreeId) - const nextWorkspaceLineage = omitRecordKey( - s.workspaceLineageByChildKey, - worktreeWorkspaceKey(worktreeId) - ) - // Clean up editor files belonging to this worktree - const newOpenFiles = s.openFiles.filter((f) => f.worktreeId !== worktreeId) - const nextBrowserTabsByWorktree = omitRecordKey(s.browserTabsByWorktree, worktreeId) - const nextActiveFileIdByWorktree = omitRecordKey(s.activeFileIdByWorktree, worktreeId) - const nextActiveBrowserTabIdByWorktree = omitRecordKey( - s.activeBrowserTabIdByWorktree, - worktreeId - ) - // Why: closeBrowserTab records a Cmd+Shift+T undo snapshot, but a deleted worktree's tabs can't be restored; purge it. - const nextRecentlyClosedBrowserTabsByWorktree = omitRecordKey( - s.recentlyClosedBrowserTabsByWorktree, - worktreeId - ) - const nextActiveTabTypeByWorktree = omitRecordKey(s.activeTabTypeByWorktree, worktreeId) - const nextActiveTabIdByWorktree = omitRecordKey(s.activeTabIdByWorktree, worktreeId) - // Why: the tab strip persists visual order per worktree; drop the entry so stale tab IDs aren't retained. - const nextTabBarOrderByWorktree = omitRecordKey(s.tabBarOrderByWorktree, worktreeId) - const nextPendingReconnectTabByWorktree = omitRecordKey( - s.pendingReconnectTabByWorktree, - worktreeId - ) - // Why: split-tab layout/group state is worktree-owned; leaving it makes a deleted worktree look restorable. - const nextUnifiedTabsByWorktree = omitRecordKey(s.unifiedTabsByWorktree, worktreeId) - const nextGroupsByWorktree = omitRecordKey(s.groupsByWorktree, worktreeId) - const nextLayoutByWorktree = omitRecordKey(s.layoutByWorktree, worktreeId) - const nextActiveGroupIdByWorktree = omitRecordKey(s.activeGroupIdByWorktree, worktreeId) - // Why: git status/compare caches stop refreshing once the worktree is deleted; remove them so no stale badges/diffs linger. - const nextGitStatusByWorktree = omitRecordKey(s.gitStatusByWorktree, worktreeId) - const nextGitStatusHeadByWorktree = omitRecordKey(s.gitStatusHeadByWorktree, worktreeId) - const nextGitBranchLineTotalByWorktree = omitRecordKey( - s.gitBranchLineTotalByWorktree, - worktreeId - ) - const nextGitIgnoredPathsByWorktree = omitRecordKey(s.gitIgnoredPathsByWorktree, worktreeId) - const nextGitConflictOperationByWorktree = omitRecordKey( - s.gitConflictOperationByWorktree, - worktreeId - ) - const nextTrackedConflictPathsByWorktree = omitRecordKey( - s.trackedConflictPathsByWorktree, - worktreeId - ) - const nextGitBranchChangesByWorktree = omitRecordKey(s.gitBranchChangesByWorktree, worktreeId) - const nextGitBranchCompareSummaryByWorktree = omitRecordKey( - s.gitBranchCompareSummaryByWorktree, - worktreeId - ) - const nextGitBranchCompareRequestKeyByWorktree = omitRecordKey( - s.gitBranchCompareRequestKeyByWorktree, - worktreeId - ) - const nextGitBranchCompareRequestStatusHeadByWorktree = omitRecordKey( - s.gitBranchCompareRequestStatusHeadByWorktree, - worktreeId - ) // Why: clean up per-file editor state for the removed worktree so stale drafts/view modes don't accumulate. - const removedFileIds = new Set() + const removedFileIds = new Set(); for (const file of s.openFiles) { if (file.worktreeId !== worktreeId) { - continue + continue; } - removedFileIds.add(file.id) + removedFileIds.add(file.id); if (file.markdownPreviewSourceFileId) { - removedFileIds.add(file.markdownPreviewSourceFileId) + removedFileIds.add(file.markdownPreviewSourceFileId); } } - const nextEditorDrafts = omitRecordKeys(s.editorDrafts, removedFileIds) - const nextMarkdownViewMode = omitRecordKeys(s.markdownViewMode, removedFileIds) - const nextMarkdownRichModeSizeOverride = omitRecordKeys( - s.markdownRichModeSizeOverride, - removedFileIds - ) - const nextEditorViewMode = omitRecordKeys(s.editorViewMode, removedFileIds) - const nextMarkdownFrontmatterVisible = omitRecordKeys( - s.markdownFrontmatterVisible, - removedFileIds - ) - // Why: editorCursorLine is keyed by fileId; clear it with the other per-file state so it doesn't leak. - const nextEditorCursorLine = omitRecordKeys(s.editorCursorLine, removedFileIds) - const nextExpandedDirs = omitRecordKey(s.expandedDirs, worktreeId) - const nextShowDotfilesByWorktree = omitRecordKey(s.showDotfilesByWorktree, worktreeId) - // Why: clear the huge-status marker so it doesn't linger after the worktree is gone. - const nextGitStatusHugeByWorktree = omitRecordKey(s.gitStatusHugeByWorktree, worktreeId) - const nextRightSidebarExplorerViewByWorktree = omitRecordKey( - s.rightSidebarExplorerViewByWorktree, - worktreeId - ) + const omitByFileId = (m: Record | undefined) => + omitRecordKeys(m, removedFileIds); // If the active file belonged to the removed worktree, clear it const activeFileCleared = s.activeFileId - ? s.openFiles.some((f) => f.id === s.activeFileId && f.worktreeId === worktreeId) - : false - const removedActiveWorktree = s.activeWorktreeId === worktreeId - const nextEverActivatedWorktreeIds = s.everActivatedWorktreeIds.has(worktreeId) - ? new Set([...s.everActivatedWorktreeIds].filter((id) => id !== worktreeId)) - : s.everActivatedWorktreeIds - const nextLastVisitedAtByWorktreeId = removeWorktreeVisitEntries( - s.lastVisitedAtByWorktreeId, - new Set([worktreeId]), - executionHostId - ) + ? s.openFiles.some( + (f) => f.id === s.activeFileId && f.worktreeId === worktreeId, + ) + : false; + const removedActiveWorktree = s.activeWorktreeId === worktreeId; return { - worktreesByRepo: next, - worktreeLineageById: nextLineage, - workspaceLineageByChildKey: nextWorkspaceLineage, - tabsByWorktree: nextTabs, - ptyIdsByTabId: nextPtyIdsByTabId, - runtimePaneTitlesByTabId: nextRuntimePaneTitlesByTabId, - automaticAgentResumeClaimsByTabId: nextAutomaticAgentResumeClaimsByTabId, - nativeChatLaunchPromptByTabId: nextNativeChatLaunchPromptByTabId, - nativeChatLaunchDraftByTabId: nextNativeChatLaunchDraftByTabId, - unverifiedPtyLossTabIds: nextUnverifiedPtyLossTabIds, - terminalLayoutsByTabId: nextLayouts, - expandedPaneByTabId: nextExpandedPaneByTabId, - canExpandPaneByTabId: nextCanExpandPaneByTabId, - deleteStateByWorktreeId: nextDeleteState, - baseStatusByWorktreeId: omitRecordKey(s.baseStatusByWorktreeId, worktreeId), - remoteBranchConflictByWorktreeId: omitRecordKey( + worktreesByRepo: nextWorktreesByRepo, + worktreeLineageById: omitByWorktree(s.worktreeLineageById), + workspaceLineageByChildKey: omitRecordKeys(s.workspaceLineageByChildKey, [ + worktreeWorkspaceKey(worktreeId), + ]), + tabsByWorktree: omitByWorktree(s.tabsByWorktree), + ptyIdsByTabId: omitByTabId(s.ptyIdsByTabId), + runtimePaneTitlesByTabId: omitByTabId(s.runtimePaneTitlesByTabId), + automaticAgentResumeClaimsByTabId: omitByTabId( + s.automaticAgentResumeClaimsByTabId, + ), + nativeChatLaunchPromptByTabId: omitByTabId( + s.nativeChatLaunchPromptByTabId, + ), + nativeChatLaunchDraftByTabId: omitByTabId(s.nativeChatLaunchDraftByTabId), + unverifiedPtyLossTabIds: omitByTabId(s.unverifiedPtyLossTabIds), + terminalLayoutsByTabId: omitByTabId(s.terminalLayoutsByTabId), + // Why: closeTab deletes these per-tab maps but removeWorktree missed them, leaking a split pane's expand flags. + expandedPaneByTabId: omitByTabId(s.expandedPaneByTabId), + canExpandPaneByTabId: omitByTabId(s.canExpandPaneByTabId), + deleteStateByWorktreeId: removeDeleteStatesForWorktreeIds( + s.deleteStateByWorktreeId, + new Set(worktreeIds), + ), + baseStatusByWorktreeId: omitByWorktree(s.baseStatusByWorktreeId), + remoteBranchConflictByWorktreeId: omitByWorktree( s.remoteBranchConflictByWorktreeId, - worktreeId ), - fileSearchStateByWorktree: omitRecordKey(s.fileSearchStateByWorktree, worktreeId), + // Why: file search state is worktree-scoped; clear it so another worktree can't inherit stale matches. + fileSearchStateByWorktree: omitByWorktree(s.fileSearchStateByWorktree), // Why: these worktree-keyed maps are re-keyed on rename but were missed by removal, leaking one entry each. - remoteStatusesByWorktree: omitRecordKey(s.remoteStatusesByWorktree, worktreeId), - recentlyClosedEditorTabsByWorktree: omitRecordKey( + remoteStatusesByWorktree: omitByWorktree(s.remoteStatusesByWorktree), + recentlyClosedEditorTabsByWorktree: omitByWorktree( s.recentlyClosedEditorTabsByWorktree, - worktreeId ), - recentlyClosedTerminalTabsByWorktree: omitRecordKey( + recentlyClosedTerminalTabsByWorktree: omitByWorktree( s.recentlyClosedTerminalTabsByWorktree, - worktreeId ), // Why: a deleted worktree's tabs can never be reopened; purge the kind list with the snapshot stacks above. - recentlyClosedTabKindsByWorktree: omitRecordKey( + recentlyClosedTabKindsByWorktree: omitByWorktree( s.recentlyClosedTabKindsByWorktree, - worktreeId ), - defaultTerminalTabsAppliedByWorktreeId: omitRecordKey( + defaultTerminalTabsAppliedByWorktreeId: omitByWorktree( s.defaultTerminalTabsAppliedByWorktreeId, - worktreeId ), activeWorktreeId: removedActiveWorktree ? null : s.activeWorktreeId, activeWorkspaceExecutionHostId: removedActiveWorktree ? null : s.activeWorkspaceExecutionHostId, - activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId, - openFiles: newOpenFiles, - browserTabsByWorktree: nextBrowserTabsByWorktree, - recentlyClosedBrowserTabsByWorktree: nextRecentlyClosedBrowserTabsByWorktree, - activeFileIdByWorktree: nextActiveFileIdByWorktree, - activeBrowserTabIdByWorktree: nextActiveBrowserTabIdByWorktree, - activeTabTypeByWorktree: nextActiveTabTypeByWorktree, - rightSidebarExplorerViewByWorktree: nextRightSidebarExplorerViewByWorktree, - activeTabIdByWorktree: nextActiveTabIdByWorktree, - tabBarOrderByWorktree: nextTabBarOrderByWorktree, - pendingReconnectTabByWorktree: nextPendingReconnectTabByWorktree, - unifiedTabsByWorktree: nextUnifiedTabsByWorktree, - groupsByWorktree: nextGroupsByWorktree, - layoutByWorktree: nextLayoutByWorktree, - activeGroupIdByWorktree: nextActiveGroupIdByWorktree, - editorDrafts: nextEditorDrafts, - markdownViewMode: nextMarkdownViewMode, - markdownRichModeSizeOverride: nextMarkdownRichModeSizeOverride, - editorViewMode: nextEditorViewMode, - markdownFrontmatterVisible: nextMarkdownFrontmatterVisible, - editorCursorLine: nextEditorCursorLine, - showDotfilesByWorktree: nextShowDotfilesByWorktree, - expandedDirs: nextExpandedDirs, - gitStatusHugeByWorktree: nextGitStatusHugeByWorktree, - gitStatusByWorktree: nextGitStatusByWorktree, - gitStatusHeadByWorktree: nextGitStatusHeadByWorktree, - gitBranchLineTotalByWorktree: nextGitBranchLineTotalByWorktree, - gitIgnoredPathsByWorktree: nextGitIgnoredPathsByWorktree, - gitConflictOperationByWorktree: nextGitConflictOperationByWorktree, - trackedConflictPathsByWorktree: nextTrackedConflictPathsByWorktree, - gitBranchChangesByWorktree: nextGitBranchChangesByWorktree, - gitBranchCompareSummaryByWorktree: nextGitBranchCompareSummaryByWorktree, - gitBranchCompareRequestKeyByWorktree: nextGitBranchCompareRequestKeyByWorktree, - gitBranchCompareRequestStatusHeadByWorktree: nextGitBranchCompareRequestStatusHeadByWorktree, + activeTabId: + s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId, + openFiles: s.openFiles.filter((f) => f.worktreeId !== worktreeId), + browserTabsByWorktree: omitByWorktree(s.browserTabsByWorktree), + // Why: closeBrowserTab records a Cmd+Shift+T undo snapshot, but a deleted worktree's tabs can't be restored; purge it. + recentlyClosedBrowserTabsByWorktree: omitByWorktree( + s.recentlyClosedBrowserTabsByWorktree, + ), + activeFileIdByWorktree: omitByWorktree(s.activeFileIdByWorktree), + activeBrowserTabIdByWorktree: omitByWorktree( + s.activeBrowserTabIdByWorktree, + ), + activeTabTypeByWorktree: omitByWorktree(s.activeTabTypeByWorktree), + rightSidebarExplorerViewByWorktree: omitByWorktree( + s.rightSidebarExplorerViewByWorktree, + ), + activeTabIdByWorktree: omitByWorktree(s.activeTabIdByWorktree), + // Why: the tab strip persists visual order per worktree; drop the entry so stale tab IDs aren't retained. + tabBarOrderByWorktree: omitByWorktree(s.tabBarOrderByWorktree), + pendingReconnectTabByWorktree: omitByWorktree( + s.pendingReconnectTabByWorktree, + ), + // Why: split-tab layout/group state is worktree-owned; leaving it makes a deleted worktree look restorable. + unifiedTabsByWorktree: omitByWorktree(s.unifiedTabsByWorktree), + groupsByWorktree: omitByWorktree(s.groupsByWorktree), + layoutByWorktree: omitByWorktree(s.layoutByWorktree), + activeGroupIdByWorktree: omitByWorktree(s.activeGroupIdByWorktree), + editorDrafts: omitByFileId(s.editorDrafts), + markdownViewMode: omitByFileId(s.markdownViewMode), + markdownRichModeSizeOverride: omitByFileId( + s.markdownRichModeSizeOverride, + ), + editorViewMode: omitByFileId(s.editorViewMode), + markdownFrontmatterVisible: omitByFileId(s.markdownFrontmatterVisible), + // Why: editorCursorLine is keyed by fileId; clear it with the other per-file state so it doesn't leak. + editorCursorLine: omitByFileId(s.editorCursorLine), + showDotfilesByWorktree: omitByWorktree(s.showDotfilesByWorktree), + expandedDirs: omitByWorktree(s.expandedDirs), + // Why: clear the huge-status marker so it doesn't linger after the worktree is gone. + gitStatusHugeByWorktree: omitByWorktree(s.gitStatusHugeByWorktree), + // Why: git status/compare caches stop refreshing once the worktree is deleted; remove them so no stale badges/diffs linger. + gitStatusByWorktree: omitByWorktree(s.gitStatusByWorktree), + gitStatusHeadByWorktree: omitByWorktree(s.gitStatusHeadByWorktree), + gitBranchLineTotalByWorktree: omitByWorktree( + s.gitBranchLineTotalByWorktree, + ), + gitIgnoredPathsByWorktree: omitByWorktree(s.gitIgnoredPathsByWorktree), + gitConflictOperationByWorktree: omitByWorktree( + s.gitConflictOperationByWorktree, + ), + trackedConflictPathsByWorktree: omitByWorktree( + s.trackedConflictPathsByWorktree, + ), + gitBranchChangesByWorktree: omitByWorktree(s.gitBranchChangesByWorktree), + gitBranchCompareSummaryByWorktree: omitByWorktree( + s.gitBranchCompareSummaryByWorktree, + ), + gitBranchCompareRequestKeyByWorktree: omitByWorktree( + s.gitBranchCompareRequestKeyByWorktree, + ), + gitBranchCompareRequestStatusHeadByWorktree: omitByWorktree( + s.gitBranchCompareRequestStatusHeadByWorktree, + ), activeFileId: activeFileCleared ? null : s.activeFileId, activeBrowserTabId: removedActiveWorktree ? null : s.activeBrowserTabId, - activeTabType: removedActiveWorktree || activeFileCleared ? 'terminal' : s.activeTabType, - everActivatedWorktreeIds: nextEverActivatedWorktreeIds, - lastVisitedAtByWorktreeId: nextLastVisitedAtByWorktreeId, - sortEpoch: s.sortEpoch + 1 - } - }) + activeTabType: + removedActiveWorktree || activeFileCleared + ? "terminal" + : s.activeTabType, + everActivatedWorktreeIds: s.everActivatedWorktreeIds.has(worktreeId) + ? new Set( + [...s.everActivatedWorktreeIds].filter((id) => id !== worktreeId), + ) + : s.everActivatedWorktreeIds, + lastVisitedAtByWorktreeId: removeWorktreeVisitEntries( + s.lastVisitedAtByWorktreeId, + new Set(worktreeIds), + executionHostId, + ), + sortEpoch: s.sortEpoch + 1, + }; + }); }