269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
import "@/index.css"
|
|
import * as Sentry from "@sentry/solid"
|
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
|
import { File } from "@opencode-ai/session-ui/file"
|
|
import { Font } from "@opencode-ai/ui/font"
|
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
|
import { MetaProvider } from "@solidjs/meta"
|
|
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
|
import {
|
|
type Component,
|
|
createMemo,
|
|
createRenderEffect,
|
|
ErrorBoundary,
|
|
type JSX,
|
|
lazy,
|
|
type ParentProps,
|
|
Show,
|
|
} from "solid-js"
|
|
import { Dynamic } from "solid-js/web"
|
|
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
|
|
import { CommentsProvider } from "@/context/comments"
|
|
import { FileProvider } from "@/context/file"
|
|
import { GlobalProvider, useGlobal } from "@/context/global"
|
|
import { HighlightsProvider } from "@/context/highlights"
|
|
import { LanguageProvider, UiI18nBridge, type Locale, useLanguage } from "@/context/language"
|
|
import { LayoutProvider } from "@/context/layout"
|
|
import { ModelsProvider } from "@/context/models"
|
|
import { usePlatform } from "@/context/platform"
|
|
import { PromptProvider } from "@/context/prompt"
|
|
import { ServerConnection, ServersProvider } from "@/context/servers"
|
|
import { SettingsProvider } from "@/context/settings"
|
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
|
import { LocationProvider } from "@/context/location"
|
|
import { WslServersProvider } from "@/wsl/context"
|
|
import { SessionUIProvider } from "@/pages/directory-layout"
|
|
import Layout from "@/pages/layout"
|
|
import { ErrorPage } from "./pages/error"
|
|
import { requireServerKey } from "./utils/session-route"
|
|
|
|
import { TargetSessionRouteContent } from "@/pages/session"
|
|
import { Home } from "@/pages/home"
|
|
import { ServerProvider } from "./context/server"
|
|
|
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
|
|
|
function TargetServerRoute(props: ParentProps) {
|
|
const params = useParams<{ serverKey: string }>()
|
|
const global = useGlobal()
|
|
const conn = createMemo(() =>
|
|
global.servers.list().find((item) => ServerConnection.key(item) === requireServerKey(params.serverKey)),
|
|
)
|
|
|
|
return (
|
|
// Owns the server-identity remount. Session changes must not remount this subtree.
|
|
<Show when={conn()} keyed>
|
|
{(conn) => <ServerProvider conn={conn}>{props.children}</ServerProvider>}
|
|
</Show>
|
|
)
|
|
}
|
|
|
|
function DraftRoute() {
|
|
const [search] = useSearchParams<{ draftId?: string }>()
|
|
const tabs = useTabs()
|
|
return (
|
|
<Show
|
|
when={tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)}
|
|
keyed
|
|
fallback={tabs.ready() && <Navigate href="/" />}
|
|
>
|
|
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
|
</Show>
|
|
)
|
|
}
|
|
|
|
function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
|
const global = useGlobal()
|
|
const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
|
|
|
|
return (
|
|
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
|
<Show when={conn()} keyed>
|
|
{(conn) => (
|
|
<ServerProvider conn={conn}>
|
|
<ModelsProvider directory={props.draft.directory}>
|
|
<LocationProvider directory={props.draft.directory}>
|
|
<SessionUIProvider directory={props.draft.directory} server={props.draft.server}>
|
|
<DraftProviders>
|
|
<NewSession draftId={props.draft.draftID} />
|
|
</DraftProviders>
|
|
</SessionUIProvider>
|
|
</LocationProvider>
|
|
</ModelsProvider>
|
|
</ServerProvider>
|
|
)}
|
|
</Show>
|
|
</Show>
|
|
)
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__OPENCODE__?: {
|
|
deepLinks?: string[]
|
|
}
|
|
api?: {
|
|
setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise<void>
|
|
exportDebugLogs?: () => Promise<string>
|
|
}
|
|
}
|
|
}
|
|
|
|
function QueryProvider(props: ParentProps) {
|
|
const client = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnReconnect: false,
|
|
refetchOnWindowFocus: false,
|
|
refetchOnMount: false,
|
|
},
|
|
},
|
|
})
|
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
|
}
|
|
|
|
function BodyTypography() {
|
|
createRenderEffect(() => {
|
|
if (typeof document === "undefined") return
|
|
document.body.classList.remove("text-12-regular")
|
|
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
|
|
})
|
|
|
|
return null
|
|
}
|
|
|
|
// Server-agnostic providers shared across every route. These live in the shared
|
|
// shell (router root) so they stay mounted regardless of the active server/route.
|
|
function DesktopCommands() {
|
|
const command = useCommand()
|
|
const language = useLanguage()
|
|
const platform = usePlatform()
|
|
|
|
command.register("desktop", () => {
|
|
const commands: CommandOption[] = []
|
|
if (platform.platform === "desktop" && platform.exportDebugLogs) {
|
|
commands.push({
|
|
id: "logs.export",
|
|
title: language.t("command.logs.export"),
|
|
category: language.t("command.category.settings"),
|
|
onSelect: () => {
|
|
void platform.exportDebugLogs?.()
|
|
},
|
|
})
|
|
}
|
|
return commands
|
|
})
|
|
|
|
return null
|
|
}
|
|
|
|
function AppLayout(props: ParentProps) {
|
|
return (
|
|
<LayoutProvider>
|
|
<Layout>{props.children}</Layout>
|
|
</LayoutProvider>
|
|
)
|
|
}
|
|
|
|
// The draft page only renders the prompt composer, so it drops TerminalProvider.
|
|
// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
|
|
function DraftProviders(props: ParentProps) {
|
|
return (
|
|
<FileProvider>
|
|
<PromptProvider>
|
|
<CommentsProvider>{props.children}</CommentsProvider>
|
|
</PromptProvider>
|
|
</FileProvider>
|
|
)
|
|
}
|
|
|
|
export function AppBaseProviders(
|
|
props: ParentProps<{
|
|
locale?: Locale
|
|
onNativeTranslations?: Parameters<typeof LanguageProvider>[0]["onNativeTranslations"]
|
|
}>,
|
|
) {
|
|
return (
|
|
<MetaProvider>
|
|
<Font />
|
|
<ThemeProvider
|
|
onThemeApplied={(_, mode, scheme) => {
|
|
void window.api?.setTitlebar?.({ mode, scheme })
|
|
}}
|
|
>
|
|
<LanguageProvider locale={props.locale} onNativeTranslations={props.onNativeTranslations}>
|
|
<UiI18nBridge>
|
|
<ErrorBoundary
|
|
fallback={(error) => {
|
|
Sentry.captureException(error)
|
|
return <ErrorPage error={error} />
|
|
}}
|
|
>
|
|
<QueryProvider>
|
|
<WslServersProvider>
|
|
<DialogProvider>
|
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
|
</DialogProvider>
|
|
</WslServersProvider>
|
|
</QueryProvider>
|
|
</ErrorBoundary>
|
|
</UiI18nBridge>
|
|
</LanguageProvider>
|
|
</ThemeProvider>
|
|
</MetaProvider>
|
|
)
|
|
}
|
|
|
|
export function AppInterface(props: {
|
|
children?: JSX.Element
|
|
defaultServer: ServerConnection.Key
|
|
canonicalLocalServer?: ServerConnection.Key
|
|
servers?: Array<ServerConnection.Any>
|
|
router?: Component<BaseRouterProps>
|
|
}) {
|
|
// The visual layout lives in the router root so it remains mounted across
|
|
// route changes. Draft and session routes override only their server-bound data
|
|
// providers beneath it.
|
|
const Root = (rootProps: ParentProps) => (
|
|
<TabsProvider>
|
|
<BodyTypography />
|
|
<CommandProvider>
|
|
<DesktopCommands />
|
|
<HighlightsProvider>
|
|
{props.children}
|
|
{rootProps.children}
|
|
</HighlightsProvider>
|
|
</CommandProvider>
|
|
</TabsProvider>
|
|
)
|
|
|
|
return (
|
|
<ServersProvider
|
|
defaultServer={props.defaultServer}
|
|
canonicalLocalServer={props.canonicalLocalServer}
|
|
servers={props.servers}
|
|
>
|
|
<SettingsProvider>
|
|
<GlobalProvider>
|
|
<Dynamic component={props.router ?? Router} root={Root}>
|
|
<Route component={AppLayout}>
|
|
<Route path="/" component={Home} />
|
|
<Route
|
|
path="/server/:serverKey/session/:id"
|
|
component={() => (
|
|
<TargetServerRoute>
|
|
<TargetSessionRouteContent />
|
|
</TargetServerRoute>
|
|
)}
|
|
/>
|
|
<Route path="/new-session" component={DraftRoute} />
|
|
</Route>
|
|
</Dynamic>
|
|
</GlobalProvider>
|
|
</SettingsProvider>
|
|
</ServersProvider>
|
|
)
|
|
}
|