Revert "fix(app): startup efficiency (#18854)"

This reverts commit 546748a461.
pull/16065/merge
Adam 2026-03-24 18:36:37 -05:00
parent cbe1337f24
commit a379eb3867
No known key found for this signature in database
GPG Key ID: 9CB48779AF150E75
33 changed files with 636 additions and 943 deletions

View File

@ -6,7 +6,7 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { File } from "@opencode-ai/ui/file" import { File } from "@opencode-ai/ui/file"
import { Font } from "@opencode-ai/ui/font" import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo" import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { ThemeProvider } from "@opencode-ai/ui/theme"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
@ -32,7 +32,7 @@ import { FileProvider } from "@/context/file"
import { GlobalSDKProvider } from "@/context/global-sdk" import { GlobalSDKProvider } from "@/context/global-sdk"
import { GlobalSyncProvider } from "@/context/global-sync" import { GlobalSyncProvider } from "@/context/global-sync"
import { HighlightsProvider } from "@/context/highlights" import { HighlightsProvider } from "@/context/highlights"
import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LanguageProvider, useLanguage } from "@/context/language"
import { LayoutProvider } from "@/context/layout" import { LayoutProvider } from "@/context/layout"
import { ModelsProvider } from "@/context/models" import { ModelsProvider } from "@/context/models"
import { NotificationProvider } from "@/context/notification" import { NotificationProvider } from "@/context/notification"
@ -130,7 +130,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
) )
} }
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { export function AppBaseProviders(props: ParentProps) {
return ( return (
<MetaProvider> <MetaProvider>
<Font /> <Font />
@ -139,7 +139,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
void window.api?.setTitlebar?.({ mode }) void window.api?.setTitlebar?.({ mode })
}} }}
> >
<LanguageProvider locale={props.locale}> <LanguageProvider>
<UiI18nBridge> <UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}> <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<QueryProvider> <QueryProvider>

View File

@ -1,4 +1,4 @@
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
@ -9,7 +9,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Spinner } from "@opencode-ai/ui/spinner" import { Spinner } from "@opencode-ai/ui/spinner"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link" import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
@ -34,25 +34,15 @@ export function DialogConnectProvider(props: { provider: string }) {
}) })
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
const fallback = createMemo<ProviderAuthMethod[]>(() => [ const methods = createMemo(
{ () =>
type: "api" as const, globalSync.data.provider_auth[props.provider] ?? [
label: language.t("provider.connect.method.apiKey"), {
}, type: "api",
]) label: language.t("provider.connect.method.apiKey"),
const [auth] = createResource( },
() => props.provider, ],
async () => {
const cached = globalSync.data.provider_auth[props.provider]
if (cached) return cached
const res = await globalSDK.client.provider.auth()
if (!alive.value) return fallback()
globalSync.set("provider_auth", res.data ?? {})
return res.data?.[props.provider] ?? fallback()
},
) )
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
const [store, setStore] = createStore({ const [store, setStore] = createStore({
methodIndex: undefined as undefined | number, methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization, authorization: undefined as undefined | ProviderAuthAuthorization,
@ -187,11 +177,7 @@ export function DialogConnectProvider(props: { provider: string }) {
index: 0, index: 0,
}) })
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => { const prompts = createMemo(() => method()?.prompts ?? [])
const value = method()
if (value?.type !== "oauth") return []
return value.prompts ?? []
})
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => { const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
if (!prompt.when) return true if (!prompt.when) return true
const actual = value[prompt.when.key] const actual = value[prompt.when.key]
@ -310,12 +296,8 @@ export function DialogConnectProvider(props: { provider: string }) {
listRef?.onKeyDown(e) listRef?.onKeyDown(e)
} }
let auto = false onMount(() => {
createEffect(() => {
if (auto) return
if (loading()) return
if (methods().length === 1) { if (methods().length === 1) {
auto = true
selectMethod(0) selectMethod(0)
} }
}) })
@ -591,14 +573,6 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="px-2.5 pb-10 flex flex-col gap-6"> <div class="px-2.5 pb-10 flex flex-col gap-6">
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}> <div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
<Switch> <Switch>
<Match when={loading()}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
<Spinner />
<span>{language.t("provider.connect.status.inProgress")}</span>
</div>
</div>
</Match>
<Match when={store.methodIndex === undefined}> <Match when={store.methodIndex === undefined}>
<MethodSelection /> <MethodSelection />
</Match> </Match>

View File

@ -1,41 +1,27 @@
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js" import { Component, Show, createMemo, createResource, type JSX } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch" import { Switch } from "@opencode-ai/ui/switch"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings" import { useSettings, monoFontFamily } from "@/context/settings"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link" import { Link } from "./link"
import { SettingsList } from "./settings-list" import { SettingsList } from "./settings-list"
let demoSoundState = { let demoSoundState = {
cleanup: undefined as (() => void) | undefined, cleanup: undefined as (() => void) | undefined,
timeout: undefined as NodeJS.Timeout | undefined, timeout: undefined as NodeJS.Timeout | undefined,
run: 0,
}
type ThemeOption = {
id: string
name: string
}
let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined
function loadFont() {
font ??= import("@opencode-ai/ui/font-loader")
return font
} }
// To prevent audio from overlapping/playing very quickly when navigating the settings menus, // To prevent audio from overlapping/playing very quickly when navigating the settings menus,
// delay the playback by 100ms during quick selection changes and pause existing sounds. // delay the playback by 100ms during quick selection changes and pause existing sounds.
const stopDemoSound = () => { const stopDemoSound = () => {
demoSoundState.run += 1
if (demoSoundState.cleanup) { if (demoSoundState.cleanup) {
demoSoundState.cleanup() demoSoundState.cleanup()
} }
@ -43,19 +29,12 @@ const stopDemoSound = () => {
demoSoundState.cleanup = undefined demoSoundState.cleanup = undefined
} }
const playDemoSound = (id: string | undefined) => { const playDemoSound = (src: string | undefined) => {
stopDemoSound() stopDemoSound()
if (!id) return if (!src) return
const run = ++demoSoundState.run
demoSoundState.timeout = setTimeout(() => { demoSoundState.timeout = setTimeout(() => {
void playSoundById(id).then((cleanup) => { demoSoundState.cleanup = playSound(src)
if (demoSoundState.run !== run) {
cleanup?.()
return
}
demoSoundState.cleanup = cleanup
})
}, 100) }, 100)
} }
@ -65,10 +44,6 @@ export const SettingsGeneral: Component = () => {
const platform = usePlatform() const platform = usePlatform()
const settings = useSettings() const settings = useSettings()
onMount(() => {
void theme.loadThemes()
})
const [store, setStore] = createStore({ const [store, setStore] = createStore({
checking: false, checking: false,
}) })
@ -129,7 +104,9 @@ export const SettingsGeneral: Component = () => {
.finally(() => setStore("checking", false)) .finally(() => setStore("checking", false))
} }
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) const themeOptions = createMemo(() =>
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
)
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
{ value: "system", label: language.t("theme.scheme.system") }, { value: "system", label: language.t("theme.scheme.system") },
@ -166,7 +143,7 @@ export const SettingsGeneral: Component = () => {
] as const ] as const
const fontOptionsList = [...fontOptions] const fontOptionsList = [...fontOptions]
const noneSound = { id: "none", label: "sound.option.none" } as const const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
const soundOptions = [noneSound, ...SOUND_OPTIONS] const soundOptions = [noneSound, ...SOUND_OPTIONS]
const soundSelectProps = ( const soundSelectProps = (
@ -181,7 +158,7 @@ export const SettingsGeneral: Component = () => {
label: (o: (typeof soundOptions)[number]) => language.t(o.label), label: (o: (typeof soundOptions)[number]) => language.t(o.label),
onHighlight: (option: (typeof soundOptions)[number] | undefined) => { onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return if (!option) return
playDemoSound(option.id === "none" ? undefined : option.id) playDemoSound(option.src)
}, },
onSelect: (option: (typeof soundOptions)[number] | undefined) => { onSelect: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return if (!option) return
@ -192,7 +169,7 @@ export const SettingsGeneral: Component = () => {
} }
setEnabled(true) setEnabled(true)
set(option.id) set(option.id)
playDemoSound(option.id) playDemoSound(option.src)
}, },
variant: "secondary" as const, variant: "secondary" as const,
size: "small" as const, size: "small" as const,
@ -344,9 +321,6 @@ export const SettingsGeneral: Component = () => {
current={fontOptionsList.find((o) => o.value === settings.appearance.font())} current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
value={(o) => o.value} value={(o) => o.value}
label={(o) => language.t(o.label)} label={(o) => language.t(o.label)}
onHighlight={(option) => {
void loadFont().then((x) => x.ensureMonoFont(option?.value))
}}
onSelect={(option) => option && settings.appearance.setFont(option.value)} onSelect={(option) => option && settings.appearance.setFont(option.value)}
variant="secondary" variant="secondary"
size="small" size="small"

View File

@ -16,6 +16,7 @@ import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
import { DialogSelectServer } from "./dialog-select-server"
const pollMs = 10_000 const pollMs = 10_000
@ -53,15 +54,11 @@ const listServersByHealth = (
}) })
} }
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => { const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
const checkServerHealth = useCheckServerHealth() const checkServerHealth = useCheckServerHealth()
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>) const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
createEffect(() => { createEffect(() => {
if (!enabled()) {
setStatus(reconcile({}))
return
}
const list = servers() const list = servers()
let dead = false let dead = false
@ -165,12 +162,6 @@ export function StatusPopover() {
const navigate = useNavigate() const navigate = useNavigate()
const [shown, setShown] = createSignal(false) const [shown, setShown] = createSignal(false)
let dialogRun = 0
let dialogDead = false
onCleanup(() => {
dialogDead = true
dialogRun += 1
})
const servers = createMemo(() => { const servers = createMemo(() => {
const current = server.current const current = server.current
const list = server.list const list = server.list
@ -178,7 +169,7 @@ export function StatusPopover() {
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))] return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
}) })
const health = useServerHealth(servers, shown) const health = useServerHealth(servers)
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
const toggleMcp = useMcpToggleMutation() const toggleMcp = useMcpToggleMutation()
const defaultServer = useDefaultServerKey(platform.getDefaultServer) const defaultServer = useDefaultServerKey(platform.getDefaultServer)
@ -309,13 +300,7 @@ export function StatusPopover() {
<Button <Button
variant="secondary" variant="secondary"
class="mt-3 self-start h-8 px-3 py-1.5" class="mt-3 self-start h-8 px-3 py-1.5"
onClick={() => { onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)}
const run = ++dialogRun
void import("./dialog-select-server").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
})
}}
> >
{language.t("status.popover.action.manageServers")} {language.t("status.popover.action.manageServers")}
</Button> </Button>

View File

@ -1,7 +1,4 @@
import { withAlpha } from "@opencode-ai/ui/theme/color" import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
import { useTheme } from "@opencode-ai/ui/theme/context"
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
import type { HexColor } from "@opencode-ai/ui/theme/types"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web" import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js" import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"

View File

@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme/context" import { useTheme } from "@opencode-ai/ui/theme"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"

View File

