feat: branch Electron local startup by runtime

This commit is contained in:
LukeParkerDev 2026-04-16 14:48:38 +10:00
parent 482dc3a15d
commit 5aa544179d
2 changed files with 109 additions and 30 deletions

View file

@ -42,15 +42,14 @@ import { createLocalServerController } from "./local-server"
import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer } from "./server"
import { getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer, spawnWslLocalServer } from "./server"
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
import type { Server } from "virtual:opencode-server"
const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" }
let mainWindow: BrowserWindow | null = null
let server: Server.Listener | null = null
let server: { stop(): void } | null = null
const loadingComplete = defer<void>()
const pendingDeepLinks: string[] = []
@ -140,26 +139,47 @@ async function initialize() {
const hostname = "127.0.0.1"
const url = `http://${hostname}:${port}`
const password = randomUUID()
const config = localServer.getState().config
const runtime =
config.mode === "wsl" && config.distro
? {
key: `local:wsl:${config.distro}`,
mode: "wsl" as const,
distro: config.distro,
}
: {
key: "local:windows",
mode: "windows" as const,
distro: null,
}
logger.log("spawning sidecar", { url })
localServer.setRuntime({ key: "local:windows", mode: "windows", distro: null })
localServer.setRuntime(runtime)
localServer.setStatus({ kind: "running", step: null })
const { listener, health } = await spawnLocalServer(hostname, port, password).catch((error) => {
localServer.setStatus({
kind: "failed",
step: null,
message: error instanceof Error ? error.message : String(error),
})
throw error
})
server = listener
const runtime = localServer.getState().runtime
serverReady.resolve({
url,
username: "opencode",
password,
local: runtime,
})
const startup = await (async () => {
try {
if (runtime.mode === "wsl") {
if (!runtime.distro) throw new Error("No WSL distro selected")
return spawnWslLocalServer(runtime.distro, port, password)
}
return spawnLocalServer(hostname, port, password)
} catch (error) {
localServer.setStatus({
kind: "failed",
step: null,
message: error instanceof Error ? error.message : String(error),
})
logger.error("local server startup failed", error)
return undefined
}
})()
server = startup?.listener ?? null
const loadingTask = (async () => {
logger.log("sidecar connection started", { url })
@ -175,23 +195,25 @@ async function initialize() {
await sqliteDone?.promise
}
await Promise.race([
health.wait,
delay(30_000).then(() => {
throw new Error("Sidecar health check timed out")
}),
])
.then(() => {
localServer.setStatus({ kind: "ready" })
})
.catch((error) => {
localServer.setStatus({
kind: "failed",
step: null,
message: error instanceof Error ? error.message : String(error),
if (startup) {
await Promise.race([
startup.health.wait,
delay(30_000).then(() => {
throw new Error("Sidecar health check timed out")
}),
])
.then(() => {
localServer.setStatus({ kind: "ready" })
})
logger.error("sidecar health check failed", error)
})
.catch((error) => {
localServer.setStatus({
kind: "failed",
step: null,
message: error instanceof Error ? error.message : String(error),
})
logger.error("sidecar health check failed", error)
})
}
logger.log("loading task finished")
})()

View file

@ -1,7 +1,9 @@
import { spawn } from "node:child_process"
import { app } from "electron"
import { DEFAULT_SERVER_URL_KEY } from "./constants"
import { getUserShell, loadShellEnv } from "./shell-env"
import { getStore } from "./store"
import { wslArgs } from "./wsl"
export type HealthCheck = { wait: Promise<void> }
@ -46,6 +48,57 @@ export async function spawnLocalServer(hostname: string, port: number, password:
return { listener, health: { wait } }
}
export async function spawnWslLocalServer(distro: string, port: number, password: string) {
const script = [
"set -e",
"OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true",
"OPENCODE_EXPERIMENTAL_FILEWATCHER=true",
"OPENCODE_CLIENT=desktop",
`OPENCODE_SERVER_USERNAME=${shellEscape("opencode")}`,
`OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
'XDG_STATE_HOME="$HOME/.local/state"',
`exec opencode --print-logs --log-level WARN serve --hostname 0.0.0.0 --port ${port}`,
].join(" ")
const child = spawn("wsl", wslArgs(["bash", "-lc", script], distro), {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
})
child.stdout.setEncoding("utf8")
child.stderr.setEncoding("utf8")
const exit = new Promise<never>((_, reject) => {
child.once("error", reject)
child.once("exit", (code, signal) => {
reject(
new Error(
`WSL local server exited before becoming healthy (code=${code ?? "null"} signal=${signal ?? "null"})`,
),
)
})
})
const wait = Promise.race([
(async () => {
const url = `http://127.0.0.1:${port}`
while (true) {
await new Promise((resolve) => setTimeout(resolve, 100))
if (await checkHealth(url, password)) return
}
})(),
exit,
])
return {
listener: {
stop() {
child.kill()
},
},
health: { wait },
}
}
function prepareServerEnv(password: string) {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
@ -62,6 +115,10 @@ function prepareServerEnv(password: string) {
Object.assign(process.env, env)
}
function shellEscape(value: string) {
return `'${value.replace(/'/g, `'"'"'`)}'`
}
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
let healthUrl: URL
try {