fix: reconcile a stale activeWorktreeId against live worktrees on hydration (#9344)
The web client persists its workspace session (activeWorktreeId, lastVisitedAtByWorktreeId, ...) in localStorage and, unlike the main-process Store, gets no load-time orphan GC. `pruneLastVisitedTimestamps` already drops stale focus-recency entries on hydration, but the persisted `activeWorktreeId` pointer is not reconciled — so a pointer to a worktree the server no longer reports lingers (the main-process path clears it via removeWorkspaceSessionOwner when a repo is removed; the web has no equivalent), and can surface a phantom / duplicate workspace that survives reloads. Extend the hydration reconcile to also clear `activeWorktreeId` once its repo is hydrated and the worktree is confirmed gone, mirroring the existing per-repo defer rule (a not-yet-hydrated repo, e.g. SSH pre-connect, keeps its pointer). Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2371c0cd8
commit
143d2232bb
|
|
@ -7156,6 +7156,34 @@ describe('markWorktreeVisited', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('pruneLastVisitedTimestamps clears a stale activeWorktreeId gone from a hydrated repo', () => {
|
||||
const store = createTestStore()
|
||||
const wt = makeWorktree({ id: 'repo1::/a', repoId: 'repo1', path: '/a' })
|
||||
store.setState({
|
||||
worktreesByRepo: { repo1: [wt] },
|
||||
activeWorktreeId: 'repo1::/gone',
|
||||
lastVisitedAtByWorktreeId: {}
|
||||
} as Partial<AppState>)
|
||||
store.getState().pruneLastVisitedTimestamps()
|
||||
expect(store.getState().activeWorktreeId).toBeNull()
|
||||
})
|
||||
|
||||
it('pruneLastVisitedTimestamps keeps a live activeWorktreeId and defers unhydrated repos', () => {
|
||||
const store = createTestStore()
|
||||
const wt = makeWorktree({ id: 'repo1::/a', repoId: 'repo1', path: '/a' })
|
||||
store.setState({
|
||||
worktreesByRepo: { repo1: [wt] },
|
||||
activeWorktreeId: 'repo1::/a'
|
||||
} as Partial<AppState>)
|
||||
store.getState().pruneLastVisitedTimestamps()
|
||||
expect(store.getState().activeWorktreeId).toBe('repo1::/a')
|
||||
|
||||
// A pointer into a not-yet-hydrated (e.g. SSH pre-connect) repo is deferred.
|
||||
store.setState({ activeWorktreeId: 'ssh-repo::/b' } as Partial<AppState>)
|
||||
store.getState().pruneLastVisitedTimestamps()
|
||||
expect(store.getState().activeWorktreeId).toBe('ssh-repo::/b')
|
||||
})
|
||||
|
||||
it('pruneLastVisitedTimestamps defers when the detected list is non-authoritative', () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
|
|
|
|||
|
|
@ -4428,7 +4428,27 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
|||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? { lastVisitedAtByWorktreeId: next } : {}
|
||||
const patch: { lastVisitedAtByWorktreeId?: Record<string, number>; activeWorktreeId?: null } =
|
||||
{}
|
||||
if (changed) {
|
||||
patch.lastVisitedAtByWorktreeId = next
|
||||
}
|
||||
// Why: the persisted active-worktree pointer is a `${repoId}::${path}` id
|
||||
// that nothing else reconciles here. The main-process Store clears a stale
|
||||
// pointer when a repo is removed (removeWorkspaceSessionOwner nulls
|
||||
// activeWorktreeId), but the web client keeps it in localStorage and gets
|
||||
// no such load-time GC — so a pointer to a worktree the server no longer
|
||||
// reports lingers and can surface a phantom/duplicate workspace. Clear it
|
||||
// once its repo is hydrated and the worktree is confirmed gone (defer while
|
||||
// the repo is unhydrated, mirroring the timestamp rule above).
|
||||
const activeId = s.activeWorktreeId
|
||||
if (activeId) {
|
||||
const activeRepoWorktreeIds = validIdsByRepo.get(getRepoIdFromWorktreeId(activeId))
|
||||
if (activeRepoWorktreeIds && !activeRepoWorktreeIds.has(activeId)) {
|
||||
patch.activeWorktreeId = null
|
||||
}
|
||||
}
|
||||
return Object.keys(patch).length > 0 ? patch : {}
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue