This commit is contained in:
LukeParkerDev
2026-04-29 08:37:16 +10:00
parent 9cd611780f
commit 2eb3072e3e
18 changed files with 55 additions and 212 deletions

View File

@@ -5,10 +5,11 @@ import { showToast } from "@opencode-ai/ui/toast"
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 { usePlatform } from "@/context/platform"
import { useWslServers } from "@/context/wsl-servers"
type WslServerStep = "wsl" | "distro" | "opencode"
const STEPS: WslServerStep[] = ["wsl", "distro", "opencode"]
function isHiddenDistro(name: string) {

View File

@@ -33,7 +33,6 @@ import { Persist, persisted } from "@/utils/persist"
import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
@@ -113,7 +112,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const dialog = useDialog()
const providers = useProviders()
const command = useCommand()
const server = useServer()
const permission = usePermission()
const language = useLanguage()
const platform = usePlatform()
@@ -1256,9 +1254,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [
loadAgentsQuery(sdk.directory, server.key),
loadProvidersQuery(null, server.key),
loadProvidersQuery(sdk.directory, server.key),
loadAgentsQuery(sdk.directory),
loadProvidersQuery(null),
loadProvidersQuery(sdk.directory),
],
}))

View File

@@ -194,11 +194,11 @@ export const Terminal = (props: TerminalProps) => {
const server = useServer()
const directory = sdk.directory
const client = sdk.client
const url = sdk.url
const auth = server.current?.http
const username = auth?.username ?? "opencode"
const password = auth?.password ?? ""
const currentUrl = () => server.current?.http.url ?? sdk.url
const sameOrigin = () => new URL(currentUrl(), location.href).origin === location.origin
const sameOrigin = new URL(url, location.href).origin === location.origin
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
const id = local.pty.id
@@ -525,18 +525,11 @@ export const Terminal = (props: TerminalProps) => {
if (disposed) return
drop?.()
const baseUrl = currentUrl()
if (sdk.url !== baseUrl) {
console.error(
`[terminal panic] sdk.url mismatch id=${id} serverKey=${server.key ?? ""} directory=${directory} sdkUrl=${sdk.url} currentUrl=${baseUrl}`,
)
}
const next = new URL(baseUrl + `/pty/${id}/connect`)
const next = new URL(url + `/pty/${id}/connect`)
next.searchParams.set("directory", directory)
next.searchParams.set("cursor", String(seek))
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
if (!sameOrigin() && password) {
if (!sameOrigin && password) {
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
// For same-origin requests, let the browser reuse the page's existing auth.
next.username = username
@@ -549,7 +542,7 @@ export const Terminal = (props: TerminalProps) => {
directory,
restoreLength: restore.length,
sdkUrl: sdk.url,
currentUrl: baseUrl,
currentUrl: url,
wsUrl: next.toString(),
})
@@ -564,7 +557,7 @@ export const Terminal = (props: TerminalProps) => {
id,
serverKey: server.key ?? null,
directory,
currentUrl: baseUrl,
currentUrl: url,
})
// Paint the saved buffer now that we've confirmed the pty really
// exists on the current sidecar. Fire-and-forget: write()'s own
@@ -630,7 +623,7 @@ export const Terminal = (props: TerminalProps) => {
directory,
code: event.code,
reason: event.reason || null,
currentUrl: baseUrl,
currentUrl: url,
})
retry(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code })))
}

View File

