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
{{ message }}
Repository navigation
removeProject without a hostId can delete the wrong host's project when repo IDs are duplicated across hosts #13071
Found while investigating #12875. This is a pre-existing bug, independent of the orphaned-workspace fix (docs/orphaned-workspace-removal.md scopes it out).
Scenario: repo ID R exists on host A and host B. Host A's row disappears from the renderer catalog (refresh drop) while its worktrees remain; host B's row is live. The user removes the "host A" project from the sidebar.
What happens: the sidebar remove flow never passes options.hostId:
WorktreeList.tsx:~6159 — has the full Repo in hand and discards its host
delete-worktree-flow.ts:~224 — has target.hostId in hand and drops it
With no hostId, findRepoForHost (repo-host-identity.ts:45-47) uses its unique-candidate fallback: exactly one row with that ID → return it regardless of host. So it returns host B's live row, and removeProject (repos.ts:3291+) proceeds to kill B's PTYs, purge B's worktrees, and drop B's row. Net: the working host-B project is destroyed and the host-A orphan rows survive.
ssh-host-remove-workspaces.ts:45-51 documents this exact hazard and defends against it by passing an explicit hostId; the sidebar flow predates that defense.
Related smaller bugs in the same flow:
RemoveFolderDialog.tsx:35 resolves the SSH host label with a bare first-by-id repos.find — can name the wrong host in the confirmation text.
delete-worktree-flow.ts:222 uses the same bare find for the display name, while the sidebar header uses last-write-wins getIndexedRepoMap — header and dialog can describe two different rows of the same ID.
Fix sketch: carry hostId in the confirm-remove-folder modal data (both openers already hold it), forward it from the dialog, and make removeProject refuse the unique-candidate/focused-host fallbacks for removals.
It carries the host through the three sidebar sites you listed, plus a fourth caller with the same omission: the orphaned per-workspace-env purge in worktrees.ts calls removeProject(repoId) bare while it's already iterating full Repo objects, so it lands on the same unique-candidate fallback.
One thing I deliberately left alone, since you raised it: making removeProject refuse the fallbacks for removals would break group removal. selectProjectGroupRemovalTargets pushes repo.id and drops the host, so removeProject is legitimately called there with nothing to pass — a blanket refusal turns those into silent no-ops and strands the projects. That site wants the host plumbed through ProjectGroupRemovalTargets first, which changes a shared selector's shape, so it felt like its own PR rather than something to smuggle into this one. Happy to pick it up either way.
Regression test models your scenario directly — host A's row dropped from the catalog, host B's row the only one left under the id — and asserts both the hostId the modal carries and the name in the confirmation, since host B's name showing up is the tell that resolution went to the wrong row. Reverting the plumbing fails it.
Rebased onto main and re-ran everything just now: lint, typecheck, build and the sidebar/store suites are clean. Worth mentioning that the workflows on it are still parked on "approve and run", so there's no CI signal on the PR yet — if someone can kick that off I'll chase down anything it turns up.
Activity
fix(sidebar): remove the project on its own host, not another host's row
manuaudio commentedon Aug 9, 2026
Fix is up in #13117.
It carries the host through the three sidebar sites you listed, plus a fourth caller with the same omission: the orphaned per-workspace-env purge in
worktrees.tscallsremoveProject(repoId)bare while it's already iterating fullRepoobjects, so it lands on the same unique-candidate fallback.One thing I deliberately left alone, since you raised it: making
removeProjectrefuse the fallbacks for removals would break group removal.selectProjectGroupRemovalTargetspushesrepo.idand drops the host, soremoveProjectis legitimately called there with nothing to pass — a blanket refusal turns those into silent no-ops and strands the projects. That site wants the host plumbed throughProjectGroupRemovalTargetsfirst, which changes a shared selector's shape, so it felt like its own PR rather than something to smuggle into this one. Happy to pick it up either way.Regression test models your scenario directly — host A's row dropped from the catalog, host B's row the only one left under the id — and asserts both the
hostIdthe modal carries and the name in the confirmation, since host B's name showing up is the tell that resolution went to the wrong row. Reverting the plumbing fails it.Rebased onto main and re-ran everything just now: lint, typecheck, build and the sidebar/store suites are clean. Worth mentioning that the workflows on it are still parked on "approve and run", so there's no CI signal on the PR yet — if someone can kick that off I'll chase down anything it turns up.
fix(projects): remove a project only on the host the user picked (#26340
Phase 3.5: fix the top SSH/remote issues at the root cause (stablyai#…