Skip to content

fix(worktree): require host-qualified identity for workspace deletion (STA-4448) - #14930

Closed
brennanb2025 wants to merge 3 commits into
mainfrom
sta-4448-host-qualified-delete
Closed

brennanb2025 wants to merge 3 commits into
mainfrom
sta-4448-host-qualified-delete

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​836 $\color{#cf222e}{\Huge{\mathbf{−}}}$​131 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​705
Prod 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​389 $\color{#cf222e}{\Huge{\mathbf{−}}}$​43 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​346

ELI5

A workspace's id is just repoId::path. It has no host in it. So your laptop, an SSH box and a paired
runtime can all publish the same id for what are three different checkouts. When you clicked delete
on the remote row, Orca looked up "which host?" by asking the active workspace — and got your local
checkout. It deleted that instead, uncommitted work and all, while the row you actually picked stayed on
screen. This PR makes the delete carry the host you confirmed, and refuses when the delete would land
somewhere else.

What Changed

Host qualification is now mandatory at the single chokepoint every delete surface shares,
removeWorktree.

  • src/shared/worktree/removal.ts — new WorktreeRemovalTarget ({ id, executionHostId }),
    toWorktreeRemovalTarget(), and the pure verdict function verifyWorktreeRemovalHost().
    executionHostId is required but nullable: null is a caller stating "this row declares no
    host" (legacy/snapshot rows, folder workspaces), not a caller forgetting the field.
  • remove-worktree.ts — removeWorktree(target, …) now takes the qualified target and calls the new
    refuseWrongHostWorktreeRemoval() guard before any destructive work. The confirmed host outranks the
    route.
  • worktree-operation-route.ts — collectWorktreeOwnerExecutionHostIds() enumerates every host that
    claims an id (unqualified rows contribute nothing, so migration can't invent collisions).
  • All destructive callers migrated to pass a host-qualified target.
  • worktree-delete-request.ts — WorktreeDeleteIdentity carries hostId, and batch-target resolution
    rejects a request whose confirmed hostId no longer matches the row a refresh swapped in.
  • worktree-removal-result.ts — extracted RendererRemoveWorktreeResult out of worktree-helpers.ts
    (import-cycle avoidance; no behavior change).
  • 16 new outcome tests, plus a shared collision fixture.

Omitting the host is a type error, so a future delete entry point cannot be written unguarded.

Why

P0 data loss. A workspace id is repoId::path with no host component, so local, SSH and paired
runtime rows can share one id. Removal routing short-circuits to the ACTIVE workspace's host, and
activeWorktreeId === id holds for a colliding id no matter which row was confirmed — so confirming the
remote row issued the destructive IPC against the local checkout and destroyed its uncommitted
work, while the row the user picked survived.

Why not another caller-side guard. #14731 guarded a caller (the cleanup preflight), which is
exactly why every other delete surface sailed straight past it. Guarding one more caller would have the
same shape and the same failure mode. This puts qualification at the one chokepoint they all share and
lets the type system enforce it.

Approach (a): fail closed, never reroute. When the confirmed host is not where the removal would
land, refuse. Rerouting a colliding row to its correct host is #14606's job and needs evidence this
layer does not have — and #14606 can now adopt this contract rather than reconcile with it.

forget-local is deliberately exempt. It removes no files and no Git state; it drops the workspace
from Orca's own records. It must keep working precisely when the owning host is gone and routing can
no longer resolve — which is the one situation the guard would refuse in.

Refusal copy reuses #14731's existing strings (auto.store.slices.workspace.cleanup.hostCollision /
.hostUnresolved). No new user-facing strings; en.json is untouched.

Per-caller verdict table (all 8 destructive callers)

# Caller Verdict How the host is obtained
1 delete-worktree-flow.ts → skip-confirm delete Qualified toWorktreeRemovalTarget(target) from the resolved row
2 worktree-delete-execution.ts → runWorktreeDeleteWithToast (confirmed delete, and the batch path) Qualified takes a WorktreeRemovalTarget; batch uses toWorktreeRemovalTarget(target)
3 worktree-delete-execution.ts → in-toast Force Delete retry Qualified reuses the same confirmed target
4 DeleteWorktreeDialog.tsx → in-dialog Force Delete Qualified looks the confirmed row up in currentWorktrees; returns early if the row is gone rather than force-deleting a bare id
5 workspace-cleanup.ts → cleanup removal Qualified scannedHostId — the host the scan actually inspected
6 WorkspaceSpaceManagerPanel.tsx → Space-manager force recovery (not in the ticket) Qualified worktree.executionHostId from the scan row (the panel lists one row per host)
7 ssh-host-remove-workspaces.ts → clear all workspaces for an SSH host (not in the ticket) Qualified toSshExecutionHostId(resolution.targetId) — every row in the resolution is pinned to that target
8 ForgetSshWorkspaceDialog.tsx → forget / delete for a disconnected SSH host (not in the ticket) Qualified; the forget path is exempt by design the dialog only opens for a workspace pinned to a named SSH target

Callers 6, 7 and 8 are the three the ticket does not mention. They were reachable by the same mechanism.

Out of scope, called out so it isn't mistaken for covered: the mobile client calls worktree.rm
directly over the wire and is not covered by this renderer guard. It is not exposed to this defect's
mechanism — a mobile session is bound to one host, so the transport is the host selection — and
qualifying it would be a wire change requiring capability negotiation per
docs/reference/remote-wire-compatibility.md.

Linked Issue

STA-4448. Related: #14731 (the caller-side guard this generalizes), #14606 (rerouting — can adopt this
contract).

Fixes #

Visual Proof

N/A — no new UI and no new copy. The only user-visible change is that a delete which would have hit
the wrong host now refuses using #14731's existing refusal strings instead of destroying the wrong
checkout. See KNOWN GAPS below regarding rendered-UI validation.

Testing

Red-first evidence (recorded by the implementing agent, on this exact code):

  • Guard removed → 8/16 RED, with the failure
    the ACTIVE local checkout must survive … removal was routed to hostId=local: expected false to be true
    — i.e. the test observes the actual data-loss mechanism, not a message string.
  • Production fully reverted → 14/16 RED. That non-vacuity count is the number that matters: the
    tests fail for the real reason, not because a type changed shape.

Automated (all run locally in this worktree):

  • pnpm run typecheck — clean across all 3 projects (node, cli, web).
  • The 6 directly affected specs — 6 files / 65 tests passed:
    worktree-removal-host-qualification.test.ts (12 new), worktree-delete-host-qualification.test.ts
    (4 new), worktrees-remote-runtime-removal.test.ts, store-worktree-removal-cascade.test.ts,
    workspace-cleanup-wrong-host-removal-guard.test.ts, delete-worktree-parallel-flow.test.ts.
  • Wider blast radius, because the rebase pulled in 29 commits of main:
    src/renderer/src/store/slices — 263 files / 2624 tests passed;
    src/renderer/src/components/sidebar — 249 files / 2179 tests passed. Zero failures, so
    nothing to classify as pre-existing.
  • Lint/format: oxlint --type-aware and oxfmt on all 29 changed files. Note: the
    check:code-quality:changed / check:react-doctor:changed wrappers crash on this box — it runs
    node v26 against the repo's required node 24, so pnpm's engine WARN lands inside oxlint's JSON
    stdout and breaks the parse. That is pre-existing and unrelated to this change, so the underlying
    oxlint scans were run directly instead. The one oxlint finding
    (workspace-cleanup.ts:217 no-unnecessary-boolean-literal-compare) is on a line this PR does not
    touch and blames to fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343) #14731 on main — pre-existing.

