mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-30 20:44:31 +00:00
tinkering
This commit is contained in:
parent
0e04141849
commit
a17ce350f1
7 changed files with 138 additions and 82 deletions
|
|
@ -16,6 +16,7 @@ import { Effect } from "effect"
|
|||
import {
|
||||
batch,
|
||||
type Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
|
|
@ -49,7 +50,7 @@ import { TerminalProvider } from "@/context/terminal"
|
|||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Layout from "@/pages/layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
import { isPlaceholderServerUrl, useCheckServerHealth } from "./utils/server-health"
|
||||
|
||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||
const loadSession = () => import("@/pages/session")
|
||||
|
|
@ -166,67 +167,80 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||
const checkServerHealth = useCheckServerHealth()
|
||||
|
||||
const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking")
|
||||
const healthTarget = createMemo(() => {
|
||||
const current = server.current
|
||||
if (props.disableHealthCheck || !current) return ""
|
||||
return [
|
||||
ServerConnection.key(current),
|
||||
current.type,
|
||||
current.http.url,
|
||||
current.http.username ?? "",
|
||||
current.http.password ?? "",
|
||||
].join("\n")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
healthTarget()
|
||||
setCheckMode("blocking")
|
||||
})
|
||||
|
||||
// performs repeated health check with a grace period for
|
||||
// non-http connections, otherwise fails instantly
|
||||
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
||||
props.disableHealthCheck
|
||||
? true
|
||||
: Effect.gen(function* () {
|
||||
if (!server.current) return true
|
||||
const { http, type } = server.current
|
||||
const [startupHealthCheck, healthCheckActions] = createResource(
|
||||
healthTarget,
|
||||
() =>
|
||||
props.disableHealthCheck
|
||||
? true
|
||||
: Effect.gen(function* () {
|
||||
if (!server.current) return true
|
||||
const { http, type } = server.current
|
||||
if (isPlaceholderServerUrl(http.url)) return false
|
||||
|
||||
while (true) {
|
||||
const res = yield* Effect.promise(() => checkServerHealth(http))
|
||||
if (res.healthy) return true
|
||||
if (checkMode() === "background" || type === "http") return false
|
||||
}
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||
Effect.runPromise,
|
||||
),
|
||||
while (true) {
|
||||
const res = yield* Effect.promise(() => checkServerHealth(http))
|
||||
if (res.healthy) return true
|
||||
if (checkMode() === "background" || type === "http") return false
|
||||
}
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||
Effect.runPromise,
|
||||
),
|
||||
)
|
||||
|
||||
const splash = (
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/*<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>*/}
|
||||
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||
<Suspense fallback={splash}>
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||
}}
|
||||
onServerSelected={(key) => {
|
||||
void withServerSwitchOverlay(() => {
|
||||
batch(() => {
|
||||
setCheckMode("blocking")
|
||||
server.setActive(key)
|
||||
})
|
||||
void healthCheckActions.refetch()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={splash}
|
||||
>
|
||||
{props.children}
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||
}}
|
||||
onServerSelected={(key) => {
|
||||
void withServerSwitchOverlay(() => {
|
||||
batch(() => {
|
||||
setCheckMode("blocking")
|
||||
server.setActive(key)
|
||||
})
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Show>
|
||||
</Show>
|
||||
{/*</Show>*/}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { List } from "@opencode-ai/ui/list"
|
|||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { batch, createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||
import { batch, createEffect, createMemo, createResource, onCleanup, Show, untrack } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { DialogWslServer } from "@/components/dialog-wsl-server"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
|
|
@ -16,7 +16,7 @@ import { useLanguage } from "@/context/language"
|
|||
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 { isPlaceholderServerUrl, type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
|
||||
import { withServerSwitchOverlay } from "@/utils/server-switch"
|
||||
|
||||
const DEFAULT_USERNAME = "opencode"
|
||||
|
|
@ -356,6 +356,15 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
.join("\n\n"),
|
||||
)
|
||||
const health = (key: ServerConnection.Key) => store.status[key] ?? cachedServerStatus.get(key)
|
||||
const isSelectable = (conn: ServerConnection.Any) => !isPlaceholderServerUrl(conn.http.url)
|
||||
const wslRuntime = (conn: ServerConnection.Any) => {
|
||||
if (conn.type !== "sidecar" || conn.variant !== "wsl") return
|
||||
return store.wslState?.servers.find((item) => item.config.id === ServerConnection.key(conn))?.runtime
|
||||
}
|
||||
const canRetryWsl = (conn: ServerConnection.Any) => {
|
||||
const runtime = wslRuntime(conn)
|
||||
return runtime?.kind === "failed" || runtime?.kind === "stopped"
|
||||
}
|
||||
|
||||
const sortedItems = createMemo(() => {
|
||||
const list = items()
|
||||
|
|
@ -378,8 +387,9 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
|
||||
async function refreshHealth() {
|
||||
const results: Record<ServerConnection.Key, ServerHealth> = {}
|
||||
const list = untrack(items)
|
||||
await Promise.all(
|
||||
items().map(async (conn) => {
|
||||
list.map(async (conn) => {
|
||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||
}),
|
||||
)
|
||||
|
|
@ -430,6 +440,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
}
|
||||
|
||||
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||
if (!isSelectable(conn)) return
|
||||
if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
|
||||
const nextKey = ServerConnection.key(conn)
|
||||
const changed = server.key !== nextKey
|
||||
|
|
@ -468,7 +479,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
const key = store.addWsl.pendingSelectKey
|
||||
if (!key) return
|
||||
const conn = items().find((item) => ServerConnection.key(item) === key)
|
||||
if (!conn) return
|
||||
if (!conn || !isSelectable(conn)) return
|
||||
const resolve = resolvePendingWslSelection
|
||||
resolvePendingWslSelection = undefined
|
||||
setStore("addWsl", "pendingSelectKey", undefined)
|
||||
|
|
@ -592,7 +603,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
const handleAddedWsl = async (distro: string) => {
|
||||
const key = ServerConnection.Key.make(`wsl:${distro}`)
|
||||
const conn = items().find((item) => ServerConnection.key(item) === key)
|
||||
if (conn) {
|
||||
if (conn && isSelectable(conn)) {
|
||||
await select(conn)
|
||||
return
|
||||
}
|
||||
|
|
@ -741,8 +752,8 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
const key = ServerConnection.key(i)
|
||||
const isWslSidecar = i.type === "sidecar" && i.variant === "wsl"
|
||||
const wslDistro = i.type === "sidecar" && i.variant === "wsl" ? i.distro : undefined
|
||||
const hasMenuActionsBeforeDelete = () =>
|
||||
i.type === "http" || (isWslSidecar && health(key)?.healthy === false)
|
||||
const blocked = () => !isSelectable(i) || health(key)?.healthy === false
|
||||
const hasMenuActionsBeforeDelete = () => i.type === "http" || (isWslSidecar && canRetryWsl(i))
|
||||
const outdated = () => {
|
||||
const check = wslCheck(i)
|
||||
return versionOlderThan(check?.version, check?.expectedVersion)
|
||||
|
|
@ -763,7 +774,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={health(key)?.healthy === false}
|
||||
dimmed={blocked()}
|
||||
status={health(key)}
|
||||
version={displayVersion(i)}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
|
|
@ -820,7 +831,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
|||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={isWslSidecar && health(key)?.healthy === false}>
|
||||
<Show when={isWslSidecar && canRetryWsl(i)}>
|
||||
<DropdownMenu.Item onSelect={() => void handleRetryWsl(i)}>
|
||||
<DropdownMenu.ItemLabel>Retry start</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
|
|||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { type Accessor, batch, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||
import { type Accessor, batch, createEffect, createMemo, For, type JSXElement, onCleanup, Show, untrack } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
|
@ -14,7 +14,7 @@ import { usePlatform } from "@/context/platform"
|
|||
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 { isPlaceholderServerUrl, useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
import { withServerSwitchOverlay } from "@/utils/server-switch"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
|
@ -56,13 +56,23 @@ const listServersByHealth = (
|
|||
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
||||
const pollKey = createMemo(() =>
|
||||
enabled()
|
||||
? servers()
|
||||
.map((conn) =>
|
||||
[ServerConnection.key(conn), conn.http.url, conn.http.username ?? "", conn.http.password ?? ""].join("\n"),
|
||||
)
|
||||
.join("\n\n")
|
||||
: "",
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!enabled()) {
|
||||
setStatus(reconcile({}))
|
||||
return
|
||||
}
|
||||
const list = servers()
|
||||
pollKey()
|
||||
const list = untrack(servers)
|
||||
let dead = false
|
||||
|
||||
const refresh = async () => {
|
||||
|
|
@ -277,7 +287,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||
<For each={sortedServers()}>
|
||||
{(s) => {
|
||||
const key = ServerConnection.key(s)
|
||||
const blocked = () => health[key]?.healthy === false
|
||||
const blocked = () => isPlaceholderServerUrl(s.http.url) || health[key]?.healthy === false
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { type Accessor, batch, createEffect, createMemo, onCleanup, untrack } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { useCheckServerHealth } from "@/utils/server-health"
|
||||
import { isPlaceholderServerUrl, useCheckServerHealth } from "@/utils/server-health"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||
|
|
@ -220,19 +220,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||
return x.healthy
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const current_ = current()
|
||||
if (!current_) return
|
||||
|
||||
if (props.disableHealthCheck) {
|
||||
setState("healthy", true)
|
||||
return
|
||||
}
|
||||
setState("healthy", undefined)
|
||||
console.log(`[server health] start polling key=${ServerConnection.key(current_)} url=${current_.http.url}`)
|
||||
onCleanup(startHealthPolling(current_))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const key = state.active
|
||||
if (typeof window === "undefined") return
|
||||
|
|
@ -245,6 +232,29 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
|
||||
() => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0],
|
||||
)
|
||||
const healthTarget = createMemo(() => {
|
||||
const conn = current()
|
||||
if (!conn) return ""
|
||||
return [ServerConnection.key(conn), conn.http.url, conn.http.username ?? "", conn.http.password ?? ""].join("\n")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
healthTarget()
|
||||
const current_ = untrack(current)
|
||||
if (!current_) return
|
||||
|
||||
if (props.disableHealthCheck) {
|
||||
setState("healthy", true)
|
||||
return
|
||||
}
|
||||
if (isPlaceholderServerUrl(current_.http.url)) {
|
||||
setState("healthy", false)
|
||||
return
|
||||
}
|
||||
setState("healthy", undefined)
|
||||
console.log(`[server health] start polling key=${ServerConnection.key(current_)} url=${current_.http.url}`)
|
||||
onCleanup(startHealthPolling(current_))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const list = allServers()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default function Home() {
|
|||
if (healthy === false) return "bg-icon-critical-base"
|
||||
return "bg-border-weak-base"
|
||||
})
|
||||
const useWebDirectoryPicker = createMemo(() => server.current?.type === "sidecar" && server.current.variant === "wsl")
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
|
|
@ -54,7 +55,7 @@ export default function Home() {
|
|||
}
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal() && !useWebDirectoryPicker()) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: language.t("command.project.open"),
|
||||
multiple: true,
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
||||
const currentDir = createMemo(() => route().dir)
|
||||
const useWebDirectoryPicker = createMemo(() => server.current?.type === "sidecar" && server.current.variant === "wsl")
|
||||
|
||||
const [state, setState] = createStore({
|
||||
autoselect: !initialDirectory,
|
||||
|
|
@ -1471,7 +1472,7 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal() && !useWebDirectoryPicker()) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: language.t("command.project.open"),
|
||||
multiple: true,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ const healthCache = new Map<
|
|||
{ at: number; done: boolean; fetch: typeof globalThis.fetch; promise: Promise<ServerHealth> }
|
||||
>()
|
||||
|
||||
export function isPlaceholderServerUrl(url: string) {
|
||||
try {
|
||||
return new URL(url).hostname.endsWith(".invalid")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function cacheKey(server: ServerConnection.HttpBase) {
|
||||
return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}`
|
||||
}
|
||||
|
|
@ -85,6 +93,7 @@ export async function checkServerHealth(
|
|||
fetch: typeof globalThis.fetch,
|
||||
opts?: CheckServerHealthOptions,
|
||||
): Promise<ServerHealth> {
|
||||
if (isPlaceholderServerUrl(server.url)) return { healthy: false }
|
||||
const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
|
||||
const signal = opts?.signal ?? timeout?.signal
|
||||
const retryCount = opts?.retryCount ?? defaultRetryCount
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue