tinkering

This commit is contained in:
LukeParkerDev 2026-04-19 20:35:43 +10:00
parent 29130af9ec
commit 0e04141849
7 changed files with 173 additions and 85 deletions

View file

@ -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()
})
}}
/>
}

View file

@ -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<ServerConnection.Key, ServerHealth>()
@ -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<void>((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 (
<Dialog title={formTitle()} dismissOutside={!isAddWslMode()}>
<Dialog
title={formTitle()}
dismissOutside={!isAddWslMode()}
fit={isAddWslMode()}
class={isAddWslMode() ? "[&_[data-slot=dialog-body]]:flex-none [&_[data-slot=dialog-body]]:overflow-visible" : undefined}
>
<div class="flex flex-col gap-2">
<Show
when={!isFormMode()}
@ -672,7 +718,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
/>
}
>
<DialogWslServer />
<DialogWslServer onAdded={handleAddedWsl} />
</Show>
}
>

View file

@ -23,7 +23,7 @@ function parseProgressPercent(text: string) {
}
interface DialogWslServerProps {
onAdded?: () => void
onAdded?: (distro: string) => void | Promise<void>
}
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 = {}) {
</Button>
</div>
</Show>
<div class="flex items-center justify-end">
<Button variant="secondary" size="large" disabled={busy() || !wslReady()} onClick={() => setStore("step", "distro")}>
Next
</Button>
</div>
</div>
</Match>
@ -386,11 +394,9 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
onClick={() => selectDistro(item.name)}
>
<div class="text-13-medium text-text-strong">{item.name}</div>
<div class="text-12-regular text-text-weak">
{[item.isDefault ? "default" : null, item.state, item.version ? `WSL ${item.version}` : null]
.filter(Boolean)
.join(" · ")}
</div>
<Show when={item.isDefault}>
<div class="text-12-regular text-text-weak">Default</div>
</Show>
</button>
)}
</For>
@ -423,7 +429,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
<div
role="radiogroup"
aria-label="Install distro"
class="max-h-44 overflow-y-auto rounded-md bg-background-base"
class="max-h-52 overflow-y-auto rounded-md bg-background-base"
>
<For each={installableDistros()}>
{(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 = {}) {
>
<div class="h-2 w-2 rounded-full bg-text-strong" classList={{ hidden: !selected() }} />
</div>
<div class="min-w-0 flex-1">
<div class="text-13-medium text-text-strong break-words">{item.label}</div>
<Show when={item.label !== item.name}>
<div class="text-12-regular text-text-weak break-words">{item.name}</div>
</Show>
</div>
<div class="min-w-0 flex-1 text-13-medium text-text-strong truncate">{item.label}</div>
</button>
)
}}
@ -499,6 +500,17 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
>
Open terminal
</Button>
<div class="flex items-center justify-end">
<Button
variant="secondary"
size="large"
disabled={busy() || !selectedDistro() || !distroReady()}
onClick={() => setStore("step", "opencode")}
>
Next
</Button>
</div>
</div>
</Match>
@ -550,10 +562,7 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
<div>Progress</div>
</div>
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">{progress().title}</div>
<div
data-scrollable
class="max-h-32 overflow-y-auto rounded-md border border-border-weak-base bg-background-base px-3 py-2 font-mono text-12-regular whitespace-pre-wrap break-words"
>
<div class="rounded-md border border-border-weak-base bg-background-base px-3 py-2 font-mono text-12-regular whitespace-pre-wrap break-words">
<For
each={
progress().lines.length
@ -580,25 +589,22 @@ export function DialogWslServer(props: DialogWslServerProps = {}) {
<Show when={current()?.lastError && (current()?.transcript.length ?? 0) > 0}>
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-2">
<div class="text-14-medium text-text-strong">Diagnostics</div>
<div class="max-h-56 overflow-y-auto rounded-md border border-border-weak-base bg-background-base px-3 py-2 font-mono text-12-regular text-text-weak whitespace-pre-wrap break-words">
<div class="rounded-md border border-border-weak-base bg-background-base px-3 py-2 font-mono text-12-regular text-text-weak whitespace-pre-wrap break-words">
<For each={current()?.transcript ?? []}>{(line) => <div>{line.text}</div>}</For>
</div>
</div>
</Show>
<div class="flex items-center justify-end gap-2">
<Button variant="ghost" size="large" disabled={store.adding} onClick={() => dialog.close()}>
Cancel
</Button>
<Button
variant="primary"
size="large"
disabled={!allReady() || !selectedDistro() || store.adding || busy()}
onClick={() => void finish()}
>
{store.adding ? "Adding..." : "Add WSL server"}
</Button>
</div>
<Show when={activeStep() === "opencode" && allReady() && selectedDistro()}>
<div class="flex items-center justify-end gap-2">
<Button variant="ghost" size="large" disabled={store.adding} onClick={() => dialog.close()}>
Cancel
</Button>
<Button variant="primary" size="large" disabled={store.adding || busy()} onClick={() => void finish()}>
{store.adding ? "Adding..." : "Add WSL server"}
</Button>
</div>
</Show>
</Show>
</div>
)

View file

@ -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<boolean> }) {
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)
})
})
}}
>
<ServerHealthIndicator health={health[key]} />

View file

@ -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({

View file

@ -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<void>((resolve) => {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => resolve())
return
}
setTimeout(resolve, 0)
})
export async function withServerSwitchOverlay(action: () => void | Promise<void>) {
const token = ++run
setServerSwitching(true)
await nextPaint()
try {
await action()
} finally {
await nextPaint()
if (run === token) setServerSwitching(false)
}
}

View file

@ -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,