feat: desktop v2 everything WSL (#23407)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user