diff options
| author | Adam <[email protected]> | 2026-03-25 06:23:25 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-25 06:25:05 -0500 |
| commit | 898456a25cf2edbfc4ae4961b37424f633419dd6 (patch) | |
| tree | 626207d26b6c338136b069bbaaa09d67a45ddde3 /packages/app | |
| parent | 53d0b58ebf3468bd161dcfcdc67cd66b6508e9f8 (diff) | |
| download | opencode-898456a25cf2edbfc4ae4961b37424f633419dd6.tar.gz opencode-898456a25cf2edbfc4ae4961b37424f633419dd6.zip | |
Revert "fix(app): startup efficiency"
Diffstat (limited to 'packages/app')
25 files changed, 485 insertions, 662 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 0eb5b4e9e..5247c951d 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -6,7 +6,7 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { File } from "@opencode-ai/ui/file" import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" -import { ThemeProvider } from "@opencode-ai/ui/theme/context" +import { ThemeProvider } from "@opencode-ai/ui/theme" import { MetaProvider } from "@solidjs/meta" import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" @@ -32,7 +32,7 @@ import { FileProvider } from "@/context/file" import { GlobalSDKProvider } from "@/context/global-sdk" import { GlobalSyncProvider } from "@/context/global-sync" import { HighlightsProvider } from "@/context/highlights" -import { LanguageProvider, type Locale, useLanguage } from "@/context/language" +import { LanguageProvider, useLanguage } from "@/context/language" import { LayoutProvider } from "@/context/layout" import { ModelsProvider } from "@/context/models" 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 ( <MetaProvider> <Font /> @@ -139,7 +139,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { void window.api?.setTitlebar?.({ mode }) }} > - <LanguageProvider locale={props.locale}> + <LanguageProvider> <UiI18nBridge> <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}> <QueryProvider> diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index e7eaa1fb2..734958dd5 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -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 { useDialog } from "@opencode-ai/ui/context/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 { TextField } from "@opencode-ai/ui/text-field" 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 { Link } from "@/components/link" 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 fallback = createMemo<ProviderAuthMethod[]>(() => [ - { - type: "api" as const, - 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 methods = createMemo( + () => + globalSync.data.provider_auth[props.provider] ?? [ + { + type: "api", + label: language.t("provider.connect.method.apiKey"), + }, + ], ) - 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({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, @@ -187,11 +177,7 @@ export function DialogConnectProvider(props: { provider: string }) { index: 0, }) - const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => { - const value = method() - if (value?.type !== "oauth") return [] - return value.prompts ?? [] - }) + const prompts = createMemo(() => method()?.prompts ?? []) const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => { if (!prompt.when) return true const actual = value[prompt.when.key] @@ -310,12 +296,8 @@ export function DialogConnectProvider(props: { provider: string }) { listRef?.onKeyDown(e) } - let auto = false - createEffect(() => { - if (auto) return - if (loading()) return + onMount(() => { if (methods().length === 1) { - auto = true 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 onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}> <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}> <MethodSelection /> </Match> diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index ee98e68cd..f523671ec 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -572,7 +572,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const open = recent() const seen = new Set(open) const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) - if (!query.trim()) return [...agents, ...pinned] const paths = await files.searchFilesAndDirectories(query) const fileOptions: AtOption[] = paths .filter((path) => !seen.has(path)) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index f4b8198e7..b768bafcc 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -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 { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Select } from "@opencode-ai/ui/select" import { Switch } from "@opencode-ai/ui/switch" 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 { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" 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 { SettingsList } from "./settings-list" let demoSoundState = { cleanup: undefined as (() => void) | 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, // delay the playback by 100ms during quick selection changes and pause existing sounds. const stopDemoSound = () => { - demoSoundState.run += 1 if (demoSoundState.cleanup) { demoSoundState.cleanup() } @@ -43,19 +29,12 @@ const stopDemoSound = () => { demoSoundState.cleanup = undefined } -const playDemoSound = (id: string | undefined) => { +const playDemoSound = (src: string | undefined) => { stopDemoSound() - if (!id) return + if (!src) return - const run = ++demoSoundState.run demoSoundState.timeout = setTimeout(() => { - void playSoundById(id).then((cleanup) => { - if (demoSoundState.run !== run) { - cleanup?.() - return - } - demoSoundState.cleanup = cleanup - }) + demoSoundState.cleanup = playSound(src) }, 100) } @@ -65,10 +44,6 @@ export const SettingsGeneral: Component = () => { const platform = usePlatform() const settings = useSettings() - onMount(() => { - void theme.loadThemes() - }) - const [store, setStore] = createStore({ checking: false, }) @@ -129,7 +104,9 @@ export const SettingsGeneral: Component = () => { .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 }[] => [ { value: "system", label: language.t("theme.scheme.system") }, @@ -166,7 +143,7 @@ export const SettingsGeneral: Component = () => { ] as const 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 soundSelectProps = ( @@ -181,7 +158,7 @@ export const SettingsGeneral: Component = () => { label: (o: (typeof soundOptions)[number]) => language.t(o.label), onHighlight: (option: (typeof soundOptions)[number] | undefined) => { if (!option) return - playDemoSound(option.id === "none" ? undefined : option.id) + playDemoSound(option.src) }, onSelect: (option: (typeof soundOptions)[number] | undefined) => { if (!option) return @@ -192,7 +169,7 @@ export const SettingsGeneral: Component = () => { } setEnabled(true) set(option.id) - playDemoSound(option.id) + playDemoSound(option.src) }, variant: "secondary" as const, size: "small" as const, @@ -344,9 +321,6 @@ export const SettingsGeneral: Component = () => { current={fontOptionsList.find((o) => o.value === settings.appearance.font())} value={(o) => o.value} label={(o) => language.t(o.label)} - onHighlight={(option) => { - void loadFont().then((x) => x.ensureMonoFont(option?.value)) - }} onSelect={(option) => option && settings.appearance.setFont(option.value)} variant="secondary" size="small" diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 8d5ecac39..464522443 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -16,6 +16,7 @@ import { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" +import { DialogSelectServer } from "./dialog-select-server" 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 [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>) createEffect(() => { - if (!enabled()) { - setStatus(reconcile({})) - return - } const list = servers() let dead = false @@ -165,12 +162,6 @@ export function StatusPopover() { const navigate = useNavigate() const [shown, setShown] = createSignal(false) - let dialogRun = 0 - let dialogDead = false - onCleanup(() => { - dialogDead = true - dialogRun += 1 - }) const servers = createMemo(() => { const current = server.current const list = server.list @@ -178,7 +169,7 @@ export function StatusPopover() { if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] 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 toggleMcp = useMcpToggleMutation() const defaultServer = useDefaultServerKey(platform.getDefaultServer) @@ -309,13 +300,7 @@ export function StatusPopover() { <Button variant="secondary" class="mt-3 self-start h-8 px-3 py-1.5" - onClick={() => { - const run = ++dialogRun - void import("./dialog-select-server").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh) - }) - }} + onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)} > {language.t("status.popover.action.manageServers")} </Button> diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 0a5a7d2d3..aed46f126 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -1,7 +1,4 @@ -import { withAlpha } from "@opencode-ai/ui/theme/color" -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 { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme" import { showToast } from "@opencode-ai/ui/toast" import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web" import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js" diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 0a41f3119..77de1a73c 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" 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 { usePlatform } from "@/context/platform" diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index cbd08e99f..2d1e50135 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -9,7 +9,17 @@ import type { } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" 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 { useLanguage } from "@/context/language" import { Persist, persisted } from "@/utils/persist" @@ -70,8 +80,6 @@ function createGlobalSync() { let active = true let projectWritten = false - let bootedAt = 0 - let bootingRoot = false onCleanup(() => { active = false @@ -250,11 +258,6 @@ function createGlobalSync() { const sdk = sdkFor(directory) await bootstrapDirectory({ directory, - global: { - config: globalStore.config, - project: globalStore.project, - provider: globalStore.provider, - }, sdk, store: child[0], setStore: child[1], @@ -275,20 +278,15 @@ function createGlobalSync() { const unsub = globalSDK.event.listen((e) => { const directory = e.name const event = e.details - const recent = bootingRoot || Date.now() - bootedAt < 1500 if (directory === "global") { applyGlobalEvent({ event, project: globalStore.project, - refresh: () => { - if (recent) return - queue.refresh() - }, + refresh: queue.refresh, setGlobalProject: setProjects, }) if (event.type === "server.connected" || event.type === "global.disposed") { - if (recent) return for (const directory of Object.keys(children.children)) { queue.push(directory) } @@ -327,19 +325,17 @@ function createGlobalSync() { }) async function bootstrap() { - bootingRoot = true - try { - await bootstrapGlobal({ - globalSDK: globalSDK.client, - requestFailedTitle: language.t("common.requestFailed"), - translate: language.t, - formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), - setGlobalStore: setBootStore, - }) - bootedAt = Date.now() - } finally { - bootingRoot = false - } + await bootstrapGlobal({ + globalSDK: globalSDK.client, + connectErrorTitle: language.t("dialog.server.add.error"), + connectErrorDescription: language.t("error.globalSync.connectFailed", { + url: globalSDK.url, + }), + requestFailedTitle: language.t("common.requestFailed"), + translate: language.t, + formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), + setGlobalStore: setBootStore, + }) } onMount(() => { @@ -396,7 +392,13 @@ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>() export function GlobalSyncProvider(props: ParentProps) { 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() { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 47be3abcb..13494b7ad 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -31,102 +31,73 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -function waitForPaint() { - return new Promise<void>((resolve) => { - let done = false - const finish = () => { - if (done) return - done = true - resolve() - } - const timer = setTimeout(finish, 50) - if (typeof requestAnimationFrame !== "function") return - requestAnimationFrame(() => { - clearTimeout(timer) - finish() - }) - }) -} - -function errors(list: PromiseSettledResult<unknown>[]) { - return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason) -} - -function runAll(list: Array<() => Promise<unknown>>) { - return Promise.allSettled(list.map((item) => item())) -} - -function showErrors(input: { - errors: unknown[] - title: string - translate: (key: string, vars?: Record<string, string | number>) => string - formatMoreCount: (count: number) => string -}) { - if (input.errors.length === 0) return - const message = formatServerError(input.errors[0], input.translate) - const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : "" - showToast({ - variant: "error", - title: input.title, - description: message + more, - }) -} - export async function bootstrapGlobal(input: { globalSDK: OpencodeClient + connectErrorTitle: string + connectErrorDescription: string requestFailedTitle: string translate: (key: string, vars?: Record<string, string | number>) => string formatMoreCount: (count: number) => string setGlobalStore: SetStoreFunction<GlobalStore> }) { - const fast = [ - () => - retry(() => - input.globalSDK.path.get().then((x) => { - input.setGlobalStore("path", x.data!) - }), - ), - () => - retry(() => - input.globalSDK.global.config.get().then((x) => { - input.setGlobalStore("config", x.data!) - }), - ), - () => - retry(() => - input.globalSDK.provider.list().then((x) => { - input.setGlobalStore("provider", normalizeProviderList(x.data!)) - }), - ), - ] + 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 slow = [ - () => - retry(() => - input.globalSDK.project.list().then((x) => { - const projects = (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - input.setGlobalStore("project", projects) - }), - ), + const tasks = [ + retry(() => + input.globalSDK.path.get().then((x) => { + input.setGlobalStore("path", x.data!) + }), + ), + retry(() => + input.globalSDK.global.config.get().then((x) => { + input.setGlobalStore("config", x.data!) + }), + ), + retry(() => + input.globalSDK.project.list().then((x) => { + const projects = (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + input.setGlobalStore("project", projects) + }), + ), + retry(() => + input.globalSDK.provider.list().then((x) => { + input.setGlobalStore("provider", normalizeProviderList(x.data!)) + }), + ), + retry(() => + input.globalSDK.provider.auth().then((x) => { + input.setGlobalStore("provider_auth", x.data ?? {}) + }), + ), ] - showErrors({ - errors: errors(await runAll(fast)), - title: input.requestFailedTitle, - translate: input.translate, - formatMoreCount: input.formatMoreCount, - }) - await waitForPaint() - showErrors({ - errors: errors(await runAll(slow)), - title: input.requestFailedTitle, - translate: input.translate, - formatMoreCount: input.formatMoreCount, - }) + const results = await Promise.allSettled(tasks) + const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason) + if (errors.length) { + const message = formatServerError(errors[0], input.translate) + const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : "" + showToast({ + variant: "error", + title: input.requestFailedTitle, + description: message + more, + }) + } input.setGlobalStore("ready", true) } @@ -140,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: { directory: string sdk: OpencodeClient @@ -152,130 +119,88 @@ export async function bootstrapDirectory(input: { vcsCache: VcsCache loadSessions: (directory: string) => Promise<void> | void translate: (key: string, vars?: Record<string, string | number>) => string - global: { - config: Config - project: Project[] - provider: ProviderListResponse - } }) { - const loading = input.store.status !== "complete" - 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 fast = [ - () => - seededProject - ? Promise.resolve() - : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", 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.session.status().then((x) => input.setStore("session_status", 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.command.list().then((x) => input.setStore("command", x.data ?? []))), - () => - 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 slow = [ - () => - retry(() => - input.sdk.provider.list().then((x) => { - input.setStore("provider", normalizeProviderList(x.data!)) - }), - ), - () => Promise.resolve(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!))), - ] - - const errs = errors(await runAll(fast)) - if (errs.length > 0) { - console.error("Failed to bootstrap instance", errs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(errs[0], input.translate), - }) + if (input.store.status !== "complete") input.setStore("status", "loading") + + const blockingRequests = { + project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)), + provider: () => + input.sdk.provider.list().then((x) => { + input.setStore("provider", normalizeProviderList(x.data!)) + }), + agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])), + config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)), } - await waitForPaint() - const slowErrs = errors(await runAll(slow)) - if (slowErrs.length > 0) { - console.error("Failed to finish bootstrap instance", slowErrs[0]) + try { + await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) + } catch (err) { + console.error("Failed to bootstrap instance", err) const project = getFilename(input.directory) showToast({ variant: "error", title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(slowErrs[0], input.translate), + description: formatServerError(err, input.translate), }) + input.setStore("status", "partial") + return } - if (loading && errs.length === 0 && slowErrs.length === 0) 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") + }) } diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index 51dc09cd7..b1edd541c 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -1,10 +1,42 @@ 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 { createSimpleContext } from "@opencode-ai/ui/context" import { Persist, persisted } from "@/utils/persist" 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 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 = | "en" @@ -27,7 +59,6 @@ export type Locale = type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten<RawDictionary> -type Source = { dict: Record<string, string> } function cookie(locale: Locale) { 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 dicts = new Map<Locale, Dictionary>([["en", base]]) - -const merge = (app: Promise<Source>, ui: Promise<Source>) => - Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) - -const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = { - zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")), - zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")), - ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")), - de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")), - es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")), - fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")), - da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")), - ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")), - pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")), - ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")), - ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")), - no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")), - 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 DICT: Record<Locale, Dictionary> = { + en: base, + zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }, + zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }, + ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }, + de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) }, + es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) }, + fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }, + da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) }, + ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }, + pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }, + ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }, + ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }, + no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) }, + br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) }, + th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) }, + bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }, + tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) }, } 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") }, ] +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 { if (typeof navigator !== "object") return "en" @@ -170,48 +203,27 @@ function detectLocale(): Locale { return "en" } -export function normalizeLocale(value: string): Locale { +function normalizeLocale(value: string): Locale { 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({ name: "Language", - init: (props: { locale?: Locale }) => { - const initial = props.locale ?? readStoredLocale() ?? detectLocale() + init: () => { const [store, setStore, _, ready] = persisted( Persist.global("language", ["language.v1"]), createStore({ - locale: initial, + locale: detectLocale() as Locale, }), ) const locale = createMemo<Locale>(() => normalizeLocale(store.locale)) + console.log("locale", locale()) const intl = createMemo(() => INTL[locale()]) - const [dict] = createResource(locale, loadDict, { - initialValue: dicts.get(initial) ?? base, - }) + const dict = createMemo<Dictionary>(() => DICT[locale()]) - const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as ( - key: keyof Dictionary, - params?: Record<string, string | number | boolean>, - ) => string + const t = i18n.translator(dict, i18n.resolveTemplate) const label = (value: Locale) => t(LABEL_KEY[value]) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 281a1ef33..04bc2fdaa 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -12,7 +12,7 @@ import { base64Encode } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" -import { playSoundById } from "@/utils/sound" +import { playSound, soundSrc } from "@/utils/sound" type NotificationBase = { directory?: string @@ -234,7 +234,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi if (session.parentID) return if (settings.sounds.agentEnabled()) { - void playSoundById(settings.sounds.agent()) + playSound(soundSrc(settings.sounds.agent())) } append({ @@ -263,7 +263,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi if (session?.parentID) return if (settings.sounds.errorsEnabled()) { - void playSoundById(settings.sounds.errors()) + playSound(soundSrc(settings.sounds.errors())) } const error = "error" in event.properties ? event.properties.error : undefined diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index eddd752eb..48788fe8e 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -104,13 +104,6 @@ function withFallback<T>(read: () => T | undefined, fallback: T) { 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({ name: "Settings", init: () => { @@ -118,11 +111,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont createEffect(() => { if (typeof document === "undefined") return - const id = store.appearance?.font ?? defaultSettings.appearance.font - if (id !== defaultSettings.appearance.font) { - void loadFont().then((x) => x.ensureMonoFont(id)) - } - document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id)) + document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font)) }) return { diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index bbf4fc5ec..66b889e2a 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -180,8 +180,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return globalSync.child(directory) } const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") - const initialMessagePageSize = 80 - const historyMessagePageSize = 200 + const messagePageSize = 200 const inflight = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>() const inflightTodo = new Map<string, Promise<void>>() @@ -464,7 +463,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined if (cached && hasSession && !opts?.force) return - const limit = meta.limit[key] ?? initialMessagePageSize + const limit = meta.limit[key] ?? messagePageSize const sessionReq = hasSession && !opts?.force ? Promise.resolve() @@ -561,7 +560,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const [, setStore] = globalSync.child(directory) touch(directory, setStore, sessionID) const key = keyFor(directory, sessionID) - const step = count ?? historyMessagePageSize + const step = count ?? messagePageSize if (meta.loading[key]) return if (meta.complete[key]) return const before = meta.cursor[key] diff --git a/packages/app/src/context/terminal-title.ts b/packages/app/src/context/terminal-title.ts index c8b18f421..3e8fa9af2 100644 --- a/packages/app/src/context/terminal-title.ts +++ b/packages/app/src/context/terminal-title.ts @@ -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 = [ - template, - "محطة طرفية {{number}}", - "Терминал {{number}}", - "ターミナル {{number}}", - "터미널 {{number}}", - "เทอร์มินัล {{number}}", - "终端 {{number}}", - "終端機 {{number}}", -] +const numbered = Array.from( + new Set([ + en["terminal.title.numbered"], + ar["terminal.title.numbered"], + br["terminal.title.numbered"], + bs["terminal.title.numbered"], + da["terminal.title.numbered"], + de["terminal.title.numbered"], + 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) { - return template.replace("{{number}}", String(number)) + return en["terminal.title.numbered"].replace("{{number}}", String(number)) } export function isDefaultTitle(title: string, number: number) { diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index da22c5552..b5cbed6e7 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -97,15 +97,10 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) { 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 = () => { - if (location.hostname.includes("opencode.ai")) return localUrl() - if (import.meta.env.DEV) return localUrl() - if (isLocalHost()) return localUrl() + if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (import.meta.env.DEV) + return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` return location.origin } diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index a8f2360bb..a25f8b4b2 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -22,7 +22,7 @@ export function useProviders() { const providers = () => { if (dir()) { const [projectStore] = globalSync.child(dir()) - if (projectStore.provider.all.length > 0) return projectStore.provider + return projectStore.provider } return globalSync.data.provider } diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index d80e9fffb..53063f48f 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,7 +1,6 @@ export { AppBaseProviders, AppInterface } from "./app" export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" export { useCommand } from "./context/command" -export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language" export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform" export { ServerConnection } from "./context/server" export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 6d3b04be9..cd5e079a6 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -2,7 +2,8 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { base64Encode } from "@opencode-ai/util/encode" 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 { LocalProvider } from "@/context/local" import { SDKProvider } from "@/context/sdk" @@ -10,18 +11,10 @@ import { SyncProvider, useSync } from "@/context/sync" import { decode64 } from "@/utils/base64" function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { - const location = useLocation() const navigate = useNavigate() const sync = useSync() 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 ( <DataProvider data={sync.data} @@ -36,31 +29,50 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { export default function Layout(props: ParentProps) { const params = useParams() + const location = useLocation() const language = useLanguage() + const globalSDK = useGlobalSDK() const navigate = useNavigate() let invalid = "" - const resolved = createMemo(() => { - if (!params.dir) return "" - return decode64(params.dir) ?? "" - }) + const [resolved] = createResource( + () => { + if (params.dir) return [location.pathname, params.dir] as const + }, + async ([pathname, b64Dir]) => { + const directory = decode64(b64Dir) - createEffect(() => { - const dir = params.dir - if (!dir) return - if (resolved()) { - invalid = "" - return - } - if (invalid === dir) return - invalid = dir - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: language.t("directory.error.invalidUrl"), - }) - navigate("/", { replace: true }) - }) + if (!directory) { + if (invalid === params.dir) return + invalid = b64Dir + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: language.t("directory.error.invalidUrl"), + }) + navigate("/", { replace: true }) + return + } + + return await globalSDK + .createClient({ + 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 ( <Show when={resolved()} keyed> diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 4c795b968..ba3a2b942 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -113,14 +113,6 @@ export default function Home() { </ul> </div> </Match> - <Match when={!sync.ready}> - <div class="mt-30 mx-auto flex flex-col items-center gap-3"> - <div class="text-12-regular text-text-weak">{language.t("common.loading")}</div> - <Button class="px-3" onClick={chooseProject}> - {language.t("command.project.open")} - </Button> - </div> - </Match> <Match when={true}> <div class="mt-30 mx-auto flex flex-col items-center gap-3"> <Icon name="folder-add-left" size="large" /> diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b5a96110f..01e151605 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -49,16 +49,21 @@ import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" -import { playSoundById } from "@/utils/sound" +import { playSound, soundSrc } from "@/utils/sound" import { createAim } from "@/utils/aim" import { setNavigate } from "@/utils/notification-click" import { Worktree as WorktreeState } from "@/utils/worktree" import { setSessionHandoff } from "@/pages/session/handoff" 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 { 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 { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" @@ -105,8 +110,6 @@ export default function Layout(props: ParentProps) { const pageReady = createMemo(() => ready()) let scrollContainerRef: HTMLDivElement | undefined - let dialogRun = 0 - let dialogDead = false const params = useParams() const globalSDK = useGlobalSDK() @@ -136,7 +139,7 @@ export default function Layout(props: ParentProps) { 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 colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = { system: "theme.scheme.system", @@ -198,8 +201,6 @@ export default function Layout(props: ParentProps) { }) onCleanup(() => { - dialogDead = true - dialogRun += 1 if (navLeave.current !== undefined) clearTimeout(navLeave.current) clearTimeout(sortNowTimeout) 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 nextThemeId = ids[nextIndex] theme.setTheme(nextThemeId) + const nextTheme = theme.themes()[nextThemeId] showToast({ 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 (settings.sounds.permissionsEnabled()) { - void playSoundById(settings.sounds.permissions()) + playSound(soundSrc(settings.sounds.permissions())) } if (settings.notifications.permissions()) { void platform.notify(title, description, href) @@ -1152,10 +1154,10 @@ export default function Layout(props: ParentProps) { }, ] - for (const [id] of availableThemeEntries()) { + for (const [id, definition] of availableThemeEntries()) { commands.push({ 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"), onSelect: () => theme.commitPreview(), onHighlight: () => { @@ -1206,27 +1208,15 @@ export default function Layout(props: ParentProps) { }) function connectProvider() { - const run = ++dialogRun - void import("@/components/dialog-select-provider").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => <x.DialogSelectProvider />) - }) + dialog.show(() => <DialogSelectProvider />) } function openServer() { - const run = ++dialogRun - void import("@/components/dialog-select-server").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => <x.DialogSelectServer />) - }) + dialog.show(() => <DialogSelectServer />) } function openSettings() { - const run = ++dialogRun - void import("@/components/dialog-settings").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => <x.DialogSettings />) - }) + dialog.show(() => <DialogSettings />) } function projectRoot(directory: string) { @@ -1453,13 +1443,7 @@ export default function Layout(props: ParentProps) { layout.sidebar.toggleWorkspaces(project.worktree) } - const showEditProjectDialog = (project: LocalProject) => { - const run = ++dialogRun - void import("@/components/dialog-edit-project").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => <x.DialogEditProject project={project} />) - }) - } + const showEditProjectDialog = (project: LocalProject) => dialog.show(() => <DialogEditProject project={project} />) async function chooseProject() { function resolve(result: string | string[] | null) { @@ -1480,14 +1464,10 @@ export default function Layout(props: ParentProps) { }) resolve(result) } else { - const run = ++dialogRun - void import("@/components/dialog-select-directory").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show( - () => <x.DialogSelectDirectory multiple={true} onSelect={resolve} />, - () => resolve(null), - ) - }) + dialog.show( + () => <DialogSelectDirectory multiple={true} onSelect={resolve} />, + () => resolve(null), + ) } } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 2d3e31355..7a3b476e8 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1184,6 +1184,8 @@ export default function Page() { on( () => sdk.directory, () => { + void file.tree.list("") + const tab = activeFileTab() if (!tab) return const path = file.pathFromTab(tab) @@ -1638,9 +1640,6 @@ export default function Page() { sessionID: () => params.id, messagesReady, visibleUserMessages, - historyMore, - historyLoading, - loadMore: (sessionID) => sync.session.history.loadMore(sessionID), turnStart: historyWindow.turnStart, currentMessageId: () => store.messageId, pendingMessage: () => ui.pendingMessage, @@ -1712,7 +1711,7 @@ export default function Page() { <div class="flex-1 min-h-0 overflow-hidden"> <Switch> <Match when={params.id}> - <Show when={messagesReady()}> + <Show when={lastUserMessage()}> <MessageTimeline mobileChanges={mobileChanges()} mobileFallback={reviewContent({ diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts index c582749d1..5fadb1f22 100644 --- a/packages/app/src/pages/session/use-session-hash-scroll.ts +++ b/packages/app/src/pages/session/use-session-hash-scroll.ts @@ -8,9 +8,6 @@ export const useSessionHashScroll = (input: { sessionID: () => string | undefined messagesReady: () => boolean visibleUserMessages: () => UserMessage[] - historyMore: () => boolean - historyLoading: () => boolean - loadMore: (sessionID: string) => Promise<void> turnStart: () => number currentMessageId: () => string | undefined pendingMessage: () => string | undefined @@ -184,21 +181,6 @@ export const useSessionHashScroll = (input: { queue(() => scrollToMessage(msg, "auto")) }) - createEffect(() => { - const sessionID = input.sessionID() - if (!sessionID || !input.messagesReady()) return - - visibleUserMessages() - - let targetId = input.pendingMessage() - if (!targetId && !clearing) targetId = messageIdFromHash(location.hash) - if (!targetId) return - if (messageById().has(targetId)) return - if (!input.historyMore() || input.historyLoading()) return - - void input.loadMore(sessionID) - }) - onMount(() => { if (typeof window !== "undefined" && "scrollRestoration" in window.history) { window.history.scrollRestoration = "manual" diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index a13fd34ef..45a323c7b 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -14,15 +14,6 @@ interface CheckServerHealthOptions { const defaultTimeoutMs = 3000 const defaultRetryCount = 2 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) { const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout @@ -96,18 +87,5 @@ export function useCheckServerHealth() { const platform = usePlatform() const fetcher = platform.fetch ?? globalThis.fetch - return (http: ServerConnection.HttpBase) => { - 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 - } + return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher) } diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts index 78e5a0c56..6dea812ec 100644 --- a/packages/app/src/utils/sound.ts +++ b/packages/app/src/utils/sound.ts @@ -1,89 +1,106 @@ -let files: Record<string, () => Promise<string>> | undefined -let loads: Record<SoundID, () => Promise<string>> | undefined - -function getFiles() { - if (files) return files - files = import.meta.glob("../../../ui/src/assets/audio/*.aac", { import: "default" }) as Record< - string, - () => Promise<string> - > - return files -} +import alert01 from "@opencode-ai/ui/audio/alert-01.aac" +import alert02 from "@opencode-ai/ui/audio/alert-02.aac" +import alert03 from "@opencode-ai/ui/audio/alert-03.aac" +import alert04 from "@opencode-ai/ui/audio/alert-04.aac" +import alert05 from "@opencode-ai/ui/audio/alert-05.aac" +import alert06 from "@opencode-ai/ui/audio/alert-06.aac" +import alert07 from "@opencode-ai/ui/audio/alert-07.aac" +import alert08 from "@opencode-ai/ui/audio/alert-08.aac" +import alert09 from "@opencode-ai/ui/audio/alert-09.aac" +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 = [ - { id: "alert-01", label: "sound.option.alert01" }, - { id: "alert-02", label: "sound.option.alert02" }, - { id: "alert-03", label: "sound.option.alert03" }, - { id: "alert-04", label: "sound.option.alert04" }, - { id: "alert-05", label: "sound.option.alert05" }, - { id: "alert-06", label: "sound.option.alert06" }, - { id: "alert-07", label: "sound.option.alert07" }, - { id: "alert-08", label: "sound.option.alert08" }, - { id: "alert-09", label: "sound.option.alert09" }, - { id: "alert-10", label: "sound.option.alert10" }, - { id: "bip-bop-01", label: "sound.option.bipbop01" }, - { id: "bip-bop-02", label: "sound.option.bipbop02" }, - { id: "bip-bop-03", label: "sound.option.bipbop03" }, - { id: "bip-bop-04", label: "sound.option.bipbop04" }, - { id: "bip-bop-05", label: "sound.option.bipbop05" }, - { id: "bip-bop-06", label: "sound.option.bipbop06" }, - { id: "bip-bop-07", label: "sound.option.bipbop07" }, - { id: "bip-bop-08", label: "sound.option.bipbop08" }, - { id: "bip-bop-09", label: "sound.option.bipbop09" }, - { id: "bip-bop-10", label: "sound.option.bipbop10" }, - { id: "staplebops-01", label: "sound.option.staplebops01" }, - { id: "staplebops-02", label: "sound.option.staplebops02" }, - { id: "staplebops-03", label: "sound.option.staplebops03" }, - { id: "staplebops-04", label: "sound.option.staplebops04" }, - { id: "staplebops-05", label: "sound.option.staplebops05" }, - { id: "staplebops-06", label: "sound.option.staplebops06" }, - { id: "staplebops-07", label: "sound.option.staplebops07" }, - { id: "nope-01", label: "sound.option.nope01" }, - { id: "nope-02", label: "sound.option.nope02" }, - { id: "nope-03", label: "sound.option.nope03" }, - { id: "nope-04", label: "sound.option.nope04" }, - { id: "nope-05", label: "sound.option.nope05" }, - { id: "nope-06", label: "sound.option.nope06" }, - { id: "nope-07", label: "sound.option.nope07" }, - { id: "nope-08", label: "sound.option.nope08" }, - { id: "nope-09", label: "sound.option.nope09" }, - { id: "nope-10", label: "sound.option.nope10" }, - { id: "nope-11", label: "sound.option.nope11" }, - { id: "nope-12", label: "sound.option.nope12" }, - { id: "yup-01", label: "sound.option.yup01" }, - { id: "yup-02", label: "sound.option.yup02" }, - { id: "yup-03", label: "sound.option.yup03" }, - { id: "yup-04", label: "sound.option.yup04" }, - { id: "yup-05", label: "sound.option.yup05" }, - { id: "yup-06", label: "sound.option.yup06" }, + { id: "alert-01", label: "sound.option.alert01", src: alert01 }, + { id: "alert-02", label: "sound.option.alert02", src: alert02 }, + { id: "alert-03", label: "sound.option.alert03", src: alert03 }, + { id: "alert-04", label: "sound.option.alert04", src: alert04 }, + { id: "alert-05", label: "sound.option.alert05", src: alert05 }, + { id: "alert-06", label: "sound.option.alert06", src: alert06 }, + { id: "alert-07", label: "sound.option.alert07", src: alert07 }, + { id: "alert-08", label: "sound.option.alert08", src: alert08 }, + { id: "alert-09", label: "sound.option.alert09", src: alert09 }, + { id: "alert-10", label: "sound.option.alert10", src: alert10 }, + { id: "bip-bop-01", label: "sound.option.bipbop01", src: bipbop01 }, + { id: "bip-bop-02", label: "sound.option.bipbop02", src: bipbop02 }, + { id: "bip-bop-03", label: "sound.option.bipbop03", src: bipbop03 }, + { id: "bip-bop-04", label: "sound.option.bipbop04", src: bipbop04 }, + { id: "bip-bop-05", label: "sound.option.bipbop05", src: bipbop05 }, + { id: "bip-bop-06", label: "sound.option.bipbop06", src: bipbop06 }, + { id: "bip-bop-07", label: "sound.option.bipbop07", src: bipbop07 }, + { id: "bip-bop-08", label: "sound.option.bipbop08", src: bipbop08 }, + { id: "bip-bop-09", label: "sound.option.bipbop09", src: bipbop09 }, + { id: "bip-bop-10", label: "sound.option.bipbop10", src: bipbop10 }, + { id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 }, + { id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 }, + { id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 }, + { id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 }, + { id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 }, + { id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 }, + { id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 }, + { id: "nope-01", label: "sound.option.nope01", src: nope01 }, + { id: "nope-02", label: "sound.option.nope02", src: nope02 }, + { id: "nope-03", label: "sound.option.nope03", src: nope03 }, + { id: "nope-04", label: "sound.option.nope04", src: nope04 }, + { id: "nope-05", label: "sound.option.nope05", src: nope05 }, + { id: "nope-06", label: "sound.option.nope06", src: nope06 }, + { id: "nope-07", label: "sound.option.nope07", src: nope07 }, + { id: "nope-08", label: "sound.option.nope08", src: nope08 }, + { id: "nope-09", label: "sound.option.nope09", src: nope09 }, + { id: "nope-10", label: "sound.option.nope10", src: nope10 }, + { id: "nope-11", label: "sound.option.nope11", src: nope11 }, + { id: "nope-12", label: "sound.option.nope12", src: nope12 }, + { id: "yup-01", label: "sound.option.yup01", src: yup01 }, + { id: "yup-02", label: "sound.option.yup02", src: yup02 }, + { id: "yup-03", label: "sound.option.yup03", src: yup03 }, + { id: "yup-04", label: "sound.option.yup04", src: yup04 }, + { id: "yup-05", label: "sound.option.yup05", src: yup05 }, + { id: "yup-06", label: "sound.option.yup06", src: yup06 }, ] as const export type SoundOption = (typeof SOUND_OPTIONS)[number] export type SoundID = SoundOption["id"] -function getLoads() { - 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>>() +const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record<SoundID, string> export function soundSrc(id: string | undefined) { - const loads = getLoads() - if (!id || !(id in loads)) return Promise.resolve(undefined) - const key = id as SoundID - const hit = cache.get(key) - if (hit) return hit - const next = loads[key]().catch(() => undefined) - cache.set(key, next) - return next + if (!id) return + if (!(id in soundById)) return + return soundById[id as SoundID] } export function playSound(src: string | undefined) { @@ -91,12 +108,10 @@ export function playSound(src: string | undefined) { if (!src) return const audio = new Audio(src) audio.play().catch(() => undefined) + + // Return a cleanup function to pause the sound. return () => { audio.pause() audio.currentTime = 0 } } - -export function playSoundById(id: string | undefined) { - return soundSrc(id).then((src) => playSound(src)) -} diff --git a/packages/app/vite.js b/packages/app/vite.js index f65a68a1c..6b8fd6137 100644 --- a/packages/app/vite.js +++ b/packages/app/vite.js @@ -1,10 +1,7 @@ -import { readFileSync } from "node:fs" import solidPlugin from "vite-plugin-solid" import tailwindcss from "@tailwindcss/vite" import { fileURLToPath } from "url" -const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url)) - /** * @type {import("vite").PluginOption} */ @@ -24,15 +21,6 @@ export default [ } }, }, - { - name: "opencode-desktop:theme-preload", - transformIndexHtml(html) { - return html.replace( - '<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>', - `<script id="oc-theme-preload-script">${readFileSync(theme, "utf8")}</script>`, - ) - }, - }, tailwindcss(), solidPlugin(), ] |