@ -9,7 +9,17 @@ import type {
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" import {
createContext,
getOwner,
Match,
onCleanup,
onMount,
type ParentProps,
Switch,
untrack,
useContext,
} from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
@ -70,8 +80,6 @@ function createGlobalSync() {
let active = true let active = true
let projectWritten = false let projectWritten = false
let bootedAt = 0
let bootingRoot = false
onCleanup(() => { onCleanup(() => {
active = false active = false
@ -250,11 +258,6 @@ function createGlobalSync() {
const sdk = sdkFor(directory) const sdk = sdkFor(directory)
await bootstrapDirectory({ await bootstrapDirectory({
directory, directory,
global: {
config: globalStore.config,
project: globalStore.project,
provider: globalStore.provider,
},
sdk, sdk,
store: child[0], store: child[0],
setStore: child[1], setStore: child[1],
@ -275,20 +278,15 @@ function createGlobalSync() {
const unsub = globalSDK.event.listen((e) => { const unsub = globalSDK.event.listen((e) => {
const directory = e.name const directory = e.name
const event = e.details const event = e.details
const recent = bootingRoot || Date.now() - bootedAt < 1500
if (directory === "global") { if (directory === "global") {
applyGlobalEvent({ applyGlobalEvent({
event, event,
project: globalStore.project, project: globalStore.project,
refresh: () => { refresh: queue.refresh,
if (recent) return
queue.refresh()
},
setGlobalProject: setProjects, setGlobalProject: setProjects,
}) })
if (event.type === "server.connected" || event.type === "global.disposed") { if (event.type === "server.connected" || event.type === "global.disposed") {
if (recent) return
for (const directory of Object.keys(children.children)) { for (const directory of Object.keys(children.children)) {
queue.push(directory) queue.push(directory)
} }
@ -327,19 +325,17 @@ function createGlobalSync() {
}) })
async function bootstrap() { async function bootstrap() {
bootingRoot = true await bootstrapGlobal({
try { globalSDK: globalSDK.client,
await bootstrapGlobal({ connectErrorTitle: language.t("dialog.server.add.error"),
globalSDK: globalSDK.client, connectErrorDescription: language.t("error.globalSync.connectFailed", {
requestFailedTitle: language.t("common.requestFailed"), url: globalSDK.url,
translate: language.t, }),
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), requestFailedTitle: language.t("common.requestFailed"),
setGlobalStore: setBootStore, translate: language.t,
}) formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
bootedAt = Date.now() setGlobalStore: setBootStore,
} finally { })
bootingRoot = false
}
} }
onMount(() => { onMount(() => {
@ -396,7 +392,13 @@ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
export function GlobalSyncProvider(props: ParentProps) { export function GlobalSyncProvider(props: ParentProps) {
const value = createGlobalSync() const value = createGlobalSync()
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider> return (
<Switch>
<Match when={value.ready}>
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
</Match>
</Switch>
)
} }
export function useGlobalSync() { export function useGlobalSync() {

View File

@ -33,11 +33,27 @@ type GlobalStore = {
export async function bootstrapGlobal(input: { export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient globalSDK: OpencodeClient
connectErrorTitle: string
connectErrorDescription: string
requestFailedTitle: string requestFailedTitle: string
translate: (key: string, vars?: Record<string, string | number>) => string translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string formatMoreCount: (count: number) => string
setGlobalStore: SetStoreFunction<GlobalStore> setGlobalStore: SetStoreFunction<GlobalStore>
}) { }) {
const health = await input.globalSDK.global
.health()
.then((x) => x.data)
.catch(() => undefined)
if (!health?.healthy) {
showToast({
variant: "error",
title: input.connectErrorTitle,
description: input.connectErrorDescription,
})
input.setGlobalStore("ready", true)
return
}
const tasks = [ const tasks = [
retry(() => retry(() =>
input.globalSDK.path.get().then((x) => { input.globalSDK.path.get().then((x) => {
@ -64,6 +80,11 @@ export async function bootstrapGlobal(input: {
input.setGlobalStore("provider", normalizeProviderList(x.data!)) input.setGlobalStore("provider", normalizeProviderList(x.data!))
}), }),
), ),
retry(() =>
input.globalSDK.provider.auth().then((x) => {
input.setGlobalStore("provider_auth", x.data ?? {})
}),
),
] ]
const results = await Promise.allSettled(tasks) const results = await Promise.allSettled(tasks)
@ -90,10 +111,6 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[])
}, {}) }, {})
} }
function projectID(directory: string, projects: Project[]) {
return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id
}
export async function bootstrapDirectory(input: { export async function bootstrapDirectory(input: {
directory: string directory: string
sdk: OpencodeClient sdk: OpencodeClient
@ -102,112 +119,88 @@ export async function bootstrapDirectory(input: {
vcsCache: VcsCache vcsCache: VcsCache
loadSessions: (directory: string) => Promise<void> | void loadSessions: (directory: string) => Promise<void> | void
translate: (key: string, vars?: Record<string, string | number>) => string translate: (key: string, vars?: Record<string, string | number>) => string
global: {
config: Config
project: Project[]
provider: ProviderListResponse
}
}) { }) {
const loading = input.store.status !== "complete" if (input.store.status !== "complete") input.setStore("status", "loading")
const seededProject = projectID(input.directory, input.global.project)
if (seededProject) input.setStore("project", seededProject)
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
input.setStore("provider", input.global.provider)
}
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
input.setStore("config", input.global.config)
}
if (loading) input.setStore("status", "partial")
const results = await Promise.allSettled([ const blockingRequests = {
seededProject project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)),
? Promise.resolve() provider: () =>
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
retry(() =>
input.sdk.provider.list().then((x) => { input.sdk.provider.list().then((x) => {
input.setStore("provider", normalizeProviderList(x.data!)) input.setStore("provider", normalizeProviderList(x.data!))
}), }),
), agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])),
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))), config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)),
retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), }
retry(() =>
input.sdk.path.get().then((x) => {
input.setStore("path", x.data!)
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
}),
),
retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
input.loadSessions(input.directory),
retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
retry(() =>
input.sdk.vcs.get().then((x) => {
const next = x.data ?? input.store.vcs
input.setStore("vcs", next)
if (next?.branch) input.vcsCache.setStore("value", next)
}),
),
retry(() =>
input.sdk.permission.list().then((x) => {
const grouped = groupBySession(
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
)
batch(() => {
for (const sessionID of Object.keys(input.store.permission)) {
if (grouped[sessionID]) continue
input.setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
input.setStore(
"permission",
sessionID,
reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
),
)
}
})
}),
),
retry(() =>
input.sdk.question.list().then((x) => {
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
batch(() => {
for (const sessionID of Object.keys(input.store.question)) {
if (grouped[sessionID]) continue
input.setStore("question", sessionID, [])
}
for (const [sessionID, questions] of Object.entries(grouped)) {
input.setStore(
"question",
sessionID,
reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
),
)
}
})
}),
),
])
const errors = results try {
.filter((item): item is PromiseRejectedResult => item.status === "rejected") await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
.map((item) => item.reason) } catch (err) {
if (errors.length > 0) { console.error("Failed to bootstrap instance", err)
console.error("Failed to bootstrap instance", errors[0])
const project = getFilename(input.directory) const project = getFilename(input.directory)
showToast({ showToast({
variant: "error", variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }), title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(errors[0], input.translate), description: formatServerError(err, input.translate),
}) })
input.setStore("status", "partial")
return return
} }
if (loading) input.setStore("status", "complete") if (input.store.status !== "complete") input.setStore("status", "partial")
Promise.all([
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
input.loadSessions(input.directory),
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
input.sdk.vcs.get().then((x) => {
const next = x.data ?? input.store.vcs
input.setStore("vcs", next)
if (next?.branch) input.vcsCache.setStore("value", next)
}),
input.sdk.permission.list().then((x) => {
const grouped = groupBySession(
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
)
batch(() => {
for (const sessionID of Object.keys(input.store.permission)) {
if (grouped[sessionID]) continue
input.setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
input.setStore(
"permission",
sessionID,
reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
),
)
}
})
}),
input.sdk.question.list().then((x) => {
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
batch(() => {
for (const sessionID of Object.keys(input.store.question)) {
if (grouped[sessionID]) continue
input.setStore("question", sessionID, [])
}
for (const [sessionID, questions] of Object.entries(grouped)) {
input.setStore(
"question",
sessionID,
reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
),
)
}
})
}),
]).then(() => {
input.setStore("status", "complete")
})
} }

View File

