mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-28 02:04:41 +00:00
tinkering
This commit is contained in:
parent
29130af9ec
commit
0e04141849
7 changed files with 173 additions and 85 deletions
|
|
@ -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()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]} />
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue