Skip to content

fix(wsl): keep a stopped distro's worktrees and re-list once it runs - #35733

Merged
nwparker merged 2 commits into
mainfrom
nwparker/wsl-worktree-scan-stopped
Oct 29, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/wsl-worktree-scan-stopped

Conversation

@nwparker

@nwparker nwparker commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​374 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​374
Prod 26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​368 $\color{#cf222e}{\Huge{\mathbf{−}}}$​15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​353

ELI5

When a WSL distro is stopped, Orca won't start it just to list worktrees. Before this change, the sidebar treated that refusal as an error and dropped the project's worktrees until someone pressed Retry. Now it keeps showing the worktrees it already knew about, calls the stopped distro a state instead of a failure, and refreshes the list by itself once the distro is running.

What Changed

Before: relaunch Orca with a WSL distro stopped, and every project in that distro shows a red "Worktree scan failed for pN … Retry" warning with no worktree rows. Nothing refreshes them until Retry is pressed after the distro has started.

After: those projects keep their rows. A grey power icon says "WSL distro Ubuntu is stopped", or "Couldn't confirm WSL distro Ubuntu is running" when WSL didn't answer. The list refreshes once a user action starts the distro (for example, opening a terminal in it) or the next running-distro read shows it running. Clicking the icon is itself a user action: it starts the distro and scans again.

Mechanism:

  • Host listing (detected-provider-listing.ts, wsl-stopped-worktree-listing.ts): when the scan fails with the gate's stopped or unverifiable refusal, the listing returns rows rebuilt from stored worktree metadata, plus the main checkout. These are the same rows a disconnected SSH repo gets. The result is non-authoritative, so nothing treats it as proof that a worktree was deleted. A new optional field, wslDistroStopped: { distro, state }, tells the client why. unavailableReason and failureKind keep their old values.
  • Re-scan (wsl-stopped-worktree-listing.ts, wsl-stopped-worktree-rescan-registry.ts): the listing registers a re-scan, keyed worktree-scan:<repoId>, with the existing background-distro gate. It fires on a settled confirmed-running read or on a 'woken' note, then sends worktrees:changed (to the window and remote clients) so the client lists again. It never probes. Each repo holds at most one pending re-scan. A successful scan drops it, and so does removing the repo, from the same store removal path that retires the repo's scan generation.
  • Client (fetched-worktree-merge.ts): when the client already has scanned rows, a stopped-distro result keeps them, because stored rows have no branch information. A fresh launch with no rows shows the stored ones.
  • Retry is a user act (wsl-distro-starting-scan.ts, IPC worktrees:scanStartingWslDistro): the click runs the scan inside runAsWslUserAct for the repo's distro, so it may start the distro, then re-lists. Before, Retry re-ran the gated background scan, which could never start it, even though the refusal text said "Retry… to start it". The bridge method is optional, so the web client and older hosts simply re-list.
  • Sidebar (RepoWslDistroStoppedIndicator.tsx): neutral text-muted-foreground icon, no destructive color. Strings are added in all 11 locales.

Folder workspaces never run Git and are unchanged; a test pins this.

Why

The gate's refusal is expected E9 behavior, not a failure. The alternatives were:

  • A new failureKind value: older clients reject unknown values when they validate a listing, so this would break them.
  • Keeping worktrees: [] and relying on the client to keep its rows: a fresh launch has no rows to keep, which is exactly this bug.

An optional field plus non-authoritative rows is wire-compatible. An older client sees the non-authoritative rows and the old warning text, and it still prunes nothing. Old-client behavior was audited:

  • The client purge, the missing-worktree terminal sweep, and main's invalid-workspace layout purge all require authoritative: true.
  • The existing test "does not purge remembered state from a non-authoritative partial refresh" pins the old client path.
  • This PR adds a stopped-distro case to the main-side purge test.

Linked Issue

N/A (internal, E9 background-boot work)

Visual Proof

Pending a Windows/WSL run by the WSL verification lane. Earlier evidence of the bug: p2-after-retry.png. Unit tests render both new indicator states.

Testing

  • src/main/ipc/worktrees/listing/wsl-stopped-worktree-listing.test.ts checks:

    • stored rows plus the main checkout, and the flag, for both stopped and unverifiable
    • other failures unchanged
    • re-scan fires once on a settled running read and once on a wake, never for another distro, and with no wsl --list probe
    • one pending re-scan per repo
    • a successful scan drops the pending re-scan
    • folder workspaces unchanged
    • Retry runs its scan inside the distro's user act, and does nothing for folder workspaces, repos outside WSL, or unknown repos
  • persistence-repo-lifecycle.test.ts: removing the repo drops its pending re-scan.

  • persistence-invalid-workspace-layout-purge.test.ts: stopped-distro rows purge no workspace layout.

  • worktrees-fetch-listing-merge.test.ts: the client keeps scanned rows and their tabs and records the flag.

  • RepoScanUnavailableIndicator.test.tsx: both states render neutral, and a click starts the distro first, then re-lists.

  • Ran pnpm tc, pnpm run typecheck:ci, check:code-quality:changed, the anti-slop oxlint, and import boundaries. Tested on macOS only; the WSL runtime path needs the Windows lane.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Reviewers: e9-reviewer, fix-reviewer-2.

User-risk: low. This only changes listings whose scan the E9 gate refused. Those listings are non-authoritative, so no path prunes from them. The remaining risk is metadata-fallback visibility: as with disconnected SSH, it fails open, so an external worktree a user hid could show while the distro is stopped.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Paired runtime clients list through a separate runtime path that this PR does not change.
  • No change to dead panes or toasts; that work lives elsewhere.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • 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 lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The changes affect worktree listings and refresh actions for local repos tied to WSL distros.

Fix the two outstanding findings before merging: stopped-distro rows can clear review links, and Retry can leave WSL stopped.

Findings

  1. P1 Stopped distro clears review links ▶
  2. P1 Start button leaves WSL stopped ▶

Summary

Keeps remembered worktrees visible when WSL refuses a background scan.

  • Stopped WSL repos keep their known worktree rows.
  • Worktree lists refresh when a stopped WSL distro wakes.
  • The sidebar can start a stopped WSL distro and scan its repo.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Background scan] --> B{WSL permits access?}
  B -->|Yes| C[Return scanned worktrees]
  B -->|No| D[Return remembered rows without proof of deletion]
  D --> E[Wait for wake or confirmed running read]
  E --> F[Notify clients to list again]
  D --> G[User clicks start and scan]
  G --> H[Scan inside WSL user-act scope]
  H --> A
