From 0e041418495ebcde09910411d78a1a8920eea79f Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:35:43 +1000 Subject: [PATCH] tinkering --- packages/app/src/app.tsx | 13 ++- .../src/components/dialog-select-server.tsx | 82 +++++++++++++++---- .../app/src/components/dialog-wsl-server.tsx | 76 +++++++++-------- .../src/components/status-popover-body.tsx | 38 +++------ packages/app/src/context/global-sdk.tsx | 15 +++- packages/app/src/utils/server-switch.tsx | 23 ++++++ packages/desktop-electron/src/main/wsl.ts | 11 ++- 7 files changed, 173 insertions(+), 85 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 5528523ab9..326db25cb8 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -14,6 +14,7 @@ import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { Effect } from "effect" import { + batch, type Component, createMemo, createResource, @@ -28,7 +29,7 @@ import { Suspense, } from "solid-js" import { Dynamic } from "solid-js/web" -import { serverSwitching } from "@/utils/server-switch" +import { serverSwitching, withServerSwitchOverlay } from "@/utils/server-switch" import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -212,9 +213,13 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { if (checkMode() === "background") void healthCheckActions.refetch() }} onServerSelected={(key) => { - setCheckMode("blocking") - server.setActive(key) - void healthCheckActions.refetch() + void withServerSwitchOverlay(() => { + batch(() => { + setCheckMode("blocking") + server.setActive(key) + }) + void healthCheckActions.refetch() + }) }} /> } diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 9fafcc1cd9..200b489d96 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -17,6 +17,7 @@ import type { WslServersState } from "@/context/platform" import { usePlatform } from "@/context/platform" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health" +import { withServerSwitchOverlay } from "@/utils/server-switch" const DEFAULT_USERNAME = "opencode" const cachedServerStatus = new Map() @@ -223,6 +224,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { }, addWsl: { showWizard: props.initialView === "add-wsl", + pendingSelectKey: undefined as ServerConnection.Key | undefined, }, editServer: { id: undefined as string | undefined, @@ -345,6 +347,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { }) const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]) + let resolvePendingWslSelection: VoidFunction | undefined const healthPollKey = createMemo(() => items() .map((conn) => @@ -428,29 +431,50 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { async function select(conn: ServerConnection.Any, persist?: boolean) { if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return - dialog.close() const nextKey = ServerConnection.key(conn) const changed = server.key !== nextKey - - if (persist && conn.type === "http") { - server.add(conn) - if (changed && typeof window !== "undefined" && window.history?.replaceState) { - window.history.replaceState(null, "", "/") - } else { - props.onNavigateHome?.() + + const apply = () => { + dialog.close() + if (persist && conn.type === "http") { + server.add(conn) + if (changed && typeof window !== "undefined" && window.history?.replaceState) { + window.history.replaceState(null, "", "/") + } else { + props.onNavigateHome?.() + } + return } + + batch(() => { + if (changed && typeof window !== "undefined" && window.history?.replaceState) { + window.history.replaceState(null, "", "/") + } else { + props.onNavigateHome?.() + } + server.setActive(nextKey) + }) + } + + if (!changed) { + apply() return } - batch(() => { - if (changed && typeof window !== "undefined" && window.history?.replaceState) { - window.history.replaceState(null, "", "/") - } else { - props.onNavigateHome?.() - } - server.setActive(nextKey) - }) + + await withServerSwitchOverlay(apply) } + createEffect(() => { + const key = store.addWsl.pendingSelectKey + if (!key) return + const conn = items().find((item) => ServerConnection.key(item) === key) + if (!conn) return + const resolve = resolvePendingWslSelection + resolvePendingWslSelection = undefined + setStore("addWsl", "pendingSelectKey", undefined) + void select(conn).finally(() => resolve?.()) + }) + const handleAddChange = (value: string) => { if (addMutation.isPending) return setStore("addServer", { url: value, error: "" }) @@ -524,6 +548,9 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { const resetForm = () => { resetAdd() resetEdit() + resolvePendingWslSelection?.() + resolvePendingWslSelection = undefined + setStore("addWsl", "pendingSelectKey", undefined) setStore("addWsl", "showWizard", false) } @@ -558,9 +585,23 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { const startAddWsl = () => { resetAdd() resetEdit() + setStore("addWsl", "pendingSelectKey", undefined) setStore("addWsl", "showWizard", true) } + const handleAddedWsl = async (distro: string) => { + const key = ServerConnection.Key.make(`wsl:${distro}`) + const conn = items().find((item) => ServerConnection.key(item) === key) + if (conn) { + await select(conn) + return + } + await new Promise((resolve) => { + resolvePendingWslSelection = resolve + setStore("addWsl", "pendingSelectKey", key) + }) + } + const submitForm = () => { if (mode() === "add") { if (addMutation.isPending) return @@ -646,7 +687,12 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { } return ( - +
} > - + } > diff --git a/packages/app/src/components/dialog-wsl-server.tsx b/packages/app/src/components/dialog-wsl-server.tsx index a396098a21..4c4cf952c6 100644 --- a/packages/app/src/components/dialog-wsl-server.tsx +++ b/packages/app/src/components/dialog-wsl-server.tsx @@ -23,7 +23,7 @@ function parseProgressPercent(text: string) { } interface DialogWslServerProps { - onAdded?: () => void + onAdded?: (distro: string) => void | Promise } export function DialogWslServer(props: DialogWslServerProps = {}) { @@ -272,7 +272,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { const selectDistro = (name: string) => { setStore("selectedDistro", name) - setStore("step", "distro") + setStore("step", undefined) } const finish = async () => { @@ -283,8 +283,11 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { setStore("adding", true) try { await api.addServer(distro) - props.onAdded?.() - dialog.close() + if (props.onAdded) { + await props.onAdded(distro) + } else { + dialog.close() + } } catch (err) { requestError(language, err) } finally { @@ -356,6 +359,11 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
+
+ +
@@ -386,11 +394,9 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { onClick={() => selectDistro(item.name)} >
{item.name}
-
- {[item.isDefault ? "default" : null, item.state, item.version ? `WSL ${item.version}` : null] - .filter(Boolean) - .join(" ยท ")} -
+ +
Default
+
)} @@ -423,7 +429,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
{(item) => { @@ -434,7 +440,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { role="radio" aria-checked={selected()} disabled={busy()} - class="w-full px-3 py-2 flex items-start gap-3 text-left border-b border-border-weak-base last:border-b-0 transition-colors" + class="w-full px-3 py-2 flex items-center gap-3 text-left border-b border-border-weak-base last:border-b-0 transition-colors" classList={{ "bg-surface-raised-base": selected(), "hover:bg-surface-base": !selected(), @@ -447,12 +453,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { >
-
-
{item.label}
- -
{item.name}
-
-
+
{item.label}
) }} @@ -499,6 +500,17 @@ export function DialogWslServer(props: DialogWslServerProps = {}) { > Open terminal + +
+ +
@@ -550,10 +562,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
Progress
{progress().title}
-
+
0}>
Diagnostics
-
+
{(line) =>
{line.text}
}
-
- - -
+ +
+ + +
+
) diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 1034f0676d..f4ccb1b954 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,7 +15,7 @@ import { useSDK } from "@/context/sdk" import { ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" -import { setServerSwitching } from "@/utils/server-switch" +import { withServerSwitchOverlay } from "@/utils/server-switch" const pollMs = 10_000 @@ -289,32 +289,18 @@ export function StatusPopoverBody(props: { shown: Accessor }) { aria-disabled={blocked()} onClick={() => { if (blocked()) return - // Paint a full-window splash BEFORE the heavy - // ServerKey remount so the user gets visual - // feedback during the multi-second synchronous - // dispose cascade (xterm + file-tree + providers). - // setTimeout(0) yields to the browser so the - // splash lands on screen before the cascade - // starts; a second setTimeout(0) after the batch - // waits for the new subtree to paint, then - // dismisses the splash. - setServerSwitching(true) - setTimeout(() => { - try { - batch(() => { - if (server.key !== key) { - if (typeof window !== "undefined" && window.history?.replaceState) { - window.history.replaceState(null, "", "/") - } - } else { - navigate("/") + void withServerSwitchOverlay(() => { + batch(() => { + if (server.key !== key) { + if (typeof window !== "undefined" && window.history?.replaceState) { + window.history.replaceState(null, "", "/") } - server.setActive(key) - }) - } finally { - setTimeout(() => setServerSwitching(false), 0) - } - }, 0) + } else { + navigate("/") + } + server.setActive(key) + }) + }) }} > diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 973ecc66bc..5666442a4d 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -95,6 +95,15 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo buffer.length = 0 } + const clearPending = () => { + if (timer) clearTimeout(timer) + timer = undefined + queue.length = 0 + buffer.length = 0 + coalesced.clear() + staleDeltas.clear() + } + const schedule = () => { if (timer) return const elapsed = Date.now() - last @@ -202,6 +211,10 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo } })().finally(() => { run = undefined + if (abort.signal.aborted || !started) { + clearPending() + return + } flush() }) return run @@ -225,7 +238,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo onCleanup(() => { stop() abort.abort() - flush() + clearPending() }) const sdk = createSdkForServer({ diff --git a/packages/app/src/utils/server-switch.tsx b/packages/app/src/utils/server-switch.tsx index 480990b184..7c9b0efb77 100644 --- a/packages/app/src/utils/server-switch.tsx +++ b/packages/app/src/utils/server-switch.tsx @@ -7,3 +7,26 @@ import { createSignal } from "solid-js" // swap and false after lets us render an overlay above the ServerKey // boundary so the freeze has visual feedback instead of looking stuck. export const [serverSwitching, setServerSwitching] = createSignal(false) + +let run = 0 + +const nextPaint = () => + new Promise((resolve) => { + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => resolve()) + return + } + setTimeout(resolve, 0) + }) + +export async function withServerSwitchOverlay(action: () => void | Promise) { + const token = ++run + setServerSwitching(true) + await nextPaint() + try { + await action() + } finally { + await nextPaint() + if (run === token) setServerSwitching(false) + } +} diff --git a/packages/desktop-electron/src/main/wsl.ts b/packages/desktop-electron/src/main/wsl.ts index 1785df0f13..abfc8a9ca6 100644 --- a/packages/desktop-electron/src/main/wsl.ts +++ b/packages/desktop-electron/src/main/wsl.ts @@ -1,4 +1,6 @@ import { spawn } from "node:child_process" +import { existsSync } from "node:fs" +import { join } from "node:path" import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../preload/types" import { runInteractiveCommand } from "./wsl-pty" @@ -320,7 +322,7 @@ export async function installWslRuntimeElevated(opts?: RunWslOptions) { export async function installWslDistro(name: string, opts?: RunWslOptions) { return runInteractiveCommand( - "wsl", + resolveSystem32Command("wsl.exe"), ["--install", "-d", name, "--web-download", "--no-launch"], withTimeout(opts, DEFAULT_WSL_INSTALL_TIMEOUT_MS), DEFAULT_WSL_INSTALL_TIMEOUT_MS, @@ -520,6 +522,13 @@ function shellEscape(value: string) { return `'${value.replace(/'/g, `'"'"'`)}'` } +function resolveSystem32Command(command: string) { + const root = process.env.SystemRoot ?? process.env.windir + if (!root) return command + const resolved = join(root, "System32", command) + return existsSync(resolved) ? resolved : command +} + function withTimeout(opts: RunWslOptions | undefined, timeoutMs: number): RunWslOptions { return { ...opts,