Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
## SolidJS

- Always prefer `createStore` over multiple `createSignal` calls
- Use effects only to synchronize with external systems; prefer derived state and explicit actions for app state.

## Architecture

- Strong ownership should make consumers simple and reduce branching.
- Prefer strict contexts with explicit domain values; do not add implicit or theoretical fallbacks.
- Keep route, server, directory, session, draft, and tab identities explicit.
- Prefer explicit transactions over synchronization between independent state stores.
- Avoid duplicate or near-duplicate logic; consolidate only when the shared concept is real.
- Capture concrete context once at the start of async work.
- Treat reduced code, branching, and compatibility machinery as refactor deliverables.

## Tool Calling

Expand Down
283 changes: 53 additions & 230 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,32 @@
import "@/index.css"
import * as Sentry from "@sentry/solid"
import { LegacyRoot } from "@/app/legacy"
import { V2Root } from "@/app/v2"
import { CommandProvider } from "@/context/command"
import { GlobalProvider } from "@/context/global"
import { HighlightsProvider } from "@/context/highlights"
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { ServerConnection, ServerProvider } from "@/context/server"
import { SettingsProvider, useSettings } from "@/context/settings"
import { NotificationServiceProvider } from "@/context/notification"
import { PermissionServiceProvider } from "@/context/permission"
import type { ServerContext } from "@/context/server-context"
import { sessionHref } from "@/utils/v2-route"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { ErrorPage } from "@/pages/error"
import { setV2Toast } from "@/utils/toast"
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 * as Sentry from "@sentry/solid"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import type { BaseRouterProps } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect"
import {
type Component,
createEffect,
createMemo,
createResource,
createSignal,
ErrorBoundary,
For,
type JSX,
lazy,
onCleanup,
type ParentProps,
Show,
} 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 { ServerSDKProvider } from "@/context/server-sdk"
import { ServerSyncProvider } from "@/context/server-sync"
import { GlobalProvider } from "@/context/global"
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 { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider, useSettings } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { TabsProvider } from "@/context/tabs"
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 SessionRoute = Object.assign(
() => (
<SessionProviders>
<Session />
</SessionProviders>
),
{ preload: Session.preload },
)
import { type Component, createEffect, ErrorBoundary, type JSX, type ParentProps, untrack } from "solid-js"

function UiI18nBridge(props: ParentProps) {
const language = useLanguage()
Expand Down Expand Up @@ -100,6 +67,7 @@ function BodyDesignClass() {
if (typeof document === "undefined") return

const enabled = settings.general.newLayoutDesigns()
setV2Toast(enabled)
document.body.classList.toggle("text-12-regular", !enabled)
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
document.body.classList.toggle("text-[13px]", enabled)
Expand All @@ -109,50 +77,6 @@ function BodyDesignClass() {
return null
}

function AppShellProviders(props: ParentProps) {
return (
<SettingsProvider>
<BodyDesignClass />
<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>
Expand Down Expand Up @@ -185,119 +109,32 @@ 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")

// 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(
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
Effect.runPromise,
),
)
const checking = createMemo(
() => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state),
)

return (
<Show
when={!checking()}
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.latest}
fallback={
<ConnectionError
onRetry={() => {
if (checkMode() === "background") void healthCheckActions.refetch()
}}
onServerSelected={(key) => {
setCheckMode("blocking")
server.setActive(key)
void healthCheckActions.refetch()
}}
/>
}
>
{props.children}
</Show>
</Show>
)
function v2NotificationHref(server: ServerContext, _directory: string, sessionID?: string) {
if (!sessionID) return "/"
return sessionHref(server.key, sessionID)
}

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 legacyNotificationHref(_server: ServerContext, directory: string, sessionID?: string) {
if (!sessionID) return `/${base64Encode(directory)}`
return `/${base64Encode(directory)}/session/${sessionID}`
}

function ServerKey(props: ParentProps) {
const server = useServer()
function Application(props: ParentProps<{ router?: Component<BaseRouterProps>; disableHealthCheck?: boolean }>) {
const platform = usePlatform()
const settings = useSettings()
const desktopV2 = platform.platform === "desktop" && untrack(settings.general.newLayoutDesigns)
const href = desktopV2 ? v2NotificationHref : legacyNotificationHref
return (
<Show when={server.key} keyed>
{props.children}
</Show>
<PermissionServiceProvider>
<NotificationServiceProvider href={href}>
{props.children}
{desktopV2 ? (
<V2Root router={props.router} />
) : (
<LegacyRoot router={props.router} disableHealthCheck={props.disableHealthCheck} />
)}
</NotificationServiceProvider>
</PermissionServiceProvider>
)
}

Expand All @@ -315,32 +152,18 @@ export function AppInterface(props: {
canonicalLocalServer={props.canonicalLocalServer}
servers={props.servers}
>
<GlobalProvider>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
<Dynamic
component={props.router ?? Router}
root={(routerProps) => (
<TabsProvider>
<ServerKey>
<QueryProvider>
<ServerSDKProvider>
<ServerSyncProvider>
<RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>
</ServerSyncProvider>
</ServerSDKProvider>
</QueryProvider>
</ServerKey>
</TabsProvider>
)}
>
<Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Dynamic>
</ConnectionGate>
</GlobalProvider>
<SettingsProvider>
<BodyDesignClass />
<CommandProvider>
<HighlightsProvider>
<GlobalProvider>
<Application router={props.router} disableHealthCheck={props.disableHealthCheck}>
{props.children}
</Application>
</GlobalProvider>
</HighlightsProvider>
</CommandProvider>
</SettingsProvider>
</ServerProvider>
)
}
Loading
Loading