diff options
Diffstat (limited to 'packages/app/src/app.tsx')
| -rw-r--r-- | packages/app/src/app.tsx | 128 |
1 files changed, 118 insertions, 10 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 2790e7d3c..1b7ffde46 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,14 +1,29 @@ import "@/index.css" -import { File } from "@opencode-ai/ui/file" import { I18nProvider } from "@opencode-ai/ui/context" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { FileComponentProvider } from "@opencode-ai/ui/context/file" import { MarkedProvider } from "@opencode-ai/ui/context/marked" +import { File } from "@opencode-ai/ui/file" import { Font } from "@opencode-ai/ui/font" +import { Splash } from "@opencode-ai/ui/logo" import { ThemeProvider } from "@opencode-ai/ui/theme" import { MetaProvider } from "@solidjs/meta" -import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" -import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js" +import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" +import { type Duration, Effect } from "effect" +import { + type Component, + createResource, + createSignal, + ErrorBoundary, + For, + type JSX, + lazy, + onCleanup, + type ParentProps, + Show, + Suspense, +} from "solid-js" +import { Dynamic } from "solid-js/web" import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -22,13 +37,13 @@ import { NotificationProvider } from "@/context/notification" import { PermissionProvider } from "@/context/permission" import { usePlatform } from "@/context/platform" import { PromptProvider } from "@/context/prompt" -import { type ServerConnection, ServerProvider, useServer } from "@/context/server" +import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { SettingsProvider } from "@/context/settings" import { TerminalProvider } from "@/context/terminal" import DirectoryLayout from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" -import { Dynamic } from "solid-js/web" +import { useCheckServerHealth } from "./utils/server-health" const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) @@ -139,15 +154,108 @@ export function AppBaseProviders(props: ParentProps) { ) } -function ServerKey(props: ParentProps) { +const effectMinDuration = + (duration: Duration.Input) => + <A, E, R>(e: Effect.Effect<A, E, R>) => + Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0])) + +function ConnectionGate(props: ParentProps) { const server = useServer() + const checkServerHealth = useCheckServerHealth() + + const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking") + + // performs repeated health check with a grace period for + // non-http connections, otherwise fails instantly + const [startupHealthCheck, healthCheckActions] = createResource(() => + 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( + effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0), + Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }), + Effect.ensuring(Effect.sync(() => setCheckMode("background"))), + Effect.runPromise, + ), + ) + return ( - <Show when={server.key} keyed> - {props.children} + <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> + } + > + <Show + when={startupHealthCheck()} + fallback={ + <ConnectionError + onRetry={() => { + if (checkMode() === "background") healthCheckActions.refetch() + }} + onServerSelected={(key) => { + setCheckMode("blocking") + server.setActive(key) + healthCheckActions.refetch() + }} + /> + } + > + {props.children} + </Show> </Show> ) } +function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { + const server = useServer() + const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) + + 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"> + <Splash class="w-12 h-15 mb-4" /> + <p class="text-14-regular text-text-base"> + Could not reach <span class="text-text-strong font-medium">{server.name || server.key}</span> + </p> + <p class="mt-1 text-12-regular text-text-weak">Retrying automatically...</p> + </div> + <Show when={others().length > 0}> + <div class="flex flex-col gap-2 w-full max-w-sm"> + <span class="text-12-regular text-text-base text-center">Other servers</span> + <div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2"> + <For each={others()}> + {(conn) => { + const key = ServerConnection.key(conn) + return ( + <button + type="button" + class="flex items-center gap-3 w-full px-3 py-2 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left" + onClick={() => props.onServerSelected?.(key)} + > + <span class="text-14-regular text-text-strong truncate">{serverName(conn)}</span> + </button> + ) + }} + </For> + </div> + </div> + </Show> + </div> + ) +} + export function AppInterface(props: { children?: JSX.Element defaultServer: ServerConnection.Key @@ -156,7 +264,7 @@ export function AppInterface(props: { }) { return ( <ServerProvider defaultServer={props.defaultServer} servers={props.servers}> - <ServerKey> + <ConnectionGate> <GlobalSDKProvider> <GlobalSyncProvider> <Dynamic @@ -171,7 +279,7 @@ export function AppInterface(props: { </Dynamic> </GlobalSyncProvider> </GlobalSDKProvider> - </ServerKey> + </ConnectionGate> </ServerProvider> ) } |
