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.
This commit is contained in:
Neil
2026-09-06 14:20:52 -07:00
parent aeb05003ab
commit aec6d440d1
3 changed files with 172 additions and 258 deletions
@@ -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<string>())).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<string>())).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({});
});
});
@@ -11,36 +11,20 @@
* to an empty record. Production always initialises them, so the fresh object here
* costs nothing at runtime.
*/
export function omitRecordKeys<T>(
record: Record<string, T> | undefined,
keys: Iterable<string>
keys: Iterable<string>,
): Record<string, T> {
if (!record) {
return {}
return {};
}
let next: Record<string, T> | null = null
let next: Record<string, T> | 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<T>(
record: Record<string, T> | undefined,
key: string
): Record<string, T> {
if (!record) {
return {}
}
if (!(key in record)) {
return record
}
const next = { ...record }
delete next[key]
return next
return next ?? record;
}
@@ -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<string>,
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 = <T>(m: Record<string, T> | undefined) =>
omitRecordKeys(m, worktreeIds);
const omitByTabId = <T>(m: Record<string, T> | 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<string>()
const removedFileIds = new Set<string>();
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 = <T>(m: Record<string, T> | 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,
};
});
}