| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- import "@/index.css"
- 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/context"
- import { MetaProvider } from "@solidjs/meta"
- import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
- import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
- import { type Duration, Effect } from "effect"
- import {
- type Component,
- createMemo,
- 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"
- import { GlobalSDKProvider } from "@/context/global-sdk"
- import { GlobalSyncProvider } from "@/context/global-sync"
- import { HighlightsProvider } from "@/context/highlights"
- import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
- import { LayoutProvider } from "@/context/layout"
- import { ModelsProvider } from "@/context/models"
- import { NotificationProvider } from "@/context/notification"
- import { PermissionProvider } from "@/context/permission"
- import { usePlatform } from "@/context/platform"
- import { PromptProvider } from "@/context/prompt"
- 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 { useCheckServerHealth } from "./utils/server-health"
- const HomeRoute = lazy(() => import("@/pages/home"))
- const Session = lazy(() => import("@/pages/session"))
- const Loading = () => <div class="size-full" />
- const SessionRoute = () => (
- <SessionProviders>
- <Session />
- </SessionProviders>
- )
- const SessionIndexRoute = () => <Navigate href="session" />
- function UiI18nBridge(props: ParentProps) {
- const language = useLanguage()
- return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
- }
- declare global {
- interface Window {
- __OPENCODE__?: {
- updaterEnabled?: boolean
- deepLinks?: string[]
- wsl?: boolean
- }
- api?: {
- setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
- }
- }
- }
- function MarkedProviderWithNativeParser(props: ParentProps) {
- const platform = usePlatform()
- return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
- }
- function QueryProvider(props: ParentProps) {
- const client = new QueryClient()
- return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
- }
- function AppShellProviders(props: ParentProps) {
- return (
- <SettingsProvider>
- <PermissionProvider>
- <LayoutProvider>
- <NotificationProvider>
- <ModelsProvider>
- <CommandProvider>
- <HighlightsProvider>
- <Layout>{props.children}</Layout>
- </HighlightsProvider>
- </CommandProvider>
- </ModelsProvider>
- </NotificationProvider>
- </LayoutProvider>
- </PermissionProvider>
- </SettingsProvider>
- )
- }
- function SessionProviders(props: ParentProps) {
- return (
- <TerminalProvider>
- <FileProvider>
- <PromptProvider>
- <CommentsProvider>{props.children}</CommentsProvider>
- </PromptProvider>
- </FileProvider>
- </TerminalProvider>
- )
- }
- function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
- return (
- <AppShellProviders>
- <Suspense fallback={<Loading />}>
- {props.appChildren}
- {props.children}
- </Suspense>
- </AppShellProviders>
- )
- }
- export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
- return (
- <MetaProvider>
- <Font />
- <ThemeProvider
- onThemeApplied={(_, mode) => {
- void window.api?.setTitlebar?.({ mode })
- }}
- >
- <LanguageProvider locale={props.locale}>
- <UiI18nBridge>
- <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
- <QueryProvider>
- <DialogProvider>
- <MarkedProviderWithNativeParser>
- <FileComponentProvider component={File}>{props.children}</FileComponentProvider>
- </MarkedProviderWithNativeParser>
- </DialogProvider>
- </QueryProvider>
- </ErrorBoundary>
- </UiI18nBridge>
- </LanguageProvider>
- </ThemeProvider>
- </MetaProvider>
- )
- }
- 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<{ disableHealthCheck?: boolean }>) {
- 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(() =>
- 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(
- 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={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 language = useLanguage()
- const server = useServer()
- const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
- const name = createMemo(() => server.name || server.key)
- const serverToken = "\u0000server\u0000"
- const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
- 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">
- {unreachable()[0]}
- <span class="text-text-strong font-medium">{name()}</span>
- {unreachable()[1]}
- </p>
- <p class="mt-1 text-12-regular text-text-weak">{language.t("app.server.retrying")}</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">{language.t("app.server.otherServers")}</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>
- )
- }
- function ServerKey(props: ParentProps) {
- const server = useServer()
- return (
- <Show when={server.key} keyed>
- {props.children}
- </Show>
- )
- }
- export function AppInterface(props: {
- children?: JSX.Element
- defaultServer: ServerConnection.Key
- servers?: Array<ServerConnection.Any>
- router?: Component<BaseRouterProps>
- disableHealthCheck?: boolean
- }) {
- return (
- <ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
- <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
- <ServerKey>
- <GlobalSDKProvider>
- <GlobalSyncProvider>
- <Dynamic
- component={props.router ?? Router}
- root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
- >
- <Route path="/" component={HomeRoute} />
- <Route path="/:dir" component={DirectoryLayout}>
- <Route path="/" component={SessionIndexRoute} />
- <Route path="/session/:id?" component={SessionRoute} />
- </Route>
- </Dynamic>
- </GlobalSyncProvider>
- </GlobalSDKProvider>
- </ServerKey>
- </ConnectionGate>
- </ServerProvider>
- )
- }
|