@ -1,10 +1,42 @@
import * as i18n from "@solid-primitives/i18n" import * as i18n from "@solid-primitives/i18n"
import { createEffect, createMemo, createResource } from "solid-js" import { createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { dict as en } from "@/i18n/en" import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import { dict as zht } from "@/i18n/zht"
import { dict as ko } from "@/i18n/ko"
import { dict as de } from "@/i18n/de"
import { dict as es } from "@/i18n/es"
import { dict as fr } from "@/i18n/fr"
import { dict as da } from "@/i18n/da"
import { dict as ja } from "@/i18n/ja"
import { dict as pl } from "@/i18n/pl"
import { dict as ru } from "@/i18n/ru"
import { dict as ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br"
import { dict as th } from "@/i18n/th"
import { dict as bs } from "@/i18n/bs"
import { dict as tr } from "@/i18n/tr"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
import { dict as uiKo } from "@opencode-ai/ui/i18n/ko"
import { dict as uiDe } from "@opencode-ai/ui/i18n/de"
import { dict as uiEs } from "@opencode-ai/ui/i18n/es"
import { dict as uiFr } from "@opencode-ai/ui/i18n/fr"
import { dict as uiDa } from "@opencode-ai/ui/i18n/da"
import { dict as uiJa } from "@opencode-ai/ui/i18n/ja"
import { dict as uiPl } from "@opencode-ai/ui/i18n/pl"
import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
import { dict as uiBs } from "@opencode-ai/ui/i18n/bs"
import { dict as uiTr } from "@opencode-ai/ui/i18n/tr"
export type Locale = export type Locale =
| "en" | "en"
@ -27,7 +59,6 @@ export type Locale =
type RawDictionary = typeof en & typeof uiEn type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary> type Dictionary = i18n.Flatten<RawDictionary>
type Source = { dict: Record<string, string> }
function cookie(locale: Locale) { function cookie(locale: Locale) {
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
@ -94,43 +125,24 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = {
} }
const base = i18n.flatten({ ...en, ...uiEn }) const base = i18n.flatten({ ...en, ...uiEn })
const dicts = new Map<Locale, Dictionary>([["en", base]]) const DICT: Record<Locale, Dictionary> = {
en: base,
const merge = (app: Promise<Source>, ui: Promise<Source>) => zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) },
Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) },
ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) },
const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = { de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) },
zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")), es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) },
zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")), fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) },
ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")), da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) },
de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")), ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) },
es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")), pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) },
fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")), ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) },
da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")), ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) },
ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")), no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) },
pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")), br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) },
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")), th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) },
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")), bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) },
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")), tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) },
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")),
tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")),
}
function loadDict(locale: Locale) {
const hit = dicts.get(locale)
if (hit) return Promise.resolve(hit)
if (locale === "en") return Promise.resolve(base)
const load = loaders[locale]
return load().then((next: Dictionary) => {
dicts.set(locale, next)
return next
})
}
export function loadLocaleDict(locale: Locale) {
return loadDict(locale).then(() => undefined)
} }
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
@ -156,6 +168,27 @@ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boole
{ locale: "tr", match: (language) => language.startsWith("tr") }, { locale: "tr", match: (language) => language.startsWith("tr") },
] ]
type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen"
const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = {
zh,
zht,
ko,
de,
es,
fr,
da,
ja,
pl,
ru,
ar,
no,
br,
th,
bs,
tr,
}
void PARITY_CHECK
function detectLocale(): Locale { function detectLocale(): Locale {
if (typeof navigator !== "object") return "en" if (typeof navigator !== "object") return "en"
@ -170,48 +203,27 @@ function detectLocale(): Locale {
return "en" return "en"
} }
export function normalizeLocale(value: string): Locale { function normalizeLocale(value: string): Locale {
return LOCALES.includes(value as Locale) ? (value as Locale) : "en" return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
} }
function readStoredLocale() {
if (typeof localStorage !== "object") return
try {
const raw = localStorage.getItem("opencode.global.dat:language")
if (!raw) return
const next = JSON.parse(raw) as { locale?: string }
if (typeof next?.locale !== "string") return
return normalizeLocale(next.locale)
} catch {
return
}
}
const warm = readStoredLocale() ?? detectLocale()
if (warm !== "en") void loadDict(warm)
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
name: "Language", name: "Language",
init: (props: { locale?: Locale }) => { init: () => {
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
Persist.global("language", ["language.v1"]), Persist.global("language", ["language.v1"]),
createStore({ createStore({
locale: initial, locale: detectLocale() as Locale,
}), }),
) )
const locale = createMemo<Locale>(() => normalizeLocale(store.locale)) const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
console.log("locale", locale())
const intl = createMemo(() => INTL[locale()]) const intl = createMemo(() => INTL[locale()])
const [dict] = createResource(locale, loadDict, { const dict = createMemo<Dictionary>(() => DICT[locale()])
initialValue: dicts.get(initial) ?? base,
})
const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as ( const t = i18n.translator(dict, i18n.resolveTemplate)
key: keyof Dictionary,
params?: Record<string, string | number | boolean>,
) => string
const label = (value: Locale) => t(LABEL_KEY[value]) const label = (value: Locale) => t(LABEL_KEY[value])

View File

@ -12,7 +12,7 @@ import { base64Encode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@opencode-ai/sdk/v2" import { EventSessionError } from "@opencode-ai/sdk/v2"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { playSoundById } from "@/utils/sound" import { playSound, soundSrc } from "@/utils/sound"
type NotificationBase = { type NotificationBase = {
directory?: string directory?: string
@ -234,7 +234,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
if (session.parentID) return if (session.parentID) return
if (settings.sounds.agentEnabled()) { if (settings.sounds.agentEnabled()) {
void playSoundById(settings.sounds.agent()) playSound(soundSrc(settings.sounds.agent()))
} }
append({ append({
@ -263,7 +263,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
if (session?.parentID) return if (session?.parentID) return
if (settings.sounds.errorsEnabled()) { if (settings.sounds.errorsEnabled()) {
void playSoundById(settings.sounds.errors()) playSound(soundSrc(settings.sounds.errors()))
} }
const error = "error" in event.properties ? event.properties.error : undefined const error = "error" in event.properties ? event.properties.error : undefined

View File

@ -104,13 +104,6 @@ function withFallback<T>(read: () => T | undefined, fallback: T) {
return createMemo(() => read() ?? fallback) return createMemo(() => read() ?? fallback)
} }
let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined
function loadFont() {
font ??= import("@opencode-ai/ui/font-loader")
return font
}
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
name: "Settings", name: "Settings",
init: () => { init: () => {
@ -118,7 +111,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
createEffect(() => { createEffect(() => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
void loadFont().then((x) => x.ensureMonoFont(store.appearance?.font))
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font)) document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
}) })

View File

@ -1,18 +1,45 @@
const template = "Terminal {{number}}" import { dict as ar } from "@/i18n/ar"
import { dict as br } from "@/i18n/br"
import { dict as bs } from "@/i18n/bs"
import { dict as da } from "@/i18n/da"
import { dict as de } from "@/i18n/de"
import { dict as en } from "@/i18n/en"
import { dict as es } from "@/i18n/es"
import { dict as fr } from "@/i18n/fr"
import { dict as ja } from "@/i18n/ja"
import { dict as ko } from "@/i18n/ko"
import { dict as no } from "@/i18n/no"
import { dict as pl } from "@/i18n/pl"
import { dict as ru } from "@/i18n/ru"
import { dict as th } from "@/i18n/th"
import { dict as tr } from "@/i18n/tr"
import { dict as zh } from "@/i18n/zh"
import { dict as zht } from "@/i18n/zht"
const numbered = [ const numbered = Array.from(
template, new Set([
"محطة طرفية {{number}}", en["terminal.title.numbered"],
"Терминал {{number}}", ar["terminal.title.numbered"],
"ターミナル {{number}}", br["terminal.title.numbered"],
"터미널 {{number}}", bs["terminal.title.numbered"],
"เทอร์มินัล {{number}}", da["terminal.title.numbered"],
"终端 {{number}}", de["terminal.title.numbered"],
"終端機 {{number}}", es["terminal.title.numbered"],
] fr["terminal.title.numbered"],
ja["terminal.title.numbered"],
ko["terminal.title.numbered"],
no["terminal.title.numbered"],
pl["terminal.title.numbered"],
ru["terminal.title.numbered"],
th["terminal.title.numbered"],
tr["terminal.title.numbered"],
zh["terminal.title.numbered"],
zht["terminal.title.numbered"],
]),
)
export function defaultTitle(number: number) { export function defaultTitle(number: number) {
return template.replace("{{number}}", String(number)) return en["terminal.title.numbered"].replace("{{number}}", String(number))
} }
export function isDefaultTitle(title: string, number: number) { export function isDefaultTitle(title: string, number: number) {

View File

@ -97,15 +97,10 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
throw new Error(getRootNotFoundError()) throw new Error(getRootNotFoundError())
} }
const localUrl = () =>
`http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
const isLocalHost = () => ["localhost", "127.0.0.1", "0.0.0.0"].includes(location.hostname)
const getCurrentUrl = () => { const getCurrentUrl = () => {
if (location.hostname.includes("opencode.ai")) return localUrl() if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV) return localUrl() if (import.meta.env.DEV)
if (isLocalHost()) return localUrl() return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin return location.origin
} }

View File

@ -22,7 +22,7 @@ export function useProviders() {
const providers = () => { const providers = () => {
if (dir()) { if (dir()) {
const [projectStore] = globalSync.child(dir()) const [projectStore] = globalSync.child(dir())
if (projectStore.provider.all.length > 0) return projectStore.provider return projectStore.provider
} }
return globalSync.data.provider return globalSync.data.provider
} }

View File

@ -1,7 +1,6 @@
export { AppBaseProviders, AppInterface } from "./app" export { AppBaseProviders, AppInterface } from "./app"
export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
export { useCommand } from "./context/command" export { useCommand } from "./context/command"
export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language"
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform" export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
export { ServerConnection } from "./context/server" export { ServerConnection } from "./context/server"
export { handleNotificationClick } from "./utils/notification-click" export { handleNotificationClick } from "./utils/notification-click"

View File

@ -2,7 +2,8 @@ import { DataProvider } from "@opencode-ai/ui/context"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/util/encode" import { base64Encode } from "@opencode-ai/util/encode"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { createEffect, createMemo, type ParentProps, Show } from "solid-js" import { createMemo, createResource, type ParentProps, Show } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { LocalProvider } from "@/context/local" import { LocalProvider } from "@/context/local"
import { SDKProvider } from "@/context/sdk" import { SDKProvider } from "@/context/sdk"
@ -10,18 +11,10 @@ import { SyncProvider, useSync } from "@/context/sync"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const sync = useSync() const sync = useSync()
const slug = createMemo(() => base64Encode(props.directory)) const slug = createMemo(() => base64Encode(props.directory))
createEffect(() => {
const next = sync.data.path.directory
if (!next || next === props.directory) return
const path = location.pathname.slice(slug().length + 1)
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
})
return ( return (
<DataProvider <DataProvider
data={sync.data} data={sync.data}
@ -36,31 +29,50 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
export default function Layout(props: ParentProps) { export default function Layout(props: ParentProps) {
const params = useParams() const params = useParams()
const location = useLocation()
const language = useLanguage() const language = useLanguage()
const globalSDK = useGlobalSDK()
const navigate = useNavigate() const navigate = useNavigate()
let invalid = "" let invalid = ""
const resolved = createMemo(() => { const [resolved] = createResource(
if (!params.dir) return "" () => {
return decode64(params.dir) ?? "" if (params.dir) return [location.pathname, params.dir] as const
}) },
async ([pathname, b64Dir]) => {
const directory = decode64(b64Dir)
createEffect(() => { if (!directory) {
const dir = params.dir if (invalid === params.dir) return
if (!dir) return invalid = b64Dir
if (resolved()) { showToast({
invalid = "" variant: "error",
return title: language.t("common.requestFailed"),
} description: language.t("directory.error.invalidUrl"),
if (invalid === dir) return })
invalid = dir navigate("/", { replace: true })
showToast({ return
variant: "error", }
title: language.t("common.requestFailed"),
description: language.t("directory.error.invalidUrl"), return await globalSDK
}) .createClient({
navigate("/", { replace: true }) directory,
}) throwOnError: true,
})
.path.get()
.then((x) => {
const next = x.data?.directory ?? directory
invalid = ""
if (next === directory) return next
const path = pathname.slice(b64Dir.length + 1)
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
})
.catch(() => {
invalid = ""
return directory
})
},
)
return ( return (
<Show when={resolved()} keyed> <Show when={resolved()} keyed>

View File

@ -49,16 +49,21 @@ import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry" import { retry } from "@opencode-ai/util/retry"
import { playSoundById } from "@/utils/sound" import { playSound, soundSrc } from "@/utils/sound"
import { createAim } from "@/utils/aim" import { createAim } from "@/utils/aim"
import { setNavigate } from "@/utils/notification-click" import { setNavigate } from "@/utils/notification-click"
import { Worktree as WorktreeState } from "@/utils/worktree" import { Worktree as WorktreeState } from "@/utils/worktree"
import { setSessionHandoff } from "@/pages/session/handoff" import { setSessionHandoff } from "@/pages/session/handoff"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { DialogSettings } from "@/components/dialog-settings"
import { useCommand, type CommandOption } from "@/context/command" import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd" import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { DebugBar } from "@/components/debug-bar" import { DebugBar } from "@/components/debug-bar"
import { Titlebar } from "@/components/titlebar" import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
@ -105,8 +110,6 @@ export default function Layout(props: ParentProps) {
const pageReady = createMemo(() => ready()) const pageReady = createMemo(() => ready())
let scrollContainerRef: HTMLDivElement | undefined let scrollContainerRef: HTMLDivElement | undefined
let dialogRun = 0
let dialogDead = false
const params = useParams() const params = useParams()
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
@ -136,7 +139,7 @@ export default function Layout(props: ParentProps) {
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir, dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
} }
}) })
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const)) const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = { const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
system: "theme.scheme.system", system: "theme.scheme.system",
@ -198,8 +201,6 @@ export default function Layout(props: ParentProps) {
}) })
onCleanup(() => { onCleanup(() => {
dialogDead = true
dialogRun += 1
if (navLeave.current !== undefined) clearTimeout(navLeave.current) if (navLeave.current !== undefined) clearTimeout(navLeave.current)
clearTimeout(sortNowTimeout) clearTimeout(sortNowTimeout)
if (sortNowInterval) clearInterval(sortNowInterval) if (sortNowInterval) clearInterval(sortNowInterval)
@ -335,9 +336,10 @@ export default function Layout(props: ParentProps) {
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
const nextThemeId = ids[nextIndex] const nextThemeId = ids[nextIndex]
theme.setTheme(nextThemeId) theme.setTheme(nextThemeId)
const nextTheme = theme.themes()[nextThemeId]
showToast({ showToast({
title: language.t("toast.theme.title"), title: language.t("toast.theme.title"),
description: theme.name(nextThemeId), description: nextTheme?.name ?? nextThemeId,
}) })
} }
@ -492,7 +494,7 @@ export default function Layout(props: ParentProps) {
if (e.details.type === "permission.asked") { if (e.details.type === "permission.asked") {
if (settings.sounds.permissionsEnabled()) { if (settings.sounds.permissionsEnabled()) {
void playSoundById(settings.sounds.permissions()) playSound(soundSrc(settings.sounds.permissions()))
} }
if (settings.notifications.permissions()) { if (settings.notifications.permissions()) {
void platform.notify(title, description, href) void platform.notify(title, description, href)
@ -1150,10 +1152,10 @@ export default function Layout(props: ParentProps) {
}, },
] ]
for (const [id] of availableThemeEntries()) { for (const [id, definition] of availableThemeEntries()) {
commands.push({ commands.push({
id: `theme.set.${id}`, id: `theme.set.${id}`,
title: language.t("command.theme.set", { theme: theme.name(id) }), title: language.t("command.theme.set", { theme: definition.name ?? id }),
category: language.t("command.category.theme"), category: language.t("command.category.theme"),
onSelect: () => theme.commitPreview(), onSelect: () => theme.commitPreview(),
onHighlight: () => { onHighlight: () => {
@ -1204,27 +1206,15 @@ export default function Layout(props: ParentProps) {
}) })
function connectProvider() { function connectProvider() {
const run = ++dialogRun dialog.show(() => <DialogSelectProvider />)
void import("@/components/dialog-select-provider").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectProvider />)
})
} }
function openServer() { function openServer() {
const run = ++dialogRun dialog.show(() => <DialogSelectServer />)
void import("@/components/dialog-select-server").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectServer />)
})
} }
function openSettings() { function openSettings() {
const run = ++dialogRun dialog.show(() => <DialogSettings />)
void import("@/components/dialog-settings").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSettings />)
})
} }
function projectRoot(directory: string) { function projectRoot(directory: string) {
@ -1451,13 +1441,7 @@ export default function Layout(props: ParentProps) {
layout.sidebar.toggleWorkspaces(project.worktree) layout.sidebar.toggleWorkspaces(project.worktree)
} }
const showEditProjectDialog = (project: LocalProject) => { const showEditProjectDialog = (project: LocalProject) => dialog.show(() => <DialogEditProject project={project} />)
const run = ++dialogRun
void import("@/components/dialog-edit-project").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogEditProject project={project} />)
})
}
async function chooseProject() { async function chooseProject() {
function resolve(result: string | string[] | null) { function resolve(result: string | string[] | null) {
@ -1478,14 +1462,10 @@ export default function Layout(props: ParentProps) {
}) })
resolve(result) resolve(result)
} else { } else {
const run = ++dialogRun dialog.show(
void import("@/components/dialog-select-directory").then((x) => { () => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
if (dialogDead || dialogRun !== run) return () => resolve(null),
dialog.show( )
() => <x.DialogSelectDirectory multiple={true} onSelect={resolve} />,
() => resolve(null),
)
})
} }
} }

View File

@ -14,15 +14,6 @@ interface CheckServerHealthOptions {
const defaultTimeoutMs = 3000 const defaultTimeoutMs = 3000
const defaultRetryCount = 2 const defaultRetryCount = 2
const defaultRetryDelayMs = 100 const defaultRetryDelayMs = 100
const cacheMs = 750
const healthCache = new Map<
string,
{ at: number; done: boolean; fetch: typeof globalThis.fetch; promise: Promise<ServerHealth> }
>()
function cacheKey(server: ServerConnection.HttpBase) {
return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}`
}
function timeoutSignal(timeoutMs: number) { function timeoutSignal(timeoutMs: number) {
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
@ -96,18 +87,5 @@ export function useCheckServerHealth() {
const platform = usePlatform() const platform = usePlatform()
const fetcher = platform.fetch ?? globalThis.fetch const fetcher = platform.fetch ?? globalThis.fetch
return (http: ServerConnection.HttpBase) => { return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher)
const key = cacheKey(http)
const hit = healthCache.get(key)
const now = Date.now()
if (hit && hit.fetch === fetcher && (!hit.done || now - hit.at < cacheMs)) return hit.promise
const promise = checkServerHealth(http, fetcher).finally(() => {
const next = healthCache.get(key)
if (!next || next.promise !== promise) return
next.done = true
next.at = Date.now()
})
healthCache.set(key, { at: now, done: false, fetch: fetcher, promise })
return promise
}
} }

View File

@ -1,89 +1,106 @@
let files: Record<string, () => Promise<string>> | undefined import alert01 from "@opencode-ai/ui/audio/alert-01.aac"
let loads: Record<SoundID, () => Promise<string>> | undefined import alert02 from "@opencode-ai/ui/audio/alert-02.aac"
import alert03 from "@opencode-ai/ui/audio/alert-03.aac"
function getFiles() { import alert04 from "@opencode-ai/ui/audio/alert-04.aac"
if (files) return files import alert05 from "@opencode-ai/ui/audio/alert-05.aac"
files = import.meta.glob("../../../ui/src/assets/audio/*.aac", { import: "default" }) as Record< import alert06 from "@opencode-ai/ui/audio/alert-06.aac"
string, import alert07 from "@opencode-ai/ui/audio/alert-07.aac"
() => Promise<string> import alert08 from "@opencode-ai/ui/audio/alert-08.aac"
> import alert09 from "@opencode-ai/ui/audio/alert-09.aac"
return files import alert10 from "@opencode-ai/ui/audio/alert-10.aac"
} import bipbop01 from "@opencode-ai/ui/audio/bip-bop-01.aac"
import bipbop02 from "@opencode-ai/ui/audio/bip-bop-02.aac"
import bipbop03 from "@opencode-ai/ui/audio/bip-bop-03.aac"
import bipbop04 from "@opencode-ai/ui/audio/bip-bop-04.aac"
import bipbop05 from "@opencode-ai/ui/audio/bip-bop-05.aac"
import bipbop06 from "@opencode-ai/ui/audio/bip-bop-06.aac"
import bipbop07 from "@opencode-ai/ui/audio/bip-bop-07.aac"
import bipbop08 from "@opencode-ai/ui/audio/bip-bop-08.aac"
import bipbop09 from "@opencode-ai/ui/audio/bip-bop-09.aac"
import bipbop10 from "@opencode-ai/ui/audio/bip-bop-10.aac"
import nope01 from "@opencode-ai/ui/audio/nope-01.aac"
import nope02 from "@opencode-ai/ui/audio/nope-02.aac"
import nope03 from "@opencode-ai/ui/audio/nope-03.aac"
import nope04 from "@opencode-ai/ui/audio/nope-04.aac"
import nope05 from "@opencode-ai/ui/audio/nope-05.aac"
import nope06 from "@opencode-ai/ui/audio/nope-06.aac"
import nope07 from "@opencode-ai/ui/audio/nope-07.aac"
import nope08 from "@opencode-ai/ui/audio/nope-08.aac"
import nope09 from "@opencode-ai/ui/audio/nope-09.aac"
import nope10 from "@opencode-ai/ui/audio/nope-10.aac"
import nope11 from "@opencode-ai/ui/audio/nope-11.aac"
import nope12 from "@opencode-ai/ui/audio/nope-12.aac"
import staplebops01 from "@opencode-ai/ui/audio/staplebops-01.aac"
import staplebops02 from "@opencode-ai/ui/audio/staplebops-02.aac"
import staplebops03 from "@opencode-ai/ui/audio/staplebops-03.aac"
import staplebops04 from "@opencode-ai/ui/audio/staplebops-04.aac"
import staplebops05 from "@opencode-ai/ui/audio/staplebops-05.aac"
import staplebops06 from "@opencode-ai/ui/audio/staplebops-06.aac"
import staplebops07 from "@opencode-ai/ui/audio/staplebops-07.aac"
import yup01 from "@opencode-ai/ui/audio/yup-01.aac"
import yup02 from "@opencode-ai/ui/audio/yup-02.aac"
import yup03 from "@opencode-ai/ui/audio/yup-03.aac"
import yup04 from "@opencode-ai/ui/audio/yup-04.aac"
import yup05 from "@opencode-ai/ui/audio/yup-05.aac"
import yup06 from "@opencode-ai/ui/audio/yup-06.aac"
export const SOUND_OPTIONS = [ export const SOUND_OPTIONS = [
{ id: "alert-01", label: "sound.option.alert01" }, { id: "alert-01", label: "sound.option.alert01", src: alert01 },
{ id: "alert-02", label: "sound.option.alert02" }, { id: "alert-02", label: "sound.option.alert02", src: alert02 },
{ id: "alert-03", label: "sound.option.alert03" }, { id: "alert-03", label: "sound.option.alert03", src: alert03 },
{ id: "alert-04", label: "sound.option.alert04" }, { id: "alert-04", label: "sound.option.alert04", src: alert04 },
{ id: "alert-05", label: "sound.option.alert05" }, { id: "alert-05", label: "sound.option.alert05", src: alert05 },
{ id: "alert-06", label: "sound.option.alert06" }, { id: "alert-06", label: "sound.option.alert06", src: alert06 },
{ id: "alert-07", label: "sound.option.alert07" }, { id: "alert-07", label: "sound.option.alert07", src: alert07 },
{ id: "alert-08", label: "sound.option.alert08" }, { id: "alert-08", label: "sound.option.alert08", src: alert08 },
{ id: "alert-09", label: "sound.option.alert09" }, { id: "alert-09", label: "sound.option.alert09", src: alert09 },
{ id: "alert-10", label: "sound.option.alert10" }, { id: "alert-10", label: "sound.option.alert10", src: alert10 },
{ id: "bip-bop-01", label: "sound.option.bipbop01" }, { id: "bip-bop-01", label: "sound.option.bipbop01", src: bipbop01 },
{ id: "bip-bop-02", label: "sound.option.bipbop02" }, { id: "bip-bop-02", label: "sound.option.bipbop02", src: bipbop02 },
{ id: "bip-bop-03", label: "sound.option.bipbop03" }, { id: "bip-bop-03", label: "sound.option.bipbop03", src: bipbop03 },
{ id: "bip-bop-04", label: "sound.option.bipbop04" }, { id: "bip-bop-04", label: "sound.option.bipbop04", src: bipbop04 },
{ id: "bip-bop-05", label: "sound.option.bipbop05" }, { id: "bip-bop-05", label: "sound.option.bipbop05", src: bipbop05 },
{ id: "bip-bop-06", label: "sound.option.bipbop06" }, { id: "bip-bop-06", label: "sound.option.bipbop06", src: bipbop06 },
{ id: "bip-bop-07", label: "sound.option.bipbop07" }, { id: "bip-bop-07", label: "sound.option.bipbop07", src: bipbop07 },
{ id: "bip-bop-08", label: "sound.option.bipbop08" }, { id: "bip-bop-08", label: "sound.option.bipbop08", src: bipbop08 },
{ id: "bip-bop-09", label: "sound.option.bipbop09" }, { id: "bip-bop-09", label: "sound.option.bipbop09", src: bipbop09 },
{ id: "bip-bop-10", label: "sound.option.bipbop10" }, { id: "bip-bop-10", label: "sound.option.bipbop10", src: bipbop10 },
{ id: "staplebops-01", label: "sound.option.staplebops01" }, { id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 },
{ id: "staplebops-02", label: "sound.option.staplebops02" }, { id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 },
{ id: "staplebops-03", label: "sound.option.staplebops03" }, { id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 },
{ id: "staplebops-04", label: "sound.option.staplebops04" }, { id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 },
{ id: "staplebops-05", label: "sound.option.staplebops05" }, { id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 },
{ id: "staplebops-06", label: "sound.option.staplebops06" }, { id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 },
{ id: "staplebops-07", label: "sound.option.staplebops07" }, { id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 },
{ id: "nope-01", label: "sound.option.nope01" }, { id: "nope-01", label: "sound.option.nope01", src: nope01 },
{ id: "nope-02", label: "sound.option.nope02" }, { id: "nope-02", label: "sound.option.nope02", src: nope02 },
{ id: "nope-03", label: "sound.option.nope03" }, { id: "nope-03", label: "sound.option.nope03", src: nope03 },
{ id: "nope-04", label: "sound.option.nope04" }, { id: "nope-04", label: "sound.option.nope04", src: nope04 },
{ id: "nope-05", label: "sound.option.nope05" }, { id: "nope-05", label: "sound.option.nope05", src: nope05 },
{ id: "nope-06", label: "sound.option.nope06" }, { id: "nope-06", label: "sound.option.nope06", src: nope06 },
{ id: "nope-07", label: "sound.option.nope07" }, { id: "nope-07", label: "sound.option.nope07", src: nope07 },
{ id: "nope-08", label: "sound.option.nope08" }, { id: "nope-08", label: "sound.option.nope08", src: nope08 },
{ id: "nope-09", label: "sound.option.nope09" }, { id: "nope-09", label: "sound.option.nope09", src: nope09 },
{ id: "nope-10", label: "sound.option.nope10" }, { id: "nope-10", label: "sound.option.nope10", src: nope10 },
{ id: "nope-11", label: "sound.option.nope11" }, { id: "nope-11", label: "sound.option.nope11", src: nope11 },
{ id: "nope-12", label: "sound.option.nope12" }, { id: "nope-12", label: "sound.option.nope12", src: nope12 },
{ id: "yup-01", label: "sound.option.yup01" }, { id: "yup-01", label: "sound.option.yup01", src: yup01 },
{ id: "yup-02", label: "sound.option.yup02" }, { id: "yup-02", label: "sound.option.yup02", src: yup02 },
{ id: "yup-03", label: "sound.option.yup03" }, { id: "yup-03", label: "sound.option.yup03", src: yup03 },
{ id: "yup-04", label: "sound.option.yup04" }, { id: "yup-04", label: "sound.option.yup04", src: yup04 },
{ id: "yup-05", label: "sound.option.yup05" }, { id: "yup-05", label: "sound.option.yup05", src: yup05 },
{ id: "yup-06", label: "sound.option.yup06" }, { id: "yup-06", label: "sound.option.yup06", src: yup06 },
] as const ] as const
export type SoundOption = (typeof SOUND_OPTIONS)[number] export type SoundOption = (typeof SOUND_OPTIONS)[number]
export type SoundID = SoundOption["id"] export type SoundID = SoundOption["id"]
function getLoads() { const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record<SoundID, string>
if (loads) return loads
loads = Object.fromEntries(
Object.entries(getFiles()).flatMap(([path, load]) => {
const file = path.split("/").at(-1)
if (!file) return []
return [[file.replace(/\.aac$/, ""), load] as const]
}),
) as Record<SoundID, () => Promise<string>>
return loads
}
const cache = new Map<SoundID, Promise<string | undefined>>()
export function soundSrc(id: string | undefined) { export function soundSrc(id: string | undefined) {
const loads = getLoads() if (!id) return
if (!id || !(id in loads)) return Promise.resolve(undefined) if (!(id in soundById)) return
const key = id as SoundID return soundById[id as SoundID]
const hit = cache.get(key)
if (hit) return hit
const next = loads[key]().catch(() => undefined)
cache.set(key, next)
return next
} }
export function playSound(src: string | undefined) { export function playSound(src: string | undefined) {
@ -91,12 +108,10 @@ export function playSound(src: string | undefined) {
if (!src) return if (!src) return
const audio = new Audio(src) const audio = new Audio(src)
audio.play().catch(() => undefined) audio.play().catch(() => undefined)
// Return a cleanup function to pause the sound.
return () => { return () => {
audio.pause() audio.pause()
audio.currentTime = 0 audio.currentTime = 0
} }
} }
export function playSoundById(id: string | undefined) {
return soundSrc(id).then((src) => playSound(src))
}

View File

@ -6,9 +6,6 @@ import {
AppBaseProviders, AppBaseProviders,
AppInterface, AppInterface,
handleNotificationClick, handleNotificationClick,
loadLocaleDict,
normalizeLocale,
type Locale,
type Platform, type Platform,
PlatformProvider, PlatformProvider,
ServerConnection, ServerConnection,
@ -249,17 +246,6 @@ listenForDeepLinks()
render(() => { render(() => {
const platform = createPlatform() const platform = createPlatform()
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
const raw = current ?? legacy
if (!raw) return
const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1]
if (!locale) return
const next = normalizeLocale(locale)
if (next !== "en") await loadLocaleDict(next)
return next satisfies Locale
}
const [windowCount] = createResource(() => window.api.getWindowCount()) const [windowCount] = createResource(() => window.api.getWindowCount())
@ -271,7 +257,6 @@ render(() => {
if (url) return ServerConnection.key({ type: "http", http: { url } }) if (url) return ServerConnection.key({ type: "http", http: { url } })
}), }),
) )
const [locale] = createResource(loadLocale)
const servers = () => { const servers = () => {
const data = sidecar() const data = sidecar()
@ -324,14 +309,15 @@ render(() => {
return ( return (
<PlatformProvider value={platform}> <PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}> <AppBaseProviders>
<Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading}> <Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading}>
{(_) => { {(_) => {
return ( return (
<AppInterface <AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")} defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()} servers={servers()}
router={MemoryRouter} router={MemoryRouter}
disableHealthCheck={(windowCount() ?? 0) > 1}
> >
<Inner /> <Inner />
</AppInterface> </AppInterface>

View File

@ -6,9 +6,6 @@ import {
AppBaseProviders, AppBaseProviders,
AppInterface, AppInterface,
handleNotificationClick, handleNotificationClick,
loadLocaleDict,
normalizeLocale,
type Locale,
type Platform, type Platform,
PlatformProvider, PlatformProvider,
ServerConnection, ServerConnection,
@ -417,17 +414,6 @@ void listenForDeepLinks()
render(() => { render(() => {
const platform = createPlatform() const platform = createPlatform()
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
const raw = current ?? legacy
if (!raw) return
const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1]
if (!locale) return
const next = normalizeLocale(locale)
if (next !== "en") await loadLocaleDict(next)
return next satisfies Locale
}
// Fetch sidecar credentials from Rust (available immediately, before health check) // Fetch sidecar credentials from Rust (available immediately, before health check)
const [sidecar] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any)) const [sidecar] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any))
@ -437,7 +423,6 @@ render(() => {
if (url) return ServerConnection.key({ type: "http", http: { url } }) if (url) return ServerConnection.key({ type: "http", http: { url } })
}), }),
) )
const [locale] = createResource(loadLocale)
// Build the sidecar server connection once credentials arrive // Build the sidecar server connection once credentials arrive
const servers = () => { const servers = () => {
@ -480,8 +465,8 @@ render(() => {
return ( return (
<PlatformProvider value={platform}> <PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}> <AppBaseProviders>
<Show when={!defaultServer.loading && !sidecar.loading && !locale.loading}> <Show when={!defaultServer.loading && !sidecar.loading}>
{(_) => { {(_) => {
return ( return (
<AppInterface <AppInterface

View File

@ -12,7 +12,6 @@
"./hooks": "./src/hooks/index.ts", "./hooks": "./src/hooks/index.ts",
"./context": "./src/context/index.ts", "./context": "./src/context/index.ts",
"./context/*": "./src/context/*.tsx", "./context/*": "./src/context/*.tsx",
"./font-loader": "./src/font-loader.ts",
"./styles": "./src/styles/index.css", "./styles": "./src/styles/index.css",
"./styles/tailwind": "./src/styles/tailwind/index.css", "./styles/tailwind": "./src/styles/tailwind/index.css",
"./theme": "./src/theme/index.ts", "./theme": "./src/theme/index.ts",

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<path d="M37.9998 23.021C33.7998 25.2889 29.5698 27.3649 24.8614 28.3069C23.8114 28.5154 22.6474 28.5154 21.5809 28.3714C20.5639 28.2439 20.0554 27.3484 20.4169 26.4064C20.7619 25.5289 21.2209 24.635 21.8119 23.9C23.0899 22.3025 24.5329 20.849 25.8289 19.268C26.6203 18.2991 27.3335 17.2689 27.9618 16.187C28.4208 15.4205 28.2078 14.4935 27.4038 14.111C26.0584 13.4556 24.6154 12.9936 23.1889 12.4986C23.0239 12.4341 22.7779 12.6096 22.4509 12.7221C22.8604 13.0881 23.1559 13.3596 23.5654 13.727C19.3339 14.447 15.3305 15.467 11.4455 16.874C11.4275 16.9535 11.396 17.0165 11.411 17.0495C11.9855 17.927 11.723 18.5975 10.886 19.1405C10.5611 19.3531 10.2732 19.6176 10.034 19.9235C12.593 20.6735 14.873 20.243 17.0539 18.821C16.9234 18.6305 16.7914 18.455 16.6609 18.263C17.4799 18.407 17.9719 18.854 18.0379 19.556C18.0544 19.7165 17.9569 19.8755 17.9074 20.036C17.7919 19.907 17.6449 19.781 17.5474 19.6355C17.4799 19.5395 17.4634 19.4285 17.4154 19.268C14.8235 20.993 12.035 21.425 8.96751 20.531C8.96751 21.137 8.93451 21.6485 8.98401 22.1435C9.01701 22.574 8.83701 22.766 8.44401 22.9895C7.55752 23.5325 6.63803 24.092 5.90003 24.8105C5.01504 25.6879 5.34354 26.7589 6.54053 27.2059C7.90102 27.7159 9.329 27.7309 10.7555 27.5569C12.4445 27.3484 14.1005 27.0769 15.9394 26.8219C13.79 27.8269 11.6735 28.5319 9.4445 28.8169C7.88452 29.0269 6.32753 29.1379 4.78554 28.6909C2.57156 28.0684 1.58607 26.4394 2.16057 24.251C2.70206 22.2065 4.01455 20.5775 5.42454 19.076C10.133 14.078 16.0864 11.5401 22.9744 11.0286C24.5824 10.9176 26.2069 11.1246 27.7143 11.7951C29.8308 12.7536 30.7173 14.78 29.6838 16.826C29.0118 18.1835 28.0758 19.4285 27.1413 20.6585C26.2234 21.872 25.1899 22.9895 24.2224 24.155C23.9434 24.506 23.6809 24.875 23.4679 25.2724C23.0569 26.0224 23.3359 26.5174 24.2059 26.4394C26.0254 26.2624 27.8808 26.1199 29.6358 25.6729C32.2098 25.0174 34.7193 24.092 37.2618 23.2775C37.5243 23.213 37.7703 23.117 37.9998 23.0225V23.021Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<path d="M37.9998 23.021C33.7998 25.2889 29.5698 27.3649 24.8614 28.3069C23.8114 28.5154 22.6474 28.5154 21.5809 28.3714C20.5639 28.2439 20.0554 27.3484 20.4169 26.4064C20.7619 25.5289 21.2209 24.635 21.8119 23.9C23.0899 22.3025 24.5329 20.849 25.8289 19.268C26.6203 18.2991 27.3335 17.2689 27.9618 16.187C28.4208 15.4205 28.2078 14.4935 27.4038 14.111C26.0584 13.4556 24.6154 12.9936 23.1889 12.4986C23.0239 12.4341 22.7779 12.6096 22.4509 12.7221C22.8604 13.0881 23.1559 13.3596 23.5654 13.727C19.3339 14.447 15.3305 15.467 11.4455 16.874C11.4275 16.9535 11.396 17.0165 11.411 17.0495C11.9855 17.927 11.723 18.5975 10.886 19.1405C10.5611 19.3531 10.2732 19.6176 10.034 19.9235C12.593 20.6735 14.873 20.243 17.0539 18.821C16.9234 18.6305 16.7914 18.455 16.6609 18.263C17.4799 18.407 17.9719 18.854 18.0379 19.556C18.0544 19.7165 17.9569 19.8755 17.9074 20.036C17.7919 19.907 17.6449 19.781 17.5474 19.6355C17.4799 19.5395 17.4634 19.4285 17.4154 19.268C14.8235 20.993 12.035 21.425 8.96751 20.531C8.96751 21.137 8.93451 21.6485 8.98401 22.1435C9.01701 22.574 8.83701 22.766 8.44401 22.9895C7.55752 23.5325 6.63803 24.092 5.90003 24.8105C5.01504 25.6879 5.34354 26.7589 6.54053 27.2059C7.90102 27.7159 9.329 27.7309 10.7555 27.5569C12.4445 27.3484 14.1005 27.0769 15.9394 26.8219C13.79 27.8269 11.6735 28.5319 9.4445 28.8169C7.88452 29.0269 6.32753 29.1379 4.78554 28.6909C2.57156 28.0684 1.58607 26.4394 2.16057 24.251C2.70206 22.2065 4.01455 20.5775 5.42454 19.076C10.133 14.078 16.0864 11.5401 22.9744 11.0286C24.5824 10.9176 26.2069 11.1246 27.7143 11.7951C29.8308 12.7536 30.7173 14.78 29.6838 16.826C29.0118 18.1835 28.0758 19.4285 27.1413 20.6585C26.2234 21.872 25.1899 22.9895 24.2224 24.155C23.9434 24.506 23.6809 24.875 23.4679 25.2724C23.0569 26.0224 23.3359 26.5174 24.2059 26.4394C26.0254 26.2624 27.8808 26.1199 29.6358 25.6729C32.2098 25.0174 34.7193 24.092 37.2618 23.2775C37.5243 23.213 37.7703 23.117 37.9998 23.0225V23.021Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,24 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
shape-rendering="geometricPrecision"
d="M9.8132 15.9038L9 18.75L8.1868 15.9038C7.75968 14.4089 6.59112 13.2403 5.09619 12.8132L2.25 12L5.09619 11.1868C6.59113 10.7597 7.75968 9.59112 8.1868 8.09619L9 5.25L9.8132 8.09619C10.2403 9.59113 11.4089 10.7597 12.9038 11.1868L15.75 12L12.9038 12.8132C11.4089 13.2403 10.2403 14.4089 9.8132 15.9038Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M18.2589 8.71454L18 9.75L17.7411 8.71454C17.4388 7.50533 16.4947 6.56117 15.2855 6.25887L14.25 6L15.2855 5.74113C16.4947 5.43883 17.4388 4.49467 17.7411 3.28546L18 2.25L18.2589 3.28546C18.5612 4.49467 19.5053 5.43883 20.7145 5.74113L21.75 6L20.7145 6.25887C19.5053 6.56117 18.5612 7.50533 18.2589 8.71454Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.8942 20.5673L16.5 21.75L16.1058 20.5673C15.8818 19.8954 15.3546 19.3682 14.6827 19.1442L13.5 18.75L14.6827 18.3558C15.3546 18.1318 15.8818 17.6046 16.1058 16.9327L16.5 15.75L16.8942 16.9327C17.1182 17.6046 17.6454 18.1318 18.3173 18.3558L19.5 18.75L18.3173 19.1442C17.6454 19.3682 17.1182 19.8954 16.8942 20.5673Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-terminal h-5 w-5 text-primary"><path d="m4 17 6-6-6-6M12 19h8"/></svg>

Before

Width:  |  Height:  |  Size: 253 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400">
<path fill="currentColor" d="M394.3,154.15v19.4c6.2-3.8,12.7-6.8,19.7-9.1,8.8-2.7,17.8-4.1,27.1-4.1h3.3v42.4h-3.3c-12.9,0-24,4.6-33.1,13.7-9.1,9.1-13.7,20.2-13.7,33.1v80.4h-42.4V154.15h42.4Z"/>
<path fill="currentColor" d="M464.5,264.65c-.3-2.9-.5-5.7-.5-8.4v-101.7h41.3v102.2c0,1.1,.1,2.3,.3,3.4,.1,1,.3,2.2,.5,3.4,1,4.5,2.9,8.5,5.7,12,2.6,3.5,5.9,6.5,9.9,8.7,3.5,2.1,7.4,3.4,11.5,3.9,4,.6,8,.4,12-.5,2.7-.7,5.3-1.7,7.7-2.9,2.4-1.3,4.6-2.8,6.5-4.6,2.4-2.1,4.4-4.5,6-7.2,1.6-2.6,2.8-5.4,3.6-8.2v-110.2h42.8v175.1h-42.9v-6.7c-1.5,.8-3,1.5-4.6,2.1-3.2,1.3-6.5,2.2-9.8,2.9-7.8,1.8-15.6,2.4-23.5,1.7-7.9-.7-15.5-2.6-22.8-5.8-10.7-4.7-19.8-11.5-27.3-20.4-7.4-8.9-12.5-19-15.1-30.4-.5-2.8-1-5.6-1.3-8.4Z"/>
<path fill="currentColor" d="M228,149.85v33.9c-5.5-3.3-11.3-6-17.5-8.1-7.8-2.4-15.8-3.6-24.2-3.6-22,0-40.6,7.7-56.1,23.2-15.5,15.5-23.3,34.3-23.3,56.2s7.8,40.6,23.3,56.1c15.4,15.5,34.1,23.3,56.1,23.3,8.3,0,16.4-1.3,24.2-3.8,6.2-1.9,12-4.6,17.5-8.1v10.6h37.7V119.95l-37.7,29.9Zm-.5,91.2l-3.7,30.2-17.9,13.5-19.6,14.7-19.8-14.8-18-13.4-3.6-30.2-1.8-16.5,21.9-9.5,21.2-9.2,25.7,11.2,17.6,7.5-2,16.5Z"/>
<path fill="currentColor" d="M792,329.95h-45v-107.2c0-11.9-4.3-31.9-33.4-31.9-12.3,0-32.8,4.1-32.8,31.3v107.8h-45v-107.8c0-23.3,8.2-42.9,23.6-56.9,13.9-12.5,33.1-19.4,54.2-19.4,46.1,0,78.4,31.6,78.4,76.9v107.2Z"/>
<rect fill="currentColor" x="293.59" y="293.67" width="29.3" height="29.3" transform="translate(-127.73 308.26) rotate(-45)"/>
<polygon fill="currentColor" points="229.7 89.95 226.4 137.75 264.7 108.85 270.4 62.55 229.7 89.95"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.2642 2.8689L12.042 8.0961M17.2642 2.8689V8.0961H12.042M17.2642 2.8689V4.30027M12.042 8.0961L6.81809 2.8689V8.0961H12.042ZM12.042 8.0961L17.2642 13.3225V20.8159L12.042 15.5887M12.042 8.0961V15.5887M12.042 8.0961L6.81892 13.3225M12.0296 2.1V21.9M12.042 15.5887L6.81892 20.8159V13.3225M6.81892 13.3225L6.81809 15.559H4.57739V8.09527H12.0412L6.81892 13.3225ZM11.9859 8.09527L17.2081 13.3225V15.559H19.4497V8.09527H11.9859Z" stroke="currentColor" stroke-width="0.825" stroke-miterlimit="10"/>
</svg>

Before

Width:  |  Height:  |  Size: 604 B

View File

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20.0483 17.1416C19.6945 17.4914 18.987 18.0161 17.7488 18.0161C17.2182 18.0161 16.5991 18.0161 16.3338 18.0161C15.98 18.0161 13.3268 18.0161 10.143 18.0161C12.4424 15.8298 14.3881 13.9932 14.565 13.8183C14.7419 13.6434 15.1841 13.2061 15.6263 12.8563C16.5107 12.0692 17.2182 11.9817 17.8373 11.9817C18.7217 11.9817 19.4292 12.3316 20.0483 12.8563C21.2864 13.9932 21.2864 16.0047 20.0483 17.1416ZM21.5518 11.457C20.6674 10.495 19.3408 9.88281 17.9257 9.88281C16.6875 9.88281 15.6263 10.3201 14.6534 11.0197C14.2997 11.3695 13.769 11.7194 13.3268 12.2441C12.9731 12.5939 5.36719 19.9401 5.36719 19.9401C5.80939 20.0276 6.34003 20.0276 6.78223 20.0276C7.22443 20.0276 16.0685 20.0276 16.4222 20.0276C17.1298 20.0276 17.6604 20.0276 18.191 19.9401C19.3408 19.8527 20.4905 19.4154 21.4633 18.5409C23.4975 16.6168 23.4975 13.381 21.5518 11.457Z" fill="currentColor"/>
<path d="M9.1701 10.9323C8.19726 10.2326 7.22442 9.88281 6.07469 9.88281C4.65965 9.88281 3.33304 10.495 2.44864 11.457C0.502952 13.4685 0.502952 16.6168 2.53708 18.6283C3.42148 19.4154 4.30589 19.8527 5.36717 19.9401L7.4013 18.0161C7.04754 18.0161 6.60533 18.0161 6.25157 18.0161C5.10185 17.9287 4.39433 17.5789 3.95212 17.1416C2.71396 15.9172 2.71396 13.9932 3.86368 12.7688C4.48277 12.1566 5.19029 11.8943 6.07469 11.8943C6.60533 11.8943 7.4013 11.9817 8.19726 12.7688C8.55102 13.1186 9.52386 13.8183 9.87763 14.1681H9.96607L11.2927 12.8563V12.7688C10.6736 12.1566 9.70075 11.3695 9.1701 10.9323Z" fill="currentColor"/>
<path d="M18.4564 8.74536C17.4836 6.12171 14.9188 4.28516 12.0003 4.28516C8.5511 4.28516 5.80945 6.82135 5.27881 9.96973C5.54413 9.96973 5.80945 9.88228 6.16321 9.88228C6.51697 9.88228 6.95917 9.96973 7.31294 9.96973C7.75514 7.78336 9.70082 6.20917 12.0003 6.20917C13.946 6.20917 15.6263 7.34608 16.4223 9.00773C16.4223 9.00773 16.5107 9.09518 16.5107 9.00773C17.1298 8.92027 17.8373 8.74536 18.4564 8.74536C18.4564 8.83282 18.4564 8.83282 18.4564 8.74536Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,4 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="10" fill="#F5F5F5" style="fill:#F5F5F5;fill:color(display-p3 0.9608 0.9608 0.9608);fill-opacity:1;"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M19.2587 5.29983C20.814 5.24008 23.9615 5.44575 26.4795 7.70668C28.5451 9.56134 29.2567 12.9997 28.7754 15.6659C28.3341 18.1098 27.2944 20.1094 27.7016 21.9609C28.1088 23.8124 29.1453 24.7252 30.027 25.1086C30.8787 25.4787 31.7019 25.3677 31.5452 26.0712C31.3884 26.7747 30.3233 27.0341 29.5455 27.0341C29.1191 27.0341 27.8294 26.9228 27.2572 26.5525C26.581 26.1149 26.4303 25.7228 26.1729 25.4075C26.1292 25.354 26.0521 25.3913 26.0624 25.4595C26.1797 26.2351 26.503 27.551 27.0276 28.3672C27.4521 29.0274 27.6275 29.5089 28.6 30.3236C29.5726 31.1381 30.0268 31.8787 29.0813 32.4033C28.2412 32.8691 26.4152 31.9159 25.3414 30.737C24.3749 29.6759 23.7019 28.367 23.4358 27.5154C23.2935 27.0601 23.151 26.2768 23.0087 25.6362C22.99 25.5519 22.8746 25.5655 22.8721 25.6518C22.8249 27.2822 22.5686 28.8393 22.1939 30.5087C21.9241 31.7105 21.2681 33.1747 20.6756 34.0635C19.9532 35.1471 18.8808 35.4853 18.0836 35.1891C17.446 34.952 17.8985 34.1463 18.0836 33.4487C18.2687 32.7513 18.6391 31.2923 18.8886 29.694C19.1144 28.2472 19.1741 26.8151 19.0735 25.4582C19.0673 25.3754 18.9402 25.3627 18.9144 25.4416C18.7066 26.078 18.3149 27.133 18.0292 27.7747C17.4332 29.1128 16.9922 29.7001 15.7702 31.1073C14.6712 32.3727 11.5105 34.1622 11.1045 32.4033C10.9935 31.922 11.5755 31.7112 12.1122 31.2923C12.9191 30.6629 13.5933 29.5459 14.2325 28.1819C14.8065 26.9571 15.0983 25.7724 15.1429 25.016C15.1472 24.943 15.0556 24.9155 15.0145 24.9758C14.7488 25.3656 14.3575 25.893 13.9266 26.3303C13.2067 27.0611 12.4083 27.8134 11.4824 28.1819C9.99368 28.7744 8.47556 28.367 8.58655 27.2934C8.67931 26.3963 9.68069 26.582 10.9272 25.222C12.1492 23.889 12.5564 22.8519 12.6305 21.704C12.6896 20.7857 11.7912 18.606 11.3345 17.6309C10.6063 16.0756 10.0681 11.6617 12.2603 8.81785C15.0004 5.26311 18.2959 5.33684 19.2587 5.29983ZM14.9842 11.5161C13.9647 11.5161 13.4549 12.2987 13.4549 13.2639C13.4549 14.2292 14.0213 15.0117 14.9842 15.0117C15.9471 15.0117 16.5135 14.2292 16.5135 13.2639C16.5135 12.2987 16.0038 11.5161 14.9842 11.5161Z" fill="currentColor"/>
<path d="M9.64258 3.10608C10.3684 3.07819 11.8376 3.17404 13.0127 4.22913C13.9766 5.09463 14.3086 6.69977 14.084 7.94397C13.878 9.08427 13.3929 10.0176 13.583 10.8815C13.7731 11.7453 14.2566 12.1713 14.668 12.3502C15.0654 12.523 15.4501 12.4711 15.377 12.7994C15.3038 13.1277 14.8062 13.2487 14.4434 13.2487C14.2442 13.2486 13.6431 13.1967 13.376 13.024C13.0604 12.8199 12.9892 12.637 12.8691 12.4899C12.8488 12.465 12.8135 12.4824 12.8184 12.5143C12.8731 12.8762 13.0238 13.4901 13.2686 13.8707C13.4666 14.1789 13.5483 14.4037 14.002 14.7838C14.4558 15.164 14.6678 15.5098 14.2266 15.7545C13.8344 15.9715 12.9824 15.5262 12.4814 14.9762C12.0305 14.481 11.716 13.8706 11.5918 13.4733C11.5254 13.2608 11.459 12.8953 11.3926 12.5963C11.3839 12.5572 11.3306 12.5633 11.3291 12.6031C11.3071 13.364 11.1875 14.0908 11.0127 14.8698C10.8868 15.4305 10.5811 16.1142 10.3047 16.5289C9.96759 17.0346 9.46673 17.1925 9.09473 17.0543C8.79714 16.9437 9.00832 16.5673 9.09473 16.2418C9.18116 15.9162 9.35337 15.2355 9.46973 14.4899C9.57509 13.8147 9.60356 13.1466 9.55664 12.5133C9.55378 12.4747 9.49448 12.4687 9.48242 12.5055C9.38545 12.8025 9.20269 13.2949 9.06934 13.5944C8.79123 14.2188 8.58485 14.4924 8.01465 15.149C7.50186 15.7395 6.02756 16.5749 5.83789 15.7545C5.78606 15.5299 6.05719 15.4313 6.30762 15.236C6.68411 14.9422 6.99955 14.4212 7.29785 13.7848C7.56564 13.2134 7.70176 12.6602 7.72266 12.3073C7.72466 12.2733 7.68135 12.2598 7.66211 12.2877C7.53811 12.4696 7.35541 12.7164 7.1543 12.9205C6.81843 13.2615 6.44565 13.6129 6.01367 13.7848C5.31916 14.061 4.6105 13.8706 4.66211 13.3698C4.7054 12.9512 5.17315 13.0376 5.75488 12.403C6.32511 11.7809 6.51524 11.297 6.5498 10.7614C6.57745 10.3328 6.15844 9.31505 5.94531 8.85999C5.60552 8.13397 5.35407 6.07467 6.37695 4.74768C7.65531 3.08929 9.19275 3.12331 9.64258 3.10608ZM8.37988 6.00745C7.90409 6.00745 7.66602 6.37241 7.66602 6.82288C7.6661 7.27324 7.9306 7.63831 8.37988 7.63831C8.8289 7.63807 9.09269 7.2731 9.09277 6.82288C9.09277 6.37255 8.85538 6.00767 8.37988 6.00745ZM11.5244 6.00745C11.0486 6.00745 10.8105 6.37241 10.8105 6.82288C10.8106 7.27324 11.0751 7.63831 11.5244 7.63831C11.9734 7.63807 12.2372 7.2731 12.2373 6.82288C12.2373 6.37255 11.9999 6.00767 11.5244 6.00745Z" fill="black" style="fill:black;fill-opacity:1;"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,9 +1,121 @@
import { Link, Style } from "@solidjs/meta"
import { Show } from "solid-js" import { Show } from "solid-js"
import { Style, Link } from "@solidjs/meta"
import inter from "../assets/fonts/inter.woff2" import inter from "../assets/fonts/inter.woff2"
import ibmPlexMonoBold from "../assets/fonts/ibm-plex-mono-bold.woff2"
import ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2" import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
import ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
import ibmPlexMonoBold from "../assets/fonts/ibm-plex-mono-bold.woff2"
import cascadiaCode from "../assets/fonts/cascadia-code-nerd-font.woff2"
import cascadiaCodeBold from "../assets/fonts/cascadia-code-nerd-font-bold.woff2"
import firaCode from "../assets/fonts/fira-code-nerd-font.woff2"
import firaCodeBold from "../assets/fonts/fira-code-nerd-font-bold.woff2"
import hack from "../assets/fonts/hack-nerd-font.woff2"
import hackBold from "../assets/fonts/hack-nerd-font-bold.woff2"
import inconsolata from "../assets/fonts/inconsolata-nerd-font.woff2"
import inconsolataBold from "../assets/fonts/inconsolata-nerd-font-bold.woff2"
import intelOneMono from "../assets/fonts/intel-one-mono-nerd-font.woff2"
import intelOneMonoBold from "../assets/fonts/intel-one-mono-nerd-font-bold.woff2"
import jetbrainsMono from "../assets/fonts/jetbrains-mono-nerd-font.woff2"
import jetbrainsMonoBold from "../assets/fonts/jetbrains-mono-nerd-font-bold.woff2"
import mesloLgs from "../assets/fonts/meslo-lgs-nerd-font.woff2"
import mesloLgsBold from "../assets/fonts/meslo-lgs-nerd-font-bold.woff2"
import robotoMono from "../assets/fonts/roboto-mono-nerd-font.woff2"
import robotoMonoBold from "../assets/fonts/roboto-mono-nerd-font-bold.woff2"
import sourceCodePro from "../assets/fonts/source-code-pro-nerd-font.woff2"
import sourceCodeProBold from "../assets/fonts/source-code-pro-nerd-font-bold.woff2"
import ubuntuMono from "../assets/fonts/ubuntu-mono-nerd-font.woff2"
import ubuntuMonoBold from "../assets/fonts/ubuntu-mono-nerd-font-bold.woff2"
import iosevka from "../assets/fonts/iosevka-nerd-font.woff2"
import iosevkaBold from "../assets/fonts/iosevka-nerd-font-bold.woff2"
import geistMono from "../assets/fonts/GeistMonoNerdFontMono-Regular.woff2"
import geistMonoBold from "../assets/fonts/GeistMonoNerdFontMono-Bold.woff2"
type MonoFont = {
family: string
regular: string
bold: string
}
export const MONO_NERD_FONTS = [
{
family: "JetBrains Mono Nerd Font",
regular: jetbrainsMono,
bold: jetbrainsMonoBold,
},
{
family: "Fira Code Nerd Font",
regular: firaCode,
bold: firaCodeBold,
},
{
family: "Cascadia Code Nerd Font",
regular: cascadiaCode,
bold: cascadiaCodeBold,
},
{
family: "Hack Nerd Font",
regular: hack,
bold: hackBold,
},
{
family: "Source Code Pro Nerd Font",
regular: sourceCodePro,
bold: sourceCodeProBold,
},
{
family: "Inconsolata Nerd Font",
regular: inconsolata,
bold: inconsolataBold,
},
{
family: "Roboto Mono Nerd Font",
regular: robotoMono,
bold: robotoMonoBold,
},
{
family: "Ubuntu Mono Nerd Font",
regular: ubuntuMono,
bold: ubuntuMonoBold,
},
{
family: "Intel One Mono Nerd Font",
regular: intelOneMono,
bold: intelOneMonoBold,
},
{
family: "Meslo LGS Nerd Font",
regular: mesloLgs,
bold: mesloLgsBold,
},
{
family: "Iosevka Nerd Font",
regular: iosevka,
bold: iosevkaBold,
},
{
family: "GeistMono Nerd Font",
regular: geistMono,
bold: geistMonoBold,
},
] satisfies MonoFont[]
const monoNerdCss = MONO_NERD_FONTS.map(
(font) => `
@font-face {
font-family: "${font.family}";
src: url("${font.regular}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "${font.family}";
src: url("${font.bold}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 700;
}`,
).join("")
export const Font = () => { export const Font = () => {
return ( return (
@ -53,6 +165,7 @@ export const Font = () => {
descent-override: 25%; descent-override: 25%;
line-gap-override: 1%; line-gap-override: 1%;
} }
${monoNerdCss}
`}</Style> `}</Style>
<Show when={typeof location === "undefined" || location.protocol !== "file:"}> <Show when={typeof location === "undefined" || location.protocol !== "file:"}>
<Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" /> <Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" />

View File

@ -1,133 +0,0 @@
type MonoFont = {
id: string
family: string
regular: string
bold: string
}
let files: Record<string, () => Promise<string>> | undefined
function getFiles() {
if (files) return files
files = import.meta.glob("./assets/fonts/*.woff2", { import: "default" }) as Record<string, () => Promise<string>>
return files
}
export const MONO_NERD_FONTS = [
{
id: "jetbrains-mono",
family: "JetBrains Mono Nerd Font",
regular: "./assets/fonts/jetbrains-mono-nerd-font.woff2",
bold: "./assets/fonts/jetbrains-mono-nerd-font-bold.woff2",
},
{
id: "fira-code",
family: "Fira Code Nerd Font",
regular: "./assets/fonts/fira-code-nerd-font.woff2",
bold: "./assets/fonts/fira-code-nerd-font-bold.woff2",
},
{
id: "cascadia-code",
family: "Cascadia Code Nerd Font",
regular: "./assets/fonts/cascadia-code-nerd-font.woff2",
bold: "./assets/fonts/cascadia-code-nerd-font-bold.woff2",
},
{
id: "hack",
family: "Hack Nerd Font",
regular: "./assets/fonts/hack-nerd-font.woff2",
bold: "./assets/fonts/hack-nerd-font-bold.woff2",
},
{
id: "source-code-pro",
family: "Source Code Pro Nerd Font",
regular: "./assets/fonts/source-code-pro-nerd-font.woff2",
bold: "./assets/fonts/source-code-pro-nerd-font-bold.woff2",
},
{
id: "inconsolata",
family: "Inconsolata Nerd Font",
regular: "./assets/fonts/inconsolata-nerd-font.woff2",
bold: "./assets/fonts/inconsolata-nerd-font-bold.woff2",
},
{
id: "roboto-mono",
family: "Roboto Mono Nerd Font",
regular: "./assets/fonts/roboto-mono-nerd-font.woff2",
bold: "./assets/fonts/roboto-mono-nerd-font-bold.woff2",
},
{
id: "ubuntu-mono",
family: "Ubuntu Mono Nerd Font",
regular: "./assets/fonts/ubuntu-mono-nerd-font.woff2",
bold: "./assets/fonts/ubuntu-mono-nerd-font-bold.woff2",
},
{
id: "intel-one-mono",
family: "Intel One Mono Nerd Font",
regular: "./assets/fonts/intel-one-mono-nerd-font.woff2",
bold: "./assets/fonts/intel-one-mono-nerd-font-bold.woff2",
},
{
id: "meslo-lgs",
family: "Meslo LGS Nerd Font",
regular: "./assets/fonts/meslo-lgs-nerd-font.woff2",
bold: "./assets/fonts/meslo-lgs-nerd-font-bold.woff2",
},
{
id: "iosevka",
family: "Iosevka Nerd Font",
regular: "./assets/fonts/iosevka-nerd-font.woff2",
bold: "./assets/fonts/iosevka-nerd-font-bold.woff2",
},
{
id: "geist-mono",
family: "GeistMono Nerd Font",
regular: "./assets/fonts/GeistMonoNerdFontMono-Regular.woff2",
bold: "./assets/fonts/GeistMonoNerdFontMono-Bold.woff2",
},
] satisfies MonoFont[]
const mono = Object.fromEntries(MONO_NERD_FONTS.map((font) => [font.id, font])) as Record<string, MonoFont>
const loads = new Map<string, Promise<void>>()
function css(font: { family: string; regular: string; bold: string }) {
return `
@font-face {
font-family: "${font.family}";
src: url("${font.regular}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "${font.family}";
src: url("${font.bold}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 700;
}
`
}
export function ensureMonoFont(id: string | undefined) {
if (!id || id === "ibm-plex-mono") return Promise.resolve()
if (typeof document !== "object") return Promise.resolve()
const font = mono[id]
if (!font) return Promise.resolve()
const styleId = `oc-font-${font.id}`
if (document.getElementById(styleId)) return Promise.resolve()
const hit = loads.get(font.id)
if (hit) return hit
const files = getFiles()
const load = Promise.all([files[font.regular]?.(), files[font.bold]?.()]).then(([regular, bold]) => {
if (!regular || !bold) return
if (document.getElementById(styleId)) return
const style = document.createElement("style")
style.id = styleId
style.textContent = css({ family: font.family, regular, bold })
document.head.appendChild(style)
})
loads.set(font.id, load)
return load
}

View File

@ -1,7 +1,7 @@
import { createEffect, onCleanup, onMount } from "solid-js" import { createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "../context/helper" import { createSimpleContext } from "../context/helper"
import oc2ThemeJson from "./themes/oc-2.json" import { DEFAULT_THEMES } from "./default-themes"
import { resolveThemeVariant, themeToCss } from "./resolve" import { resolveThemeVariant, themeToCss } from "./resolve"
import type { DesktopTheme } from "./types" import type { DesktopTheme } from "./types"
@ -15,101 +15,14 @@ const STORAGE_KEYS = {
} as const } as const
const THEME_STYLE_ID = "oc-theme" const THEME_STYLE_ID = "oc-theme"
let files: Record<string, () => Promise<{ default: DesktopTheme }>> | undefined
let ids: string[] | undefined
let known: Set<string> | undefined
function getFiles() {
if (files) return files
files = import.meta.glob<{ default: DesktopTheme }>("./themes/*.json")
return files
}
function themeIDs() {
if (ids) return ids
ids = Object.keys(getFiles())
.map((path) => path.slice("./themes/".length, -".json".length))
.sort()
return ids
}
function knownThemes() {
if (known) return known
known = new Set(themeIDs())
return known
}
const names: Record<string, string> = {
"oc-2": "OC-2",
amoled: "AMOLED",
aura: "Aura",
ayu: "Ayu",
carbonfox: "Carbonfox",
catppuccin: "Catppuccin",
"catppuccin-frappe": "Catppuccin Frappe",
"catppuccin-macchiato": "Catppuccin Macchiato",
cobalt2: "Cobalt2",
cursor: "Cursor",
dracula: "Dracula",
everforest: "Everforest",
flexoki: "Flexoki",
github: "GitHub",
gruvbox: "Gruvbox",
kanagawa: "Kanagawa",
"lucent-orng": "Lucent Orng",
material: "Material",
matrix: "Matrix",
mercury: "Mercury",
monokai: "Monokai",
nightowl: "Night Owl",
nord: "Nord",
"one-dark": "One Dark",
onedarkpro: "One Dark Pro",
opencode: "OpenCode",
orng: "Orng",
"osaka-jade": "Osaka Jade",
palenight: "Palenight",
rosepine: "Rose Pine",
shadesofpurple: "Shades of Purple",
solarized: "Solarized",
synthwave84: "Synthwave '84",
tokyonight: "Tokyonight",
vercel: "Vercel",
vesper: "Vesper",
zenburn: "Zenburn",
}
const oc2Theme = oc2ThemeJson as DesktopTheme
function normalize(id: string | null | undefined) { function normalize(id: string | null | undefined) {
return id === "oc-1" ? "oc-2" : id return id === "oc-1" ? "oc-2" : id
} }
function read(key: string) {
if (typeof localStorage !== "object") return null
try {
return localStorage.getItem(key)
} catch {
return null
}
}
function write(key: string, value: string) {
if (typeof localStorage !== "object") return
try {
localStorage.setItem(key, value)
} catch {}
}
function drop(key: string) {
if (typeof localStorage !== "object") return
try {
localStorage.removeItem(key)
} catch {}
}
function clear() { function clear() {
drop(STORAGE_KEYS.THEME_CSS_LIGHT) localStorage.removeItem(STORAGE_KEYS.THEME_CSS_LIGHT)
drop(STORAGE_KEYS.THEME_CSS_DARK) localStorage.removeItem(STORAGE_KEYS.THEME_CSS_DARK)
} }
function ensureThemeStyleElement(): HTMLStyleElement { function ensureThemeStyleElement(): HTMLStyleElement {
@ -122,7 +35,6 @@ function ensureThemeStyleElement(): HTMLStyleElement {
} }
function getSystemMode(): "light" | "dark" { function getSystemMode(): "light" | "dark" {
if (typeof window !== "object") return "light"
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
} }
@ -133,7 +45,9 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
const css = themeToCss(tokens) const css = themeToCss(tokens)
if (themeId !== "oc-2") { if (themeId !== "oc-2") {
write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css) try {
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
} catch {}
} }
const fullCss = `:root { const fullCss = `:root {
@ -155,122 +69,74 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
const variant = isDark ? theme.dark : theme.light const variant = isDark ? theme.dark : theme.light
const tokens = resolveThemeVariant(variant, isDark) const tokens = resolveThemeVariant(variant, isDark)
const css = themeToCss(tokens) const css = themeToCss(tokens)
write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css) try {
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
} catch {}
} }
} }
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme", name: "Theme",
init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => { init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
const themeId = normalize(read(STORAGE_KEYS.THEME_ID) ?? props.defaultTheme) ?? "oc-2"
const colorScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
const mode = colorScheme === "system" ? getSystemMode() : colorScheme
const [store, setStore] = createStore({ const [store, setStore] = createStore({
themes: { themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
"oc-2": oc2Theme, themeId: normalize(props.defaultTheme) ?? "oc-2",
} as Record<string, DesktopTheme>, colorScheme: "system" as ColorScheme,
themeId, mode: getSystemMode(),
colorScheme,
mode,
previewThemeId: null as string | null, previewThemeId: null as string | null,
previewScheme: null as ColorScheme | null, previewScheme: null as ColorScheme | null,
}) })
const loads = new Map<string, Promise<DesktopTheme | undefined>>() window.addEventListener("storage", (e) => {
if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) setStore("themeId", e.newValue)
if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
setStore("colorScheme", e.newValue as ColorScheme)
setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as any))
}
})
const load = (id: string) => { onMount(() => {
const next = normalize(id) const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
if (!next) return Promise.resolve(undefined) const handler = () => {
const hit = store.themes[next] if (store.colorScheme === "system") {
if (hit) return Promise.resolve(hit) setStore("mode", getSystemMode())
const pending = loads.get(next) }
if (pending) return pending }
const file = getFiles()[`./themes/${next}.json`] mediaQuery.addEventListener("change", handler)
if (!file) return Promise.resolve(undefined) onCleanup(() => mediaQuery.removeEventListener("change", handler))
const task = file()
.then((mod) => { const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
const theme = mod.default const themeId = normalize(savedTheme)
setStore("themes", next, theme) const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
return theme if (themeId && store.themes[themeId]) {
}) setStore("themeId", themeId)
.finally(() => { }
loads.delete(next) if (savedTheme && themeId && savedTheme !== themeId) {
}) localStorage.setItem(STORAGE_KEYS.THEME_ID, themeId)
loads.set(next, task) clear()
return task }
} if (savedScheme) {
setStore("colorScheme", savedScheme)
if (savedScheme !== "system") {
setStore("mode", savedScheme)
}
}
const currentTheme = store.themes[store.themeId]
if (currentTheme) {
cacheThemeVariants(currentTheme, store.themeId)
}
})
const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => { const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
applyThemeCss(theme, themeId, mode) applyThemeCss(theme, themeId, mode)
props.onThemeApplied?.(theme, mode) props.onThemeApplied?.(theme, mode)
} }
const ids = () => {
const extra = Object.keys(store.themes)
.filter((id) => !knownThemes().has(id))
.sort()
const all = themeIDs()
if (extra.length === 0) return all
return [...all, ...extra]
}
const loadThemes = () => Promise.all(themeIDs().map(load)).then(() => store.themes)
const onStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) {
const next = normalize(e.newValue)
if (!next) return
if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return
setStore("themeId", next)
if (next === "oc-2") {
clear()
return
}
void load(next).then((theme) => {
if (!theme || store.themeId !== next) return
cacheThemeVariants(theme, next)
})
}
if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
setStore("colorScheme", e.newValue as ColorScheme)
setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as "light" | "dark"))
}
}
if (typeof window === "object") {
window.addEventListener("storage", onStorage)
onCleanup(() => window.removeEventListener("storage", onStorage))
}
onMount(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const onMedia = () => {
if (store.colorScheme !== "system") return
setStore("mode", getSystemMode())
}
mediaQuery.addEventListener("change", onMedia)
onCleanup(() => mediaQuery.removeEventListener("change", onMedia))
const rawTheme = read(STORAGE_KEYS.THEME_ID)
const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2"
const savedScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
if (rawTheme && rawTheme !== savedTheme) {
write(STORAGE_KEYS.THEME_ID, savedTheme)
clear()
}
if (savedTheme !== store.themeId) setStore("themeId", savedTheme)
if (savedScheme !== store.colorScheme) setStore("colorScheme", savedScheme)
setStore("mode", savedScheme === "system" ? getSystemMode() : savedScheme)
void load(savedTheme).then((theme) => {
if (!theme || store.themeId !== savedTheme) return
cacheThemeVariants(theme, savedTheme)
})
})
createEffect(() => { createEffect(() => {
const theme = store.themes[store.themeId] const theme = store.themes[store.themeId]
if (!theme) return if (theme) {
applyTheme(theme, store.themeId, store.mode) applyTheme(theme, store.themeId, store.mode)
}
}) })
const setTheme = (id: string) => { const setTheme = (id: string) => {
@ -279,26 +145,23 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
console.warn(`Theme "${id}" not found`) console.warn(`Theme "${id}" not found`)
return return
} }
if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) { const theme = store.themes[next]
if (!theme) {
console.warn(`Theme "${id}" not found`) console.warn(`Theme "${id}" not found`)
return return
} }
setStore("themeId", next) setStore("themeId", next)
localStorage.setItem(STORAGE_KEYS.THEME_ID, next)
if (next === "oc-2") { if (next === "oc-2") {
write(STORAGE_KEYS.THEME_ID, next)
clear() clear()
return return
} }
void load(next).then((theme) => { cacheThemeVariants(theme, next)
if (!theme || store.themeId !== next) return
cacheThemeVariants(theme, next)
write(STORAGE_KEYS.THEME_ID, next)
})
} }
const setColorScheme = (scheme: ColorScheme) => { const setColorScheme = (scheme: ColorScheme) => {
setStore("colorScheme", scheme) setStore("colorScheme", scheme)
write(STORAGE_KEYS.COLOR_SCHEME, scheme) localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
setStore("mode", scheme === "system" ? getSystemMode() : scheme) setStore("mode", scheme === "system" ? getSystemMode() : scheme)
} }
@ -306,9 +169,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
themeId: () => store.themeId, themeId: () => store.themeId,
colorScheme: () => store.colorScheme, colorScheme: () => store.colorScheme,
mode: () => store.mode, mode: () => store.mode,
ids,
name: (id: string) => store.themes[id]?.name ?? names[id] ?? id,
loadThemes,
themes: () => store.themes, themes: () => store.themes,
setTheme, setTheme,
setColorScheme, setColorScheme,
@ -316,28 +176,24 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
previewTheme: (id: string) => { previewTheme: (id: string) => {
const next = normalize(id) const next = normalize(id)
if (!next) return if (!next) return
if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return const theme = store.themes[next]
if (!theme) return
setStore("previewThemeId", next) setStore("previewThemeId", next)
void load(next).then((theme) => { const previewMode = store.previewScheme
if (!theme || store.previewThemeId !== next) return ? store.previewScheme === "system"
const mode = store.previewScheme ? getSystemMode()
? store.previewScheme === "system" : store.previewScheme
? getSystemMode() : store.mode
: store.previewScheme applyTheme(theme, next, previewMode)
: store.mode
applyTheme(theme, next, mode)
})
}, },
previewColorScheme: (scheme: ColorScheme) => { previewColorScheme: (scheme: ColorScheme) => {
setStore("previewScheme", scheme) setStore("previewScheme", scheme)
const mode = scheme === "system" ? getSystemMode() : scheme const previewMode = scheme === "system" ? getSystemMode() : scheme
const id = store.previewThemeId ?? store.themeId const id = store.previewThemeId ?? store.themeId
void load(id).then((theme) => { const theme = store.themes[id]
if (!theme) return if (theme) {
if ((store.previewThemeId ?? store.themeId) !== id) return applyTheme(theme, id, previewMode)
if (store.previewScheme !== scheme) return }
applyTheme(theme, id, mode)
})
}, },
commitPreview: () => { commitPreview: () => {
if (store.previewThemeId) { if (store.previewThemeId) {
@ -352,10 +208,10 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
cancelPreview: () => { cancelPreview: () => {
setStore("previewThemeId", null) setStore("previewThemeId", null)
setStore("previewScheme", null) setStore("previewScheme", null)
void load(store.themeId).then((theme) => { const theme = store.themes[store.themeId]
if (!theme) return if (theme) {
applyTheme(theme, store.themeId, store.mode) applyTheme(theme, store.themeId, store.mode)
}) }
}, },
} }
}, },