Repository navigation
Conversation
STA-4343: worktree.id is repoId::path with no host, so local and SSH can publish the same id. Unqualified getWorktreeOnHostFromState used to return rows[0], which let deleting the SSH card destroy the local checkout. Fail closed when host is omitted and more than one host has the id; a single unambiguous row still resolves. The delete funnel already uses this selector, so a colliding unqualified delete now toasts and no-ops. Fixes stablyai#18279
|
Warning Review limit reachedNext included review available in 12 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
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.
✅ No new issues found.
Reviewed changes
- Selector fail-close —
getWorktreeOnHostFromStatereturnsundefinedwhenhostIdis omitted and two hosts share the id, instead of first-winsrows[0]. - Delete funnel comment —
runWorktreeDelete's STA-4343 rationale updated to reflect the fail-closed unqualified path. - Selector tests — four tests pinning each branch (unqualified collision →
undefined, named host → that row, single unambiguous → row, empty →undefined). - Funnel test — new end-to-end case asserting both disk markers survive, nothing is routed, the stale-list toast fires, and both store rows remain when no host is named.
The change is correct and well-scoped. worktreeId is repoId::path with no host, hostId is optional on legacy rows, and worktreesById buckets distinct hosts per id, so rows.length !== 1 reliably means a genuine two-host collision rather than a same-host duplicate. Every caller of this selector is a destructive path, so fail-closed is the right behavior everywhere; row callers that conditionally omit expectedHostId (e.g. HostedReviewActions.tsx:158) only land here for legacy unqualified rows, which fail-closing correctly refuses. Named-host deletes (rows.find) are untouched. I traced the one subtle secondary effect — the focus-commit check at worktree-delete-execution.ts:176 now sees activeRow as undefined on an unqualified collision — and it converges safely because focusNextWorktreeAfterActiveDelete no-ops unless activeWorktreeId is already null.
The new tests would fail against the old rows[0] behavior (the unqualified collision would resolve to the first-seeded local row and delete it), so the coverage is meaningful rather than theatre.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Thanks @fettpl for catching this and for the clear fail-closed write-up! This is already fixed on main by #33818 (0efc989), which carries your approach forward (you're credited as co-author): |

ELI5
A workspace id is
repoId::pathwith no host. Local and SSH can publish the same id for two different checkouts. An unqualified lookup used to pick the first row, so deleting the SSH card could wipe the local directory. If two hosts share an id and nobody names the host, we now refuse to guess.What Changed
getWorktreeOnHostFromStatefail-closes whenhostIdis omitted and more than one host publishes the id:undefinedhostId→rows.find(row => row.hostId === hostId)as beforerunWorktreeDeletealready looks up through this selector, so a colliding unqualified delete shows the existing "Workspace list changed" toast and does not call remove. Sidebar/row callers already passexpectedHostId: worktree.hostId.Why
STA-4343: deleting the SSH row without a host destroyed the local checkout at the same path. First-wins is silent-wrong inventory for a destructive action. Fail-closed is the honest outcome until the caller names the host. Follow-up (not this PR): key
worktreeMaponcomposeWorktreeHostIdentity.Related open #17799 is host-blind folder
connectionId, not this map. Do not merge them.Linked Issue
Fixes #18279
Visual Proof
N/A — no UI or interaction change. The user-visible difference is a stale-list toast instead of deleting the wrong host's checkout when two hosts share an id and the caller omitted the host.
Testing
pnpm test src/renderer/src/store/selectors.test.ts src/renderer/src/store/worktree-host-collision-index.test.ts src/renderer/src/components/sidebar— 290 files, 2479 passedNew selector tests failed on current
rows[0], then passed after the fail-closeNew delete-funnel test: two-host collision with no
expectedHostIdleaves both disk markers, routes to no host, toasts, keeps both store rowsExisting qualification tests still delete only the named SSH or local row
pnpm tc:web— exit 0pnpm run check:code-quality:changed— 0 findingsPlatforms actually exercised: unit tests on macOS. Collision fixtures use
localvsssh:ssh-1/ssh:build-boxandpath.join/os.tmpdir. No Electron UI session.I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Internal contributor — skip.
Review
Code-reviewer subagent: ready to merge. Selector fail-closes exactly as specified; delete funnel reuses the existing stale-list toast; tests would fail on
rows[0].path.joinandos.tmpdir.rows.lengthcheck on an already-indexed list.worktree-runtime-owner.tsleft alone: delete does not use it; no test showed a destructive fall-through to'local'.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
A collision must not delete the local checkout when the user clicked the SSH card.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Local verification used the plan commands (
pnpm tc:web, targeted tests,check:code-quality:changed). Fullpnpm lint/pnpm buildskipped per executor instructions.