mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
clean
This commit is contained in:
@@ -12,14 +12,9 @@ import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
batch,
|
||||
type Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
ErrorBoundary,
|
||||
For,
|
||||
type JSX,
|
||||
@@ -51,7 +46,6 @@ import { WslServersProvider } from "@/context/wsl-servers"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Layout from "@/pages/layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
|
||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||
const loadSession = () => import("@/pages/session")
|
||||
@@ -167,47 +161,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
|
||||
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
const server = useServer()
|
||||
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(
|
||||
healthTarget,
|
||||
() =>
|
||||
props.disableHealthCheck
|
||||
? true
|
||||
: Effect.gen(function* () {
|
||||
if (!server.current) return true
|
||||
const { http, type } = server.current
|
||||
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 healthy = createMemo(() => props.disableHealthCheck || server.healthy())
|
||||
|
||||
const splash = (
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
@@ -218,23 +172,14 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
return (
|
||||
<Show when={server.ready()} fallback={splash}>
|
||||
<Suspense fallback={splash}>
|
||||
<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={splash}
|
||||
>
|
||||
<Show when={healthy() !== undefined} fallback={splash}>
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
when={healthy()}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||
}}
|
||||
onServerSelected={(key) => {
|
||||
startTransition(() => {
|
||||
batch(() => {
|
||||
setCheckMode("blocking")
|
||||
server.setActive(key)
|
||||
})
|
||||
server.setActive(key)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
@@ -248,7 +193,7 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
)
|
||||
}
|
||||
|
||||
function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
|
||||
function ConnectionError(props: { onServerSelected?: (key: ServerConnection.Key) => void }) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
@@ -259,9 +204,6 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
|
||||
const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
|
||||
const canManage = createMemo(() => server.current?.type === "sidecar" && server.current?.variant === "wsl")
|
||||
|
||||
const timer = setInterval(() => props.onRetry?.(), 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
|
||||
return (
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base gap-6 p-6">
|
||||
<div class="flex flex-col items-center max-w-md text-center">
|
||||
|
||||
@@ -12,7 +12,6 @@ import { batch, createEffect, createMemo, onCleanup, Show, startTransition, untr
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { DialogWslServer } from "@/components/dialog-wsl-server"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useDefaultServer } from "@/context/default-server"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
@@ -147,7 +146,6 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const { defaultKey, canDefault, setDefault } = useDefaultServer()
|
||||
const wslServers = useWslServers()
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
let disposed = false
|
||||
@@ -274,7 +272,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
},
|
||||
onSuccess: async (key) => {
|
||||
server.remove(key)
|
||||
if (defaultKey() === key) await setDefault(null)
|
||||
if (server.defaultKey() === key) await server.setDefault(null)
|
||||
},
|
||||
onError: (err) => showRequestError(language, err),
|
||||
}))
|
||||
@@ -549,7 +547,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
|
||||
async function handleRemove(key: ServerConnection.Key) {
|
||||
server.remove(key)
|
||||
if (defaultKey() === key) await setDefault(null)
|
||||
if (server.defaultKey() === key) await server.setDefault(null)
|
||||
}
|
||||
|
||||
function handleRemoveWsl(conn: ServerConnection.Any) {
|
||||
@@ -621,7 +619,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
const wsl = isWslSidecar(i)
|
||||
const wslDistro = wsl ? i.distro : undefined
|
||||
const blocked = () => health(key)?.healthy === false
|
||||
const canChangeDefault = () => canDefault() && i.type !== "ssh"
|
||||
const canChangeDefault = () => server.canDefault() && i.type !== "ssh"
|
||||
const canRemove = () => i.type === "http" || wsl
|
||||
const hasMenuActionsBeforeDelete = () => canRemove() && (i.type === "http" || canChangeDefault() || canRetryWsl(i))
|
||||
const outdated = () => {
|
||||
@@ -651,7 +649,7 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
version={wslCheck(i)?.version ?? undefined}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<Show when={server.defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
@@ -708,15 +706,15 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
<DropdownMenu.ItemLabel>Retry start</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canChangeDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => void setDefault(key)}>
|
||||
<Show when={canChangeDefault() && server.defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => void server.setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canChangeDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => void setDefault(null)}>
|
||||
<Show when={canChangeDefault() && server.defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => void server.setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
|
||||
@@ -6,17 +6,13 @@ 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, startTransition, untrack } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useDefaultServer } from "@/context/default-server"
|
||||
import { type Accessor, batch, createEffect, createMemo, For, type JSXElement, onCleanup, Show, startTransition } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ServerRow } from "@/components/server/server-row"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { ServerConnection, useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||
const parts = value.split(file)
|
||||
@@ -30,72 +26,6 @@ const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||
)
|
||||
}
|
||||
|
||||
const listServersByHealth = (
|
||||
list: ServerConnection.Any[],
|
||||
active: ServerConnection.Key | undefined,
|
||||
status: Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||
) => {
|
||||
if (!list.length) return list
|
||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||
const rank = (value?: ServerHealth) => {
|
||||
if (value?.healthy === true) return 0
|
||||
if (value?.healthy === false) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
return list.slice().sort((a, b) => {
|
||||
if (ServerConnection.key(a) === active) return -1
|
||||
if (ServerConnection.key(b) === active) return 1
|
||||
const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
|
||||
if (diff !== 0) return diff
|
||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
pollKey()
|
||||
const list = untrack(servers)
|
||||
let dead = false
|
||||
|
||||
const refresh = async () => {
|
||||
const results: Record<string, ServerHealth> = {}
|
||||
await Promise.all(
|
||||
list.map(async (conn) => {
|
||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||
}),
|
||||
)
|
||||
if (dead) return
|
||||
setStatus(reconcile(results))
|
||||
}
|
||||
|
||||
void refresh()
|
||||
const id = setInterval(() => void refresh(), pollMs)
|
||||
onCleanup(() => {
|
||||
dead = true
|
||||
clearInterval(id)
|
||||
})
|
||||
})
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
const useMcpToggleMutation = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
@@ -125,7 +55,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const language = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const defaultServer = useDefaultServer()
|
||||
|
||||
const [load, setLoad] = createStore({
|
||||
lspDone: false,
|
||||
@@ -193,8 +122,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
||||
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
||||
})
|
||||
const health = useServerHealth(servers, props.shown)
|
||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||
const toggleMcp = useMcpToggleMutation()
|
||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||
@@ -219,7 +146,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
>
|
||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
|
||||
{servers().length > 0 ? `${servers().length} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||
@@ -239,45 +166,35 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
<Tabs.Content value="servers">
|
||||
<div class="flex flex-col px-2 pb-2">
|
||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||
<For each={sortedServers()}>
|
||||
<For each={servers()}>
|
||||
{(s) => {
|
||||
const key = ServerConnection.key(s)
|
||||
const blocked = () => health[key]?.healthy === false
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
||||
classList={{
|
||||
"hover:bg-surface-raised-base-hover": !blocked(),
|
||||
"cursor-not-allowed": blocked(),
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left hover:bg-surface-raised-base-hover"
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
batch(() => {
|
||||
if (server.key !== key) {
|
||||
if (typeof window !== "undefined" && window.history?.replaceState) {
|
||||
window.history.replaceState(null, "", "/")
|
||||
}
|
||||
} else {
|
||||
navigate("/")
|
||||
}
|
||||
server.setActive(key)
|
||||
})
|
||||
})
|
||||
}}
|
||||
aria-disabled={blocked()}
|
||||
onClick={() => {
|
||||
if (blocked()) return
|
||||
startTransition(() => {
|
||||
batch(() => {
|
||||
if (server.key !== key) {
|
||||
if (typeof window !== "undefined" && window.history?.replaceState) {
|
||||
window.history.replaceState(null, "", "/")
|
||||
}
|
||||
} else {
|
||||
navigate("/")
|
||||
}
|
||||
server.setActive(key)
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ServerHealthIndicator health={health[key]} />
|
||||
>
|
||||
<ServerRow
|
||||
conn={s}
|
||||
dimmed={blocked()}
|
||||
status={health[key]}
|
||||
class="flex items-center gap-2 w-full min-w-0"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="text-12-regular text-text-weak truncate"
|
||||
badge={
|
||||
<Show when={key === defaultServer.defaultKey()}>
|
||||
<Show when={key === server.defaultKey()}>
|
||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||
{language.t("common.default")}
|
||||
</span>
|
||||
@@ -301,10 +218,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const run = ++dialogRun
|
||||
void import("./dialog-select-server").then((x) => {
|
||||
if (dialogDead || dialogRun !== run) return
|
||||
dialog.show(
|
||||
() => <x.DialogSelectServer onNavigateHome={() => navigate("/")} />,
|
||||
() => void defaultServer.query.refetch(),
|
||||
)
|
||||
dialog.show(() => <x.DialogSelectServer onNavigateHome={() => navigate("/")} />)
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { queryOptions, skipToken, useMutation, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
import { ServerConnection } from "./server"
|
||||
|
||||
const defaultServerQueryKey = ["platform", "defaultServer"] as const
|
||||
|
||||
function defaultServerQueryOptions(getDefaultServer: ReturnType<typeof usePlatform>["getDefaultServer"]) {
|
||||
return queryOptions<ServerConnection.Key | null>({
|
||||
queryKey: defaultServerQueryKey,
|
||||
queryFn: getDefaultServer
|
||||
? () => getDefaultServer().then((next) => (next ? ServerConnection.Key.make(next) : null))
|
||||
: skipToken,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDefaultServer() {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const queryClient = useQueryClient()
|
||||
const query = useQuery(() => ({ ...defaultServerQueryOptions(platform.getDefaultServer) }))
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: async (key: ServerConnection.Key | null) => {
|
||||
if (!platform.setDefaultServer) return key
|
||||
await platform.setDefaultServer(key)
|
||||
return key
|
||||
},
|
||||
onSuccess: (key) => {
|
||||
queryClient.setQueryData(defaultServerQueryKey, key)
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
canDefault: () => !!platform.getDefaultServer && !!platform.setDefaultServer,
|
||||
defaultKey: () => query.data ?? null,
|
||||
query,
|
||||
setDefault(key: ServerConnection.Key | null) {
|
||||
if (!platform.setDefaultServer) return Promise.resolve(key)
|
||||
return mutation.mutateAsync(key)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
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 { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||
@@ -101,6 +104,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
servers?: Array<ServerConnection.Any>
|
||||
}) => {
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const serversReady = () => props.serversReady ?? true
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
@@ -139,6 +144,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
|
||||
const [state, setState] = createStore({
|
||||
active: props.defaultServer,
|
||||
default: props.defaultServer as ServerConnection.Key | null,
|
||||
healthy: undefined as boolean | undefined,
|
||||
})
|
||||
|
||||
@@ -173,6 +179,22 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
if (state.active !== input) setState("active", input)
|
||||
}
|
||||
|
||||
async function setDefault(input: ServerConnection.Key | null) {
|
||||
if (!platform.setDefaultServer) return input
|
||||
try {
|
||||
await platform.setDefaultServer(input)
|
||||
setState("default", input)
|
||||
return input
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function nextActiveKey(exclude?: ServerConnection.Key) {
|
||||
const available = allServers().filter((conn) => ServerConnection.key(conn) !== exclude)
|
||||
const preferred = available.find((conn) => ServerConnection.key(conn) === props.defaultServer)
|
||||
@@ -286,6 +308,13 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
get current() {
|
||||
return current()
|
||||
},
|
||||
canDefault() {
|
||||
return !!platform.getDefaultServer && !!platform.setDefaultServer
|
||||
},
|
||||
defaultKey() {
|
||||
return state.default
|
||||
},
|
||||
setDefault,
|
||||
setActive,
|
||||
add,
|
||||
remove,
|
||||
|
||||
@@ -65,12 +65,11 @@ import {
|
||||
PlatformProvider,
|
||||
ServerConnection,
|
||||
useCommand,
|
||||
type WslServersEvent,
|
||||
type WslServersState,
|
||||
useWslServers,
|
||||
} from "@opencode-ai/app"
|
||||
import type { AsyncStorage } from "@solid-primitives/storage"
|
||||
import { MemoryRouter } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, onCleanup, onMount } from "solid-js"
|
||||
import { render } from "solid-js/web"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
@@ -354,24 +353,6 @@ render(() => {
|
||||
|
||||
const [defaultServer] = createResource(() => platform.getDefaultServer?.())
|
||||
const [locale] = createResource(loadLocale)
|
||||
const [wslServers, setWslServers] = createSignal<WslServersState | undefined>()
|
||||
const [wslReady, setWslReady] = createSignal(!platform.wslServers)
|
||||
if (platform.wslServers) {
|
||||
void platform.wslServers
|
||||
.getState()
|
||||
.then((state) => {
|
||||
setWslServers(state)
|
||||
setWslReady(true)
|
||||
})
|
||||
.catch(() => {
|
||||
setWslReady(true)
|
||||
})
|
||||
const off = platform.wslServers.subscribe((event: WslServersEvent) => {
|
||||
setWslServers(event.state)
|
||||
setWslReady(true)
|
||||
})
|
||||
onCleanup(off)
|
||||
}
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||
@@ -400,6 +381,7 @@ render(() => {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const wslServers = useWslServers()
|
||||
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" />
|
||||
@@ -428,7 +410,7 @@ render(() => {
|
||||
},
|
||||
})
|
||||
}
|
||||
for (const item of wslServers()?.servers ?? []) {
|
||||
for (const item of wslServers.data?.servers ?? []) {
|
||||
const runtime = item.runtime
|
||||
if (runtime.kind !== "ready") continue
|
||||
list.push({
|
||||
@@ -453,7 +435,7 @@ render(() => {
|
||||
return (
|
||||
<AppInterface
|
||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make(startup.latest?.sidecar?.local.key ?? "local:windows")}
|
||||
serversReady={wslReady()}
|
||||
serversReady={!platform.wslServers || !wslServers.isPending}
|
||||
servers={servers()}
|
||||
router={MemoryRouter}
|
||||
>
|
||||
|
||||
8
todo.md
8
todo.md
@@ -17,9 +17,9 @@
|
||||
- [x] [High][M] Remove deferred terminal provider `disposeAll()` macrotask workaround unless a minimal repro proves it is still needed.
|
||||
- [x] [High][M] Remove `unsupportedWorkspace` null-object fallback for WSL `/mnt/` in `context/terminal.tsx`; block earlier or let PTY failure surface.
|
||||
- [x] [High][M] Fold or delete `packages/desktop-electron/src/main/wsl-pty.ts`; it has one importer and can live in `wsl.ts` if kept.
|
||||
- [ ] [High][L] Remove duplicated WSL state subscription in Electron renderer or `WslServersProvider`; keep one WSL state owner.
|
||||
- [ ] [High][L] Revisit `DefaultServer` context; fold into server ownership or make it a real provider instead of ad hoc query hooks.
|
||||
- [ ] [High][L] Collapse duplicated server health loops/logging across `ConnectionGate`, `ServerProvider`, dialog, and status popover.
|
||||
- [x] [High][L] Remove duplicated WSL state subscription in Electron renderer or `WslServersProvider`; keep one WSL state owner.
|
||||
- [x] [High][L] Revisit `DefaultServer` context; fold into server ownership or make it a real provider instead of ad hoc query hooks.
|
||||
- [x] [High][L] Collapse duplicated server health loops/logging across `ConnectionGate`, `ServerProvider`, dialog, and status popover.
|
||||
|
||||
- [ ] [Medium][S] Remove `initialView?: "add-wsl"` from `DialogSelectServer` if no callsites need it.
|
||||
- [ ] [Medium][S] Remove `handleRemoveWsl`, `handleRetryWsl`, `handleUpdateWsl` guard helpers; callers already know when item is WSL.
|
||||
@@ -48,7 +48,7 @@
|
||||
- [ ] [Medium][M] Collapse WSL persisted config + state mutations into one owner/path, or derive runtime state instead of mutating both.
|
||||
- [ ] [Medium][M] Remove bespoke WSL subscribe/unsubscribe IPC lifecycle if broadcast event pattern is acceptable.
|
||||
- [ ] [Medium][M] Collapse repeated WSL IPC method lists in `ipc.ts`, `index.ts`, and preload into one simpler mapping or smaller API surface.
|
||||
- [ ] [Medium][M] Remove duplicated health polling/sorting between status popover and server dialog; keep one or let dialog own detailed health.
|
||||
- [x] [Medium][M] Remove duplicated health polling/sorting between status popover and server dialog; keep one or let dialog own detailed health.
|
||||
- [ ] [Medium][M] Remove duplicated server-switch navigation/batching from status popover, dialog, and connection error; keep one simple path.
|
||||
|
||||
- [ ] [Low][S] Revert dialog timer/lock refactor if it was style churn unrelated to behavior.
|
||||
|
||||
Reference in New Issue
Block a user