Rebase note, and it doubles as evidence the approach works: rebasing onto current main pulled in
two test call sites main had added while this branch was based on an older commit. They were still
passing a bare string id. The type system caught both (TS2345) instead of letting them reach
removeWorktree and crash on removalTarget.id. That is exactly the failure mode approach (a) was
chosen to make impossible for future entry points. Both were migrated to their row's actual host
(local and ssh:shared-target) rather than null, preserving the two-distinct-hosts scenario the
test is named for.

Platforms: the change is renderer-side host-identity logic with no path or shell handling, so it is
platform-agnostic; verified on Linux. SSH and paired-runtime rows are covered directly by the new
tests, and folder workspaces are covered by the unqualified-target path (still deletes while the id has
at most one declared owner).

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

KNOWN GAPS — stated plainly

  1. No rendered-UI validation. The $electron skill is not installed on this box and there is no
    desktop session. Per standing orders, computer-use was not substituted. Risk is low because
    there is no new UI and no new copy, but this is a real gap, not a formality.
  2. Real two-host field validation is outstanding. All filesystem-level evidence here comes from the
    test suite, not from two live hosts sharing a path.
  3. Mobile is not covered by this guard — see the out-of-scope note above.

AI Disclosure

Review

Ensure no issues in: Security, Cross-platform support (Linux, Windows, Mac), Remote SSH, Mobile, general
backwards compatibility, performance.

  • Security / data integrity: this is the point of the PR — it closes a path that destroyed the wrong
    host's uncommitted work.
  • Backwards compatibility: unqualified (executionHostId: null) targets still delete while the id
    has at most one declared owner, so pre-host-qualified snapshot rows and folder workspaces keep
    working. No wire change; no IPC signature change.
  • SSH / remote: covered by new tests including HUB-owned SSH rows and paired-runtime transport.
  • Mobile: unchanged and uncovered — see above.
  • Performance: the new owner enumeration is one pass over already-loaded in-memory state, on the
    delete path only.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm typecheck and the relevant test suites pass locally; lint run directly (see Testing).
    pnpm build not run locally — left to CI.

