You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In OpenCode Desktop, I cannot open a repository/directory whose name is a string prefix of another directory already added to the project list. Example: I have a repo/dir vesper and another repo/dir vesper-ios. When I open the project picker and try to choose vesper-ios, the app selects vesper instead (or refuses to open vesper-ios entirely).
Expected behavior: Selecting a directory opens exactly that directory, regardless of whether another directory's name is a prefix of it.
Actual behavior: The shorter name matches as a prefix, so the wrong project is opened and I can't work in vesper-ios.
This is distinct from the existing issues about identical project names or directories that share the same git remote (e.g. #34285, #34067, #39676, #36344, #35714) — here the two directories have different names, but one is a string prefix of the other.
Plugins
None relevant.
OpenCode version
N/A — reproduced on the current Desktop beta.
Steps to reproduce
Add a directory named vesper to OpenCode Desktop.
Attempt to open a new project and choose a directory named vesper-ios.
Observe that vesper-ios cannot be selected; the picker resolves to vesper instead.
I spent some time investigating this and wanted to share what I found in case it's useful.
The project picker uses exact matching throughout the selection path — pathKey=== / Map.get / findIndex(x => key(x) === active()) across dialog-select-directory[-v2], home.project.add, layout.projects.open, rootFor, server.projects.open, and use-filtered-list. I couldn't find a name-prefix / startsWith collision anywhere in the picker.
The only collision I could reproduce is shared git root commit — i.e. when vesper-ios was created by copying/cloning vesper (so they share .git history). Project.resolve (packages/core/src/project.ts:115) falls back to root(repo) = the first root-commit hash for repos without a usable remote; two copies share that hash → same project ID → fromDirectory adds vesper-ios to vesper's sandboxes → rootFor(vesper-ios) resolves to vesper. The name prefix is incidental — any copied dir collides regardless of name.
Two scenarios I tested locally (in packages/opencode/test/project/project.test.ts, modeled on the existing suite):
two sibling git init repos with different initial commits (different file content) → distinct project IDs, no collision ✅
cp -R vesper vesper-ios (shared root commit, no remote) → same project ID, vesper-ios folded into vesper.sandboxes
The shared-ID case is already asserted as intended by project.test.ts:344 ("separate clones of the same repo should share project ID"), so a change that separates copies would conflict with that test and the clone-grouping design.
@Hyperion2220 — could you confirm whether vesper-ios was created by copying or cloning vesper (e.g. cp -R, git clone of a local path)? If they're genuinely distinct repos with different root commits, the above wouldn't explain it and I'd like to reproduce your exact setup.
Happy to implement whichever direction you all want here (e.g. an explicit "open as separate project" override for explicitly-chosen dirs), but that's a design call I'll defer to the maintainers.
@AliAltivate Thanks for digging into this. To answer your question: vesper-ios was not copied or cloned from vesper. They're independent repos with different GitHub remotes and different initial commits, and they resolve to different project IDs. Both have usable remotes, so Project.resolve returns the git-remote: hash and never reaches the root-commit fallback — that path isn't involved here.
You're also right that it isn't a name-prefix collision. But there is a real bug: my project table had two projects claiming the same worktree, with the real vesper-ios directory demoted to a sandbox of the iOS project. The trigger is a renamed repo, not a copied one.
Cause
fromDirectory pins worktree permanently for every non-global project:
sandboxes entries are pruned against the filesystem a few lines later, but worktree is never revalidated — so a stale path survives indefinitely, even once it belongs to a different repository.
The desktop symptom follows from there. rootFor (packages/app/src/context/layout.tsx:473) builds a sandbox → worktree map, so opening the renamed directory resolves back to the old path. And the effect at layout.tsx:495-505 removes any open project whose rootFor differs from its own worktree, which is why the project wouldn't stay open at all. That accounts for both behaviors in my original report.
Reproduction — no copying or cloning required
git init foo && cd foo && git remote add origin <some-url> && git commit --allow-empty -m init
# open foo in OpenCode Desktop
cd .. && mv foo bar
# open bar in OpenCode Desktop
bar is folded into foo as a sandbox, and the picker resolves it back to foo. Any rename or move of a tracked repo reproduces it; the names don't need to share a prefix.
I confirmed this against fromDirectory directly on 1882c33. After the rename the project ID is stable, worktree stays on the old path — which no longer exists on disk — and the renamed directory is appended to sandboxes:
[repro] project id stable: true
[repro] worktree after mv: /…/T/opencode-test-xzwcavrmesm
[repro] renamed dir: /…/T/opencode-test-xzwcavrmesm-renamed
[repro] sandboxes after mv: ["/…/T/opencode-test-xzwcavrmesm-renamed"]
Test used (passes today, documenting current behavior)
it.live("renaming a tracked repo folds the new path into the old worktree",()=>Effect.gen(function*(){constproject=yield*Project.Serviceconsttmp=yield*tmpdirScoped({git: true})yield*Effect.promise(()=>$`git remote add origin https://github.com/example/foo.git`.cwd(tmp).quiet())constfirst=yield*project.fromDirectory(tmp)expect(first.project.worktree).toBe(tmp)expect(first.project.sandboxes).toEqual([])constrenamed=path.join(path.dirname(tmp),path.basename(tmp)+"-renamed")yield*Effect.promise(()=>$`mv ${tmp}${renamed}`.quiet())constsecond=yield*project.fromDirectory(renamed)expect(second.project.id).toBe(first.project.id)expect(second.project.worktree).toBe(tmp)// stale: this path is goneexpect(second.project.sandboxes).toContain(renamed)}),)
The existing project.test.ts suite (36 tests) is green alongside it.
On the test conflict
I don't think this collides with project.test.ts:344 ("separate clones of the same repo should share project ID"). Genuine clones still resolve to the same ID under any fix here — the question is only which directory owns worktree. A targeted change would be: when data.directory !== existing.worktree, re-resolve existing.worktree first; if that path no longer exists, or now resolves to a different project ID, promote data.directory to worktree rather than appending it to sandboxes. Two live clones of one repo both still resolve to the same ID, so the grouping behavior that test asserts is unchanged.
Workaround
Fixing the row directly resolves it, with OpenCode closed:
UPDATE project
SET worktree ='<real path>', sandboxes ='[]'WHERE id ='<project id>';
Fixed by #45043 and #45735. When a tracked repo is renamed or moved, the project's canonical directory now moves to the new path if the old one no longer exists, instead of folding the new path into the old project as a sandbox. Covered by the 'canonical directory is renamed' test in packages/core/test/project.test.ts.
Activity
AliAltivate commentedon Aug 2, 2026
I spent some time investigating this and wanted to share what I found in case it's useful.
The project picker uses exact matching throughout the selection path —
pathKey===/Map.get/findIndex(x => key(x) === active())acrossdialog-select-directory[-v2],home.project.add,layout.projects.open,rootFor,server.projects.open, anduse-filtered-list. I couldn't find a name-prefix /startsWithcollision anywhere in the picker.The only collision I could reproduce is shared git root commit — i.e. when
vesper-ioswas created by copying/cloningvesper(so they share.githistory).Project.resolve(packages/core/src/project.ts:115) falls back toroot(repo)= the first root-commit hash for repos without a usable remote; two copies share that hash → same project ID →fromDirectoryaddsvesper-iostovesper'ssandboxes→rootFor(vesper-ios)resolves tovesper. The name prefix is incidental — any copied dir collides regardless of name.Two scenarios I tested locally (in
packages/opencode/test/project/project.test.ts, modeled on the existing suite):git initrepos with different initial commits (different file content) → distinct project IDs, no collision ✅cp -R vesper vesper-ios(shared root commit, no remote) → same project ID,vesper-iosfolded intovesper.sandboxesThe shared-ID case is already asserted as intended by
project.test.ts:344("separate clones of the same repo should share project ID"), so a change that separates copies would conflict with that test and the clone-grouping design.@Hyperion2220 — could you confirm whether
vesper-ioswas created by copying or cloningvesper(e.g.cp -R,git cloneof a local path)? If they're genuinely distinct repos with different root commits, the above wouldn't explain it and I'd like to reproduce your exact setup.Happy to implement whichever direction you all want here (e.g. an explicit "open as separate project" override for explicitly-chosen dirs), but that's a design call I'll defer to the maintainers.
Hyperion2220 commentedon Aug 2, 2026
@AliAltivate Thanks for digging into this. To answer your question:
vesper-ioswas not copied or cloned fromvesper. They're independent repos with different GitHub remotes and different initial commits, and they resolve to different project IDs. Both have usable remotes, soProject.resolvereturns thegit-remote:hash and never reaches the root-commit fallback — that path isn't involved here.You're also right that it isn't a name-prefix collision. But there is a real bug: my
projecttable had two projects claiming the same worktree, with the realvesper-iosdirectory demoted to a sandbox of the iOS project. The trigger is a renamed repo, not a copied one.Cause
fromDirectorypinsworktreepermanently for every non-global project:So when a repo is renamed or moved and reopened, the new path falls through to the sandbox branch instead of correcting the worktree:
sandboxesentries are pruned against the filesystem a few lines later, butworktreeis never revalidated — so a stale path survives indefinitely, even once it belongs to a different repository.The desktop symptom follows from there.
rootFor(packages/app/src/context/layout.tsx:473) builds a sandbox → worktree map, so opening the renamed directory resolves back to the old path. And the effect atlayout.tsx:495-505removes any open project whoserootFordiffers from its own worktree, which is why the project wouldn't stay open at all. That accounts for both behaviors in my original report.Reproduction — no copying or cloning required
baris folded intofooas a sandbox, and the picker resolves it back tofoo. Any rename or move of a tracked repo reproduces it; the names don't need to share a prefix.I confirmed this against
fromDirectorydirectly on1882c33. After the rename the project ID is stable,worktreestays on the old path — which no longer exists on disk — and the renamed directory is appended tosandboxes:Test used (passes today, documenting current behavior)
The existing
project.test.tssuite (36 tests) is green alongside it.On the test conflict
I don't think this collides with
project.test.ts:344("separate clones of the same repo should share project ID"). Genuine clones still resolve to the same ID under any fix here — the question is only which directory ownsworktree. A targeted change would be: whendata.directory !== existing.worktree, re-resolveexisting.worktreefirst; if that path no longer exists, or now resolves to a different project ID, promotedata.directorytoworktreerather than appending it tosandboxes. Two live clones of one repo both still resolve to the same ID, so the grouping behavior that test asserts is unchanged.Workaround
Fixing the row directly resolves it, with OpenCode closed:
Verified on
1882c33.twittwer commentedon Aug 25, 2026
I have the same problem on Windows 11.
neriousy commentedon Oct 22, 2026
Fixed by #45043 and #45735. When a tracked repo is renamed or moved, the project's canonical directory now moves to the new path if the old one no longer exists, instead of folding the new path into the old project as a sandbox. Covered by the 'canonical directory is renamed' test in packages/core/test/project.test.ts.