feat: desktop v2 everything WSL (#23407)

This commit is contained in:
Luke Parker
2026-06-06 14:38:06 +10:00
committed by GitHub
parent 09d9cf01f9
commit bd7eb0603f
37 changed files with 2620 additions and 202 deletions
+60 -87
View File
@@ -13,17 +13,20 @@ import {
PlatformProvider,
ServerConnection,
useCommand,
useWslServers,
} from "@opencode-ai/app"
import * as Sentry from "@sentry/solid"
import type { AsyncStorage } from "@solid-primitives/storage"
import { MemoryRouter } from "@solidjs/router"
import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js"
import { createEffect, createMemo, createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { initializationData, initializationReady } from "./initialization"
import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
import { availableStartupServer, readyWslConnections } from "./wsl/connections"
import "./styles.css"
import { Splash } from "@opencode-ai/ui/logo"
import { useTheme } from "@opencode-ai/ui/theme/context"
const root = document.getElementById("root")
@@ -80,27 +83,6 @@ const createPlatform = (): Platform => {
return undefined
})()
const isWslEnabled = async () => {
if (os !== "windows") return false
return window.api
.getWslConfig()
.then((config) => config.enabled)
.catch(() => false)
}
const wslHome = async () => {
if (!(await isWslEnabled())) return undefined
return window.api.wslPath("~", "windows").catch(() => undefined)
}
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
if (!result || !(await isWslEnabled())) return result
if (Array.isArray(result)) {
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
}
return window.api.wslPath(result, "linux").catch(() => result) as any
}
const runDesktopMenuAction: Platform["runDesktopMenuAction"] = (action) => {
switch (action) {
case "view.resetZoom":
@@ -144,37 +126,34 @@ const createPlatform = (): Platform => {
}
})()
const wslServersApi = os === "windows" ? window.api.wslServers : undefined
return {
platform: "desktop",
os,
version: pkg.version,
async openDirectoryPickerDialog(opts) {
const defaultPath = await wslHome()
const result = await window.api.openDirectoryPicker({
return window.api.openDirectoryPicker({
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFolder"),
defaultPath,
})
return await handleWslPicker(result)
},
async openFilePickerDialog(opts) {
const result = await window.api.openFilePicker({
return window.api.openFilePicker({
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFile"),
accept: opts?.accept ?? ACCEPTED_FILE_TYPES,
extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS,
})
return handleWslPicker(result)
},
async saveFilePickerDialog(opts) {
const result = await window.api.saveFilePicker({
return window.api.saveFilePicker({
title: opts?.title ?? t("desktop.dialog.saveFile"),
defaultPath: opts?.defaultPath,
})
return handleWslPicker(result)
},
openLink(url: string) {
@@ -183,14 +162,7 @@ const createPlatform = (): Platform => {
async openPath(path: string, app?: string) {
if (os === "windows") {
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
const resolvedPath = await (async () => {
if (await isWslEnabled()) {
const converted = await window.api.wslPath(path, "windows").catch(() => null)
if (converted) return converted
}
return path
})()
return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
return window.api.openPath(path, resolvedApp ?? undefined)
}
return window.api.openPath(path, app)
},
@@ -247,12 +219,6 @@ const createPlatform = (): Platform => {
return fetch(input, init)
},
getWslEnabled: () => isWslEnabled(),
setWslEnabled: async (enabled) => {
await window.api.setWslConfig({ enabled })
},
getDefaultServer: async () => {
const url = await window.api.getDefaultServerUrl().catch(() => null)
if (!url) return null
@@ -263,6 +229,8 @@ const createPlatform = (): Platform => {
await window.api.setDefaultServerUrl(url)
},
wslServers: wslServersApi,
getDisplayBackend: async () => {
return window.api.getDisplayBackend().catch(() => null)
},
@@ -304,7 +272,6 @@ listenForDeepLinks()
render(() => {
const platform = createPlatform()
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
@@ -322,29 +289,9 @@ render(() => {
// Fetch sidecar credentials (available immediately, before health check)
const [sidecar] = createResource(() => window.api.awaitInitialization())
const [defaultServer] = createResource(() =>
platform.getDefaultServer?.().then((url) => {
if (url) return ServerConnection.key({ type: "http", http: { url } })
}),
)
const [defaultServer] = createResource(() => platform.getDefaultServer?.())
const [locale] = createResource(loadLocale)
const servers = () => {
const data = initializationData(sidecar)
if (!data) return []
const server: ServerConnection.Sidecar = {
displayName: "Local Server",
type: "sidecar",
variant: "base",
http: {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
},
}
return [server] as ServerConnection.Any[]
}
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
@@ -371,6 +318,52 @@ render(() => {
return null
}
function App() {
const wslServers = useWslServers()
const splash = (
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
</div>
)
const ready = createMemo(
() => !defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading,
)
const servers = createMemo(() => {
const data = initializationData(sidecar)
const list: ServerConnection.Any[] = []
if (data) {
list.push({
displayName: "Local Server",
type: "sidecar",
variant: "base",
http: {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
},
})
}
list.push(...readyWslConnections(wslServers.data))
return list
})
const effectiveDefaultServer = createMemo(() =>
ServerConnection.Key.make(availableStartupServer(defaultServer.latest, wslServers.data)),
)
return (
<Show when={ready()} fallback={splash}>
<Show when={effectiveDefaultServer()} keyed>
{(key) => (
<AppInterface defaultServer={key} servers={servers()} router={MemoryRouter}>
<Inner />
</AppInterface>
)}
</Show>
</Show>
)
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
@@ -381,27 +374,7 @@ render(() => {
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}>
<Show
when={
!defaultServer.loading &&
initializationReady(sidecar) &&
!windowConfig.loading &&
!windowCount.loading &&
!locale.loading
}
>
{(_) => {
return (
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()}
router={MemoryRouter}
>
<Inner />
</AppInterface>
)
}}
</Show>
<Show when={true}>{(_) => <App />}</Show>
</AppBaseProviders>
</PlatformProvider>
)
@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import type { WslServersState } from "@opencode-ai/app/wsl/types"
import { availableStartupServer, readyWslConnections } from "./connections"
const state = (kind: "starting" | "ready" | "failed" | "stopped"): WslServersState => ({
runtime: null,
installed: [],
online: [],
distroProbes: {},
opencodeChecks: {},
pendingRestart: false,
job: null,
servers: [
{
config: { id: "wsl:Debian", distro: "Debian" },
runtime: runtime(kind),
},
],
})
function runtime(kind: "starting" | "ready" | "failed" | "stopped") {
if (kind === "ready") return { kind, url: "http://127.0.0.1:4096", username: "opencode", password: "secret" }
if (kind === "failed") return { kind, message: "boom" }
return { kind }
}
describe("WSL desktop connections", () => {
test("publishes a WSL server only after it reports ready", () => {
expect(readyWslConnections(state("starting"))).toEqual([])
expect(readyWslConnections(state("failed"))).toEqual([])
expect(readyWslConnections(state("stopped"))).toEqual([])
expect(readyWslConnections(state("ready"))).toEqual([
expect.objectContaining({ displayName: "Debian", label: "WSL" }),
])
})
test("does not block desktop startup on a configured WSL default", () => {
const key = "wsl:Debian"
expect(availableStartupServer(key, undefined)).toBe("sidecar")
expect(availableStartupServer(key, state("starting"))).toBe("sidecar")
expect(availableStartupServer(key, state("ready"))).toBe(key)
})
})
@@ -0,0 +1,28 @@
import type { WslServersState } from "@opencode-ai/app/wsl/types"
export function readyWslConnections(state?: WslServersState) {
return (state?.servers ?? []).flatMap((item) => {
if (item.runtime.kind !== "ready") return []
return [
{
displayName: item.config.distro,
label: "WSL",
type: "sidecar" as const,
variant: "wsl" as const,
distro: item.config.distro,
http: {
url: item.runtime.url,
username: item.runtime.username ?? undefined,
password: item.runtime.password ?? undefined,
},
},
]
})
}
export function availableStartupServer(defaultServer: string | null | undefined, state?: WslServersState) {
const key = defaultServer ?? "sidecar"
if (!key.startsWith("wsl:")) return key
if (state?.servers.some((item) => item.config.id === key && item.runtime.kind === "ready")) return key
return "sidecar"
}