diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 9870377b2169..d0cd84eb4e25 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -238,10 +238,19 @@ const layer = Layer.effect( vcs: data.vcs?.type ?? fakeVcs, time: { ...existing.time, updated: Date.now() }, } + // Only add the directory as a sandbox when it is a genuine linked git + // worktree (common .git dir lives outside the working tree). Independent + // clones of the same remote share a project ID by design, but each clone + // is its own primary worktree and must NOT be treated as a sandbox of the + // first one — otherwise the front-end surfaces stale branch info from the + // first directory. + const isLinkedWorktree = + data.vcs?.type === "git" && !FSUtil.contains(data.directory, data.vcs.store) if ( projectID !== ProjectV2.ID.global && data.directory !== result.worktree && - !result.sandboxes.includes(data.directory) + !result.sandboxes.includes(data.directory) && + isLinkedWorktree ) result.sandboxes.push(data.directory) result.sandboxes = yield* Effect.forEach( diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 804b92b08ec4..8cef642ea486 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -362,6 +362,30 @@ describe("Project.fromDirectory with worktrees", () => { }), ) + it.live("separate clones of the same repo should not add clone as sandbox", () => + Effect.gen(function* () { + const project = yield* Project.Service + const tmp = yield* tmpdirScoped({ git: true }) + + // Create a bare remote, push, then clone into a second directory + const bare = tmp + "-bare" + const clone = tmp + "-clone" + yield* Effect.addFinalizer(() => + Effect.promise(() => $`rm -rf ${bare} ${clone}`.quiet().nothrow()).pipe(Effect.ignore), + ) + yield* Effect.promise(() => $`git clone --bare ${tmp} ${bare}`.quiet()) + yield* Effect.promise(() => $`git clone ${bare} ${clone}`.quiet()) + + yield* project.fromDirectory(tmp) + const next = yield* project.fromDirectory(clone) + + // The clone is an independent checkout, not a linked git worktree. + // It must NOT appear in sandboxes — otherwise the front-end surfaces + // stale branch info from the first directory. + expect(next.project.sandboxes).not.toContain(clone) + }), + ) + it.live("should accumulate multiple worktrees in sandboxes", () => Effect.gen(function* () { const project = yield* Project.Service