Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
f83d73b
fix(server): fall back to direct fd read on EACCES bootstrap
Jgratton24 May 11, 2026
0ae9a14
feat(desktop): add WSL backend mode
Jgratton24 May 11, 2026
3b9f86c
fix(desktop): address bot review findings on WSL backend swap
Jgratton24 May 11, 2026
80eddbb
fix(desktop): clear swap flow timer in finally + drop duplicate env-s…
Jgratton24 May 11, 2026
3b18e6d
fix(desktop): resolve WSL user home for tilde paths, fix select value…
Jgratton24 May 11, 2026
b6c25ca
fix(desktop): namespace backend-runtime select sentinels and drop red…
Jgratton24 May 11, 2026
4506e73
fix(desktop): treat re-pick of resolved-default distro as a no-op
Jgratton24 May 11, 2026
6d32481
fix(desktop): widen swap flow ceiling to cover stacked rollback waits
Jgratton24 May 11, 2026
e15edd9
fix(desktop): drop dead WslConfig.enabled and extend toast suppressio…
Jgratton24 May 11, 2026
cb8fbe5
fix(desktop): baseline welcome env-id from the atom to avoid immediat…
Jgratton24 May 11, 2026
4f3a90c
fix(desktop): preserve existing WSLENV entries
Jgratton24 May 16, 2026
b20a966
feat(desktop): scaffold DesktopBackendPool for concurrent backends
Jgratton24 May 16, 2026
a8fc784
feat(desktop): reshape DesktopBackendManager into per-instance factory
Jgratton24 May 16, 2026
425c7d0
feat(desktop): move backend-ready latch from DesktopState onto the wi…
Jgratton24 May 16, 2026
563820e
feat(desktop): route backend child output through a per-instance log …
Jgratton24 May 16, 2026
a0eaf56
feat(desktop): add register/unregister to DesktopBackendPool
Jgratton24 May 16, 2026
b162219
feat(desktop): replace wslMode swap toggle with wslBackendEnabled
Jgratton24 May 17, 2026
31ce3ad
feat(desktop): split DesktopBackendConfiguration into primary + wsl r…
Jgratton24 May 17, 2026
627c80c
feat(desktop): orchestrate the WSL backend as a second pool instance
Jgratton24 May 17, 2026
3fe02a4
docs(desktop): update DesktopBackendPool header for step-5 state
Jgratton24 May 17, 2026
bad6604
feat(desktop): widen local-environment bootstrap IPC to return all in…
Jgratton24 May 17, 2026
5d80468
feat(desktop): route pickFolder by target environment id
Jgratton24 May 17, 2026
eb5a03e
feat(web): replace WSL swap dialog with a toggle + distro picker
Jgratton24 May 17, 2026
60de6af
docs(desktop): refresh DesktopBackendPool header for steps 6-8 state
Jgratton24 May 17, 2026
1c7e787
feat(web): register WSL backend as a desktop-local environment
Jgratton24 May 17, 2026
38e8477
feat(web): allow "Open from file manager" against desktop-local envs
Jgratton24 May 17, 2026
c17897b
fix(web): write WSL bearer before upserting record to avoid auth race
Jgratton24 May 17, 2026
0e1d78e
docs(desktop): record final state + browser validation in pool docblock
Jgratton24 May 17, 2026
df23691
fix(desktop): route WSL renderer fetches through the distro's eth0 IP
Jgratton24 May 18, 2026
2c1620d
fix(server): make the desktop-bootstrap grant reusable for 24h
Jgratton24 May 18, 2026
24b012d
fix(web): retry local-secondary reconcile during WSL cold boot
Jgratton24 May 18, 2026
ad7e3d7
fix(web): use a container icon for desktopLocal threads in the sidebar
Jgratton24 May 18, 2026
0dbdd9f
fix(web): keep auto-retrying when the WSL bootstrap hasn't appeared yet
Jgratton24 May 18, 2026
943e1ed
fix(web): use container icon on desktopLocal-only project headers
Jgratton24 May 18, 2026
6bf864d
fix(web): timeout each WSL register attempt + expose reconciler trace
Jgratton24 May 18, 2026
c47ec5d
feat(web): show a sidebar status alert while WSL backend is connecting
Jgratton24 May 18, 2026
3558e21
fix(web): keep WSL connecting indicator steady between retry attempts
Jgratton24 May 18, 2026
a7a2e1b
fix(web): drop per-thread container icon when the whole project is WSL
Jgratton24 May 18, 2026
449e18f
feat(web): confirm before disable / distro-switch when WSL has state
Jgratton24 May 18, 2026
e00ad61
feat: add wsl-only mode + project icon dedup
Jgratton24 May 18, 2026
3c82c95
feat: auto-restart on wsl-only toggle
Jgratton24 May 18, 2026
d71f3b1
feat(web): consolidate WSL backend toggle and distro picker into one …
Jgratton24 May 18, 2026
1ebb0e5
fix(desktop): load renderer from primary's real URL, not local exposure
Jgratton24 May 18, 2026
05c5295
feat(web): pick WSL mode at enable time, clean up Off-from-wsl-only path
Jgratton24 May 18, 2026
aebb4fa
perf(web): park the local-secondary auto-retry on hosts with no WSL
Jgratton24 May 18, 2026
c3374a8
fix: review feedback from PR #2751
Jgratton24 May 18, 2026
ba31706
fix(desktop): stop every pool instance before update install, not jus…
Jgratton24 May 18, 2026
8a55a55
fix(desktop): stop every pool instance on normal shutdown, not just p…
Jgratton24 May 18, 2026
ce93b7a
fix(desktop): use loopback for WSL backend URL when in mirrored mode
Jgratton24 May 18, 2026
b62348b
fix(desktop): hide preflight-failed bootstraps + serialize WSL reconcile
Jgratton24 May 18, 2026
7e1368d
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 May 28, 2026
d4df9bb
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 May 28, 2026
9df7012
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 May 28, 2026
604de5d
fix(desktop): don't let spec.onShutdown abort stop() teardown
Jgratton24 May 28, 2026
ee7c0f8
refactor(desktop): single source of truth for the wsl: instance-id pr…
Jgratton24 May 28, 2026
a934809
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 May 29, 2026
da200cf
fix(desktop): two bugbot follow-ups on parallel-backends scaffolding
Jgratton24 May 29, 2026
3a5c764
Merge upstream/main into josh/desktop-wsl-parallel-backends
Jgratton24 May 31, 2026
defdf57
fix(desktop): fall back to Windows primary when wsl-only but WSL unav…
Jgratton24 May 31, 2026
d2b68cd
fix(desktop): log window-open failures after backend readiness
Jgratton24 Jun 1, 2026
2462f0e
fix(web): give a recovery control when WSL becomes unavailable
Jgratton24 Jun 1, 2026
3be6625
fix(desktop): make bootstrap-token get-or-create atomic
Jgratton24 Jun 1, 2026
d729b00
fix(desktop): primary label follows the resolved backend, not just se…
Jgratton24 Jun 1, 2026
c61ea48
Fix WSL picker and backend restart edge cases
Jgratton24 Jun 1, 2026
d84971a
Fix backend shutdown edge cases
Jgratton24 Jun 1, 2026
bbc10df
Resolve primary backend label lazily
Jgratton24 Jun 2, 2026
9752f9e
fix(desktop): resolve WSL availability once so the sync IPC path is safe
Jgratton24 Jun 2, 2026
b9e12d3
Improve WSL project opening from command palette
Jgratton24 Jun 2, 2026
e3448d0
Avoid duplicate backend shutdown notifications
Jgratton24 Jun 2, 2026
43187e6
Merge remote-tracking branch 'origin/main' into pr-2751
Jgratton24 Jun 2, 2026
2afae37
fix(desktop): apply WSL distro changes in wsl-only mode + unblock rec…
Jgratton24 Jun 3, 2026
ae03db4
Merge upstream/main into josh/desktop-wsl-parallel-backends
Jgratton24 Jun 5, 2026
993261f
fix(desktop): call handleBackendReady with URL in off-origin nav test
Jgratton24 Jun 5, 2026
56c0512
fix(desktop): tolerate numeric networkInterfaces family in mirrored-m…
Jgratton24 Jun 5, 2026
7d15110
fix(desktop): preserve existing WSLENV verbatim when forwarding secrets
Jgratton24 Jun 5, 2026
adaa183
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 Jun 5, 2026
3ca0696
test: point WSL test imports at vite-plus/test after upstream sync
Jgratton24 Jun 5, 2026
bd82399
refactor(desktop): use Effect.orElseSucceed for WSL probe fallbacks
Jgratton24 Jun 5, 2026
6ebdc74
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 Jun 6, 2026
19c2146
fix(desktop): resolve nvm/fnm/asdf-managed node for the WSL backend
Jgratton24 Jun 7, 2026
383d41c
fix(desktop): cap WSL preflight failures, surface them, fall back to …
Jgratton24 Jun 7, 2026
fe77570
fix(desktop): surface WSL preflight failures cleanly
Jgratton24 Jun 7, 2026
aba2f1b
fix(desktop): reuse SSH node resolver for WSL
Jgratton24 Jun 7, 2026
ac4d02e
fix(web): retry local secondary registration after connect failure
Jgratton24 Jun 7, 2026
d72c85a
fix(desktop): retry idle WSL backend after preflight cap
Jgratton24 Jun 7, 2026
b7bbad9
fix(desktop): pass WSL node engine range to resolver
Jgratton24 Jun 7, 2026
495c2dd
fix(desktop): clear stale WSL readiness state
Jgratton24 Jun 7, 2026
382d699
fix(desktop): relaunch when enabling WSL-only backend
Jgratton24 Jun 7, 2026
e560f70
fix(desktop): keep WSL fallback state in memory
Jgratton24 Jun 7, 2026
3d04133
fix(desktop): clear WSL-only state when disabling WSL
Jgratton24 Jun 7, 2026
0bb1553
Merge remote-tracking branch 'upstream/main' into josh/desktop-wsl-pa…
Jgratton24 Jun 9, 2026
da782da
fix(desktop): ship a prebuilt Linux node-pty for the packaged WSL bac…
Jgratton24 Jun 11, 2026
6ad3acc
ci(release): don't block macOS/Linux builds on the WSL node-pty prebuild
Jgratton24 Jun 11, 2026
185032b
fix(desktop): tighten WSL environment affordances per review
Jgratton24 Jun 12, 2026
bbb7ba7
fix(desktop): unpack the full node_modules tree for the WSL backend
Jgratton24 Jun 12, 2026
ad2a095
fix(desktop): preflight a WSL server dependency so a missing-deps reg…
Jgratton24 Jun 12, 2026
70db2ed
fix(desktop): repair the WSL backend's PATH so provider CLIs resolve
Jgratton24 Jun 13, 2026
4cb3a5c
fix(web): update providers in every connected local environment, not …
Jgratton24 Jun 13, 2026
76bd75b
fix(server): run provider update commands through the shell on Windows
Jgratton24 Jun 13, 2026
5086978
fix(web): surface secondary backend update failures instead of report…
Jgratton24 Jun 13, 2026
c275e23
refactor(web): drop the now-unused single-backend provider snapshot c…
Jgratton24 Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,64 @@ jobs:
echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT"
echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT"

# node-pty publishes no Linux prebuilt and the WSL backend runs under the
# distro's own (Linux) Node, which can't load the Windows/Electron binary. We
# build the Linux pty.node here, on Linux, and hand it to the Windows packaging
# job — the Windows artifact then ships a ready WSL backend binary with no
# cross-compiling and no first-launch compiler/node-gyp/network on the user's
# machine. node-pty is N-API, so one binary works across all WSL Node versions.
build_wsl_node_pty:
name: Build WSL node-pty (linux-x64)
needs: [preflight]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
fetch-depth: 0

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Build node-pty linux-x64 prebuild
shell: bash
run: |
set -euo pipefail
# Resolve node-pty from apps/server (where it's a dependency) and build
# its native binary from source for Linux. node-addon-api resolves from
# node-pty's own dependency tree, so node-gyp has everything it needs.
pty_pkg="$(node -e "console.log(require.resolve('node-pty/package.json', { paths: ['$GITHUB_WORKSPACE/apps/server'] }))")"
pty_dir="$(dirname "$pty_pkg")"
( cd "$pty_dir" && npx --yes node-gyp rebuild )
mkdir -p wsl-prebuild
cp "$pty_dir/build/Release/pty.node" wsl-prebuild/pty.node
file wsl-prebuild/pty.node

- name: Upload node-pty linux-x64 prebuild
uses: actions/upload-artifact@v7
with:
name: wsl-node-pty-x64
path: wsl-prebuild/pty.node
if-no-files-found: error

build:
name: Build ${{ matrix.label }}
Comment thread
Jgratton24 marked this conversation as resolved.
needs: [preflight, relay_public_config]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
# build_wsl_node_pty stays in `needs` so it runs first and its artifact is
# available to download, but only the Windows matrix entry consumes it. We
# therefore gate the job on preflight + relay (must succeed) WITHOUT requiring
# build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux
# builds. `!cancelled()` (not `!failure()`) lets the job run even when
# build_wsl_node_pty failed; the Windows-only download step below then fails
# that single platform if the prebuild is missing.
needs: [preflight, relay_public_config, build_wsl_node_pty]
if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
Comment thread
Jgratton24 marked this conversation as resolved.
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
env:
Expand Down Expand Up @@ -275,6 +329,13 @@ jobs:
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"

- name: Download WSL node-pty prebuild
if: matrix.platform == 'win'
uses: actions/download-artifact@v7
with:
name: wsl-node-pty-x64
path: wsl-prebuild

- name: Install Spectre-mitigated MSVC libs
if: matrix.platform == 'win'
shell: pwsh
Expand Down Expand Up @@ -413,6 +474,10 @@ jobs:
echo "macOS signing disabled (missing one or more Apple signing secrets)."
fi
elif [[ "${{ matrix.platform }}" == "win" ]]; then
# Bundle the Linux node-pty binary built by the build_wsl_node_pty job
# so the packaged WSL backend ships a ready binary (no first-launch
# compile). Required for a working WSL backend on Windows.
args+=(--wsl-prebuild "$GITHUB_WORKSPACE/wsl-prebuild/pty.node")
if has_all \
"$AZURE_TENANT_ID" \
"$AZURE_CLIENT_ID" \
Expand Down
28 changes: 23 additions & 5 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopCloudAuth from "./DesktopCloudAuth.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopObservability from "./DesktopObservability.ts";
Expand All @@ -22,6 +22,7 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
Expand Down Expand Up @@ -132,11 +133,13 @@ const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) =>
handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));

const bootstrap = Effect.gen(function* () {
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const primaryBackend = yield* pool.primary;
const state = yield* DesktopState.DesktopState;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
yield* logBootstrapInfo("bootstrap start");

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
Expand Down Expand Up @@ -180,8 +183,13 @@ const bootstrap = Effect.gen(function* () {
yield* logBootstrapInfo("bootstrap ipc handlers registered");

if (!(yield* Ref.get(state.quitting))) {
yield* backendManager.start;
yield* primaryBackend.start;
yield* logBootstrapInfo("bootstrap backend start requested");
// Bring up the WSL backend if the user previously enabled it. The
// primary is already starting; reconcile fires off the WSL register
// in parallel rather than blocking primary readiness on a possibly
// slow first wsl.exe spawn.
yield* Effect.forkScoped(wslBackend.reconcile);
}
}).pipe(Effect.withSpan("desktop.bootstrap"));

Expand Down Expand Up @@ -230,10 +238,20 @@ const scopedProgram = Effect.scoped(
yield* Effect.annotateCurrentSpan({ scope: "desktop", runId });

const shutdown = yield* DesktopLifecycle.DesktopShutdown;
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;

yield* Effect.addFinalizer(() =>
backendManager.stop().pipe(Effect.ensuring(shutdown.markComplete)),
Effect.gen(function* () {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
// Stop every backend in the pool, not just the primary. The
// electronApp.quit() path can race ahead of the layer-scope
// cascade, so leaving the WSL instance for its parent scope
// finalizer means it gets hard-killed by the OS instead of
// receiving SIGTERM + grace. Stops run concurrently.
const instances = yield* pool.list;
yield* Effect.forEach(instances, (instance) => instance.stop(), {
concurrency: "unbounded",
});
}).pipe(Effect.ensuring(shutdown.markComplete)),
);

yield* startup;
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ describe("DesktopObservability", () => {
}).pipe(Effect.provide(environmentLayer));

yield* Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
const outputLog = yield* factory.forInstance("primary");
yield* outputLog.writeSessionBoundary({
phase: "START",
details: "pid=123 port=3773 cwd=/repo",
Expand All @@ -145,13 +146,15 @@ describe("DesktopObservability", () => {
assert.equal(boundary.level, "INFO");
assert.equal(boundary.annotations.component, "desktop-backend-child");
assert.equal(boundary.annotations.runId, "test-run");
assert.equal(boundary.annotations.instanceId, "primary");
assert.equal(boundary.annotations.phase, "START");
assert.equal(boundary.annotations.details, "pid=123 port=3773 cwd=/repo");

assert.equal(output.message, "backend child process output");
assert.equal(output.level, "INFO");
assert.equal(output.annotations.component, "desktop-backend-child");
assert.equal(output.annotations.runId, "test-run");
assert.equal(output.annotations.instanceId, "primary");
assert.equal(output.annotations.stream, "stdout");
assert.equal(output.annotations.text, "hello server\n");
}).pipe(
Expand Down
Loading
Loading