Author

🤖 Generated with Claude Code

… (STA-4448)

A workspace id is `repoId::path` with no host component, so the local host,
an SSH host and a paired runtime can all publish the SAME id. Removal routing
short-circuits to the ACTIVE workspace's host, and `activeWorktreeId === id`
holds for a colliding id no matter which row was confirmed — so confirming the
remote row issued the destructive IPC against the LOCAL checkout and destroyed
its uncommitted work, while the row the user picked survived.

#14731 guarded a caller (the cleanup preflight), which is why every other
delete surface sailed past it. Make host qualification MANDATORY at the one
chokepoint they all share instead: `removeWorktree` now takes a host-qualified
`WorktreeRemovalTarget` and fails closed whenever the confirmed host is not
where the removal would actually land. Omitting the host is a type error, so a
future entry point cannot be written unguarded.

Fail closed, never reroute — rerouting a colliding row to its right host is
#14606's job, and it can adopt this contract rather than reconcile with it.

forget-local stays exempt: it removes no files or Git state, and it must keep
working precisely when the owning host is gone and routing cannot resolve.

Refusal copy reuses the strings #14731 added, so both delete surfaces explain
a wrong-host refusal in the same words.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd1532b0-ecb7-4a2b-97ff-bb48abcd926e

📥 Commits

Reviewing files that changed from the base of the PR and between fd52aef and b8bdfb2.

📒 Files selected for processing (2)
  • src/renderer/src/store/slices/worktree-removal-host-qualification.test.ts
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Worktree removal now uses structured targets with execution-host identity. Shared validation rejects host collisions and unresolved routing before destructive removal, while forget-local remains exempt. Sidebar, cleanup, SSH, status-bar, automatic, parallel, and force-delete flows pass host-qualified targets. Existing tests were updated, and new tests cover collision, routing, legacy, runtime, SSH, local, and folder-workspace removal paths.

Merge Risk: 🟡 Moderate · up to b8bdf

The PR hardens host qualification for deletion, but batch deletion still has a bounded correctness risk: using only the worktree ID can drop the selected row when two hosts share that ID. This should be fixed or explicitly accepted before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: requiring host-qualified identity for worktree deletion.
Description check ✅ Passed The description follows the template, explains the change and rationale, documents testing and known gaps, and identifies the linked issue.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/shared/worktree/removal.ts (1)

127-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Condense the host-qualification and collision-test comments. Keep the non-obvious host-collision invariant and filesystem/transport-routing rationale, but remove step-by-step walkthroughs and fixture details that are already expressed by the code, test names, and assertions. Prefer brief comments focused on non-obvious behavior.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e4094f30-a842-46a8-8ade-9c13f2be104f

📥 Commits

Reviewing files that changed from the base of the PR and between 516a269 and 83393f7.

📒 Files selected for processing (29)
  • src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx
  • src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.tsx
  • src/renderer/src/components/sidebar/delete-worktree-flow.test.ts
  • src/renderer/src/components/sidebar/delete-worktree-flow.ts
  • src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts
  • src/renderer/src/components/sidebar/ssh-host-remove-workspaces.ts
  • src/renderer/src/components/sidebar/worktree-delete-execution.ts
  • src/renderer/src/components/sidebar/worktree-delete-host-qualification.test.ts
  • src/renderer/src/components/sidebar/worktree-delete-request.ts
  • src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx
  • src/renderer/src/lib/worktree-operation-route.ts
  • src/renderer/src/store/slices/editor-state-worktree-purge-leak.test.ts
  • src/renderer/src/store/slices/generation-records-worktree-removal-leak.test.ts
  • src/renderer/src/store/slices/native-chat-launch-draft-teardown.test.ts
  • src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts
  • src/renderer/src/store/slices/tab-worktree-orphan-map-purge-leak.test.ts
  • src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts
  • src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal-guard.test.ts
  • src/renderer/src/store/slices/workspace-cleanup.ts
  • src/renderer/src/store/slices/worktree-helpers.ts
  • src/renderer/src/store/slices/worktree-removal-host-collision-fixture.ts
  • src/renderer/src/store/slices/worktree-removal-host-qualification.test.ts
  • src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts
  • src/renderer/src/store/slices/worktree-removal-result.ts
  • src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts
  • src/renderer/src/store/slices/worktrees-removal-state-cleanup.test.ts
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree-host-guard.ts
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts
  • src/shared/worktree/removal.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 3 remain after this review.

