mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
.
This commit is contained in:
@@ -47,7 +47,7 @@ function versionOlderThan(current: string | null | undefined, expected: string |
|
||||
}
|
||||
|
||||
interface DialogSelectServerProps {
|
||||
initialView?: "list" | "add-wsl"
|
||||
initialView?: "add-wsl"
|
||||
onNavigateHome?: () => void
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown
|
||||
})
|
||||
}
|
||||
|
||||
function isWslSidecar(conn: ServerConnection.Any): conn is ServerConnection.Sidecar & { variant: "wsl" } {
|
||||
return conn.type === "sidecar" && conn.variant === "wsl"
|
||||
}
|
||||
|
||||
function useServerPreview() {
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
|
||||
@@ -182,6 +186,10 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
const wslServers = useWslServers()
|
||||
const { previewStatus } = useServerPreview()
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
let disposed = false
|
||||
onCleanup(() => {
|
||||
disposed = true
|
||||
})
|
||||
const [store, setStore] = createStore({
|
||||
status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||
addServer: {
|
||||
@@ -355,7 +363,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
const health = (key: ServerConnection.Key) => store.status[key] ?? cachedServerStatus.get(key)
|
||||
const isSelectable = (conn: ServerConnection.Any) => !isPlaceholderServerUrl(conn.http.url)
|
||||
const wslRuntime = (conn: ServerConnection.Any) => {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return
|
||||
if (!isWslSidecar(conn)) return
|
||||
return wslState()?.servers.find((item) => item.config.id === ServerConnection.key(conn))?.runtime
|
||||
}
|
||||
const canRetryWsl = (conn: ServerConnection.Any) => {
|
||||
@@ -390,6 +398,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||
}),
|
||||
)
|
||||
if (disposed) return
|
||||
for (const [key, value] of Object.entries(results)) {
|
||||
cachedServerStatus.set(ServerConnection.Key.make(key), value)
|
||||
}
|
||||
@@ -404,13 +413,12 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
})
|
||||
|
||||
const wslCheck = (conn: ServerConnection.Any) => {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return null
|
||||
if (!isWslSidecar(conn)) return null
|
||||
return wslState()?.opencodeChecks[conn.distro] ?? null
|
||||
}
|
||||
|
||||
const displayVersion = (conn: ServerConnection.Any) => {
|
||||
if (conn.type === "sidecar" && conn.variant === "wsl") return wslCheck(conn)?.version ?? undefined
|
||||
return undefined
|
||||
return wslCheck(conn)?.version ?? undefined
|
||||
}
|
||||
|
||||
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||
@@ -630,17 +638,17 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
}
|
||||
|
||||
function handleRemoveWsl(conn: ServerConnection.Any) {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return
|
||||
if (!isWslSidecar(conn)) return
|
||||
removeWslMutation.mutate(ServerConnection.key(conn))
|
||||
}
|
||||
|
||||
function handleRetryWsl(conn: ServerConnection.Any) {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return
|
||||
if (!isWslSidecar(conn)) return
|
||||
retryWslMutation.mutate(ServerConnection.key(conn))
|
||||
}
|
||||
|
||||
function handleUpdateWsl(conn: ServerConnection.Any) {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return
|
||||
if (!isWslSidecar(conn)) return
|
||||
updateWslMutation.mutate(conn.distro)
|
||||
}
|
||||
|
||||
@@ -697,11 +705,11 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
const isWslSidecar = i.type === "sidecar" && i.variant === "wsl"
|
||||
const wslDistro = i.type === "sidecar" && i.variant === "wsl" ? i.distro : undefined
|
||||
const wsl = isWslSidecar(i)
|
||||
const wslDistro = wsl ? i.distro : undefined
|
||||
const blocked = () => !isSelectable(i) || health(key)?.healthy === false
|
||||
const canChangeDefault = () => canDefault() && i.type !== "ssh"
|
||||
const canRemove = () => i.type === "http" || isWslSidecar
|
||||
const canRemove = () => i.type === "http" || wsl
|
||||
const hasMenuActionsBeforeDelete = () => canRemove() && (i.type === "http" || canChangeDefault() || canRetryWsl(i))
|
||||
const outdated = () => {
|
||||
const check = wslCheck(i)
|
||||
@@ -739,7 +747,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-3 pl-4">
|
||||
<Show when={isWslSidecar && opencodeAction()}>
|
||||
<Show when={wsl && opencodeAction()}>
|
||||
{(label) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -782,7 +790,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={isWslSidecar && canRetryWsl(i)}>
|
||||
<Show when={wsl && canRetryWsl(i)}>
|
||||
<DropdownMenu.Item onSelect={() => handleRetryWsl(i)}>
|
||||
<DropdownMenu.ItemLabel>Retry start</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -807,7 +815,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
<Show when={canRemove()}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (isWslSidecar) {
|
||||
if (wsl) {
|
||||
handleRemoveWsl(i)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { WslServerStep } from "@/context/platform"
|
||||
import type { WslServersPlatform, WslServerStep } from "@/context/platform"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useWslServers } from "@/context/wsl-servers"
|
||||
|
||||
@@ -41,6 +41,15 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
})
|
||||
const current = () => wslServers.data
|
||||
const wslApi = () => platform.wslServers
|
||||
const withWslApi = async <T,>(run: (api: WslServersPlatform) => Promise<T>) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
return run(api)
|
||||
}
|
||||
let disposed = false
|
||||
onCleanup(() => {
|
||||
disposed = true
|
||||
})
|
||||
const busy = createMemo(() => !!current()?.job || store.adding)
|
||||
const selectedDistro = () => store.selectedDistro
|
||||
const selectedProbe = createMemo(() => {
|
||||
@@ -132,75 +141,39 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
const activeStep = createMemo(() => store.step ?? recommendedStep())
|
||||
|
||||
const probeRuntimeMutation = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.probeRuntime()
|
||||
},
|
||||
mutationFn: () => withWslApi((api) => api.probeRuntime()),
|
||||
}))
|
||||
|
||||
const refreshDistrosMutation = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.refreshDistros()
|
||||
},
|
||||
mutationFn: () => withWslApi((api) => api.refreshDistros()),
|
||||
}))
|
||||
|
||||
const installWslMutation = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.installWsl()
|
||||
},
|
||||
mutationFn: () => withWslApi((api) => api.installWsl()),
|
||||
}))
|
||||
|
||||
const installDistroMutation = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.installDistro(name)
|
||||
},
|
||||
mutationFn: (name: string) => withWslApi((api) => api.installDistro(name)),
|
||||
}))
|
||||
|
||||
const probeDistroMutation = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.probeDistro(name)
|
||||
},
|
||||
mutationFn: (name: string) => withWslApi((api) => api.probeDistro(name)),
|
||||
}))
|
||||
|
||||
const probeOpencodeMutation = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.probeOpencode(name)
|
||||
},
|
||||
mutationFn: (name: string) => withWslApi((api) => api.probeOpencode(name)),
|
||||
}))
|
||||
|
||||
const installOpencodeMutation = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.installOpencode(name)
|
||||
},
|
||||
mutationFn: (name: string) => withWslApi((api) => api.installOpencode(name)),
|
||||
}))
|
||||
|
||||
const openTerminalMutation = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
await api.openTerminal(name)
|
||||
},
|
||||
mutationFn: (name: string) => withWslApi((api) => api.openTerminal(name)),
|
||||
}))
|
||||
|
||||
const addServerMutation = useMutation(() => ({
|
||||
mutationFn: async (distro: string) => {
|
||||
const api = wslApi()
|
||||
if (!api) return
|
||||
return api.addServer(distro)
|
||||
},
|
||||
mutationFn: (distro: string) => withWslApi((api) => api.addServer(distro)),
|
||||
}))
|
||||
|
||||
const autoProbe = createMemo(() => {
|
||||
@@ -233,6 +206,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
try {
|
||||
await probe.run()
|
||||
} catch (err) {
|
||||
if (disposed) return
|
||||
// Allow the same probe to run again when reactive inputs next change
|
||||
// (e.g. user reselects a distro). Without this the user would be stuck
|
||||
// on a transient wsl.exe failure until they pick a different distro.
|
||||
@@ -309,7 +283,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
}
|
||||
})
|
||||
|
||||
const run = async (action: () => Promise<void>) => {
|
||||
const run = async (action: () => Promise<unknown>) => {
|
||||
try {
|
||||
await action()
|
||||
} catch (err) {
|
||||
@@ -317,6 +291,12 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const runSelectedDistro = (action: (distro: string) => Promise<unknown>) => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => action(distro))
|
||||
}
|
||||
|
||||
const selectDistro = (name: string) => {
|
||||
setStore("selectedDistro", name)
|
||||
setStore("step", undefined)
|
||||
@@ -427,11 +407,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={busy()}
|
||||
onClick={() => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => probeDistroMutation.mutateAsync(distro))
|
||||
}}
|
||||
onClick={() => runSelectedDistro((distro) => probeDistroMutation.mutateAsync(distro))}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -560,11 +536,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy() || !selectedInstalled()}
|
||||
onClick={() => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => openTerminalMutation.mutateAsync(distro))
|
||||
}}
|
||||
onClick={() => runSelectedDistro((distro) => openTerminalMutation.mutateAsync(distro))}
|
||||
>
|
||||
Open terminal
|
||||
</Button>
|
||||
@@ -572,11 +544,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
variant="ghost"
|
||||
size="large"
|
||||
disabled={busy() || !selectedDistro()}
|
||||
onClick={() => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => probeDistroMutation.mutateAsync(distro))
|
||||
}}
|
||||
onClick={() => runSelectedDistro((distro) => probeDistroMutation.mutateAsync(distro))}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -605,11 +573,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
variant="ghost"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => probeOpencodeMutation.mutateAsync(distro))
|
||||
}}
|
||||
onClick={() => runSelectedDistro((distro) => probeOpencodeMutation.mutateAsync(distro))}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -619,11 +583,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => {
|
||||
const distro = selectedDistro()
|
||||
if (!distro) return
|
||||
void run(() => installOpencodeMutation.mutateAsync(distro))
|
||||
}}
|
||||
onClick={() => runSelectedDistro((distro) => installOpencodeMutation.mutateAsync(distro))}
|
||||
>
|
||||
{opencodeCheck()?.resolvedPath ? "Update OpenCode" : "Install OpenCode"}
|
||||
</Button>
|
||||
|
||||
@@ -352,7 +352,7 @@ function wireMenu() {
|
||||
}
|
||||
|
||||
registerIpcHandlers({
|
||||
httpFetch: (input) => bridgedHttpFetch(input),
|
||||
httpFetch: (input) => bridgedHttpFetch(input, readyWslUrls()),
|
||||
killSidecar: () => killSidecar(),
|
||||
relaunch: () => relaunchApp(),
|
||||
awaitInitialization: async (sendStep) => {
|
||||
@@ -401,6 +401,12 @@ registerIpcHandlers({
|
||||
setBackgroundColor: (color) => setBackgroundColor(color),
|
||||
})
|
||||
|
||||
function readyWslUrls() {
|
||||
return wslServers
|
||||
.getState()
|
||||
.servers.flatMap((item) => (item.runtime.kind === "ready" ? [item.runtime.url] : []))
|
||||
}
|
||||
|
||||
function killSidecar() {
|
||||
if (!server) return
|
||||
server.stop()
|
||||
@@ -421,13 +427,19 @@ function relaunchApp() {
|
||||
// silently drops idle loopback sockets, so reusing one hangs until timeout.
|
||||
// `agent: false` + `Connection: close` forces a fresh TCP connection per
|
||||
// request, which is the only reliable way to hit a WSL-forwarded port.
|
||||
function bridgedHttpFetch(input: {
|
||||
url: string
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
timeoutMs?: number
|
||||
}): Promise<{
|
||||
const BRIDGED_HTTP_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
||||
const MAX_BRIDGED_HTTP_BODY_BYTES = 25 * 1024 * 1024
|
||||
|
||||
function bridgedHttpFetch(
|
||||
input: {
|
||||
url: string
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
timeoutMs?: number
|
||||
},
|
||||
allowedUrls: string[],
|
||||
): Promise<{
|
||||
status: number
|
||||
statusText: string
|
||||
headers: Record<string, string>
|
||||
@@ -445,12 +457,25 @@ function bridgedHttpFetch(input: {
|
||||
reject(new Error(`httpFetch: only http: is supported (got ${parsed.protocol})`))
|
||||
return
|
||||
}
|
||||
if (!allowedUrls.some((url) => sameOrigin(parsed, url))) {
|
||||
reject(new Error("httpFetch: url is not an active WSL sidecar"))
|
||||
return
|
||||
}
|
||||
const method = input.method.toUpperCase()
|
||||
if (!BRIDGED_HTTP_METHODS.has(method)) {
|
||||
reject(new Error(`httpFetch: unsupported method ${input.method}`))
|
||||
return
|
||||
}
|
||||
if (input.body && Buffer.byteLength(input.body) > MAX_BRIDGED_HTTP_BODY_BYTES) {
|
||||
reject(new Error(`httpFetch: request body exceeded ${MAX_BRIDGED_HTTP_BODY_BYTES} bytes`))
|
||||
return
|
||||
}
|
||||
|
||||
const req = nodeHttp.request({
|
||||
host: parsed.hostname,
|
||||
port: parsed.port ? Number(parsed.port) : 80,
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
method: input.method,
|
||||
method,
|
||||
headers: { ...input.headers, connection: "close" },
|
||||
agent: false,
|
||||
})
|
||||
@@ -468,7 +493,15 @@ function bridgedHttpFetch(input: {
|
||||
|
||||
req.once("response", (res) => {
|
||||
const chunks: Buffer[] = []
|
||||
res.on("data", (chunk: Buffer) => chunks.push(chunk))
|
||||
let bytes = 0
|
||||
res.on("data", (chunk: Buffer) => {
|
||||
bytes += chunk.length
|
||||
if (bytes <= MAX_BRIDGED_HTTP_BODY_BYTES) {
|
||||
chunks.push(chunk)
|
||||
return
|
||||
}
|
||||
res.destroy(new Error(`httpFetch: response exceeded ${MAX_BRIDGED_HTTP_BODY_BYTES} bytes`))
|
||||
})
|
||||
res.once("end", () => {
|
||||
const headers: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(res.headers)) {
|
||||
@@ -492,6 +525,15 @@ function bridgedHttpFetch(input: {
|
||||
})
|
||||
}
|
||||
|
||||
function sameOrigin(input: URL, allowed: string) {
|
||||
try {
|
||||
const url = new URL(allowed)
|
||||
return input.protocol === url.protocol && input.hostname === url.hostname && input.port === url.port
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureLoopbackNoProxy() {
|
||||
const loopback = ["127.0.0.1", "localhost", "::1"]
|
||||
const upsert = (key: string) => {
|
||||
|
||||
@@ -78,79 +78,147 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
console.log(`[store ${op}] ${JSON.stringify({ name, key, ...meta })}`)
|
||||
}
|
||||
|
||||
const offWslServers = deps.onWslServersEvent((payload) => {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (win.isDestroyed()) continue
|
||||
win.webContents.send("wsl-servers-event", payload)
|
||||
}
|
||||
})
|
||||
app.once("will-quit", offWslServers)
|
||||
const requireString = (name: string, value: unknown) => {
|
||||
if (typeof value === "string" && value.length > 0) return value
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
const trustedSender = (event: IpcMainEvent | IpcMainInvokeEvent) => {
|
||||
const raw = event.senderFrame?.url ?? event.sender.getURL()
|
||||
try {
|
||||
const url = new URL(raw)
|
||||
if (url.protocol === "oc:" && url.hostname === "renderer") return true
|
||||
if (!app.isPackaged && (url.hostname === "127.0.0.1" || url.hostname === "localhost")) return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const requireTrustedSender = (event: IpcMainEvent | IpcMainInvokeEvent) => {
|
||||
if (trustedSender(event)) return
|
||||
throw new Error("Untrusted IPC sender")
|
||||
}
|
||||
|
||||
const handle = <Args extends unknown[]>(
|
||||
channel: string,
|
||||
listener: (event: IpcMainInvokeEvent, ...args: Args) => unknown,
|
||||
) => {
|
||||
ipcMain.handle(channel, (event, ...args) => {
|
||||
requireTrustedSender(event)
|
||||
return listener(event, ...(args as Args))
|
||||
})
|
||||
}
|
||||
|
||||
const on = <Args extends unknown[]>(channel: string, listener: (event: IpcMainEvent, ...args: Args) => void) => {
|
||||
ipcMain.on(channel, (event, ...args) => {
|
||||
if (!trustedSender(event)) return
|
||||
listener(event, ...(args as Args))
|
||||
})
|
||||
}
|
||||
|
||||
const wslSubscriptions = new Map<number, () => void>()
|
||||
const unsubscribeWsl = (id: number) => {
|
||||
const off = wslSubscriptions.get(id)
|
||||
if (!off) return
|
||||
off()
|
||||
wslSubscriptions.delete(id)
|
||||
}
|
||||
|
||||
app.once("will-quit", () => {
|
||||
for (const off of wslSubscriptions.values()) off()
|
||||
wslSubscriptions.clear()
|
||||
})
|
||||
|
||||
handle(
|
||||
"http-fetch",
|
||||
(
|
||||
_event: IpcMainInvokeEvent,
|
||||
input: { url: string; method: string; headers: Record<string, string>; body?: string; timeoutMs?: number },
|
||||
) => deps.httpFetch(input),
|
||||
)
|
||||
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
||||
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
||||
handle("kill-sidecar", () => deps.killSidecar())
|
||||
handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||
return deps.awaitInitialization(send)
|
||||
})
|
||||
ipcMain.handle("wsl-servers-get-state", () => deps.getWslServersState())
|
||||
ipcMain.handle("wsl-servers-probe-runtime", () => deps.wslServersProbeRuntime())
|
||||
ipcMain.handle("wsl-servers-refresh-distros", () => deps.wslServersRefreshDistros())
|
||||
ipcMain.handle("wsl-servers-install-wsl", () => deps.wslServersInstallWsl())
|
||||
ipcMain.handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersInstallDistro(name),
|
||||
handle("wsl-servers-subscribe", (event) => {
|
||||
const id = event.sender.id
|
||||
if (wslSubscriptions.has(id)) return
|
||||
wslSubscriptions.set(
|
||||
id,
|
||||
deps.onWslServersEvent((payload) => {
|
||||
if (event.sender.isDestroyed()) {
|
||||
unsubscribeWsl(id)
|
||||
return
|
||||
}
|
||||
event.sender.send("wsl-servers-event", payload)
|
||||
}),
|
||||
)
|
||||
event.sender.once("destroyed", () => unsubscribeWsl(id))
|
||||
})
|
||||
handle("wsl-servers-unsubscribe", (event) => unsubscribeWsl(event.sender.id))
|
||||
handle("wsl-servers-get-state", () => deps.getWslServersState())
|
||||
handle("wsl-servers-probe-runtime", () => deps.wslServersProbeRuntime())
|
||||
handle("wsl-servers-refresh-distros", () => deps.wslServersRefreshDistros())
|
||||
handle("wsl-servers-install-wsl", () => deps.wslServersInstallWsl())
|
||||
handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersInstallDistro(requireString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-probe-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersProbeDistro(name),
|
||||
handle("wsl-servers-probe-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersProbeDistro(requireString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-probe-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersProbeOpencode(name),
|
||||
handle("wsl-servers-probe-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersProbeOpencode(requireString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-install-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersInstallOpencode(name),
|
||||
handle("wsl-servers-install-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersInstallOpencode(requireString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-open-terminal", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersOpenTerminal(name),
|
||||
handle("wsl-servers-open-terminal", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.wslServersOpenTerminal(requireString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-add", (_event: IpcMainInvokeEvent, distro: string) => deps.wslServersAddServer(distro))
|
||||
ipcMain.handle("wsl-servers-remove", (_event: IpcMainInvokeEvent, id: string) => deps.wslServersRemoveServer(id))
|
||||
ipcMain.handle("wsl-servers-start", (_event: IpcMainInvokeEvent, id: string) => deps.wslServersStartServer(id))
|
||||
ipcMain.handle("wsl-servers-stop", (_event: IpcMainInvokeEvent, id: string) => deps.wslServersStopServer(id))
|
||||
ipcMain.handle("wsl-servers-cancel", () => deps.wslServersCancelJob())
|
||||
ipcMain.handle(
|
||||
handle("wsl-servers-add", (_event: IpcMainInvokeEvent, distro: string) =>
|
||||
deps.wslServersAddServer(requireString("distro", distro)),
|
||||
)
|
||||
handle("wsl-servers-remove", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
deps.wslServersRemoveServer(requireString("server id", id)),
|
||||
)
|
||||
handle("wsl-servers-start", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
deps.wslServersStartServer(requireString("server id", id)),
|
||||
)
|
||||
handle("wsl-servers-stop", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
deps.wslServersStopServer(requireString("server id", id)),
|
||||
)
|
||||
handle("wsl-servers-cancel", () => deps.wslServersCancelJob())
|
||||
handle(
|
||||
"wsl-servers-update-acknowledgements",
|
||||
(_event: IpcMainInvokeEvent, id: string, acks: Partial<WslServerAcknowledgements>) =>
|
||||
deps.wslServersUpdateAcknowledgements(id, acks),
|
||||
deps.wslServersUpdateAcknowledgements(requireString("server id", id), acks),
|
||||
)
|
||||
ipcMain.handle("get-window-config", () => deps.getWindowConfig())
|
||||
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
||||
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
handle("get-window-config", () => deps.getWindowConfig())
|
||||
handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
||||
handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
deps.setDefaultServerUrl(url),
|
||||
)
|
||||
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
|
||||
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
|
||||
handle("get-display-backend", () => deps.getDisplayBackend())
|
||||
handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
|
||||
deps.setDisplayBackend(backend),
|
||||
)
|
||||
ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
|
||||
ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
|
||||
ipcMain.handle(
|
||||
handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
|
||||
handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
|
||||
handle(
|
||||
"wsl-path",
|
||||
(_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null, distro?: string | null) =>
|
||||
deps.wslPath(path, mode, distro),
|
||||
)
|
||||
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
||||
ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
|
||||
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
|
||||
ipcMain.handle("check-update", () => deps.checkUpdate())
|
||||
ipcMain.handle("install-update", () => deps.installUpdate())
|
||||
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
||||
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
||||
on("loading-window-complete", () => deps.loadingWindowComplete())
|
||||
handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
|
||||
handle("check-update", () => deps.checkUpdate())
|
||||
handle("install-update", () => deps.installUpdate())
|
||||
handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
||||
handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
const store = getStore(name)
|
||||
const value = store.get(key)
|
||||
debugStore("get", name, key, {
|
||||
@@ -165,27 +233,27 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
if (value === undefined || value === null) return null
|
||||
return typeof value === "string" ? value : JSON.stringify(value)
|
||||
})
|
||||
ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
||||
handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
||||
debugStore("set", name, key, { length: value.length })
|
||||
getStore(name).set(key, value)
|
||||
})
|
||||
ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
debugStore("delete", name, key)
|
||||
getStore(name).delete(key)
|
||||
})
|
||||
ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
getStore(name).clear()
|
||||
})
|
||||
ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
const store = getStore(name)
|
||||
return Object.keys(store.store)
|
||||
})
|
||||
ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
const store = getStore(name)
|
||||
return Object.keys(store.store).length
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
handle(
|
||||
"open-directory-picker",
|
||||
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
@@ -198,7 +266,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
},
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
handle(
|
||||
"open-file-picker",
|
||||
async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
@@ -215,7 +283,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
},
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
handle(
|
||||
"save-file-picker",
|
||||
async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
@@ -227,11 +295,11 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
},
|
||||
)
|
||||
|
||||
ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
|
||||
on("open-link", (_event: IpcMainEvent, url: string) => {
|
||||
void shell.openExternal(url)
|
||||
})
|
||||
|
||||
ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
|
||||
handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
|
||||
if (!app) return shell.openPath(path)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const [cmd, args] =
|
||||
@@ -240,7 +308,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle("read-clipboard-image", () => {
|
||||
handle("read-clipboard-image", () => {
|
||||
const image = clipboard.readImage()
|
||||
if (image.isEmpty()) return null
|
||||
const buffer = image.toPNG().buffer
|
||||
@@ -248,34 +316,34 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
return { buffer, width: size.width, height: size.height }
|
||||
})
|
||||
|
||||
ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
|
||||
on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
|
||||
new Notification({ title, body }).show()
|
||||
})
|
||||
|
||||
ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
|
||||
handle("get-window-count", () => BrowserWindow.getAllWindows().length)
|
||||
|
||||
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
||||
handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return win?.isFocused() ?? false
|
||||
})
|
||||
|
||||
ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
|
||||
handle("set-window-focus", (event: IpcMainInvokeEvent) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.focus()
|
||||
})
|
||||
|
||||
ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
|
||||
handle("show-window", (event: IpcMainInvokeEvent) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.show()
|
||||
})
|
||||
|
||||
ipcMain.on("relaunch", () => {
|
||||
on("relaunch", () => {
|
||||
deps.relaunch()
|
||||
})
|
||||
|
||||
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
|
||||
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
|
||||
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
||||
handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
|
||||
handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
|
||||
handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return
|
||||
setTitlebar(win, theme)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { getStore } from "./store"
|
||||
import { type WslCommandLine, resolveWslOpencode, wslArgs } from "./wsl"
|
||||
import { type WslCommandLine, resolveWslOpencode, shellEscape, wslArgs } from "./wsl"
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
@@ -228,10 +228,6 @@ function prepareServerEnv(password: string) {
|
||||
Object.assign(process.env, env)
|
||||
}
|
||||
|
||||
function shellEscape(value: string) {
|
||||
return `'${value.replace(/'/g, `'"'"'`)}'`
|
||||
}
|
||||
|
||||
function forwardLines(
|
||||
stream: NodeJS.ReadableStream,
|
||||
source: WslCommandLine["stream"],
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
probeWslRuntime,
|
||||
readWslCommandVersion,
|
||||
resolveWslOpencode,
|
||||
summarize,
|
||||
upgradeWslOpencode,
|
||||
wslNeedsRestart,
|
||||
} from "./wsl"
|
||||
@@ -53,7 +54,6 @@ export function wslServerIdForDistro(distro: string) {
|
||||
}
|
||||
|
||||
export function createWslServersController(appVersion: string, spawnSidecar: SpawnSidecar, logger?: ControllerLogger) {
|
||||
const mainLogger: ControllerLogger | undefined = logger
|
||||
let state: WslServersState = initialState()
|
||||
const listeners = new Set<(event: WslServersEvent) => void>()
|
||||
const sidecars = new Map<string, RunningSidecar>()
|
||||
@@ -151,6 +151,17 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
setOpencodeCheck(distro, opencodeCheck(distro, resolved, version, appVersion))
|
||||
}
|
||||
|
||||
const refreshDistroLists = async (opts: { signal?: AbortSignal; onLine?: (line: WslCommandLine) => void }) => {
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
listInstalledWslDistros(opts),
|
||||
listOnlineWslDistros(opts),
|
||||
])
|
||||
return {
|
||||
installed: installedResult.status === "fulfilled" ? installedResult.value : [],
|
||||
online: onlineResult.status === "fulfilled" ? onlineResult.value : [],
|
||||
}
|
||||
}
|
||||
|
||||
const nextStartAttempt = (id: string) => {
|
||||
const next = (startAttempts.get(id) ?? 0) + 1
|
||||
startAttempts.set(id, next)
|
||||
@@ -172,7 +183,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
await stopServerInternal(id)
|
||||
if (!isCurrentStartAttempt(id, attempt)) return
|
||||
setRuntime(id, { kind: "starting" })
|
||||
mainLogger?.log("wsl sidecar starting", { id, distro: item.config.distro })
|
||||
logger?.log("wsl sidecar starting", { id, distro: item.config.distro })
|
||||
try {
|
||||
const sidecar = await spawnSidecar(item.config.distro)
|
||||
if (!isCurrentStartAttempt(id, attempt)) {
|
||||
@@ -195,26 +206,26 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
sidecars.delete(id)
|
||||
const message = startupFailure(code, signal)
|
||||
setRuntime(id, { kind: "failed", message })
|
||||
mainLogger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal })
|
||||
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)
|
||||
mainLogger?.error("wsl opencode check failed", { id, distro: item.config.distro, message })
|
||||
logger?.error("wsl opencode check failed", { id, distro: item.config.distro, message })
|
||||
})
|
||||
mainLogger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
|
||||
logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (!isCurrentStartAttempt(id, attempt)) return
|
||||
if (isMissingDistroError(message)) {
|
||||
removeMissingServer(id)
|
||||
mainLogger?.error("wsl server removed after missing distro", { id, distro: item.config.distro, message })
|
||||
logger?.error("wsl server removed after missing distro", { id, distro: item.config.distro, message })
|
||||
return
|
||||
}
|
||||
setRuntime(id, { kind: "failed", message })
|
||||
// Without this, an Ubuntu-style silent failure leaves no trace in
|
||||
// main.log — the controller captures the message in its state but
|
||||
// nothing surfaces unless the user opens the WSL servers dialog.
|
||||
mainLogger?.error("wsl sidecar failed to start", { id, distro: item.config.distro, message })
|
||||
logger?.error("wsl sidecar failed to start", { id, distro: item.config.distro, message })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,13 +285,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
async refreshDistros() {
|
||||
await runJob({ kind: "distros", startedAt: Date.now() }, async (abort) => {
|
||||
appendTranscript({ stream: "system", text: "Listing WSL distros" })
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
listInstalledWslDistros({ signal: abort.signal, onLine }),
|
||||
listOnlineWslDistros({ signal: abort.signal, onLine }),
|
||||
])
|
||||
const installed = installedResult.status === "fulfilled" ? installedResult.value : []
|
||||
const online = onlineResult.status === "fulfilled" ? onlineResult.value : []
|
||||
setState({ installed, online })
|
||||
setState(await refreshDistroLists({ signal: abort.signal, onLine }))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -309,16 +314,10 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
const message = summarize(result.stderr || result.stdout) || `Failed to install distro: ${name}`
|
||||
throw new Error(message)
|
||||
}
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
listInstalledWslDistros({ signal: abort.signal, onLine }),
|
||||
listOnlineWslDistros({ signal: abort.signal, onLine }),
|
||||
])
|
||||
const installed = installedResult.status === "fulfilled" ? installedResult.value : []
|
||||
const online = onlineResult.status === "fulfilled" ? onlineResult.value : []
|
||||
const distros = await refreshDistroLists({ signal: abort.signal, onLine })
|
||||
const probe = await probeWslDistro(name, { signal: abort.signal, onLine })
|
||||
setState({
|
||||
installed,
|
||||
online,
|
||||
...distros,
|
||||
distroProbes: { ...state.distroProbes, [name]: probe },
|
||||
})
|
||||
})
|
||||
@@ -534,14 +533,6 @@ function opencodeCheck(
|
||||
}
|
||||
}
|
||||
|
||||
function summarize(value: string) {
|
||||
return value
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function isMissingDistroError(message: string) {
|
||||
return /WSL_E_DISTRO_NOT_FOUND|There is no distribution with the supplied name/i.test(message)
|
||||
}
|
||||
|
||||
@@ -186,12 +186,6 @@ export type WslRegistryDistro = {
|
||||
version: number
|
||||
}
|
||||
|
||||
// Distros that are designed to run as root and don't have a user-level
|
||||
// first-run setup. Ubuntu/Debian/Kali/etc. all run a first-boot hook that
|
||||
// prompts for a UNIX username on first invocation; if that never runs,
|
||||
// wsl.exe -d <distro> hangs silently forever.
|
||||
const ALWAYS_ROOT_DISTROS = new Set(["docker-desktop", "docker-desktop-data"])
|
||||
|
||||
// Read LXSS metadata from the Windows registry. This never invokes
|
||||
// wsl.exe, so it is safe to call when wsl.exe itself is wedged.
|
||||
// DefaultUid === 0 on a user-oriented distro means the first-run
|
||||
@@ -243,20 +237,6 @@ export async function readWslDistrosFromRegistry(opts?: RunWslOptions): Promise<
|
||||
return out
|
||||
}
|
||||
|
||||
export type WslFirstRunCheck =
|
||||
| { status: "ok" }
|
||||
| { status: "needs-first-run"; defaultUid: number }
|
||||
| { status: "not-installed" }
|
||||
|
||||
export async function checkWslDistroFirstRun(distro: string, opts?: RunWslOptions): Promise<WslFirstRunCheck> {
|
||||
const distros = await readWslDistrosFromRegistry(opts)
|
||||
const entry = distros.find((d) => d.name === distro)
|
||||
if (!entry) return { status: "not-installed" }
|
||||
if (ALWAYS_ROOT_DISTROS.has(entry.name)) return { status: "ok" }
|
||||
if (entry.defaultUid === 0) return { status: "needs-first-run", defaultUid: entry.defaultUid }
|
||||
return { status: "ok" }
|
||||
}
|
||||
|
||||
export function runWslSh(script: string, distro?: string | null, opts?: RunWslOptions) {
|
||||
return runWslInDistro(["sh", "-lc", script], distro, opts)
|
||||
}
|
||||
@@ -307,10 +287,6 @@ export async function listOnlineWslDistros(opts?: RunWslOptions) {
|
||||
return parseOnlineDistros(result.stdout)
|
||||
}
|
||||
|
||||
export async function installWslRuntime(opts?: RunWslOptions) {
|
||||
return runWsl(["--install", "--no-distribution"], withTimeout(opts, DEFAULT_WSL_INSTALL_TIMEOUT_MS))
|
||||
}
|
||||
|
||||
export async function installWslRuntimeElevated(opts?: RunWslOptions) {
|
||||
const script = [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
@@ -510,7 +486,7 @@ function firstLine(value: string) {
|
||||
)
|
||||
}
|
||||
|
||||
function summarize(value: string) {
|
||||
export function summarize(value: string) {
|
||||
return value
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
@@ -518,7 +494,7 @@ function summarize(value: string) {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function shellEscape(value: string) {
|
||||
export function shellEscape(value: string) {
|
||||
return `'${value.replace(/'/g, `'"'"'`)}'`
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ const api: ElectronAPI = {
|
||||
subscribe: (cb) => {
|
||||
const handler = (_: unknown, event: WslServersEvent) => cb(event)
|
||||
ipcRenderer.on("wsl-servers-event", handler)
|
||||
return () => ipcRenderer.removeListener("wsl-servers-event", handler)
|
||||
void ipcRenderer.invoke("wsl-servers-subscribe")
|
||||
return () => {
|
||||
ipcRenderer.removeListener("wsl-servers-event", handler)
|
||||
void ipcRenderer.invoke("wsl-servers-unsubscribe")
|
||||
}
|
||||
},
|
||||
probeRuntime: () => ipcRenderer.invoke("wsl-servers-probe-runtime"),
|
||||
refreshDistros: () => ipcRenderer.invoke("wsl-servers-refresh-distros"),
|
||||
|
||||
@@ -14,8 +14,6 @@ export type ServerReadyData = {
|
||||
|
||||
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
|
||||
|
||||
export type WslServerStep = "wsl" | "distro" | "opencode"
|
||||
|
||||
export type WslRuntimeCheck = {
|
||||
available: boolean
|
||||
version: string | null
|
||||
|
||||
@@ -180,13 +180,14 @@ const createPlatform = (): Platform => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
const handleWslPicker = async <T extends string | string[] | null>(result: T): Promise<T> => {
|
||||
const distro = activeWslDistro()
|
||||
if (!result || !distro) return result
|
||||
const convert = (path: string) => window.api.wslPath(path, "linux", distro).catch(() => path)
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => window.api.wslPath(path, "linux", distro).catch(() => path))) as any
|
||||
return (await Promise.all(result.map(convert))) as T
|
||||
}
|
||||
return window.api.wslPath(result, "linux", distro).catch(() => result) as any
|
||||
return (await convert(result)) as T
|
||||
}
|
||||
|
||||
const storage = (() => {
|
||||
|
||||
@@ -67,18 +67,18 @@ pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Resu
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_wsl_config(_app: AppHandle) -> Result<WslConfig, String> {
|
||||
// let store = app
|
||||
// .store(SETTINGS_STORE)
|
||||
// .map_err(|e| format!("Failed to open settings store: {}", e))?;
|
||||
pub fn get_wsl_config(app: AppHandle) -> Result<WslConfig, String> {
|
||||
let store = app
|
||||
.store(SETTINGS_STORE)
|
||||
.map_err(|e| format!("Failed to open settings store: {}", e))?;
|
||||
|
||||
// let enabled = store
|
||||
// .get(WSL_ENABLED_KEY)
|
||||
// .as_ref()
|
||||
// .and_then(|v| v.as_bool())
|
||||
// .unwrap_or(false);
|
||||
let enabled = store
|
||||
.get(WSL_ENABLED_KEY)
|
||||
.as_ref()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(WslConfig { enabled: false })
|
||||
Ok(WslConfig { enabled })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -76,14 +76,15 @@ const createPlatform = (): Platform => {
|
||||
return commands.wslPath("~", "windows").catch(() => undefined)
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
const handleWslPicker = async <T extends string | string[] | null>(result: T): Promise<T> => {
|
||||
if (!result) return result
|
||||
const wsl = await commands.getWslConfig().catch(() => null)
|
||||
if (!wsl?.enabled) return result
|
||||
const convert = (path: string) => commands.wslPath(path, "linux").catch(() => path)
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => commands.wslPath(path, "linux").catch(() => path))) as any
|
||||
return (await Promise.all(result.map(convert))) as T
|
||||
}
|
||||
return commands.wslPath(result, "linux").catch(() => result) as any
|
||||
return (await convert(result)) as T
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { ComponentProps, createEffect, JSXElement, Match, ParentProps, Show, Switch, useContext } from "solid-js"
|
||||
import { ComponentProps, JSXElement, Match, ParentProps, Show, Switch } from "solid-js"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { DialogContext } from "../context/dialog"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface DialogProps extends ParentProps {
|
||||
@@ -20,12 +19,6 @@ export interface DialogProps extends ParentProps {
|
||||
|
||||
export function Dialog(props: DialogProps) {
|
||||
const i18n = useI18n()
|
||||
const dialogCtx = useContext(DialogContext)
|
||||
createEffect(() => {
|
||||
if (!dialogCtx) return
|
||||
if (props.dismissOutside === undefined) return
|
||||
dialogCtx.active?.setDismissOutside(props.dismissOutside)
|
||||
})
|
||||
return (
|
||||
<div
|
||||
data-component="dialog"
|
||||
@@ -49,6 +42,9 @@ export function Dialog(props: DialogProps) {
|
||||
autofocusEl.focus()
|
||||
}
|
||||
}}
|
||||
onInteractOutside={(e) => {
|
||||
if (props.dismissOutside === false) e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<Show when={props.title || props.action}>
|
||||
<div data-slot="dialog-header">
|
||||
|
||||
@@ -23,14 +23,10 @@ type Active = {
|
||||
owner: Owner
|
||||
onClose?: () => void
|
||||
setClosing: (closing: boolean) => void
|
||||
dismissOutside: () => boolean
|
||||
setDismissOutside: (value: boolean) => void
|
||||
}
|
||||
|
||||
const Context = createContext<ReturnType<typeof init>>()
|
||||
|
||||
export const DialogContext = Context
|
||||
|
||||
function init() {
|
||||
const [active, setActive] = createSignal<Active | undefined>()
|
||||
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
@@ -93,17 +89,12 @@ function init() {
|
||||
const id = Math.random().toString(36).slice(2)
|
||||
let dispose: (() => void) | undefined
|
||||
let setClosing: ((closing: boolean) => void) | undefined
|
||||
let setDismissOutsideSignal: ((value: boolean) => void) | undefined
|
||||
let dismissOutsideAccessor: (() => boolean) | undefined
|
||||
|
||||
const node = runWithOwner(owner, () =>
|
||||
createRoot((d: () => void) => {
|
||||
dispose = d
|
||||
const [closing, setClosingSignal] = createSignal(false)
|
||||
setClosing = setClosingSignal
|
||||
const [dismissOutside, setDismissOutside] = createSignal(true)
|
||||
dismissOutsideAccessor = dismissOutside
|
||||
setDismissOutsideSignal = setDismissOutside
|
||||
return (
|
||||
<Kobalte
|
||||
modal
|
||||
@@ -114,12 +105,7 @@ function init() {
|
||||
}}
|
||||
>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay
|
||||
data-component="dialog-overlay"
|
||||
onClick={() => {
|
||||
if (dismissOutside()) close()
|
||||
}}
|
||||
/>
|
||||
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||
{element()}
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
@@ -127,7 +113,7 @@ function init() {
|
||||
}),
|
||||
)
|
||||
|
||||
if (!dispose || !setClosing || !dismissOutsideAccessor || !setDismissOutsideSignal) return
|
||||
if (!dispose || !setClosing) return
|
||||
|
||||
setActive({
|
||||
id,
|
||||
@@ -136,8 +122,6 @@ function init() {
|
||||
owner,
|
||||
onClose,
|
||||
setClosing,
|
||||
dismissOutside: dismissOutsideAccessor,
|
||||
setDismissOutside: setDismissOutsideSignal,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,8 +166,5 @@ export function useDialog() {
|
||||
close() {
|
||||
ctx.close()
|
||||
},
|
||||
setDismissOutside(value: boolean) {
|
||||
ctx.active?.setDismissOutside(value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user