Repository navigation
Conversation
📝 WalkthroughWalkthroughProject removal now carries the repository execution host ID through sidebar flows and confirmation dialogs. Repository and SSH lookups use host-aware resolution. Main worktree deletion passes the target host to the confirmation modal. Orphaned repository cleanup tracks host-qualified repository identities and passes each repository’s host ID to 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cd6f3c7a-71ca-4d69-94b9-186f508bec99
📥 Commits
Reviewing files that changed from the base of the PR and between ce20a10 and 60dca470b504a4584be2685cfca398cc7d758e7d.
📒 Files selected for processing (5)
src/renderer/src/components/sidebar/RemoveFolderDialog.tsxsrc/renderer/src/components/sidebar/WorktreeList.tsxsrc/renderer/src/components/sidebar/delete-worktree-flow.test.tssrc/renderer/src/components/sidebar/delete-worktree-flow.tssrc/renderer/src/store/slices/worktrees.ts
|
Adding my X handle per CONTRIBUTING: @manuaudiomix |
|
This one is the same defect a level up from the fix, and you're right. Fixed in 8d825a9915 by keying the set with |
When a repo id exists on two hosts and the owning host's row has dropped from the renderer catalog, findRepoForHost's unique-candidate fallback returns the remaining row regardless of host. The sidebar remove flow never passed a hostId, so removal tore down the other host's live project — killing its PTYs and purging its worktrees — while the orphan rows it was meant to clean up survived. Every caller already had the host in hand and dropped it. Carry it: from the worktree target, from the Repo row, through the confirm modal, and into removeProject. Resolve the confirmation's display name and SSH host label with findRepoForHost too, so the dialog names the row that will actually be removed. Closes stablyai#13071 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ZcrKK54K7sRnSkT6a6Jut
…o id Follow-up from review: removeProject now takes a host, but the purged-set guard still compared bare ids. With one repo id on two destroyed SSH hosts, purging the first host marked the id seen and skipped the second host's orphan row, stranding it. Key the set with getRepoHostIdentityForParts, matching the composite identity the rest of the host-aware lookups already use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ZcrKK54K7sRnSkT6a6Jut
…ject The opener carrying the host and removeProject honouring it were both covered, but nothing tested the link between them. A dropped host there would leave the fix inert while the tests at either end still passed. Also pins that a malformed host degrades to undefined rather than being handed through as a bogus execution host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ZcrKK54K7sRnSkT6a6Jut
36c0313 to
4521410
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Closing this as superseded by the broader root-cause fix in #26340, now merged through #26483. That change makes the host mandatory for every project removal, covers the orphan purge and group deletion, and closed #13071. This PR remains useful prior-art and is referenced in #26340, but rebasing it would duplicate the merged behavior. |
Closes #13071.
Summary
Removing a project from the sidebar can no longer destroy a different host's project.
When a repo id exists on two hosts and the owning host's row has dropped out of the renderer catalog, the sidebar remove flow resolved the id to whichever row was left — the other host's live one. Removal then killed that host's PTYs, purged its worktrees and dropped its row, while the orphan rows it was supposed to clean up survived. You lose the working project and keep the broken one.
Root cause
findRepoForHost(store/slices/repo-host-identity.ts:45-47) has a unique-candidate fallback: when nohostIdis supplied and exactly one row carries the id, it returns that row regardless of which host it belongs to.removeProjectalready acceptsoptions.hostIdand already scopes its RPC target and local row deletion to the owning host — the sidebar callers simply never passed it, even though every one of them had the host in hand:RemoveFolderDialog.tsx:62WorktreeList.tsx:6159Repodelete-worktree-flow.ts:224target.hostIdssh-host-remove-workspaces.ts:45-51documents this exact hazard and defends against it with an explicithostId; the sidebar flow predates that defense.What changed
delete-worktree-flow.ts— carriestarget.hostIdinto the modal, and resolves the display name withfindRepoForHostinstead of a barerepos.find.WorktreeList.tsx—handleRemoveProjectcarriesgetRepoExecutionHostId(repo).RemoveFolderDialog.tsx— reads the host from modal data, forwards it toremoveProject, and resolves the SSH host label viafindRepoForHostso the confirmation text names the row that will actually be removed.worktrees.ts— the orphaned per-workspace-env purge keeps each row's own host instead of mapping down to bare ids.The host is narrowed out of modal data with
normalizeExecutionHostIdrather than cast, so a malformed value degrades to today's behavior instead of lying to the type system.Screenshots
No visual change. The only user-visible difference in text is that the removal confirmation can now name the correct SSH host.
Testing
pnpm lintpnpm typecheckpnpm test— 5 pre-existing failures, evidence belowpnpm buildRebased onto current
mainand re-run from scratch:pnpm lint,pnpm typecheckandpnpm buildall clean, and 4331 tests across the touched trees (components/sidebar/,store/slices/) pass in 370 files.On the full-suite failures
pnpm testexits 1 on this branch. Rather than assert that's unrelated, I ran a full-suite baseline on cleanmainand a second full run on the branch:maincheck-root-directory-entriesagent-exec-handlerrun-electron-vite-dev-webremote-runtime-shared-control-connectionorchestration-adopted-run-bindingai-vault-session-worktree-mapOnly the first two are constant, and they reproduce identically on clean
main, so they are local-environment artifacts rather than regressions: the root-directory guard shells out to a script usingdeclare -A(bash 4+) while macOS ships bash 3.2, andagent-exec-handlerasserts the spawned env contains all ofprocess.env, which fails on any dev machine whose shell exports a var the handler overrides (e.g.GIT_ASKPASS).The rest reshuffle between runs, which is the tell — a regression fails deterministically. Every one of them is a wall-clock or throughput budget, not a behavioural assertion:
orchestration-adopted-run-binding—expected 0.107 to be less than 0.0887(scaling ratio)ai-vault-session-worktree-map—expected 156.2 to be less than 150(a 4% miss on a 150ms budget; runs in 25ms in isolation)remote-runtime-shared-control-connection— throughput under a stalled socketRun 1 took 389s against the baseline's 294s because a full
pnpm buildhad just finished on the same machine; run 2, unloaded, came in at 298s and cleared all three. Each passes in isolation on this branch.None of these files import any of the six this PR changes — it is renderer-only, four files under
components/sidebar/plus one store slice. Flagging the pattern rather than burying it, in case the timing budgets are worth loosening on CI runners.New regression test in
delete-worktree-flow.test.tsmodels the issue's scenario directly: host A's row dropped from the catalog, host B's live row the only one left under the id. It asserts the modal carrieshostId: 'ssh:host-a'and that the confirmation names host A's project — host B's name appearing is the tell that resolution went to the wrong row.Verified non-blind: reverting the plumbing makes it fail with exactly the bug —
On a full
pnpm testrun, the handful that don't pass are local-environment artifacts that reproduce on a clean checkout with this branch stashed:check-root-directory-entries×3 (the guard shells out to a script usingdeclare -A, and macOS ships bash 3.2) andagent-exec-handler×2 (asserts the spawned env contains all ofprocess.env, which fails on any dev machine whose shell exports a var the handler overrides, e.g.GIT_ASKPASS).AI Review Report
Reviewed with Claude Opus 5 against the CONTRIBUTING checklist.
metaKey/ctrlKeybranch. This is identity plumbing between renderer modules, so there is nothing for a platform check to guard.ssh:<target>host.removeProject's existing per-host RPC targeting andremoveForHostbranch are unchanged — they simply now receive the host the user actually meant. No wire format changes, so mixed client/host versions are unaffected.isMainWorktreebranch and now carries its host.findRepoForHostreplacesArray.findwith a filter over the same list, on a user-initiated removal. Not a hot path.What it flagged and I acted on: the first cut resolved the host with a cast out of modal data. That silently accepts a malformed value, so it now goes through
normalizeExecutionHostIdand degrades to the previous behavior instead.Security Audit
Reviewed for input handling, command execution, path handling, auth, secrets, dependency and IPC risk.
normalizeExecutionHostId, not cast, so a malformed value falls back rather than propagating into an RPC target.removeProject's existing channel and itshostIdparameter are unchanged; this only populates a parameter that was already there and already validated on the other side.No follow-up needed for this PR. The related gap worth its own change is noted below.
Notes
On the
worktrees.tschange. Flagged as possibly out of scope, so to be explicit: this is the same defect, not adjacent cleanup. #13071's title is "removeProject without a hostId can delete the wrong host's project". The orphaned per-workspace-env purge callsremoveProject(repoId)with no host while iterating fullRepoobjects — so it hits that exact fallback and can purge another host's row. The issue body enumerates the three sidebar sites; this is a fourth caller of the same function with the same omission, and it had the host in hand. Leaving it out would fix the bug on three paths and leave it live on a fourth. Happy to split it into its own PR if you'd rather keep this one to the enumerated sites.What I deliberately did not do. The issue also suggests making
removeProjectrefuse the unique-candidate / focused-host fallbacks for removals. I left that alone because it would silently break group removal:selectProjectGroupRemovalTargets(store/slices/project-group-removal-targets.ts:26-30) iterates fullRepoobjects and pushesrepo.id, discarding the host — soremoveProjectis legitimately called there with no host available. A blanket refusal would turn those calls into silent no-ops and strand the projects. That site is a fifth instance of the same defect class and deserves its own fix (carry the host throughProjectGroupRemovalTargets), but it changes a shared selector's shape and its callers, so it does not belong here. Glad to follow up with it, or to add the guard here once that site can supply a host.X: @manuaudiomix