diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 326db25cb8..921ee1a649 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -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 = (
+
+
+
)
return (
-
-
-
- }
- >
- {/*
-
-
- }
- >*/}
- {checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
+
{
- 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}
+ {
+ if (checkMode() === "background") void healthCheckActions.refetch()
+ }}
+ onServerSelected={(key) => {
+ void withServerSwitchOverlay(() => {
+ batch(() => {
+ setCheckMode("blocking")
+ server.setActive(key)
+ })
+ })
+ }}
+ />
+ }
+ >
+ {props.children}
+
- {/**/}
)
}
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
index 200b489d96..10c533bef9 100644
--- a/packages/app/src/components/dialog-select-server.tsx
+++ b/packages/app/src/components/dialog-select-server.tsx
@@ -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 = {}
+ 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 = {}) {
{language.t("dialog.server.menu.edit")}
-
+
void handleRetryWsl(i)}>
Retry start
diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx
index f4ccb1b954..bc257df618 100644
--- a/packages/app/src/components/status-popover-body.tsx
+++ b/packages/app/src/components/status-popover-body.tsx
@@ -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, enabled: Accessor) => {
const checkServerHealth = useCheckServerHealth()
const [status, setStatus] = createStore({} as Record)
+ 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 }) {
{(s) => {
const key = ServerConnection.key(s)
- const blocked = () => health[key]?.healthy === false
+ const blocked = () => isPlaceholderServerUrl(s.http.url) || health[key]?.healthy === false
return (