From d8499fae161d0247b86e80caef2148e498a90c2c Mon Sep 17 00:00:00 2001 From: Yunqian Fan Date: Fri, 24 Jul 2026 15:25:39 +0800 Subject: [PATCH] fix: don't mint worktreeMeta from a sidebar sort-order snapshot (#9342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `worktrees:persistSortOrder` (ipc/worktrees.ts) and `persistManagedWorktreeSortOrder` (orca-runtime.ts) call `store.setWorktreeMeta(id, { sortOrder })` for every id the renderer sends. `setWorktreeMeta` has no repo-existence check, so a stale id the client still lists — e.g. a removed repo's `${repoId}::${path}` that lingers in the renderer's order — gets a brand-new `worktreeMeta` entry minted on every sidebar snapshot, resurrecting an orphan/duplicate workspace on the next launch. A sort-order snapshot must only reorder worktrees that already exist, never create one. Guard both call sites with `getWorktreeMeta` so absent ids are skipped. Co-authored-by: fanyunqian.1 Co-authored-by: Claude Opus 4.8 (1M context) --- src/main/ipc/worktrees.test.ts | 16 ++++++++++++++++ src/main/ipc/worktrees.ts | 8 ++++++++ src/main/runtime/orca-runtime.ts | 6 ++++++ 3 files changed, 30 insertions(+) diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index d61b92852..f7af28221 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -540,6 +540,22 @@ describe('registerWorktreeHandlers', () => { expect(handlers['worktrees:getBranchRenameFailureOutput']).toBeDefined() }) + it('persistSortOrder only reorders existing worktrees and never mints meta for a stale id', () => { + const liveId = 'repo-1::/workspace/repo' + const staleId = 'removed-repo::/workspace/gone' + // Only the live worktree has meta; the stale id (e.g. a removed repo the + // renderer still lists) has none and must be skipped, not created. + store.getWorktreeMeta.mockImplementation((id: string) => + id === liveId ? ({ instanceId: 'x' } as never) : undefined + ) + + handlers['worktrees:persistSortOrder'](null, { orderedIds: [liveId, staleId] }) + + const orderedTargets = store.setWorktreeMeta.mock.calls.map((call) => call[0]) + expect(orderedTargets).toContain(liveId) + expect(orderedTargets).not.toContain(staleId) + }) + it('prefetches the local default create base through the runtime refresh cache', async () => { const repo = { id: 'repo-1', diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index 40cf7efd1..5acd40c4f 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -2067,6 +2067,14 @@ export function registerWorktreeHandlers( } const now = Date.now() for (let i = 0; i < args.orderedIds.length; i++) { + // Why: a sidebar-order snapshot must only reorder worktrees that already + // exist — it must never create one. Without this guard a stale id the + // renderer still lists (e.g. a removed repo's `${repoId}::${path}`) gets a + // fresh worktreeMeta entry minted here, resurrecting an orphan/duplicate + // workspace on the next launch. setWorktreeMeta has no repo-existence check. + if (!store.getWorktreeMeta(args.orderedIds[i])) { + continue + } // Descending timestamps: first item gets highest sortOrder so b - a sorts first-wins on cold start. store.setWorktreeMeta(args.orderedIds[i], { sortOrder: now - i * 1000 }) } diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 083039800..66765544e 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -19973,6 +19973,12 @@ export class OrcaRuntimeService { const now = Date.now() let updated = 0 for (let i = 0; i < orderedIds.length; i++) { + // Why: a sort-order snapshot must only reorder existing worktrees, never + // mint new meta — a stale id would otherwise resurrect an orphan workspace + // (setWorktreeMeta has no repo-existence check). + if (!this.store.getWorktreeMeta(orderedIds[i])) { + continue + } this.store.setWorktreeMeta(orderedIds[i], { sortOrder: now - i * 1000 }) updated++ }