Skip to content

feat(wsl-mover): move dialog with preview, blockers, progress and result (D1b-9) - #31429

Closed
nwparker wants to merge 4 commits into
nwparker/wsl-mover-d1b-driverfrom
nwparker/wsl-mover-d1b-9-move-ux
Closed

nwparker wants to merge 4 commits into
nwparker/wsl-mover-d1b-driverfrom
nwparker/wsl-mover-d1b-9-move-ux

Conversation

@nwparker

@nwparker nwparker commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1192 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1192
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1979 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1968

Stacked on #31417 (the move driver, nwparker/wsl-mover-d1b-driver). The chain is main → #31414 → #29825 → 6d-1 → 7a → 7c → 7d → 7e → D1b-8 #31401 → #31417 → this PR. Everything here shows only with the internal ORCA_INTERNAL_WSL_HOSTS=1 gate on and Settings → Experimental → WSL hosts switched on. Not for merge by the agent.

ELI5

Orca can now move a WSL project onto that distro's own Orca server (the "WSL host"), but until now the only way to start a move was through code. This PR adds the screens a person uses for it. You pick the projects, see what would block the move, watch it run, and get told plainly how it ended.

What Changed

Before: No screen anywhere started a move. The driver (#31417) had wslMove:preview/start/cancel/listUnfinished and a progress event, and nothing called them.

After (gate on only):

  • Three ways in:
    • The WSL host row in Settings → Remote Orca Servers has a new menu item, "Move projects from this distro…".
    • A local WSL project's sidebar menu has "Move to host…".
    • In the project's settings, the Windows/WSL runtime toggle becomes a "Move project" row. This happens only for a project inside a distro that has a host, per the owner decision "the runtime toggle becomes Move project".
    • The design also lists a banner shown when a \\wsl.localhost folder is opened (D2). It is N/A: that surface does not exist yet.
  • Preview:
    • Projects: every project of the distro gets a checkbox, folder workspaces included. Opened from one project, only that project starts checked.
    • Counts: the line under the list counts the selection's projects, workspaces, terminals and chats.
    • Not moving: lists projects already on the host, projects changed by an older Orca, and projects that cannot move (for example a Windows-drive repo). Each says why in plain words.
    • Blockers: grouped by kind, such as "A terminal is running a program". Each names its project, says what to do, and offers "Leave out of this move" where main allows skipping. "Check again" previews again.
    • Chats: the decision-C line appears verbatim: "Existing chats stay on this computer. New chats in this project start on the WSL host."
  • Progress:
    • A stepper: Preparing → Pausing terminals → Copying (MB of MB, with a bar) → Switching → Done.
    • "Cancel move" works only until the switch starts. Main honours a cancel only until it asks the host to commit.
    • Closing the dialog hides it without stopping the move. It opens again with the result.
  • Result:
    • Success: says "Reload now". The reload goes through restart preparation (requestLazyChunkRecoveryReload), so unsaved editors are backed up first.
    • Pending automations: says which automations are still off on the host.
    • Undo: points at "Use on this computer again" in the sidebar (feat(wsl-mover): "Use on this computer again" brings a moved project back as it was before the move #29833).
    • Refusals: each of the 13 codes has its own plain message. The export reasons and the too-large message come from main and are shown as given.
    • Reset: shows "Nothing was moved".
    • Waiting for host: explains that the projects stay read-only until the host answers.
  • Unfinished moves: listUnfinished drives a small "automations still off" line under the WSL host list.

Mechanism:

  • Dialog state: lives in its own small store (components/wsl-move/wsl-move-dialog-store.ts), not in the app's single activeModal slot. Any other openModal would evict a running move and lose its reload prompt.
  • Mounting: the dialog is root-mounted in AppRootSurfaces, inside an error boundary.
  • Root progress listener: it is active only while the gate and Experimental switch are on. It adopts a run this window did not start, for example after the window reloaded mid-move. A run that then reaches done gets the same reload prompt.
  • Gate check: every entry point goes through useWslHostsEnabled(), which needs the preload's gated WSL API and the Experimental switch. Main also refuses every call with the gate closed.

Why

  • Own store instead of activeModal: a move can't be lost to an unrelated dialog, and the window can't be left on rewritten state without a reload prompt. The alternative was a new activeModal id, which any keyboard shortcut opening Quick Open would evict.
  • Hide instead of block: a long or hung move must not trap the user behind a modal. Only a successful move whose reload has not been tried yet locks the dialog. If that reload is refused (unsaved files could not be backed up), the dialog can close and leaves a persistent toast with "Reload now".
  • Start settles an earlier move: main's preview always refuses while an earlier move of the distro is unfinished, and only a start settles it. The dialog therefore lists the projects and allows a start in that state, with copy saying a start finishes or undoes the earlier move first.
  • Reuse: the dialog uses the shadcn Dialog, Checkbox, Progress, Badge and DropdownMenuItem, LoadingSpinner, the existing ServerListRow menu, the project actions menu, SettingsRow, and the existing restart-preparation reload. Nothing is restyled.

Linked Issue

Internal (WSL via orcad tracker, row D1b-9).

Visual Proof

Before: none of these surfaces exist. The WSL host row menu had only "Remove…", and nothing in the app started a move.

After (CDP screenshots of the hidden test window, from tests/e2e/wsl-move-dialog.spec.ts):

wsl-move-host-row-menu.png wsl-move-blockers.png
wsl-move-host-row-menu.png wsl-move-blockers.png
wsl-move-preview.png wsl-move-progress.png
wsl-move-preview.png wsl-move-progress.png
wsl-move-refusal.png wsl-move-success.png
wsl-move-refusal.png wsl-move-success.png
wsl-move-unfinished-notice.png
wsl-move-unfinished-notice.png

Testing

  • Unit tests (components/wsl-move/*.test.ts(x), 39 tests):

    • States: each state renders: preview, checking, blockers with and without skip, the unfinished-earlier-move start, a refusal with reasons, progress with bytes, cancel before and after the switch, success with automations pending, a refused reload, reset, waiting-for-host, and a distinct jargon-free message for every refusal code.
    • Store: whole-distro and single-project selection; a stale preview reply is dropped; skip; the catalog fallback; recheck after a refused distro preview; hide while running and show again; locked until the reload is tried, then a toast; cancel refused before vs. after the switch; adopting a run this window did not start; back to the same projects after a refusal.
    • Scope: eligibility (no host, Windows drive, another host), counts, and blocker-to-project mapping.
    • Gate: with the gate closed the project menu item renders nothing.
  • E2E tests/e2e/wsl-move-dialog.spec.ts: the fake WSL hosts transport, with main's wslMove:* handlers swapped for a scripted host and the renderer and preload left real.

    • First test: host row menu → preview with a blocker → skip → start → refusal → back → start → progress events → hide and reopen → switch disables cancel → moved → Esc ignored → reload through restart preparation → the unfinished notice after the reload.
    • Second test: with the gate closed, main refuses and no entry or dialog renders.
    • Ran locally on macOS with ORCA_BACKGROUND_LAUNCH=1 and the hidden window.
  • Checks: pnpm tc, the neighbouring settings/sidebar/app-shell suites (5030 tests), check:code-quality:changed, oxlint, localization verify, and the max-lines, ts-nocheck, e2e-typecheck, repo-host-read, worktree-id-composer, path-literal, id-only-store-write, focus-read, layout-writer and dead-classes ratchets. No baseline changes.

  • I manually tested these changes locally

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

Review

An adversarial review (gate leaks, misleading copy, stuck states) ran before opening. It found no gate leaks.

Fixed:

  • After a refused reload the app was locked behind the dialog. It can now close to a persistent reload toast.
  • A refused distro preview left "Check again" doing nothing. It now re-checks the whole distro.
  • An unfinished earlier move could never be settled from the UI. Start is now allowed in that state.
  • A running move was an undismissable modal with no timeout. It now hides.
  • A window reloaded mid-move lost the move. The root listener now adopts the run.
  • Opening from a project that cannot move checked every project. It now checks nothing.
  • Copy fixes for "already in flight", failed IPC calls, waiting-for-host, cancel "too late", read-only before pausing, and raw browser-login reason codes.
  • The project entries are hidden when the host server files are missing.
  • The dialog is inside an error boundary.
  • A race in "Back to projects".

Residuals:

  • Undo copy: the success screen points at "Use on this computer again" in the sidebar. That action is feat(wsl-mover): "Use on this computer again" brings a moved project back as it was before the move #29833, which is not in this stack yet, so the line is ahead of the code until feat(wsl-mover): "Use on this computer again" brings a moved project back as it was before the move #29833 lands (see Notes).
  • Stale writes before the reload: between the moved result and the reload, the old window can still write stale focus or last-active ids for the moved projects. Restart preparation has no write suppression; it lands a checkpoint of the current state. Fixing this cheaply would need a renderer-side freeze of UI-routing writes for the moved project ids. Left for a follow-up or D1b-10.
  • Adopted runs: a run adopted after a reload that ends in a refusal or reset sends no final progress event. The dialog then stays on its last step until closed. Closing only hides it.
  • Experimental switch: turning it off while the dialog is open does not close the dialog. Main still refuses only on the env gate.

Agent skill upstream boundary

  • Not applicable.

Notes

Shared paths this touches (outside components/wsl-move/):

  • src/renderer/src/app-shell/AppRootSurfaces.tsx: mounts WslMoveDialogHost.
  • src/renderer/src/components/settings/WslHostsSection.tsx: the host row menu item and the unfinished notice.
  • src/renderer/src/components/sidebar/worktree-list/rows/repo-header-project-actions.tsx: the project menu item.
  • src/renderer/src/components/settings/RepositoryWindowsRuntimeSection.tsx and RepositoryPane.tsx: the runtime toggle becomes "Move project" (one new repo prop).
  • src/shared/wsl-move/wsl-move-driver-types.ts: one optional browserLoginsNotCopiedReason on the moved result. It is a hook for 7f; nothing sets it yet, and it is wire-safe.
  • src/renderer/src/i18n/locales/en.json: the wslMove.* strings.

Other notes:

  • SSH and remote: none of this applies to SSH hosts or paired clients. The web build's wslMove stub refuses, and nothing renders there.
  • Cross-platform: no shortcuts were added. The entry points exist only where the WSL host API exists: Windows, or the e2e fake.

Copy that needs an owner decision:

  1. The undo hint names "Use on this computer again" before feat(wsl-mover): "Use on this computer again" brings a moved project back as it was before the move #29833 is in this stack.
  2. The already-in-flight wording, "A move of is still finishing. Starting this move first finishes or undoes it where Orca can."
  3. Whether "Move project" should replace the runtime toggle for WSL projects with a host, or sit beside it.

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 (typecheck, touched and neighbouring tests, changed-code quality and ratchets pass locally; CI covers the rest)

@nwparker

Copy link
Copy Markdown
Contributor Author

Contained in the dark landing PR #34059 (its head commit is an ancestor of the landing branch). Closing in favour of it; branch kept.

@nwparker nwparker closed this Oct 26, 2026
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