Loading

Reviews (3) · Last reviewed commit: "fix(wsl): make the stopped-distro Retry ..." · Reviewed by Greptile

Comment on lines +215 to +217
// Stored rows from a stopped WSL distro lack branch state the scanned rows already have.
const keepCurrent = worktrees.length === 0 || !!args.refresh.result.wslDistroStopped
if (!args.refresh.result.authoritative && keepCurrent && currentForHost.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stopped distro clears review links

This guard runs too late to preserve scanned rows. When an existing worktree has linkedPR or pushTarget, the stored row's empty branch reaches routeListingBranchSwitchesThroughGitIdentity first. That helper treats it as detached HEAD, clears the branch and review links, and schedules the cleared links to be saved. Returning here does not undo those changes.

Skip branch-switch updates for stopped-distro rows before calling that helper, and add a test with a linked review.

Knowledge Base Used: Workspace and worktree lifecycle

@nwparker
nwparker force-pushed the nwparker/wsl-worktree-scan-stopped branch from 3f2ef17 to 4e287de Compare October 29, 2026 06:20
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

When a WSL distro is stopped or its running state cannot be verified, worktree listing returns known rows with non-authoritative status metadata and schedules a rescan. Renderer merge logic retains existing rows, and the sidebar shows the distro state and a start-and-scan action. Pending rescans are released after a successful scan or repository removal.

Priority: Normal

Merge Risk: Moderate · up to 5fac4

Stopped WSL repositories can lose review state, refresh the wrong same-ID host, or fail to respond to the start-and-scan action. Resolve these edge cases before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title is concise, specific, and accurately summarizes the primary change: preserving worktrees for stopped WSL distributions and re-listing them when the distribution runs.
Description check Passed The description is detailed and covers the required explanation, user-visible behavior, implementation, rationale, testing, risks, compatibility, and checklist items. Visual proof is still pending, an…
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.
Full details: Description check

Explanation

The description is detailed and covers the required explanation, user-visible behavior, implementation, rationale, testing, risks, compatibility, and checklist items. Visual proof is still pending, and manual testing was not completed locally, but the overall description is substantially complete.

Full details: Docstring Coverage

Explanation

Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 15 functions across 21 files. (11 skipped: 11 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c206f45a-ccd8-4e36-b932-7710ab13c6e3
Commits

Reviewing files that changed from the base of the PR and between ae811d0 and 3f2ef17.

Files selected for processing: 27
  • src/main/ipc/worktrees/listing/detected-provider-listing.ts
  • src/main/ipc/worktrees/listing/register-detected-worktree-handlers.ts
  • src/main/ipc/worktrees/listing/wsl-stopped-worktree-listing.test.ts
  • src/main/ipc/worktrees/listing/wsl-stopped-worktree-listing.ts
  • src/main/persistence/loading-store/repo-lifecycle-operations.ts
  • src/main/persistence/tracking-repos/persistence-invalid-workspace-layout-purge.test.ts
  • src/main/persistence/tracking-repos/persistence-repo-lifecycle.test.ts
  • src/main/wsl-stopped-worktree-rescan-registry.ts
  • src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.test.tsx
  • src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.tsx
  • src/renderer/src/components/sidebar/worktree-list/rows/RepoWslDistroStoppedIndicator.tsx
  • src/renderer/src/i18n/locales/de.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/pt.json
  • src/renderer/src/i18n/locales/ru.json
  • src/renderer/src/i18n/locales/tr.json
  • src/renderer/src/i18n/locales/zh-Hant.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/store/slices/worktrees-fetch-listing-merge.test.ts
  • src/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.ts
  • src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts
  • src/renderer/src/store/slices/worktrees/listing/worktree-catalog-visibility.ts
  • src/shared/worktree/types.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Comment on lines +215 to +217
// Stored rows from a stopped WSL distro lack branch state the scanned rows already have.
const keepCurrent = worktrees.length === 0 || !!args.refresh.result.wslDistroStopped
if (!args.refresh.result.authoritative && keepCurrent && currentForHost.length > 0) {

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.

Major · Functional Correctness · Quick win

Supported by static analysis

Script executed:

cat src/renderer/src/store/slices/worktree-listing-branch-switch.ts
rg -n 'routeListingBranchSwitchesThroughGitIdentity' src
sed -n '150,260p' src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts

Repository: stablyai/orca

Length of output: 10060


Script executed:

set -o pipefail
printf '%s\n' '--- identifiers and files ---'
rg -n -F --glob '*.ts' -e 'listWslStoppedWorktrees' -e 'disconnected' -e 'fetchWorktrees' -e 'mergeFetchedWorktrees' -e 'wslDistroStopped' src/renderer/src src/main | head -240
printf '%s\n' '--- fetched-worktree-merge complete logical region ---'
sed -n '1,290p' src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts
printf '%s\n' '--- fetchWorktrees definitions and callers ---'
rg -n -F --glob '*.ts' -e 'function fetchWorktrees' -e 'const fetchWorktrees' -e 'fetchWorktrees:' -e 'mergeFetchedWorktrees(' -e 'fetchWorktrees(' src/renderer/src src/main || test "$?" -eq 1
printf '%s\n' '--- likely stopped-listing definitions ---'
rg -n -F --glob '*.ts' -e 'listWslStoppedWorktrees' -e 'disconnectedDetectedWorktrees' -e 'detectedWorktrees' src/renderer/src/store src/main/ipc src/main | head -240

Repository: stablyai/orca

Length of output: 41803


Script executed:

set -o pipefail
printf '%s\n' '--- stopped WSL producer ---'
nl -ba src/main/ipc/worktrees/listing/wsl-stopped-worktree-listing.ts
printf '%s\n' '--- detected provider listing around stopped result ---'
nl -ba src/main/ipc/worktrees/listing/detected-provider-listing.ts | sed -n '150,225p'
printf '%s\n' '--- exact merge/fetch symbols ---'
rg -n -F --glob '*.ts' -e 'mergeFetchedWorktrees' -e 'fetchWorktrees =' -e 'fetchWorktrees:' -e 'fetchWorktrees(' src/renderer/src/store/slices/worktrees src/renderer/src/store/slices/worktree-helpers.ts
printf '%s\n' '--- renderer listing files ---'
rg --files src/renderer/src/store/slices/worktrees/listing | sort
printf '%s\n' '--- fetch implementation candidates ---'
for f in $(rg -l -F --glob '*.ts' 'mergeFetchedWorktrees' src/renderer/src/store/slices/worktrees); do
  echo "### $f"
  nl -ba "$f" | sed -n '1,280p'
done

Repository: stablyai/orca

Length of output: 42099


Script executed:

set -o pipefail
printf '%s\n' '--- fallback helper definitions ---'
rg -n -F --glob '*.ts' -e 'function listDisconnectedSshWorktrees' -e 'function buildDisconnectedDetectedWorktrees' -e 'export function listDisconnectedSshWorktrees' -e 'export function buildDisconnectedDetectedWorktrees' src/main/ipc/worktrees/listing
for f in $(rg -l -F --glob '*.ts' -e 'listDisconnectedSshWorktrees' -e 'buildDisconnectedDetectedWorktrees' src/main/ipc/worktrees/listing); do
  case "$f" in
    *ssh-worktree-fallback.ts|*folder-workspace-catalog.ts)
      echo "### $f"
      nl -ba "$f" | sed -n '1,260p'
      ;;
  esac
done
printf '%s\n' '--- renderer fetchWorktrees implementation ---'
nl -ba src/renderer/src/store/slices/worktrees/listing/fetch-worktrees.ts
printf '%s\n' '--- toVisibleWorktrees definition ---'
rg -n -F --glob '*.ts' 'function toVisibleWorktrees' src/renderer/src/store/slices/worktrees/listing
for f in $(rg -l -F --glob '*.ts' 'function toVisibleWorktrees' src/renderer/src/store/slices/worktrees/listing); do
  echo "### $f"
  nl -ba "$f" | sed -n '1,220p'
done

Repository: stablyai/orca

Length of output: 32964


Script executed:

set -o pipefail
printf '%s\n' '--- mergeWorktree definition ---'
rg -n -F --glob '*.ts' -e 'function mergeWorktree' -e 'export function mergeWorktree' src/main
for f in $(rg -l -F --glob '*.ts' -e 'function mergeWorktree' -e 'export function mergeWorktree' src/main); do
  echo "### $f"
  nl -ba "$f" | sed -n '1,180p'
done
printf '%s\n' '--- updateWorktreeGitIdentity definition ---'
rg -n -F --glob '*.ts' -e 'updateWorktreeGitIdentity' src/renderer/src/store/slices | head -80
for f in $(rg -l -F --glob '*.ts' 'updateWorktreeGitIdentity' src/renderer/src/store/slices); do
  echo "### $f"
  nl -ba "$f" | sed -n '1,380p'
done

Repository: stablyai/orca

Length of output: 42580


Script executed:

set -o pipefail
printf '%s\n' '--- updater declaration locations ---'
rg -n -F --glob '*.ts' -e 'updateWorktreeGitIdentity:' -e 'updateWorktreeGitIdentity =' -e 'function updateWorktreeGitIdentity' -e 'updateWorktreeGitIdentity(' src/renderer/src/store | head -120

Repository: stablyai/orca

Length of output: 10256


Script executed:

set -o pipefail
rg -n -F --glob '*.ts' -e 'createUpdateWorktreeGitIdentity' -e 'function createUpdateWorktreeGitIdentity' src/renderer/src

Repository: stablyai/orca

Length of output: 548


Script executed:

nl -ba src/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts | sed -n '1,240p'

Repository: stablyai/orca

Length of output: 12179


Skip branch reconciliation for stopped-WSL fallback rows.

When wslDistroStopped is set, fallback rows have branch: '' and head: ''. The merge routes these rows before the keepCurrent guard. If the request-started and current rows still agree, a current row with branch-scoped review context can call updateWorktreeGitIdentity, which replaces its identity with the detached fallback and clears that context. The later guard does not undo this update.

🐛 Suggested fix
     let incoming = toVisibleWorktrees(refreshResult, args.hostId, args.setup)
-    incoming = routeListingBranchSwitchesThroughGitIdentity({
-      requestStarted: args.requestStartedWorktrees,
-      current: s.worktreesByRepo[args.repoId],
-      incoming,
-      matchesRefreshHost: (worktree) => worktreeMatchesHost(worktree, args.hostId, matchOptions),
-      hasBranchScopedReviewContext: hasBranchScopedHostedReviewContext,
-      updateWorktreeGitIdentity: s.updateWorktreeGitIdentity
-    })
+    if (!args.refresh.result.wslDistroStopped) {
+      incoming = routeListingBranchSwitchesThroughGitIdentity({
+        requestStarted: args.requestStartedWorktrees,
+        current: s.worktreesByRepo[args.repoId],
+        incoming,
+        matchesRefreshHost: (worktree) =>
+          worktreeMatchesHost(worktree, args.hostId, matchOptions),
+        hasBranchScopedReviewContext: hasBranchScopedHostedReviewContext,
+        updateWorktreeGitIdentity: s.updateWorktreeGitIdentity
+      })
+    }

Comment on lines +22 to +24
await runAsWslUserAct(distro, () =>
listDetectedWorktreesForCapturedRepo(store, repo, () => isCapturedRepoCurrent(store, repo))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Start button leaves WSL stopped

Clicking “Start distro and scan” can leave the distro stopped when a background listing is already checking it. scanWorktreesStartingWslDistro uses the same scan cache, so it can join a scan started outside the user-act scope. That scan still rejects with WslDistroStoppedError instead of starting Git. The click finishes, and the follow-up listing is another background read, so the user must click again.

Retry with a new user-act scan after that refusal, or keep user-act scans from joining background scans.

After a relaunch with a WSL distro stopped, the E9 gate refused the
worktree scan and the sidebar showed "Worktree scan failed" with no rows
until the user pressed Retry. The listing now answers with the stored
rows (non-authoritative, so nothing is pruned), marks the distro stopped
or unverifiable in a new optional field, and re-lists once a wake or a
settled running read shows the distro running. The re-scan is held once
per repo and dropped when the repo is removed. The sidebar shows a
neutral state instead of a failure.
… distro

The indicator's click now runs the scan inside runAsWslUserAct for the
repo's distro (new worktrees:scanStartingWslDistro IPC), then re-lists,
so clicking it can start the distro instead of re-running the gated
background scan that never could.
@nwparker
nwparker force-pushed the nwparker/wsl-worktree-scan-stopped branch from 4e287de to 5fac488 Compare October 29, 2026 12:22

@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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Pass the execution host through the start-and-scan IPC… · wsl-distro-starting-scan.ts:9-24

src/main/ipc/worktrees/listing/wsl-distro-starting-scan.ts:9-24
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass the execution host through the start-and-scan IPC operation.

When two repositories share repo.id, findExactRepoOwner(store, repoId) finds multiple valid candidates and returns undefined. The indicator already has the selected repository's execution host, but sends only repoId. The operation therefore returns before runAsWslUserAct and performs no scan.

Pass getRepoExecutionHostId(repo) through the IPC boundary. Use it in both findExactRepoOwner and isCapturedRepoCurrent.

Suggested fix
--- a/src/renderer/src/components/sidebar/worktree-list/rows/RepoWslDistroStoppedIndicator.tsx
+++ b/src/renderer/src/components/sidebar/worktree-list/rows/RepoWslDistroStoppedIndicator.tsx
@@
-      await window.api.worktrees.scanStartingWslDistro?.({ repoId: repo.id })
+      await window.api.worktrees.scanStartingWslDistro?.({
+        repoId: repo.id,
+        executionHostId: getRepoExecutionHostId(repo)
+      })
--- a/src/preload/api/worktree-api.ts
+++ b/src/preload/api/worktree-api.ts
@@
-  scanStartingWslDistro?: (args: { repoId: string }) => Promise<void>
+  scanStartingWslDistro?: (args: {
+    repoId: string
+    executionHostId: ExecutionHostId
+  }) => Promise<void>
--- a/src/main/ipc/worktrees/listing/wsl-distro-starting-scan.ts
+++ b/src/main/ipc/worktrees/listing/wsl-distro-starting-scan.ts
@@
 import type { Store } from '../../../persistence/loading-store/store'
+import type { ExecutionHostId } from '../../../../shared/execution-host'
@@
-export async function scanWorktreesStartingWslDistro(store: Store, repoId: string): Promise<void> {
-  const repo = findExactRepoOwner(store, repoId)
+export async function scanWorktreesStartingWslDistro(
+  store: Store,
+  repoId: string,
+  executionHostId: ExecutionHostId
+): Promise<void> {
+  const repo = findExactRepoOwner(store, repoId, executionHostId)
@@
-    listDetectedWorktreesForCapturedRepo(store, repo, () => isCapturedRepoCurrent(store, repo))
+    listDetectedWorktreesForCapturedRepo(store, repo, () =>
+      isCapturedRepoCurrent(store, repo, executionHostId)
+    )

Update the main handler to validate and forward executionHostId as well.


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ee834459-d75a-4405-ab80-168de182048c
Commits

Reviewing files that changed from the base of the PR and between 4e287de and 5fac488.

Files selected for processing: 12
  • src/main/persistence/loading-store/repo-lifecycle-operations.ts
  • src/renderer/src/i18n/locales/de.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/pt.json
  • src/renderer/src/i18n/locales/ru.json
  • src/renderer/src/i18n/locales/tr.json
  • src/renderer/src/i18n/locales/zh-Hant.json
  • src/renderer/src/i18n/locales/zh.json
Files skipped from review as they are similar to previous changes: 11
  • src/renderer/src/i18n/locales/de.json
  • src/renderer/src/i18n/locales/pt.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/zh-Hant.json
  • src/renderer/src/i18n/locales/tr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/ru.json
  • src/renderer/src/i18n/locales/fr.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Comment on lines 101 to 107
)
if (!idStillPresent) {
retireLocalWorktreeScanGeneration(id)
releaseWslStoppedWorktreeRescan(id)
dropRepoRestoreHints(this, id)
delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id]
delete this[repoLifecycleOperationsContext].runtime.state.retiredWorktreeNamesByRepo?.[id]

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.

Minor · Data Integrity & Integration · Quick win

Supported by static analysis

Script executed:

sed -n '70,125p' src/main/persistence/loading-store/repo-lifecycle-operations.ts
sed -n '1,120p' src/main/wsl-stopped-worktree-rescan-registry.ts
sed -n '1,80p' src/main/ipc/watched-worktree-catalog-notification.ts
sed -n '830,875p' src/main/ipc/worktree-remote.ts
rg -n "removeProjectForHost|releaseWslStoppedWorktreeRescan|rescanWorktreesWhenWslDistroRuns|notifyWatchedWorktreeCatalogChanged" src/main src

Repository: stablyai/orca

Length of output: 40543


Script executed:

set -o pipefail
printf '%s\n' '--- stopped WSL listing ---'
nl -ba src/main/ipc/worktrees/listing/wsl-stopped-worktree-listing.ts | sed -n '1,110p'
printf '%s\n' '--- detected handler registration ---'
nl -ba src/main/ipc/worktrees/listing/register-detected-worktree-handlers.ts | sed -n '1,55p'
printf '%s\n' '--- cleanup owner ---'
nl -ba src/main/persistence/loading-store/repo-lifecycle-operations.ts | sed -n '80,155p'
printf '%s\n' '--- diff for cleanup ---'
git diff 010d0d586ad97a7da97f203779db0edf45450cf 5fac488179a46a436460d20e7d579945236e7aef -- src/main/persistence/loading-store/repo-lifecycle-operations.ts

Repository: stablyai/orca

Length of output: 12996


Script executed:

nl -ba src/main/ipc/worktrees/listing/detected-provider-listing.ts | sed -n '125,205p'
printf '%s\n' '--- scan entrypoint and host conditions ---'
rg -n -F --glob '*.ts' -e 'listDetectedWorktreesForCapturedRepo' -e 'runAsWslUser' -e 'rescanWorktreesWhenWslDistroRuns' src/main/ipc/worktrees/listing src/main

Repository: stablyai/orca

Length of output: 15871


Script executed:

nl -ba src/main/ipc/worktrees/listing/detected-provider-listing.ts | sed -n '1,130p'
printf '%s\n' '--- host-qualified dispatch ---'
nl -ba src/main/ipc/worktrees/listing/host-qualified-worktree-listing.ts | sed -n '1,125p'
printf '%s\n' '--- detected handler dispatch ---'
nl -ba src/main/ipc/worktrees/listing/register-detected-worktree-handlers.ts | sed -n '90,155p'

Repository: stablyai/orca

Length of output: 13711


Release the rescan when the removed host is local.

The stopped-WSL rescan is registered only by the non-folder, non-SSH path. That path represents the local host. The current cleanup releases it only when no same-ID repository remains. If an SSH repository retains the ID, the local rescan remains pending after local removal.

When it later runs, the callback sends a shared repoId notification. notifyWorktreesChanged invalidates by that ID and emits the event without a host discriminator. This can invalidate and refresh the surviving same-ID repository.

Suggested fix
--- "a/src/main/persistence/loading-store/repo-lifecycle-operations.ts"
+++ "b/src/main/persistence/loading-store/repo-lifecycle-operations.ts"
@@ -93,16 +93,18 @@
     this[repoLifecycleOperationsContext].runtime.state.repos = this[
       repoLifecycleOperationsContext
     ].runtime.state.repos.filter((r) => !(r.id === id && getRepoExecutionHostId(r) === hostId))
     if (repoRemoved) {
       bumpLocalWorktreeScanGeneration(id)
     }
+    if (parseExecutionHostId(hostId)?.kind === 'local') {
+      releaseWslStoppedWorktreeRescan(id)
+    }
     const idStillPresent = this[repoLifecycleOperationsContext].runtime.state.repos.some(
       (r) => r.id === id
     )
     if (!idStillPresent) {
       retireLocalWorktreeScanGeneration(id)
-      releaseWslStoppedWorktreeRescan(id)
       dropRepoRestoreHints(this, id)
       delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id]
       delete this[repoLifecycleOperationsContext].runtime.state.retiredWorktreeNamesByRepo?.[id]
     }

@nwparker
nwparker merged commit 6eebdbb into main Oct 29, 2026
63 checks passed
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