Comment on lines +39 to +42
targets: readonly Pick<
Worktree,
'id' | 'instanceId' | 'displayName' | 'repoId' | 'path' | 'hostId'
>[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use host-qualified identity for batch deletion.

hostId is now part of the deletion identity. However, Line 46 still deduplicates by target.id, and Line 82 still resolves the current row by that ID. If two confirmed targets have the same ID on different hosts, the later target replaces the earlier target. One selected workspace is then skipped.

Key deduplication and freshness lookup by both id and hostId. Add a regression test with two hosts that share one worktree ID.

brennanb2025 added a commit that referenced this pull request Aug 17, 2026
… host

Adds the end-to-end sidebar case #14606 had no coverage for at all: the
reported user path, driven through the real runWorktreeDelete /
runWorktreeDeletesInParallel funnel into the real store action, with real
per-host temp directories that the transports actually rm -rf.

The decisive assertion is two-sided — the confirmed SSH checkout is GONE
and the ACTIVE local one SURVIVES. #14930 could only prove the second half.

Two production gaps this surfaced:
- the two sidebar row entry points (card quick action, context menu) passed
  a bare id, so with two rows rendered either one deleted the same
  workspace. They now name their row's host via expectedHostId.
- the batch path re-checked staleness through the id-keyed map, so on a
  collision it read the other host's row, found a mismatched instanceId,
  and silently dropped a delete the user confirmed. It now revalidates on
  (id, host) via getWorktreeOnHostFromState.

Cleanup guard gains force coverage via describe.each — force is the least
re-checked destructive path — plus a no-false-refusal control proving an
ordinary single-host delete still works under force.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual proof — what the refusal looks like, and a finding that changes the case for this PR

Rig: isolated dev instance per branch (ORCA_DEV_USER_DATA_PATH, fresh profile confirmed by repos.list() === []), scratch repo, a real child worktree, then a same-id twin row seeded on a second execution host (ssh:qa-linux). Sidebar right-click → Delete → confirm.

This PR (83393f7d54)

refusal-toast.png

Failed to delete workspace collide
Workspace identity is ambiguous across hosts. Refresh projects and try again.

Both workspaces survive. Data is safe.

But current main does the same thing

main-refusal.png

Same repro on main (7b4e10b104), without this PR — byte-identical toast, both workspaces survive:

Failed to delete workspace collide-2
Workspace identity is ambiguous across hosts. Refresh projects and try again.

createRemoveWorktree on main already refuses when resolveWorktreeOperationRoute returns null (WORKTREE_REMOVAL_AMBIGUOUS_ERROR). So for the case I could exercise, this PR adds no new prevention.

What this PR does add — and why my test could not reach it

The distinction is which failure the guard catches:

case main this PR
route unresolvable refuses ✅ refuses ✅
route resolves, but to a different host than the user confirmed cannot detect — main takes no confirmed host at all; removeWorktree(worktreeId, force, options) has nothing to compare against refuses ✅

The second row is the data-loss case: the delete lands on a real host that isn't the one the user pointed at. verifyWorktreeRemovalHost is exactly that comparison:

if (args.routeExecutionHostId === args.confirmedExecutionHostId) return { kind: 'allowed' }
return { kind: 'refused', reason: collides ? 'collision' : 'unresolved' }

My fixture cannot exercise it. The synthetic ssh:qa-linux host does not resolve, so routing returns null and main's pre-existing guard fires first. Reaching the real case needs two genuinely reachable hosts — a local checkout plus a live SSH host at the same path. That leg is unrun, and it is the leg that actually justifies this PR.

Pre-existing defect, NOT introduced here

On both branches the refused row is left permanently showing a greyed-out spinner and "Deleting…" — deleteStateByWorktreeId keeps { isDeleting: true, phase: 'deleting' }, still stuck 40s later. The toast that carries the explanation auto-dismisses, so the end state a user is left with is a workspace that looks like it is mid-delete forever. Reproduced identically on main, so it is not a regression from this PR — but it is what the refusal actually looks like once the toast goes.

Fixture caveat

An earlier malformed seed of mine (a twin row spread from an undefined source, so no path) crashed WorktreeCard via parseWorkspaceKey and latched the sidebar error boundary. That was my fixture's fault, not a product defect — recorded so nobody chases it. All results above are from a clean re-seed after a reload.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Follow-up: the wording above is being replaced — #15178

The toast in the screenshots above is bad copy and it is now fixed in #15178 (off main, since the refusal is pre-existing there and not introduced by this PR).

It had three problems: "Workspace identity is ambiguous across hosts" is implementation vocabulary; "Refresh projects and try again" is a remedy that cannot work, because refreshing does not disambiguate two real checkouts; and View opens the workspace diff by an id both machines share, so it either does nothing useful or opens the other machine's changes.

The new toast

final2.png

Failed to delete workspace feature-auth
This workspace exists on more than one machine at the same path, so Orca can't tell which one to remove. Nothing was deleted. We're improving this — for now, delete it directly on the machine you want it gone from.

View is hidden for this case, and it now renders as info rather than a destructive error, because nothing was lost.

The suggested remedy is verified rather than assumed: removing the checkout on the machine clears the collision on Orca's next scan, with no orphaned row and no Force Delete needed.

The stuck "Deleting…" row noted above is still unaddressed and still reproduces on main; it needs its own fix, since the toast auto-dismisses after 10s and that row is what the user is ultimately left looking at.

The refusal said 'Workspace identity is ambiguous across hosts. Refresh
projects and try again.' — implementation vocabulary, and the remedy does
not work: refreshing cannot disambiguate two real checkouts, and the View
action opens the diff by an id both machines share.

Say what happened to their workspace, confirm nothing was lost, and give
the one step that does resolve it. Verified: removing the checkout on the
machine clears the collision on the next scan, with no orphaned row.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

The refusal wording is fixed in this PR now — fd52aef

The toast in my screenshots above was bad copy. Rewritten and pushed to this branch.

Three problems with the old text:

  1. "Workspace identity is ambiguous across hosts" — implementation vocabulary. Execution-host routing is Orca's problem, not the user's.
  2. "Refresh projects and try again" — a remedy that cannot work. Refreshing does not disambiguate two real checkouts at the same path, so it sent the user around a loop that always ends back here.
  3. View — opens the workspace diff by an id both machines share, so it either did nothing useful or opened the other machine's changes.

The new toast

final2.png

Failed to delete workspace feature-auth
This workspace exists on more than one machine at the same path, so Orca can't tell which one to remove. Nothing was deleted. We're improving this — for now, delete it directly on the machine you want it gone from.

View is hidden for this case, and it renders as info rather than a destructive error, because nothing was lost.

The remedy is verified, not assumed: I removed the checkout on the machine and Orca's row resolved on the next scan — no orphaned row, no Force Delete needed. Worth checking before putting an instruction in shipped copy.

What changed

isAmbiguousHostRemovalError in shared/worktree/removal.ts alongside the existing lock/PTY predicates; a case in getDeleteWorktreeToastCopy following the same shape as the locked-by-Git and running-terminals cases; the showViewChanges gate; the string in en.json. 4 files, +43/-2.

typecheck 0 · sidebar + shared/worktree suites 262 files / 2,337 tests green · copy confirmed in a live dev instance (screenshot above).

Still open

The stuck "Deleting…" row from my earlier comment is unchanged and still reproduces on main. Since this toast auto-dismisses after 10s, that greyed-out row is what the user is ultimately left looking at. It needs its own fix — say the word and I'll add it here too.

Callers mark rows deleting up front for immediate sidebar feedback, but
both refusals return before removeWorktree's try/catch, so nothing ever
cleared the flag. The toast auto-dismisses after 10s and the workspace was
left on a 'Deleting…' spinner indefinitely — with the explanation already
gone.

Pre-existing on main; the new copy made it more visible.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Superseded by #15013, which merged as 64de8dd.

This PR's core was a fail-closed refusal when a colliding id could not be routed. #15013 does the better thing per Brennan's ruling — it routes the delete to the confirmed host instead of refusing — so the refusal logic here is redundant.

The two things on this branch that #15013 did not include are handled separately:

Branch kept for history; nothing here is stranded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant