From 49738314f5b7cb2e57e172f8d00b0e4973d23157 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 6 Jun 2026 10:58:19 +0200 Subject: [PATCH 1/7] fix: wsl beta stuff --- .../src/components/dialog-select-server.tsx | 15 ++++++++----- packages/app/src/wsl/dialog-add-server.tsx | 22 +++++++++---------- packages/app/src/wsl/settings.tsx | 6 +---- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index cc63c82450c0..08769068909c 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -509,11 +509,16 @@ export function useServerManagementController(options: { onSelect?: () => void; resetEdit() }) - async function handleRemove(url: ServerConnection.Key) { - tabs.removeServer(url) - server.remove(url) - if ((await platform.getDefaultServer?.()) === url) { - void platform.setDefaultServer?.(null) + async function handleRemove(key: ServerConnection.Key) { + try { + if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key) + tabs.removeServer(key) + server.remove(key) + if ((await platform.getDefaultServer?.()) === key) { + await setDefault(null) + } + } catch (err) { + showRequestError(language, err) } } diff --git a/packages/app/src/wsl/dialog-add-server.tsx b/packages/app/src/wsl/dialog-add-server.tsx index 5673d58b14b2..6c79824136fa 100644 --- a/packages/app/src/wsl/dialog-add-server.tsx +++ b/packages/app/src/wsl/dialog-add-server.tsx @@ -71,6 +71,17 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) { if (!distro) return null return current()?.opencodeChecks[distro] ?? null }) + const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart) + const distroReady = createMemo(() => { + const probe = selectedProbe() + if (!probe || !selectedDistro()) return false + if (selectedInstalled()?.version === 1) return false + return probe.canExecute && probe.hasBash && probe.hasCurl + }) + const opencodeReady = createMemo(() => { + const check = opencodeCheck() + return !!check?.resolvedPath && !check.error + }) const distroWarningProbe = createMemo(() => { const probe = selectedProbe() if (!probe) return null @@ -106,17 +117,6 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) { const job = current()?.job return job?.kind === "install-opencode" && job.distro === selectedDistro() }) - const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart) - const distroReady = createMemo(() => { - const probe = selectedProbe() - if (!probe || !selectedDistro()) return false - if (selectedInstalled()?.version === 1) return false - return probe.canExecute && probe.hasBash && probe.hasCurl - }) - const opencodeReady = createMemo(() => { - const check = opencodeCheck() - return !!check?.resolvedPath && !check.error - }) const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady()) const addDisabled = createMemo(() => { const job = current()?.job diff --git a/packages/app/src/wsl/settings.tsx b/packages/app/src/wsl/settings.tsx index 0a3d56fef35c..746a5861c52c 100644 --- a/packages/app/src/wsl/settings.tsx +++ b/packages/app/src/wsl/settings.tsx @@ -76,11 +76,7 @@ export function WslServerSettings(props: { })) const remove = (key: ServerConnection.Key) => { - if (!api) return - request.mutate(async () => { - await api.removeServer(key) - await props.controller.handleRemove(key) - }) + request.mutate(() => props.controller.handleRemove(key)) } return ( From 57f95e4fbd9d52da32f68304a7970fccab78913b Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 6 Jun 2026 11:14:23 +0200 Subject: [PATCH 2/7] fix: opencode version check --- packages/desktop/src/main/wsl/servers.ts | 28 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts index dfd0112429d1..dbde0d211d00 100644 --- a/packages/desktop/src/main/wsl/servers.ts +++ b/packages/desktop/src/main/wsl/servers.ts @@ -119,6 +119,28 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa setOpencodeCheck(distro, opencodeCheck(distro, resolved, version, appVersion)) } + const refreshOpencodeCheckBackground = (id: string, distro: string) => { + void refreshOpencodeCheck(distro).catch((error) => { + const message = error instanceof Error ? error.message : String(error) + logger?.error("wsl opencode check failed", { id, distro, message }) + }) + } + + const refreshOpencodeChecks = async () => { + await Promise.all( + state.servers.map((item) => + refreshOpencodeCheck(item.config.distro).catch((error) => { + const message = error instanceof Error ? error.message : String(error) + logger?.error("wsl opencode check failed", { + id: item.config.id, + distro: item.config.distro, + message, + }) + }), + ), + ) + } + const refreshDistroLists = async (opts: { signal?: AbortSignal }) => { const [installed, online] = await Promise.all([listInstalledWslDistros(opts), listOnlineWslDistros(opts)]) return { installed, online } @@ -170,10 +192,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa setRuntime(id, { kind: "failed", message }) logger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal }) }) - void refreshOpencodeCheck(item.config.distro).catch((error) => { - const message = error instanceof Error ? error.message : String(error) - logger?.error("wsl opencode check failed", { id, distro: item.config.distro, message }) - }) + refreshOpencodeCheckBackground(id, item.config.distro) logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url }) } catch (error) { const message = error instanceof Error ? error.message : String(error) @@ -225,6 +244,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa async initialize() { refreshFromStore() + void refreshOpencodeChecks() for (const id of wslServerIdsToStartOnInitialize(state.servers.map((item) => item.config))) void startServer(id) }, From 0f252257ac4d0272531ae3249e121d099e60a3f1 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sun, 7 Jun 2026 00:37:55 +0200 Subject: [PATCH 3/7] feat: guard wsl only to win32 --- packages/desktop/src/main/index.ts | 4 +- packages/desktop/src/main/wsl/ipc.ts | 43 ++++++++++++ packages/desktop/src/main/wsl/servers.test.ts | 65 ++++++++++++++++++- packages/desktop/src/main/wsl/servers.ts | 21 ++++-- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index a4ad49083b09..6e57f5b1ef14 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -326,7 +326,9 @@ const main = Effect.gen(function* () { password, }) - void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error)) + if (process.platform === "win32") { + void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error)) + } yield* Effect.promise(() => health.wait).pipe( Effect.timeout("30 seconds"), diff --git a/packages/desktop/src/main/wsl/ipc.ts b/packages/desktop/src/main/wsl/ipc.ts index c31fd680c036..ed98004beeb3 100644 --- a/packages/desktop/src/main/wsl/ipc.ts +++ b/packages/desktop/src/main/wsl/ipc.ts @@ -2,8 +2,14 @@ import { app, ipcMain } from "electron" import type { IpcMainInvokeEvent } from "electron" import type { WslServersController } from "./servers" import { requireWslIpcString } from "./policy" +import type { WslServersState } from "../../preload/types" export function registerWslIpcHandlers(controller: WslServersController) { + if (process.platform !== "win32") { + registerUnavailableWslIpcHandlers() + return + } + const subscriptions = new Map void>() const unsubscribe = (id: number) => { const off = subscriptions.get(id) @@ -62,3 +68,40 @@ export function registerWslIpcHandlers(controller: WslServersController) { controller.startServer(requireWslIpcString("server id", id)), ) } + +function registerUnavailableWslIpcHandlers() { + const unavailable = () => { + throw new Error("WSL is only available on Windows") + } + const state = (): WslServersState => ({ + runtime: { + available: false, + version: null, + error: "WSL is only available on Windows", + }, + installed: [], + online: [], + distroProbes: {}, + opencodeChecks: {}, + pendingRestart: false, + servers: [], + job: null, + }) + + ipcMain.handle("wsl-servers-subscribe", (event) => { + event.sender.send("wsl-servers-event", { type: "state", state: state() }) + }) + ipcMain.handle("wsl-servers-unsubscribe", () => undefined) + ipcMain.handle("wsl-servers-get-state", () => state()) + ipcMain.handle("wsl-servers-probe-runtime", unavailable) + ipcMain.handle("wsl-servers-refresh-distros", unavailable) + ipcMain.handle("wsl-servers-install-wsl", unavailable) + ipcMain.handle("wsl-servers-install-distro", unavailable) + ipcMain.handle("wsl-servers-probe-distro", unavailable) + ipcMain.handle("wsl-servers-probe-opencode", unavailable) + ipcMain.handle("wsl-servers-install-opencode", unavailable) + ipcMain.handle("wsl-servers-open-terminal", unavailable) + ipcMain.handle("wsl-servers-add", unavailable) + ipcMain.handle("wsl-servers-remove", unavailable) + ipcMain.handle("wsl-servers-start", unavailable) +} diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index 98f88133ca24..901d658d9f08 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test" +import { expect, mock, test } from "bun:test" import { clearWslDistroState, requireWslIpcString, wslServerIdToRestart, wslTerminalArgs } from "./policy" import { expectOpencodeVersion, @@ -7,6 +7,37 @@ import { wslServerIdsToStartOnInitialize, } from "./startup" +let persistedServers: unknown[] = [] +let releaseOpencodeResolve: (() => void) | undefined + +mock.module("../store", () => ({ + getStore: () => ({ + get: () => ({ servers: persistedServers }), + set: (_key: string, value: { servers?: unknown[] }) => { + persistedServers = value.servers ?? [] + }, + }), +})) + +mock.module("./runtime", () => ({ + installWslDistro: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), + installWslOpencode: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), + installWslRuntimeElevated: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), + listInstalledWslDistros: async () => [], + listOnlineWslDistros: async () => [], + openWslTerminal: async () => undefined, + probeWslDistro: async (name: string) => ({ name, canExecute: true, hasBash: true, hasCurl: true, error: null }), + probeWslRuntime: async () => ({ available: true, version: "WSL version: 2", error: null }), + readWslCommandVersion: async () => "1.16.2", + resolveWslOpencode: async () => { + await new Promise((resolve) => { + releaseOpencodeResolve = resolve + }) + return "/home/me/.opencode/bin/opencode" + }, + summarize: (value: string) => value.trim(), +})) + test("starts every configured WSL server on initialization", () => { expect( wslServerIdsToStartOnInitialize([ @@ -91,3 +122,35 @@ test("derives a required Windows restart from the post-install runtime probe", ( expect(pendingRestartAfterWslInstall({ available: false, version: null, error: "WSL unavailable" })).toBe(true) expect(pendingRestartAfterWslInstall({ available: true, version: "WSL version: 2.6.1", error: null })).toBe(false) }) + +test("ignores stale background OpenCode checks after removing a WSL server", async () => { + persistedServers = [] + releaseOpencodeResolve = undefined + const { createWslServersController } = await import("./servers") + const controller = createWslServersController("1.16.2", async () => ({ + listener: { + stop: () => undefined, + onExit: () => undefined, + }, + url: "http://127.0.0.1:4096", + username: "opencode", + password: "secret", + })) + + await controller.addServer("Debian") + await waitFor(() => !!releaseOpencodeResolve) + await controller.removeServer("wsl:Debian") + releaseOpencodeResolve?.() + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(controller.getState().servers).toEqual([]) + expect(controller.getState().opencodeChecks).toEqual({}) +}) + +async function waitFor(check: () => boolean) { + for (let attempt = 0; attempt < 20; attempt++) { + if (check()) return + await new Promise((resolve) => setTimeout(resolve, 0)) + } + throw new Error("Timed out waiting for condition") +} diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts index dbde0d211d00..12e209f770e4 100644 --- a/packages/desktop/src/main/wsl/servers.ts +++ b/packages/desktop/src/main/wsl/servers.ts @@ -113,17 +113,26 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa }) } - const refreshOpencodeCheck = async (distro: string, opts?: { signal?: AbortSignal }) => { + const checkOpencode = async (distro: string, opts?: { signal?: AbortSignal }) => { const resolved = await resolveWslOpencode(distro, opts) const version = resolved ? await readWslCommandVersion(resolved, distro, opts) : null - setOpencodeCheck(distro, opencodeCheck(distro, resolved, version, appVersion)) + return opencodeCheck(distro, resolved, version, appVersion) + } + + const refreshOpencodeCheck = async (distro: string, opts?: { signal?: AbortSignal }) => { + setOpencodeCheck(distro, await checkOpencode(distro, opts)) } const refreshOpencodeCheckBackground = (id: string, distro: string) => { - void refreshOpencodeCheck(distro).catch((error) => { - const message = error instanceof Error ? error.message : String(error) - logger?.error("wsl opencode check failed", { id, distro, message }) - }) + void checkOpencode(distro) + .then((check) => { + if (!state.servers.some((item) => item.config.id === id && item.config.distro === distro)) return + setOpencodeCheck(distro, check) + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + logger?.error("wsl opencode check failed", { id, distro, message }) + }) } const refreshOpencodeChecks = async () => { From 97f7f42e0801e6aa8330e99052c8a942fbdc0965 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sun, 7 Jun 2026 10:37:10 +0200 Subject: [PATCH 4/7] fix: dont repopulate removed server data --- packages/desktop/src/main/wsl/servers.test.ts | 23 +++++++++++++++++ packages/desktop/src/main/wsl/servers.ts | 25 +++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index 901d658d9f08..2edb7e4b90e0 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -10,6 +10,13 @@ import { let persistedServers: unknown[] = [] let releaseOpencodeResolve: (() => void) | undefined +mock.module("electron", () => ({ + app: { + getPath: () => "/tmp/opencode-desktop-test", + isPackaged: false, + }, +})) + mock.module("../store", () => ({ getStore: () => ({ get: () => ({ servers: persistedServers }), @@ -147,6 +154,22 @@ test("ignores stale background OpenCode checks after removing a WSL server", asy expect(controller.getState().opencodeChecks).toEqual({}) }) +test("ignores stale startup OpenCode checks after removing a WSL server", async () => { + persistedServers = [{ id: "wsl:Debian", distro: "Debian" }] + releaseOpencodeResolve = undefined + const { createWslServersController } = await import("./servers") + const controller = createWslServersController("1.16.2", async () => new Promise(() => undefined)) + + await controller.initialize() + await waitFor(() => !!releaseOpencodeResolve) + await controller.removeServer("wsl:Debian") + releaseOpencodeResolve?.() + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(controller.getState().servers).toEqual([]) + expect(controller.getState().opencodeChecks).toEqual({}) +}) + async function waitFor(check: () => boolean) { for (let attempt = 0; attempt < 20; attempt++) { if (check()) return diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts index 12e209f770e4..039b61864a7a 100644 --- a/packages/desktop/src/main/wsl/servers.ts +++ b/packages/desktop/src/main/wsl/servers.ts @@ -123,10 +123,14 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa setOpencodeCheck(distro, await checkOpencode(distro, opts)) } + const hasServer = (id: string, distro: string) => { + return state.servers.some((item) => item.config.id === id && item.config.distro === distro) + } + const refreshOpencodeCheckBackground = (id: string, distro: string) => { void checkOpencode(distro) .then((check) => { - if (!state.servers.some((item) => item.config.id === id && item.config.distro === distro)) return + if (!hasServer(id, distro)) return setOpencodeCheck(distro, check) }) .catch((error) => { @@ -138,14 +142,19 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa const refreshOpencodeChecks = async () => { await Promise.all( state.servers.map((item) => - refreshOpencodeCheck(item.config.distro).catch((error) => { - const message = error instanceof Error ? error.message : String(error) - logger?.error("wsl opencode check failed", { - id: item.config.id, - distro: item.config.distro, - message, + checkOpencode(item.config.distro) + .then((check) => { + if (!hasServer(item.config.id, item.config.distro)) return + setOpencodeCheck(item.config.distro, check) }) - }), + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + logger?.error("wsl opencode check failed", { + id: item.config.id, + distro: item.config.distro, + message, + }) + }), ), ) } From bf561269557fa3f46562c7fc239f3376d764d024 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sun, 7 Jun 2026 10:52:51 +0200 Subject: [PATCH 5/7] remove mocked tests --- packages/desktop/src/main/constants.ts | 5 +- packages/desktop/src/main/index.ts | 6 +- packages/desktop/src/main/store-keys.ts | 4 + packages/desktop/src/main/store.ts | 6 +- packages/desktop/src/main/wsl/servers.test.ts | 86 ++++++++----------- packages/desktop/src/main/wsl/servers.ts | 35 ++++++-- 6 files changed, 76 insertions(+), 66 deletions(-) create mode 100644 packages/desktop/src/main/store-keys.ts diff --git a/packages/desktop/src/main/constants.ts b/packages/desktop/src/main/constants.ts index 7991570aefda..e5b7581c9911 100644 --- a/packages/desktop/src/main/constants.ts +++ b/packages/desktop/src/main/constants.ts @@ -1,11 +1,8 @@ import { app } from "electron" +export { DEFAULT_SERVER_URL_KEY, PINCH_ZOOM_ENABLED_KEY, SETTINGS_STORE, WSL_SERVERS_KEY } from "./store-keys" type Channel = "dev" | "beta" | "prod" const raw = import.meta.env.OPENCODE_CHANNEL export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev" -export const SETTINGS_STORE = "opencode.settings" -export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" -export const WSL_SERVERS_KEY = "wslServers" -export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev" diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 6e57f5b1ef14..d527445b3ac7 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -145,8 +145,10 @@ const main = Effect.gen(function* () { }) }, { - log: (message, meta) => logger.log(message, meta), - error: (message, meta) => logger.error(message, meta), + logger: { + log: (message, meta) => logger.log(message, meta), + error: (message, meta) => logger.error(message, meta), + }, }, ) const stopSidecars = async () => { diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts new file mode 100644 index 000000000000..f05018a26920 --- /dev/null +++ b/packages/desktop/src/main/store-keys.ts @@ -0,0 +1,4 @@ +export const SETTINGS_STORE = "opencode.settings" +export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" +export const WSL_SERVERS_KEY = "wslServers" +export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" diff --git a/packages/desktop/src/main/store.ts b/packages/desktop/src/main/store.ts index a591f878decf..b99497f9b1c5 100644 --- a/packages/desktop/src/main/store.ts +++ b/packages/desktop/src/main/store.ts @@ -1,7 +1,7 @@ import Store from "electron-store" -import { app } from "electron" +import electron from "electron" -import { SETTINGS_STORE } from "./constants" +import { SETTINGS_STORE } from "./store-keys" const cache = new Map() @@ -14,7 +14,7 @@ export function getStore(name = SETTINGS_STORE) { if (cached) return cached const next = new Store({ name, - cwd: app.getPath("userData"), + cwd: electron.app.getPath("userData"), fileExtension: "", accessPropertiesByDotNotation: false, }) diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index 2edb7e4b90e0..fcca712a0471 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -1,4 +1,4 @@ -import { expect, mock, test } from "bun:test" +import { expect, test } from "bun:test" import { clearWslDistroState, requireWslIpcString, wslServerIdToRestart, wslTerminalArgs } from "./policy" import { expectOpencodeVersion, @@ -6,45 +6,11 @@ import { pollWslHealth, wslServerIdsToStartOnInitialize, } from "./startup" +import { createWslServersController, type WslServerConfig } from "./servers" -let persistedServers: unknown[] = [] +let persistedServers: WslServerConfig[] = [] let releaseOpencodeResolve: (() => void) | undefined -mock.module("electron", () => ({ - app: { - getPath: () => "/tmp/opencode-desktop-test", - isPackaged: false, - }, -})) - -mock.module("../store", () => ({ - getStore: () => ({ - get: () => ({ servers: persistedServers }), - set: (_key: string, value: { servers?: unknown[] }) => { - persistedServers = value.servers ?? [] - }, - }), -})) - -mock.module("./runtime", () => ({ - installWslDistro: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), - installWslOpencode: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), - installWslRuntimeElevated: async () => ({ code: 0, signal: null, stdout: "", stderr: "" }), - listInstalledWslDistros: async () => [], - listOnlineWslDistros: async () => [], - openWslTerminal: async () => undefined, - probeWslDistro: async (name: string) => ({ name, canExecute: true, hasBash: true, hasCurl: true, error: null }), - probeWslRuntime: async () => ({ available: true, version: "WSL version: 2", error: null }), - readWslCommandVersion: async () => "1.16.2", - resolveWslOpencode: async () => { - await new Promise((resolve) => { - releaseOpencodeResolve = resolve - }) - return "/home/me/.opencode/bin/opencode" - }, - summarize: (value: string) => value.trim(), -})) - test("starts every configured WSL server on initialization", () => { expect( wslServerIdsToStartOnInitialize([ @@ -133,16 +99,19 @@ test("derives a required Windows restart from the post-install runtime probe", ( test("ignores stale background OpenCode checks after removing a WSL server", async () => { persistedServers = [] releaseOpencodeResolve = undefined - const { createWslServersController } = await import("./servers") - const controller = createWslServersController("1.16.2", async () => ({ - listener: { - stop: () => undefined, - onExit: () => undefined, - }, - url: "http://127.0.0.1:4096", - username: "opencode", - password: "secret", - })) + const controller = createWslServersController( + "1.16.2", + async () => ({ + listener: { + stop: () => undefined, + onExit: () => undefined, + }, + url: "http://127.0.0.1:4096", + username: "opencode", + password: "secret", + }), + testControllerOptions(), + ) await controller.addServer("Debian") await waitFor(() => !!releaseOpencodeResolve) @@ -157,8 +126,11 @@ test("ignores stale background OpenCode checks after removing a WSL server", asy test("ignores stale startup OpenCode checks after removing a WSL server", async () => { persistedServers = [{ id: "wsl:Debian", distro: "Debian" }] releaseOpencodeResolve = undefined - const { createWslServersController } = await import("./servers") - const controller = createWslServersController("1.16.2", async () => new Promise(() => undefined)) + const controller = createWslServersController( + "1.16.2", + async () => new Promise(() => undefined), + testControllerOptions(), + ) await controller.initialize() await waitFor(() => !!releaseOpencodeResolve) @@ -177,3 +149,19 @@ async function waitFor(check: () => boolean) { } throw new Error("Timed out waiting for condition") } + +function testControllerOptions() { + return { + readServers: () => persistedServers, + writeServers: (servers: WslServerConfig[]) => { + persistedServers = servers + }, + readCommandVersion: async () => "1.16.2", + resolveOpencode: async () => { + await new Promise((resolve) => { + releaseOpencodeResolve = resolve + }) + return "/home/me/.opencode/bin/opencode" + }, + } +} diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts index 039b61864a7a..0528f2eb59ac 100644 --- a/packages/desktop/src/main/wsl/servers.ts +++ b/packages/desktop/src/main/wsl/servers.ts @@ -11,7 +11,7 @@ import type { WslServersEvent, WslServersState, } from "../../preload/types" -import { WSL_SERVERS_KEY } from "../constants" +import { WSL_SERVERS_KEY } from "../store-keys" import { getStore } from "../store" import { expectOpencodeVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup" import { clearWslDistroState, wslServerIdToRestart } from "./policy" @@ -43,18 +43,33 @@ type ControllerLogger = { error: (message: string, meta?: unknown) => void } +type WslServersControllerOptions = { + logger?: ControllerLogger + readServers?: () => WslServerConfig[] + writeServers?: (servers: WslServerConfig[]) => void + resolveOpencode?: typeof resolveWslOpencode + readCommandVersion?: typeof readWslCommandVersion +} + export type WslServersController = ReturnType export function wslServerIdForDistro(distro: string) { return `wsl:${distro}` } -export function createWslServersController(appVersion: string, spawnSidecar: SpawnSidecar, logger?: ControllerLogger) { +export function createWslServersController( + appVersion: string, + spawnSidecar: SpawnSidecar, + options?: WslServersControllerOptions, +) { let state: WslServersState = initialState() const listeners = new Set<(event: WslServersEvent) => void>() const sidecars = new Map() const startAttempts = new Map() let jobAbort: AbortController | undefined + const logger = options?.logger + const readServers = options?.readServers ?? readPersistedServers + const writeServers = options?.writeServers ?? writePersistedServers const emit = () => { for (const listener of listeners) listener({ type: "state", state }) @@ -66,7 +81,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa } const persistServers = (servers: WslServerConfig[]) => { - getStore().set(WSL_SERVERS_KEY, { servers }) + writeServers(servers) } const updateServer = (id: string, update: (item: WslServerItem) => WslServerItem) => { @@ -89,7 +104,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa } const refreshFromStore = () => { - const persisted = readPersistedServers() + const persisted = readServers() const items: WslServerItem[] = persisted.map((config) => { const existing = state.servers.find((item) => item.config.id === config.id) return { @@ -114,8 +129,8 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa } const checkOpencode = async (distro: string, opts?: { signal?: AbortSignal }) => { - const resolved = await resolveWslOpencode(distro, opts) - const version = resolved ? await readWslCommandVersion(resolved, distro, opts) : null + const resolved = await (options?.resolveOpencode ?? resolveWslOpencode)(distro, opts) + const version = resolved ? await (options?.readCommandVersion ?? readWslCommandVersion)(resolved, distro, opts) : null return opencodeCheck(distro, resolved, version, appVersion) } @@ -349,7 +364,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa id, distro, } - persistServers([...readPersistedServers(), config]) + persistServers([...readServers(), config]) setState({ servers: [...state.servers, { config, runtime: { kind: "starting" } }], }) @@ -361,7 +376,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa const distro = state.servers.find((item) => item.config.id === id)?.config.distro invalidateStartAttempt(id) await stopServerInternal(id) - const remaining = readPersistedServers().filter((item) => item.id !== id) + const remaining = readServers().filter((item) => item.id !== id) persistServers(remaining) setState({ servers: state.servers.filter((item) => item.config.id !== id), @@ -409,6 +424,10 @@ function readPersistedServers(): WslServerConfig[] { return [] } +function writePersistedServers(servers: WslServerConfig[]) { + getStore().set(WSL_SERVERS_KEY, { servers }) +} + function normalizePersistedServer(value: unknown): WslServerConfig[] { if (!value || typeof value !== "object") return [] const record = value as Record From 72f58d641155f12ea05e829395438877ab713fd7 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sun, 7 Jun 2026 10:53:24 +0200 Subject: [PATCH 6/7] fix: SyntaxError: Export named 'crashReporter' not found in module --- packages/desktop/src/main/server.ts | 3 ++- packages/desktop/src/main/shell-env.ts | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index 329cf9d6d433..e0a873e4ccb8 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url" import { app, utilityProcess } from "electron" import type { Details } from "electron" import { DEFAULT_SERVER_URL_KEY } from "./constants" +import { getLogger } from "./logging" import { getUserShell, loadShellEnv } from "./shell-env" import { getStore } from "./store" @@ -43,7 +44,7 @@ export function setDefaultServerUrl(url: string | null) { export function preferAppEnv(userDataPath: string) { const shell = process.platform === "win32" ? null : getUserShell() Object.assign(process.env, { - ...(shell ? loadShellEnv(shell) : null), + ...(shell ? loadShellEnv(shell, getLogger()) : null), OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", OPENCODE_CLIENT: "desktop", diff --git a/packages/desktop/src/main/shell-env.ts b/packages/desktop/src/main/shell-env.ts index deb43033aefc..082ed5e930db 100644 --- a/packages/desktop/src/main/shell-env.ts +++ b/packages/desktop/src/main/shell-env.ts @@ -1,11 +1,13 @@ import { spawnSync } from "node:child_process" import { userInfo } from "node:os" import { basename } from "node:path" -import { getLogger } from "./logging" const TIMEOUT = 5_000 type Probe = { type: "Loaded"; value: Record } | { type: "Timeout" } | { type: "Unavailable" } +type ShellEnvLogger = { + log: (message: string) => void +} export function resolveUserShell(envShell: string | undefined, loginShell: string | null | undefined) { const resolvedLoginShell = loginShell && loginShell !== "unknown" ? loginShell : undefined @@ -65,8 +67,7 @@ export function isNushell(shell: string) { return name === "nu" || name === "nu.exe" || raw.endsWith("\\nu.exe") } -export function loadShellEnv(shell: string) { - const logger = getLogger() +export function loadShellEnv(shell: string, logger: ShellEnvLogger) { if (isNushell(shell)) { logger.log(`[server] Skipping shell env probe for nushell: ${shell}`) return null From ac7146312635f5b36a56d1af31ef5bbf5d022592 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sun, 7 Jun 2026 11:00:25 +0200 Subject: [PATCH 7/7] fix: dont reexport keys: --- packages/desktop/src/main/constants.ts | 1 - packages/desktop/src/main/server.ts | 2 +- packages/desktop/src/main/windows.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/main/constants.ts b/packages/desktop/src/main/constants.ts index e5b7581c9911..258deb7c6de7 100644 --- a/packages/desktop/src/main/constants.ts +++ b/packages/desktop/src/main/constants.ts @@ -1,5 +1,4 @@ import { app } from "electron" -export { DEFAULT_SERVER_URL_KEY, PINCH_ZOOM_ENABLED_KEY, SETTINGS_STORE, WSL_SERVERS_KEY } from "./store-keys" type Channel = "dev" | "beta" | "prod" const raw = import.meta.env.OPENCODE_CHANNEL diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index e0a873e4ccb8..b213dbc82a23 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -2,10 +2,10 @@ import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" import { app, utilityProcess } from "electron" import type { Details } from "electron" -import { DEFAULT_SERVER_URL_KEY } from "./constants" import { getLogger } from "./logging" import { getUserShell, loadShellEnv } from "./shell-env" import { getStore } from "./store" +import { DEFAULT_SERVER_URL_KEY } from "./store-keys" export type HealthCheck = { wait: Promise } diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 1bdfb1042459..e0179f54c656 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -6,9 +6,9 @@ import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } f import { dirname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import type { TitlebarTheme } from "../preload/types" -import { PINCH_ZOOM_ENABLED_KEY } from "./constants" import { exportDebugLogs, write as writeLog } from "./logging" import { getStore } from "./store" +import { PINCH_ZOOM_ENABLED_KEY } from "./store-keys" import { createUnresponsiveSampler } from "./unresponsive" const root = dirname(fileURLToPath(import.meta.url))