@@ -14,7 +14,6 @@ import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server"
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
@@ -41,12 +40,11 @@ type GlobalStore = {
reload: undefined | "pending" | "complete"
}
export const loadSessionsQuery = (directory: string, serverKey: string | undefined) =>
queryOptions<null>({ queryKey: [serverKey, directory, "loadSessions"], queryFn: skipToken })
export const loadSessionsQuery = (directory: string) =>
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const server = useServer()
const language = useLanguage()
const owner = getOwner()
if (!owner) throw new Error("GlobalSync must be created within owner")
@@ -171,7 +169,7 @@ function createGlobalSync() {
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient
.fetchQuery({
...loadSessionsQuery(directory, server.key),
...loadSessionsQuery(directory),
queryFn: () =>
loadRootSessionsWithFallback({
directory,
@@ -237,7 +235,6 @@ function createGlobalSync() {
const sdk = sdkFor(directory)
await bootstrapDirectory({
directory,
serverKey: server.key,
global: {
config: globalStore.config,
path: globalStore.path,
@@ -337,7 +334,6 @@ function createGlobalSync() {
try {
await bootstrapGlobal({
globalSDK: globalSDK.client,
serverKey: server.key,
requestFailedTitle: language.t("common.requestFailed"),
translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),

View File

@@ -68,7 +68,6 @@ function runAll(list: Array<() => Promise<unknown>>) {
export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient
serverKey: string | undefined
requestFailedTitle: string
translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string
@@ -87,7 +86,7 @@ export async function bootstrapGlobal(input: {
const slow = [
() =>
input.queryClient.fetchQuery({
...loadProvidersQuery(null, input.serverKey),
...loadProvidersQuery(null),
queryFn: () =>
retry(() =>
input.globalSDK.provider.list().then((x) => {
@@ -180,17 +179,16 @@ function warmSessions(input: {
).then(() => undefined)
}
export const loadProvidersQuery = (directory: string | null, serverKey: string | undefined) =>
queryOptions<null>({ queryKey: [serverKey, directory, "providers"], queryFn: skipToken })
export const loadProvidersQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
export const loadAgentsQuery = (
directory: string | null,
serverKey: string | undefined,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
) =>
queryOptions<null>({
queryKey: [serverKey, directory, "agents"],
queryKey: [directory, "agents"],
queryFn:
sdk && transform
? () =>
@@ -224,7 +222,6 @@ export const loadPathQuery = (
export async function bootstrapDirectory(input: {
directory: string
serverKey: string | undefined
sdk: OpencodeClient
store: Store<State>
setStore: SetStoreFunction<State>
@@ -266,9 +263,7 @@ export async function bootstrapDirectory(input: {
() => Promise.resolve(input.loadSessions(input.directory)),
() =>
input.queryClient.ensureQueryData(
loadAgentsQuery(input.directory, input.serverKey, input.sdk, (x) =>
input.setStore("agent", normalizeAgentList(x.data)),
),
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
),
() =>
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
@@ -354,7 +349,7 @@ export async function bootstrapDirectory(input: {
),
() =>
input.queryClient.ensureQueryData({
...loadProvidersQuery(input.directory, input.serverKey),
...loadProvidersQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.provider.list())
.then((x) => {

View File

@@ -9,17 +9,13 @@ type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: stri
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
type UpdateInfo = { updateAvailable: boolean; version?: string }
export type WslServerStep = "wsl" | "distro" | "opencode"
export type WslRuntimeCheck = {
available: boolean
version: string | null
status: string | null
error: string | null
}
export type WslInstalledDistro = {
name: string
state: string | null
version: number | null
isDefault: boolean
}
@@ -32,7 +28,6 @@ export type WslDistroProbe = {
canExecute: boolean
hasBash: boolean
hasCurl: boolean
username: string | null
isRoot: boolean | null
error: string | null
}
@@ -95,8 +90,6 @@ export type WslServersPlatform = {
addServer(distro: string): Promise<WslServerConfig>
removeServer(id: string): Promise<void>
startServer(id: string): Promise<void>
stopServer(id: string): Promise<void>
cancelJob(): Promise<void>
}
export type Platform = {

View File

@@ -228,19 +228,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
})
}
const check = (conn: ServerConnection.Any) =>
checkServerHealth(conn.http).then((x) => {
if (!x.healthy) {
// Electron's console-message bridge only preserves the first
// console argument, so pre-stringify everything into one string.
console.warn(
`[server health] unhealthy key=${ServerConnection.key(conn)} url=${conn.http.url} hasAuth=${!!(
conn.http.username || conn.http.password
)}`,
)
}
return x.healthy
})
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
createEffect(() => {
const key = state.active
@@ -275,7 +263,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
return
}
setState("healthy", undefined)
console.log(`[server health] start polling key=${ServerConnection.key(current_)} url=${current_.http.url}`)
onCleanup(startHealthPolling(current_))
})

View File

@@ -4,23 +4,22 @@ import { createEffect, onCleanup } from "solid-js"
import type { WslServersPlatform, WslServersState } from "./platform"
import { usePlatform } from "./platform"
export const wslServersQueryKey = ["platform", "wslServers"] as const
export function wslServersQueryOptions(api: WslServersPlatform | undefined) {
return queryOptions<WslServersState>({
queryKey: wslServersQueryKey,
queryFn: api ? () => api.getState() : skipToken,
staleTime: Number.POSITIVE_INFINITY,
gcTime: Number.POSITIVE_INFINITY,
})
}
const wslServersQueryKey = ["platform", "wslServers"] as const
export const { use: useWslServers, provider: WslServersProvider } = createSimpleContext({
name: "WslServers",
init: () => {
const platform = usePlatform()
const queryClient = useQueryClient()
const query = useQuery(() => ({ ...wslServersQueryOptions(platform.wslServers) }))
const query = useQuery(() => {
const api = platform.wslServers
return queryOptions<WslServersState>({
queryKey: wslServersQueryKey,
queryFn: api ? () => api.getState() : skipToken,
staleTime: Number.POSITIVE_INFINITY,
gcTime: Number.POSITIVE_INFINITY,
})
})
createEffect(() => {
const api = platform.wslServers

View File

@@ -16,7 +16,6 @@ export {
type WslServersEvent,
type WslServersPlatform,
type WslServersState,
type WslServerStep,
} from "./context/platform"
export { ServerConnection } from "./context/server"
export { handleNotificationClick } from "./utils/notification-click"

View File

@@ -16,7 +16,6 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
import { type LocalProject } from "@/context/layout"
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { useServer } from "@/context/server"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
import { sortedRootSessions, workspaceKey } from "./helpers"
import { useQuery } from "@tanstack/solid-query"
@@ -320,19 +319,12 @@ export const SortableWorkspace = (props: {
})
const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
// Guard against `props.project` being transiently undefined during a
// server-switch cascade. The parent renders
// <For each={workspaces()}>{(dir) => <SortableWorkspace project={project()!} ... />}</For>
// where `project()` can flip to undefined while the enclosing <Show when={project()}>
// gate hasn't yet unmounted this child. Bootstrap's setStore can then fire
// these memos with stale props.
const local = createMemo(() => props.directory === (props.project?.worktree ?? ""))
const local = createMemo(() => props.directory === props.project.worktree)
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
const server = useServer()
const workspaceValue = createMemo(() => {
const branch = workspaceStore.vcs?.branch
const name = branch ?? getFilename(props.directory)
const projectId = props.project?.id
const projectId = props.project.id
if (!projectId) return name
return props.ctx.workspaceName(props.directory, projectId, branch) ?? name
})
@@ -340,7 +332,7 @@ export const SortableWorkspace = (props: {
const boot = createMemo(() => open() || active())
const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree, server.key) }))
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => query.isLoading && count() === 0
const touch = createMediaQuery("(hover: none)")
@@ -364,7 +356,7 @@ export const SortableWorkspace = (props: {
InlineEditor={props.ctx.InlineEditor}
renameWorkspace={props.ctx.renameWorkspace}
setEditor={props.ctx.setEditor}
projectId={props.project?.id ?? ""}
projectId={props.project.id ?? ""}
/>
)
@@ -433,7 +425,7 @@ export const SortableWorkspace = (props: {
openEditor={props.ctx.openEditor}
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
root={props.project?.worktree ?? props.directory}
root={props.project.worktree}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
navigateToNewSession={() => navigate(`/${slug()}/session`)}
/>
@@ -467,33 +459,20 @@ export const LocalWorkspace = (props: {
}): JSX.Element => {
const globalSync = useGlobalSync()
const language = useLanguage()
const server = useServer()
// Same guard pattern as SortableWorkspace: the parent passes
// `project={project()!}` but `project()` can transiently flip to
// undefined during a server-switch cascade before this component
// unmounts, so every reactive memo reading props.project has to
// tolerate undefined.
const worktree = createMemo(() => props.project?.worktree ?? "")
const worktree = createMemo(() => props.project.worktree)
const workspace = createMemo(() => {
const dir = worktree()
if (!dir) return undefined
const [store, setStore] = globalSync.child(dir)
const [store, setStore] = globalSync.child(worktree())
return { store, setStore }
})
const slug = createMemo(() => (worktree() ? base64Encode(worktree()) : ""))
const sessions = createMemo(() => {
const store = workspace()?.store
return store ? sortedRootSessions(store, props.sortNow()) : []
})
const booted = createMemo((prev) => prev || workspace()?.store.status === "complete", false)
const slug = createMemo(() => base64Encode(worktree()))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const count = createMemo(() => sessions()?.length ?? 0)
const query = useQuery(() => ({ ...loadSessionsQuery(worktree(), server.key) }))
const query = useQuery(() => ({ ...loadSessionsQuery(worktree()) }))
const loading = createMemo(() => query.isPending && count() === 0)
const hasMore = createMemo(() => (workspace()?.store.sessionTotal ?? 0) > count())
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loadMore = async () => {
const dir = worktree()
if (!dir) return
workspace()?.setStore("limit", (limit) => (limit ?? 0) + 5)
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(dir)
}

View File

@@ -65,21 +65,6 @@ function retryable(error: unknown, signal?: AbortSignal) {
return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
}
function serializeError(error: unknown): unknown {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
}
}
return error
}
function stringifyLog(label: string, value: unknown) {
return `${label} ${JSON.stringify(value)}`
}
export async function checkServerHealth(
server: ServerConnection.HttpBase,
fetch: typeof globalThis.fetch,
@@ -89,19 +74,7 @@ export async function checkServerHealth(
const signal = opts?.signal ?? timeout?.signal
const retryCount = opts?.retryCount ?? defaultRetryCount
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
const logFailure = (phase: string, count: number, error: unknown) => {
console.error(
stringifyLog("[server health] request failed", {
phase,
attempt: count + 1,
url: server.url,
hasAuth: !!server.password,
error: serializeError(error),
}),
)
}
const next = (count: number, error: unknown) => {
logFailure("retry", count, error)
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
return wait(retryDelayMs * (count + 1), signal)
.then(() => attempt(count + 1))

View File

@@ -181,18 +181,11 @@ async function initialize() {
const hostname = "127.0.0.1"
const url = `http://${hostname}:${port}`
const password = randomUUID()
const key = "local:windows"
const startupData: ServerReadyData = {
url,
username: "opencode",
password,
local: {
key,
url,
username: "opencode",
password,
},
}
const loadingTask = (async () => {
logger.log("sidecar connection started", { url })
@@ -371,8 +364,6 @@ registerIpcHandlers({
wslServersAddServer: (distro) => wslServers.addServer(distro),
wslServersRemoveServer: (id) => wslServers.removeServer(id),
wslServersStartServer: (id) => wslServers.startServer(id),
wslServersStopServer: (id) => wslServers.stopServer(id),
wslServersCancelJob: () => wslServers.cancelJob(),
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
getDefaultServerUrl: () => getDefaultServerUrl(),

View File

@@ -37,8 +37,6 @@ type Deps = {
wslServersAddServer: (distro: string) => Promise<WslServerConfig> | WslServerConfig
wslServersRemoveServer: (id: string) => Promise<void> | void
wslServersStartServer: (id: string) => Promise<void> | void
wslServersStopServer: (id: string) => Promise<void> | void
wslServersCancelJob: () => Promise<void> | void
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
consumeInitialDeepLinks: () => Promise<string[]> | string[]
getDefaultServerUrl: () => Promise<string | null> | string | null
@@ -57,13 +55,6 @@ type Deps = {
}
export function registerIpcHandlers(deps: Deps) {
const debugStore = (op: string, name: string, key: string, meta?: Record<string, unknown>) => {
if (app.isPackaged) return
if (!name.startsWith("opencode.workspace.")) return
if (!key.includes("terminal")) return
console.log(`[store ${op}] ${JSON.stringify({ name, key, ...meta })}`)
}
const requireString = (name: string, value: unknown) => {
if (typeof value === "string" && value.length > 0) return value
throw new Error(`Invalid ${name}`)
@@ -165,10 +156,6 @@ export function registerIpcHandlers(deps: Deps) {
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("get-window-config", () => deps.getWindowConfig())
handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
handle("get-default-server-url", () => deps.getDefaultServerUrl())
@@ -195,24 +182,13 @@ export function registerIpcHandlers(deps: Deps) {
handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
const store = getStore(name)
const value = store.get(key)
debugStore("get", name, key, {
found: value !== undefined && value !== null,
length:
typeof value === "string"
? value.length
: value === undefined || value === null
? 0
: JSON.stringify(value).length,
})
if (value === undefined || value === null) return null
return typeof value === "string" ? value : JSON.stringify(value)
})
handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
debugStore("set", name, key, { length: value.length })
getStore(name).set(key, value)
})
handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
debugStore("delete", name, key)
getStore(name).delete(key)
})
handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {

View File

@@ -120,14 +120,11 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
}
const refreshDistroLists = async (opts: { signal?: AbortSignal }) => {
const [installedResult, onlineResult] = await Promise.allSettled([
const [installed, online] = await Promise.all([
listInstalledWslDistros(opts),
listOnlineWslDistros(opts),
])
return {
installed: installedResult.status === "fulfilled" ? installedResult.value : [],
online: onlineResult.status === "fulfilled" ? onlineResult.value : [],
}
return { installed, online }
}
const nextStartAttempt = (id: string) => {
@@ -316,12 +313,6 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
await openWslTerminal(name)
},
async cancelJob() {
jobAbort?.abort()
jobAbort = undefined
setState({ job: null })
},
async addServer(distro: string): Promise<WslServerConfig> {
const id = wslServerIdForDistro(distro)
if (state.servers.some((item) => item.config.id === id)) {
@@ -349,12 +340,6 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
startServer,
async stopServer(id: string) {
invalidateStartAttempt(id)
await stopServerInternal(id)
setRuntime(id, { kind: "stopped" })
},
stopAll() {
for (const item of state.servers) invalidateStartAttempt(item.config.id)
for (const existing of sidecars.values()) {

View File

@@ -277,16 +277,13 @@ export async function probeWslRuntime(opts?: RunWslOptions): Promise<WslRuntimeC
return {
available: false,
version: null,
status: null,
error: summarize(version.stderr || version.stdout) || "WSL is unavailable",
}
}
const status = await runWsl(["--status"], opts).catch(() => undefined)
return {
available: true,
version: firstLine(version.stdout),
status: status?.code === 0 ? summarize(status.stdout) : null,
error: null,
}
}
@@ -351,7 +348,6 @@ export async function probeWslDistro(name: string, opts?: RunWslOptions): Promis
canExecute: false,
hasBash: false,
hasCurl: false,
username: null,
isRoot: null,
error: summarize(executable.stderr || executable.stdout) || "Cannot execute commands in distro",
}
@@ -369,7 +365,6 @@ export async function probeWslDistro(name: string, opts?: RunWslOptions): Promis
canExecute: true,
hasBash: bash.code === 0 && summarize(bash.stdout) === "yes",
hasCurl: curl.code === 0 && summarize(curl.stdout) === "yes",
username: username || null,
isRoot: username ? username === "root" : null,
error: null,
}
@@ -472,14 +467,13 @@ function parseInstalledDistros(output: string) {
return output.split(/\r?\n/g).flatMap((line) => {
const trimmed = line.trim()
if (!trimmed) return []
const match = line.match(/^\s*(\*)?\s*(.*?)\s{2,}(\S+)\s+(\d+)\s*$/)
const match = line.match(/^\s*(\*)?\s*(.*?)\s{2,}\S+\s+(\d+)\s*$/)
if (!match) return []
const [, marker, name, state, version] = match
const [, marker, name, version] = match
if (!name || /^name$/i.test(name)) return []
return [
{
name: name.trim(),
state: state || null,
version: Number.isNaN(Number.parseInt(version, 10)) ? null : Number.parseInt(version, 10),
isDefault: marker === "*",
} satisfies WslInstalledDistro,

View File

@@ -33,8 +33,6 @@ const api: ElectronAPI = {
addServer: (distro) => ipcRenderer.invoke("wsl-servers-add", distro),
removeServer: (id) => ipcRenderer.invoke("wsl-servers-remove", id),
startServer: (id) => ipcRenderer.invoke("wsl-servers-start", id),
stopServer: (id) => ipcRenderer.invoke("wsl-servers-stop", id),
cancelJob: () => ipcRenderer.invoke("wsl-servers-cancel"),
},
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),

View File

@@ -4,12 +4,6 @@ export type ServerReadyData = {
url: string
username: string | null
password: string | null
local: {
key: string
url: string
username: string | null
password: string | null
}
}
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
@@ -17,12 +11,10 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | {
export type WslRuntimeCheck = {
available: boolean
version: string | null
status: string | null
error: string | null
}
export type WslInstalledDistro = {
name: string
state: string | null
version: number | null
isDefault: boolean
}
@@ -35,7 +27,6 @@ export type WslDistroProbe = {
canExecute: boolean
hasBash: boolean
hasCurl: boolean
username: string | null
isRoot: boolean | null
error: string | null
}
@@ -98,8 +89,6 @@ export type WslServersAPI = {
addServer: (distro: string) => Promise<WslServerConfig>
removeServer: (id: string) => Promise<void>
startServer: (id: string) => Promise<void>
stopServer: (id: string) => Promise<void>
cancelJob: () => Promise<void>
}
export type LinuxDisplayBackend = "wayland" | "auto"

View File

@@ -134,13 +134,13 @@ const createPlatform = (): Platform => {
const wslHome = async () => {
const distro = activeWslDistro()
if (!distro) return undefined
return window.api.wslPath("~", "windows", distro).catch(() => undefined)
return window.api.wslPath("~", "windows", distro)
}
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)
const convert = (path: string) => window.api.wslPath(path, "linux", distro)
if (Array.isArray(result)) {
return (await Promise.all(result.map(convert))) as T
}
@@ -217,10 +217,7 @@ const createPlatform = (): Platform => {
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
const resolvedPath = await (async () => {
const distro = activeWslDistro()
if (distro) {
const converted = await window.api.wslPath(path, "windows", distro).catch(() => null)
if (converted) return converted
}
if (distro) return window.api.wslPath(path, "windows", distro)
return path
})()
return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
@@ -404,9 +401,9 @@ render(() => {
type: "sidecar",
variant: "base",
http: {
url: data.local.url,
username: data.local.username ?? undefined,
password: data.local.password ?? undefined,
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
},
})
}
@@ -434,7 +431,7 @@ render(() => {
return (
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make(startup.latest?.sidecar?.local.key ?? "local:windows")}
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("local:windows")}
serversReady={!platform.wslServers || !wslServers.isPending}
servers={servers()}
router={MemoryRouter}