mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
+143
-196
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user