From 1baa87bf0ddc23d543d9a9b399515dfc7678a475 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:22:51 +1000 Subject: [PATCH] fix: tighten local server wizard dialog flow --- .../src/components/dialog-local-server.tsx | 633 ++++++++---------- .../src/components/dialog-select-server.tsx | 17 +- packages/ui/src/components/dialog.tsx | 7 + packages/ui/src/context/dialog.tsx | 2 +- 4 files changed, 286 insertions(+), 373 deletions(-) diff --git a/packages/app/src/components/dialog-local-server.tsx b/packages/app/src/components/dialog-local-server.tsx index 853f90661b..5d01d71e11 100644 --- a/packages/app/src/components/dialog-local-server.tsx +++ b/packages/app/src/components/dialog-local-server.tsx @@ -6,9 +6,9 @@ import { useLanguage } from "@/context/language" import type { LocalServerConfig, LocalServerState, LocalServerStep } from "@/context/platform" import { usePlatform } from "@/context/platform" -const STEP_ORDER: LocalServerStep[] = ["wsl", "distro", "opencode", "switch"] +const WSL_STEPS: LocalServerStep[] = ["wsl", "distro", "opencode", "switch"] -export function DialogLocalServer() { +export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { const language = useLanguage() const platform = usePlatform() const [store, setStore] = createStore({ @@ -44,8 +44,10 @@ export function DialogLocalServer() { const current = () => store.state const localServer = () => platform.localServer + const targetMode = createMemo<"windows" | "wsl">( + () => props.targetMode ?? (current()?.config.mode === "wsl" ? "wsl" : "wsl"), + ) const busy = createMemo(() => !!current()?.job) - const mode = createMemo(() => current()?.config.mode ?? "windows") const selectedProbe = createMemo(() => current()?.checks.distro?.selected) const selectedInstalled = createMemo(() => (current()?.checks.distro?.installed ?? []).find((item) => item.name === current()?.config.distro), @@ -78,6 +80,7 @@ export function DialogLocalServer() { const opencodeReady = createMemo(() => !!current()?.checks.opencode?.resolvedPath) const switchReady = createMemo(() => wslReady() && distroReady() && opencodeReady()) const recommendedStep = createMemo(() => { + if (targetMode() === "windows") return "switch" if (!wslReady()) return "wsl" if (!distroReady()) return "distro" if (!opencodeReady()) return "opencode" @@ -86,10 +89,6 @@ export function DialogLocalServer() { const activeStep = createMemo(() => store.step ?? current()?.job?.step ?? recommendedStep()) createEffect(() => { - if (mode() !== "wsl") { - if (store.step) setStore("step", undefined) - return - } const next = current()?.job?.step ?? recommendedStep() if (!store.step || stepIndex(store.step) > stepIndex(next)) { setStore("step", next) @@ -106,25 +105,6 @@ export function DialogLocalServer() { const plainConfig = (config: LocalServerConfig): LocalServerConfig => structuredClone(unwrap(config)) - const setMode = async (next: "windows" | "wsl") => { - const state = current() - if (!state || !localServer()) return - const config = plainConfig(state.config) - if (next === "wsl") setStore("step", "wsl") - await run(() => - localServer()!.setConfig({ - ...config, - mode: next, - onboarding: { - ...config.onboarding, - complete: next === "windows", - pendingRestart: next === "windows" ? false : config.onboarding.pendingRestart, - step: next === "windows" ? null : (config.onboarding.step ?? "wsl"), - }, - }), - ) - } - const selectDistro = async (name: string) => { const state = current() if (!state || !localServer()) return @@ -144,14 +124,31 @@ export function DialogLocalServer() { ) } + const swapToWindows = async () => { + const state = current() + if (!state || !localServer()) return + const config = plainConfig(state.config) + await run(() => + localServer()!.setConfig({ + ...config, + mode: "windows", + distro: null, + onboarding: { + ...config.onboarding, + complete: true, + pendingRestart: false, + step: null, + }, + }), + ) + } + const steps = createMemo(() => - STEP_ORDER.map((step) => ({ + WSL_STEPS.filter((step) => targetMode() === "wsl" || step === "switch").map((step) => ({ step, title: stepTitle(step), - subtitle: stepSubtitle(step, { - current: current(), - selectedInstalled: selectedInstalled(), - selectedProbe: selectedProbe(), + state: stepState(step, { + active: activeStep(), wslReady: wslReady(), distroReady: distroReady(), opencodeReady: opencodeReady(), @@ -159,15 +156,6 @@ export function DialogLocalServer() { needsRestart: needsRestart(), }), locked: stepIndex(step) > stepIndex(recommendedStep()), - state: stepState(step, { - active: activeStep(), - current: current(), - wslReady: wslReady(), - distroReady: distroReady(), - opencodeReady: opencodeReady(), - switchReady: switchReady(), - needsRestart: needsRestart(), - }), })), ) @@ -177,331 +165,276 @@ export function DialogLocalServer() { when={!store.loading} fallback={
Loading local server...
} > -
-
-
-
Local runtime
-
Choose where the managed Local Server should run.
-
-
- - -
+ +
+ + {(item) => ( + + )} +
-
- Current runtime:{" "} - {current()?.runtime.mode === "wsl" ? `wsl:${current()?.runtime.distro ?? "unknown"}` : "windows"} -
- -
- Select Run in WSL to start the WSL setup flow. -
-
-
+ - -
-
Setup flow
-
- - {(item) => ( - - )} - -
-
- - - -
-
-
-
Step 1: Verify WSL
-
- {current()?.checks.wsl?.error ?? - current()?.checks.wsl?.status ?? - current()?.checks.wsl?.version ?? - "WSL has not been checked yet."} -
-
-
- - -
-
- -
-
- Windows restart required to finish WSL installation. -
- -
-
-
-
- - -
-
-
-
Step 2: Choose a distro
-
- {current()?.checks.distro?.error ?? - current()?.config.distro ?? - "Pick a distro or install one below."} -
-
+ + +
+
+
Verify WSL
+
+
+
+
+ {current()?.checks.wsl?.error ?? + current()?.checks.wsl?.status ?? + current()?.checks.wsl?.version ?? + "WSL has not been checked yet."} +
+ +
+
Windows restart required.
+ +
+
+
+
+ +
+
+
Choose a distro
+ +
+
+ {current()?.checks.distro?.error ?? current()?.config.distro ?? "Pick a distro or install one below."} +
+ +
+ + +
+ + 0}>
+ + {(item) => ( + + )} + +
+
+ +
+ 0} + fallback={
No distros detected yet.
} + > + + {(item) => ( + + )} + +
+
+ + + {(probe) => ( +
+
+ User: {probe().username ?? "unknown"} + {probe().isRoot ? " · root" : ""} + {selectedInstalled()?.version === 1 ? " · WSL 1" : ""} +
+
+ bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "} + {probe().canExecute ? "yes" : "no"} +
+ +
WSL 2 is required.
+
+
+ )} +
+ + +
+
+ + +
+
+
Install OpenCode
+
- - 0}> -
-
Other distros
-
- - {(item) => ( - - )} - +
+
+ {current()?.checks.opencode?.error ?? + current()?.checks.opencode?.resolvedPath ?? + "OpenCode has not been checked in this distro yet."} +
+ + {(check) => ( +
+
Path: {check().resolvedPath ?? "not found"}
+
+ Version: {check().version ?? "unknown"} + + {(expected) => {` · desktop ${expected()}`}} +
+ +
+ Installed version does not match the desktop app version. +
+
-
+ )} + +
+ -
-
Installed distros
- 0} - fallback={
No distros detected yet.
} - > - - {(item) => ( - - )} - + +
+
+
+ {targetMode() === "windows" ? "Swap to Windows" : "Switch Local Server"} +
+
+ + -
- - - {(probe) => ( -
-
Selected distro checks
-
- User: {probe().username ?? "unknown"} - {probe().isRoot ? " · root" : ""} - {selectedInstalled()?.version === 1 ? " · WSL 1" : ""} -
-
- bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "} - {probe().canExecute ? "yes" : "no"} -
- -
- WSL 2 is required. Convert this distro before continuing. -
-
-
- )} -
- -
-
-
- - - -
-
-
-
Step 3: Install OpenCode
-
- {current()?.checks.opencode?.error ?? - current()?.checks.opencode?.resolvedPath ?? - "OpenCode has not been checked in this distro yet."} -
-
-
- - -
-
- - - {(check) => ( -
-
Path: {check().resolvedPath ?? "not found"}
-
- Version: {check().version ?? "unknown"} - - {(expected) => {` · desktop ${expected()}`}} - -
- -
- Installed version does not match the desktop app version. -
-
-
- )} -
-
-
- - -
-
-
-
Step 4: Switch Local Server
-
- {needsRestart() - ? "Restart OpenCode to apply your WSL local runtime configuration." - : "WSL local runtime is configured and active."} -
-
-
- -
-
- Configured runtime:{" "} - {configuredRuntime().mode === "wsl" ? `wsl:${configuredRuntime().distro ?? "unknown"}` : "windows"} -
-
- Current runtime:{" "} - {current()?.runtime.mode === "wsl" ? `wsl:${current()?.runtime.distro ?? "unknown"}` : "windows"} -
- -
- Complete the earlier setup steps before switching. -
-
-
-
- - + +
+ {targetMode() === "windows" + ? configuredRuntime().mode === "windows" + ? "Restart OpenCode to finish switching back to Windows." + : "Switch the Local Server target back to Windows." + : needsRestart() + ? "Restart OpenCode to finish switching to WSL." + : "WSL Local Server is active."} +
+ +
+
+ Configured:{" "} + {configuredRuntime().mode === "wsl" ? `wsl:${configuredRuntime().distro ?? "unknown"}` : "windows"} +
+
+ Current:{" "} + {current()?.runtime.mode === "wsl" ? `wsl:${current()?.runtime.distro ?? "unknown"}` : "windows"} +
+ +
Complete the earlier steps first.
+
+
+
+
+ 0}>
@@ -526,55 +459,20 @@ function requestError(language: ReturnType, err: unknown) { } function stepIndex(step: LocalServerStep) { - return STEP_ORDER.indexOf(step) -} - -function stepNumber(step: LocalServerStep) { - return `${stepIndex(step) + 1}` + return WSL_STEPS.indexOf(step) } function stepTitle(step: LocalServerStep) { - if (step === "wsl") return "WSL" - if (step === "distro") return "Distro" - if (step === "opencode") return "OpenCode" + if (step === "wsl") return "Verify WSL" + if (step === "distro") return "Choose distro" + if (step === "opencode") return "Install OpenCode" return "Switch" } -function stepSubtitle( - step: LocalServerStep, - state: { - current?: LocalServerState - selectedInstalled?: LocalServerState["checks"]["distro"] extends infer T ? any : never - selectedProbe?: LocalServerState["checks"]["distro"] extends infer T ? any : never - wslReady: boolean - distroReady: boolean - opencodeReady: boolean - switchReady: boolean - needsRestart: boolean - }, -) { - if (step === "wsl") { - if (state.wslReady) return "Ready" - return state.current?.checks.wsl?.error ?? "Install or verify WSL" - } - if (step === "distro") { - if (state.distroReady) return state.current?.config.distro ?? "Ready" - if (state.selectedInstalled?.version === 1) return "Convert to WSL 2" - return state.current?.checks.distro?.error ?? state.current?.config.distro ?? "Choose a distro" - } - if (step === "opencode") { - if (state.opencodeReady) return state.current?.checks.opencode?.version ?? "Ready" - return state.current?.checks.opencode?.error ?? "Install OpenCode" - } - if (!state.switchReady) return "Complete prior steps" - return state.needsRestart ? "Restart to apply" : "Active" -} - function stepState( step: LocalServerStep, state: { active: LocalServerStep - current?: LocalServerState wslReady: boolean distroReady: boolean opencodeReady: boolean @@ -582,7 +480,6 @@ function stepState( needsRestart: boolean }, ) { - if (state.current?.job?.step === step) return "current" if (state.active === step) return "current" if (step === "wsl") return state.wslReady ? "done" : "warning" if (step === "distro") diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 3d375c34a7..45e67d88dc 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -22,6 +22,7 @@ const DEFAULT_USERNAME = "opencode" interface DialogSelectServerProps { initialView?: "list" | "local" + initialTargetMode?: "windows" | "wsl" } interface ServerFormProps { @@ -198,6 +199,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { }, localServer: { showPage: props.initialView === "local", + targetMode: props.initialTargetMode as "windows" | "wsl" | undefined, }, editServer: { id: undefined as string | undefined, @@ -443,6 +445,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { resetAdd() resetEdit() setStore("localServer", "showPage", false) + setStore("localServer", "targetMode", undefined) } const startAdd = () => { @@ -473,10 +476,11 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { }) } - const startLocal = () => { + const startLocal = (targetMode?: "windows" | "wsl") => { resetAdd() resetEdit() setStore("localServer", "showPage", true) + setStore("localServer", "targetMode", targetMode) } const localSwapLabel = (conn: ServerConnection.Any) => { @@ -484,6 +488,11 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { return conn.variant === "wsl" ? "Swap to Windows" : "Swap to WSL" } + const localSwapTarget = (conn: ServerConnection.Any) => { + if (conn.type !== "sidecar") return undefined + return conn.variant === "wsl" ? "windows" : "wsl" + } + const submitForm = () => { if (mode() === "add") { if (addMutation.isPending) return @@ -533,7 +542,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { } return ( - +
} > - + } > @@ -607,7 +616,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) { class="shrink-0" onClick={(event: MouseEvent) => { event.stopPropagation() - startLocal() + startLocal(localSwapTarget(i)) }} > {localSwapLabel(i)} diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 981e3f45d7..88f43178ca 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -12,6 +12,7 @@ export interface DialogProps extends ParentProps { classList?: ComponentProps<"div">["classList"] fit?: boolean transition?: boolean + dismissOutside?: boolean } export function Dialog(props: DialogProps) { @@ -31,6 +32,12 @@ export function Dialog(props: DialogProps) { ...props.classList, [props.class ?? ""]: !!props.class, }} + onInteractOutside={(e) => { + if (props.dismissOutside === false) e.preventDefault() + }} + onPointerDownOutside={(e) => { + if (props.dismissOutside === false) e.preventDefault() + }} onOpenAutoFocus={(e) => { const target = e.currentTarget as HTMLElement | null const autofocusEl = target?.querySelector("[autofocus]") as HTMLElement | null diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index c1c56212b5..b4d866768b 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -105,7 +105,7 @@ function init() { }} > - + {element()}