Skip to content

feat(wsl): Windows path actions for WSL hosts through one owner-routed capability (WSL-via-orcad C8) - #27955

Merged
nwparker merged 7 commits into
mainfrom
nwparker/wsl-c8-windows-affordances
Oct 13, 2026
Merged

nwparker merged 7 commits into
mainfrom
nwparker/wsl-c8-windows-affordances

Conversation

@nwparker

@nwparker nwparker commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​779 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​769
Prod 26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​682 $\color{#cf222e}{\Huge{\mathbf{−}}}$​101 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​581

Stacked on #27934 (C7 host UI). Do not merge before it.

ELI5

A WSL distro added as a host keeps its files inside Linux, but Windows can still reach them at \\wsl.localhost\<distro>\.... Until now Orca treated a WSL host like any other remote server, so "Reveal in File Explorer", "Open in VS Code", dragging a folder from Explorer, and Windows' own folder picker were all off for it. This turns them back on for WSL hosts only, on Windows only.

What Changed

Before: for a project on a WSL host, Reveal and Open in editor were disabled ("Local only"). Dropping any folder on the sidebar while the WSL host was focused was refused. Add Project's Browse opened only Orca's in-app browser.

After (Windows, WSL host owner):

  • Reveal in File Explorer opens \\wsl.localhost\<distro>\<path>.
  • Open in editor: VS Code and Cursor get --remote wsl+<distro> <linux path>. This reuses the existing launcher logic from fix(wsl): open WSL paths in Cursor's WSL mode; ignore Windows-side CLIs in WSL hook setup #27681, which turns a WSL network path into that form. Other editors get the \\wsl.localhost path. The Open In menu labels these entries "WSL".
  • Explorer drop on the sidebar: a \\wsl.localhost\<distro>\... or \\wsl$\... folder goes to the WSL host whose distro it names, whatever host is focused. Add Project opens with that host selected and the Linux path filled in. A C:\ drop is never sent to a WSL host (D7): with the WSL host focused it is refused with a "stays on this computer" message, and otherwise it behaves as before.
  • Add Project → Browse on a WSL host opens Windows' folder picker at \\wsl.localhost\<distro>\<home>. The chosen folder is turned back into its Linux path. A folder on a drive or in another distro is refused, and the message says why.

Mechanism: one capability, windowsPathAccess(ownerHostId) in src/shared/windows-path-access.ts, returns { toWindows, fromWindows } or null. It is non-null only when the platform is win32 and the owner host is a runtime environment backed by a WSL host record. It is keyed on the resource's owner host, never on the focused host. Main resolves it from its WSL host records (resolveWindowsPathAccess). The renderer resolves it from the known runtime environments (getWindowsPathAccess). Every guard goes through it:

  • isRevealOrEditorOpenBlocked (the reveal call sites and the Open In menu) and getExternalEditorOpenCapability
  • shell.ts resolveOwnedPathTarget (reveal and external editor)
  • the sidebar drop route
  • the Add Project picker

Main translates the path again itself. It never trusts a path the renderer already translated.

All translation is plain string work on the desktop: \\wsl.localhost and the legacy \\wsl$ prefix, plus the \\?\UNC\ long-path form. Distro names match without regard to case, and paths are always written with the distro name as registered (issue #23960 class). Nothing runs inside the distro.

Deliberately unchanged:

  • Open with system default (markdown links, terminal links) stays blocked. Those callers still send "local" as the owner, and that is a separate change.
  • Local terminal create for a WSL-owned worktree stays refused. That terminal belongs in the distro, through orcad. A Windows shell sitting in a \\wsl.localhost folder is what D5 avoids.
  • Absolute paths in tab entries and git ignore writes keep the strict "this computer only" guard.

Why

The design calls for exactly one owner-routed capability (D5, risk R16). A single function means every guard can be found with one grep, and every non-WSL host (local, SSH, paired server, macOS/Linux desktops) gets null, so their behavior does not change. Converting paths on the desktop avoids WSL interop, which can be disabled or broken (the A-track issues). The alternatives were:

  • Spread wsl: id checks into each guard. That repeats the rule and drifts.
  • Call wslpath inside the distro. That needs interop and a round trip.

Linked Issue

Fixes #

Visual Proof

Taken through CDP from hidden windows by the fake-WSL-transport e2e. The renderer is told it is Windows, and main's native dialog is stubbed.

Before / off Windows, Browse on a WSL host opens the in-app browser:
wsl-browse-in-app.png

After, Windows' picker returned \\wsl.localhost\ubuntu\... (lowercase), and the host path is filled with the Linux path:
wsl-windows-folder-picker-filled.png

After, picking a C:\ folder is refused with D7 copy:
wsl-windows-folder-picker-drive.png

Testing

  • Unit tests:

    • windows-path-access.test.ts: owner routing, win32-only, casing, spaces, unicode, \\wsl$, \\?\UNC, .., drive and other-distro refusals.
    • shell-wsl-owner.test.ts: Explorer gets the UNC path; VS Code and Cursor get --remote wsl+Ubuntu /home/me/my repo; other editors get the UNC path; a paired server is still refused; system-default open is still refused.
    • windows-path-access-guards.test.ts: renderer guards, web client and non-Windows give null.
    • sidebar-project-drop.test.ts: drop routing.
    • add-repo-wsl-host-folder-picker.test.ts
    • add-repo-browse-authority.test.ts
  • Renderer tests: useSidebarProjectDrop.os-file-drop.test.tsx covers a UNC drop going to the WSL host, a drive drop refused while the WSL host is focused, and the experiment off. use-add-repo-wsl-host-paths.test.tsx covers the picker filling the path, a stale pick after close being dropped, and a dropped path applied once its host is selected.

  • e2e: tests/e2e/wsl-host-fake-transport.spec.ts now covers the Windows folder picker (a pick that lands on the host, and a drive refusal). Reveal and open in editor can't be exercised end to end off Windows, because main resolves the capability from process.platform. They are covered by the main handler tests above.

  • Real Windows host: not run. The agent session refused to send commands to the win-lowspec terminal, so \\wsl.localhost in Explorer and code --remote wsl+<distro> are still unverified on real hardware.

  • Gates run locally: pnpm tc, oxlint, pnpm format, check:code-quality:changed, React Doctor changed-lines (warnings only, none new in the gate), focus-read, layout-writer, max-lines, ts-nocheck and runtime-electron ratchets, Zustand fan-out, reliability gates, dead classes, anti-slop, native and type-aware code-quality, localization extraction, catalogs and coverage.

  • I manually tested these changes locally

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

AI Disclosure

Review

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

Cross-platform: everything is gated on win32 (renderer user agent, main process.platform). macOS and Linux desktops are unchanged. SSH and paired servers resolve to null, so they keep refusing exactly as before. Wire compatibility: no RPC or stream changes; only desktop IPC was touched (shell handlers now accept a WSL owner). Folder workspaces go through the same owner path.

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)

@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from cba0769 to 6168b4a Compare October 12, 2026 10:39
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from 4af56c6 to fa32932 Compare October 12, 2026 10:39
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from 6168b4a to e4f518b Compare October 12, 2026 12:00
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from ec58b58 to c6f81c8 Compare October 12, 2026 12:00
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from e4f518b to bb1fc63 Compare October 12, 2026 12:56
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from c6f81c8 to 661b793 Compare October 12, 2026 13:05
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from bb1fc63 to 83b1b27 Compare October 12, 2026 13:30
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from 661b793 to 3d1c331 Compare October 12, 2026 13:36
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from 83b1b27 to 54961ca Compare October 12, 2026 14:56
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from 3d1c331 to 0f076fd Compare October 12, 2026 15:05
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch 2 times, most recently from c167054 to 741e9ff Compare October 12, 2026 16:25
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch 2 times, most recently from 0afe9ca to b8e0d00 Compare October 13, 2026 00:34
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from 3387656 to c6bd817 Compare October 13, 2026 05:33
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from b8e0d00 to 73b44f9 Compare October 13, 2026 05:39
@nwparker
nwparker force-pushed the nwparker/wsl-c7-host-ui branch from c6bd817 to 3fb1e7a Compare October 13, 2026 15:22
@nwparker
nwparker changed the base branch from nwparker/wsl-c7-host-ui to main October 13, 2026 16:42
@nwparker
nwparker marked this pull request as ready for review October 13, 2026 16:42
@nwparker
nwparker force-pushed the nwparker/wsl-c8-windows-affordances branch from 73b44f9 to 9a1bfed Compare October 13, 2026 16:42
@nwparker

Copy link
Copy Markdown
Contributor Author

Merge-train review (blast radius outside WSL): the remote server-path step skipped the host folder browser whenever a path was already filled. That also fired for SSH and paired hosts, for example after re-selecting the same host with a path left over from an earlier pick, which exposed stale paths without the browser's host-side lookup. The skip now applies only to a path that Windows' folder picker or an Explorer drop handed over (tracked in useAddRepoWslHostPaths). Any other filled path opens the browser as before. A regression test was added. Risk after the fix: LOW.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The changes add Windows path conversion for WSL hosts and use it in shell file actions, renderer reveal and editor actions, Add Project folder browsing, and project-folder drops. The renderer resolves WSL hosts from runtime environments and uses host ownership when opening paths. Tests cover path conversion, rejected paths, host selection, folder-picker behavior, and drop routing.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 9a1bf

A WSL folder whose name ends in a space can be interpreted as a different project folder. This is a narrow path case, but the conversion should be fixed before relying on those selections.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 35 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
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.
Title check Passed The title clearly identifies the main change: Windows path actions for WSL hosts through owner-routed capability.
Description check Passed The description is detailed and covers the user impact, implementation, rationale, testing, visual proof, platform scope, and checklist. The required linked issue is still missing because it contains …
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 35 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ 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
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6adbff5a-6f5c-42c4-b376-60dddbed2d60
📥 Commits

Reviewing files that changed from the base of the PR and between deb6a71 and 9a1bfed.

📒 Files selected for processing (36)
  • src/main/ipc/shell-owned-path-target.ts
  • src/main/ipc/shell-wsl-owner.test.ts
  • src/main/ipc/shell.ts
  • src/main/windows-path-access-host.ts
  • src/main/wsl/wsl-host-service.ts
  • src/renderer/src/components/editor/EditorPanelHeaderPath.tsx
  • src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.tsx
  • src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx
  • src/renderer/src/components/sidebar/AddRepoDialog.tsx
  • src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx
  • src/renderer/src/components/sidebar/AddRepoDialogStepContent.tsx
  • src/renderer/src/components/sidebar/AddRepoServerStartStep.tsx
  • src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx
  • src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts
  • src/renderer/src/components/sidebar/add-repo-browse-authority.ts
  • src/renderer/src/components/sidebar/add-repo-wsl-host-folder-picker.test.ts
  • src/renderer/src/components/sidebar/add-repo-wsl-host-folder-picker.ts
  • src/renderer/src/components/sidebar/sidebar-project-drop.test.ts
  • src/renderer/src/components/sidebar/sidebar-project-drop.ts
  • src/renderer/src/components/sidebar/use-add-repo-host-selection.ts
  • src/renderer/src/components/sidebar/use-add-repo-wsl-host-paths.test.tsx
  • src/renderer/src/components/sidebar/use-add-repo-wsl-host-paths.ts
  • src/renderer/src/components/sidebar/useSidebarProjectDrop.os-file-drop.test.tsx
  • src/renderer/src/components/sidebar/useSidebarProjectDrop.ts
  • src/renderer/src/components/tab-bar/EditorFileTabContextMenu.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/lib/external-editor-open-capability.ts
  • src/renderer/src/lib/local-path-open-guard.ts
  • src/renderer/src/lib/reveal-in-file-manager.ts
  • src/renderer/src/lib/windows-path-access-guards.test.ts
  • src/renderer/src/lib/windows-path-access.ts
  • src/renderer/src/store/index.ts
  • src/shared/shell-open-types.ts
  • src/shared/windows-path-access.test.ts
  • src/shared/windows-path-access.ts
  • tests/e2e/wsl-host-fake-transport.spec.ts

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

return normalized === null ? null : toWindowsWslUncPath(normalized, distro)
},
fromWindows: (windowsPath) => {
const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath.trim()))

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 | 🟡 Minor | ⚡ Quick win

Preserve trailing spaces in WSL paths.

If a selected UNC path ends in a space, windowsPath.trim() removes part of its final filename. For example, \\wsl.localhost\Ubuntu\repo becomes /repo instead of /repo . A folder-picker selection or Explorer drop can then add the wrong project if both folders exist. Parse the path without trimming it. Linux filenames can contain spaces. (pubs.opengroup.org)

Proposed change
--- "a/src/shared/windows-path-access.ts"
+++ "b/src/shared/windows-path-access.ts"
@@ -46,7 +46,7 @@
       return normalized === null ? null : toWindowsWslUncPath(normalized, distro)
     },
     fromWindows: (windowsPath) => {
-      const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath.trim()))
+      const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath))
       // Why case-insensitive: Windows folds the distro in UNC paths (`\\wsl.localhost\ubuntu`).
       if (!parsed || !sameWslDistroName(parsed.distro, distro)) {
         return null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath.trim()))
const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath))

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