perf(ui): defer locale and overlay bundles
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
import { Component, For, Show, Suspense, createMemo, createEffect, createSignal, lazy, onMount, onCleanup } from "solid-js"
|
||||||
import { Dialog } from "@kobalte/core/dialog"
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
import { Toaster } from "solid-toast"
|
import { Toaster } from "solid-toast"
|
||||||
import useMediaQuery from "@suid/material/useMediaQuery"
|
import useMediaQuery from "@suid/material/useMediaQuery"
|
||||||
import { Minimize2 } from "lucide-solid"
|
import { Minimize2 } from "lucide-solid"
|
||||||
import AlertDialog from "./components/alert-dialog"
|
import AlertDialog from "./components/alert-dialog"
|
||||||
import FolderSelectionView from "./components/folder-selection-view"
|
|
||||||
import { showConfirmDialog } from "./stores/alerts"
|
import { showConfirmDialog } from "./stores/alerts"
|
||||||
import InstanceTabs from "./components/instance-tabs"
|
import InstanceTabs from "./components/instance-tabs"
|
||||||
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
|
||||||
import InstanceShell from "./components/instance/instance-shell2"
|
import InstanceShell from "./components/instance/instance-shell2"
|
||||||
import { SettingsScreen } from "./components/settings-screen"
|
|
||||||
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
||||||
import { initMarkdown } from "./lib/markdown"
|
import { initMarkdown } from "./lib/markdown"
|
||||||
import { initGithubStars } from "./stores/github-stars"
|
import { initGithubStars } from "./stores/github-stars"
|
||||||
@@ -54,10 +51,16 @@ import {
|
|||||||
} from "./stores/sessions"
|
} from "./stores/sessions"
|
||||||
|
|
||||||
import { getInstanceSessionIndicatorStatus } from "./stores/session-status"
|
import { getInstanceSessionIndicatorStatus } from "./stores/session-status"
|
||||||
import { openSettings } from "./stores/settings-screen"
|
import { openSettings, settingsOpen } from "./stores/settings-screen"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
|
const LazyFolderSelectionView = lazy(() => import("./components/folder-selection-view"))
|
||||||
|
const LazyInstanceDisconnectedModal = lazy(() => import("./components/instance-disconnected-modal"))
|
||||||
|
const LazySettingsScreen = lazy(() =>
|
||||||
|
import("./components/settings-screen").then((module) => ({ default: module.SettingsScreen })),
|
||||||
|
)
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -409,12 +412,16 @@ const App: Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InstanceDisconnectedModal
|
<Show when={Boolean(disconnectedInstance())}>
|
||||||
open={Boolean(disconnectedInstance())}
|
<Suspense fallback={null}>
|
||||||
folder={disconnectedInstance()?.folder}
|
<LazyInstanceDisconnectedModal
|
||||||
reason={disconnectedInstance()?.reason}
|
open={Boolean(disconnectedInstance())}
|
||||||
onClose={handleDisconnectedInstanceClose}
|
folder={disconnectedInstance()?.folder}
|
||||||
/>
|
reason={disconnectedInstance()?.reason}
|
||||||
|
onClose={handleDisconnectedInstanceClose}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Dialog open={Boolean(launchError())} modal>
|
<Dialog open={Boolean(launchError())} modal>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
@@ -527,28 +534,36 @@ const App: Component = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FolderSelectionView
|
<Suspense fallback={<div class="flex-1 min-h-0" />}>
|
||||||
onSelectFolder={handleSelectFolder}
|
<LazyFolderSelectionView
|
||||||
isLoading={isSelectingFolder()}
|
onSelectFolder={handleSelectFolder}
|
||||||
/>
|
isLoading={isSelectingFolder()}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={showFolderSelection()}>
|
<Show when={showFolderSelection()}>
|
||||||
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<FolderSelectionView
|
<Suspense fallback={<div class="w-full h-full" />}>
|
||||||
onSelectFolder={handleSelectFolder}
|
<LazyFolderSelectionView
|
||||||
isLoading={isSelectingFolder()}
|
onSelectFolder={handleSelectFolder}
|
||||||
onClose={() => {
|
isLoading={isSelectingFolder()}
|
||||||
setShowFolderSelection(false)
|
onClose={() => {
|
||||||
clearLaunchError()
|
setShowFolderSelection(false)
|
||||||
}}
|
clearLaunchError()
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<SettingsScreen />
|
<Show when={settingsOpen()}>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazySettingsScreen />
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<AlertDialog />
|
<AlertDialog />
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
For,
|
For,
|
||||||
Show,
|
Show,
|
||||||
|
Suspense,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
|
lazy,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
onMount,
|
onMount,
|
||||||
type Accessor,
|
type Accessor,
|
||||||
@@ -22,11 +24,7 @@ import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-regi
|
|||||||
|
|
||||||
import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette"
|
import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette"
|
||||||
import Kbd from "../kbd"
|
import Kbd from "../kbd"
|
||||||
import InstanceWelcomeView from "../instance-welcome-view"
|
|
||||||
import InfoView from "../info-view"
|
|
||||||
import CommandPalette from "../command-palette"
|
|
||||||
import PermissionNotificationBanner from "../permission-notification-banner"
|
import PermissionNotificationBanner from "../permission-notification-banner"
|
||||||
import PermissionApprovalModal from "../permission-approval-modal"
|
|
||||||
import SessionView from "../session/session-view"
|
import SessionView from "../session/session-view"
|
||||||
import { formatTokenTotal } from "../../lib/formatters"
|
import { formatTokenTotal } from "../../lib/formatters"
|
||||||
import ContextMeter from "../context-meter"
|
import ContextMeter from "../context-meter"
|
||||||
@@ -34,7 +32,6 @@ import { sseManager } from "../../lib/sse-manager"
|
|||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
||||||
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances"
|
import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances"
|
||||||
import SessionSidebar from "./shell/SessionSidebar"
|
import SessionSidebar from "./shell/SessionSidebar"
|
||||||
@@ -60,6 +57,14 @@ import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
|||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
const LazyInstanceWelcomeView = lazy(() => import("../instance-welcome-view"))
|
||||||
|
const LazyInfoView = lazy(() => import("../info-view"))
|
||||||
|
const LazyCommandPalette = lazy(() => import("../command-palette"))
|
||||||
|
const LazyBackgroundProcessOutputDialog = lazy(() =>
|
||||||
|
import("../background-process-output-dialog").then((module) => ({ default: module.BackgroundProcessOutputDialog })),
|
||||||
|
)
|
||||||
|
const LazyPermissionApprovalModal = lazy(() => import("../permission-approval-modal"))
|
||||||
|
|
||||||
interface InstanceShellProps {
|
interface InstanceShellProps {
|
||||||
instance: Instance
|
instance: Instance
|
||||||
// Provided by App-level instance tabs; lets us pause heavy rendering
|
// Provided by App-level instance tabs; lets us pause heavy rendering
|
||||||
@@ -834,7 +839,9 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="info-view-pane flex flex-col flex-1 min-h-0 overflow-y-auto">
|
<div class="info-view-pane flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||||
<InfoView instanceId={props.instance.id} />
|
<Suspense fallback={<div class="flex-1 min-h-0" />}>
|
||||||
|
<LazyInfoView instanceId={props.instance.id} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -850,30 +857,49 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
class="instance-shell2 flex flex-col flex-1 min-h-0"
|
class="instance-shell2 flex flex-col flex-1 min-h-0"
|
||||||
data-instance-id={props.instance.id}
|
data-instance-id={props.instance.id}
|
||||||
>
|
>
|
||||||
<Show when={hasSessions()} fallback={<InstanceWelcomeView instance={props.instance} />}>
|
<Show
|
||||||
|
when={hasSessions()}
|
||||||
|
fallback={
|
||||||
|
<Suspense fallback={<div class="flex-1 min-h-0" />}>
|
||||||
|
<LazyInstanceWelcomeView instance={props.instance} />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
>
|
||||||
{sessionLayout}
|
{sessionLayout}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommandPalette
|
<Show when={paletteOpen()}>
|
||||||
open={paletteOpen()}
|
<Suspense fallback={null}>
|
||||||
onClose={() => hideCommandPalette(props.instance.id)}
|
<LazyCommandPalette
|
||||||
commands={instancePaletteCommands()}
|
open={paletteOpen()}
|
||||||
onExecute={props.onExecuteCommand}
|
onClose={() => hideCommandPalette(props.instance.id)}
|
||||||
/>
|
commands={instancePaletteCommands()}
|
||||||
|
onExecute={props.onExecuteCommand}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<BackgroundProcessOutputDialog
|
<Show when={showBackgroundOutput()}>
|
||||||
open={showBackgroundOutput()}
|
<Suspense fallback={null}>
|
||||||
instanceId={props.instance.id}
|
<LazyBackgroundProcessOutputDialog
|
||||||
process={selectedBackgroundProcess()}
|
open={showBackgroundOutput()}
|
||||||
onClose={closeBackgroundOutput}
|
instanceId={props.instance.id}
|
||||||
/>
|
process={selectedBackgroundProcess()}
|
||||||
|
onClose={closeBackgroundOutput}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<PermissionApprovalModal
|
<Show when={permissionModalOpen()}>
|
||||||
instanceId={props.instance.id}
|
<Suspense fallback={null}>
|
||||||
isOpen={permissionModalOpen()}
|
<LazyPermissionApprovalModal
|
||||||
onClose={() => setPermissionModalOpen(false)}
|
instanceId={props.instance.id}
|
||||||
/>
|
isOpen={permissionModalOpen()}
|
||||||
|
onClose={() => setPermissionModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou
|
|||||||
import type { ParentComponent } from "solid-js"
|
import type { ParentComponent } from "solid-js"
|
||||||
import { useConfig } from "../../stores/preferences"
|
import { useConfig } from "../../stores/preferences"
|
||||||
import { enMessages } from "./messages/en"
|
import { enMessages } from "./messages/en"
|
||||||
import { esMessages } from "./messages/es"
|
|
||||||
import { frMessages } from "./messages/fr"
|
|
||||||
import { ruMessages } from "./messages/ru"
|
|
||||||
import { jaMessages } from "./messages/ja"
|
|
||||||
import { zhHansMessages } from "./messages/zh-Hans"
|
|
||||||
|
|
||||||
type Messages = Record<string, string>
|
type Messages = Record<string, string>
|
||||||
|
|
||||||
@@ -16,13 +11,16 @@ export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
|||||||
|
|
||||||
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
||||||
|
|
||||||
const messagesByLocale: Record<Locale, Messages> = {
|
const localeMessagesCache = new Map<Locale, Messages>([["en", enMessages]])
|
||||||
en: enMessages,
|
const localeMessagesPromises = new Map<Locale, Promise<Messages>>()
|
||||||
es: esMessages,
|
|
||||||
fr: frMessages,
|
const localeLoaders: Record<Locale, () => Promise<Messages>> = {
|
||||||
ru: ruMessages,
|
en: async () => enMessages,
|
||||||
ja: jaMessages,
|
es: async () => (await import("./messages/es")).esMessages,
|
||||||
"zh-Hans": zhHansMessages,
|
fr: async () => (await import("./messages/fr")).frMessages,
|
||||||
|
ru: async () => (await import("./messages/ru")).ruMessages,
|
||||||
|
ja: async () => (await import("./messages/ja")).jaMessages,
|
||||||
|
"zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocaleTag(value: string): string {
|
function normalizeLocaleTag(value: string): string {
|
||||||
@@ -84,8 +82,45 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [globalRevision, setGlobalRevision] = createSignal(0)
|
const [globalRevision, setGlobalRevision] = createSignal(0)
|
||||||
const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en"
|
let globalMessages: Messages = enMessages
|
||||||
let globalMessages: Messages = messagesByLocale[initialGlobalLocale]
|
|
||||||
|
function getMessagesForLocale(locale: Locale): Messages {
|
||||||
|
return localeMessagesCache.get(locale) ?? enMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLocaleMessages(locale: Locale): Promise<Messages> {
|
||||||
|
const cached = localeMessagesCache.get(locale)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = localeMessagesPromises.get(locale)
|
||||||
|
if (pending) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = localeLoaders[locale]
|
||||||
|
const promise = loader()
|
||||||
|
.then((messages) => {
|
||||||
|
localeMessagesCache.set(locale, messages)
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
localeMessagesPromises.set(locale, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadLocaleMessages(preferredLocale?: string | null): Promise<Locale> {
|
||||||
|
const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en"
|
||||||
|
globalMessages = await loadLocaleMessages(resolvedLocale)
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return resolvedLocale
|
||||||
|
}
|
||||||
|
|
||||||
export function tGlobal(key: string, params?: TranslateParams): string {
|
export function tGlobal(key: string, params?: TranslateParams): string {
|
||||||
globalRevision()
|
globalRevision()
|
||||||
@@ -102,8 +137,7 @@ const I18nContext = createContext<I18nContextValue>()
|
|||||||
export const I18nProvider: ParentComponent = (props) => {
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
||||||
|
const [resolvedLocale, setResolvedLocale] = createSignal<Locale>("en")
|
||||||
const previousMessages = globalMessages
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const detected = detectNavigatorLocale()
|
const detected = detectNavigatorLocale()
|
||||||
@@ -115,19 +149,42 @@ export const I18nProvider: ParentComponent = (props) => {
|
|||||||
return configured ?? detectedLocale() ?? "en"
|
return configured ?? detectedLocale() ?? "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
|
const messages = createMemo<Messages>(() => getMessagesForLocale(resolvedLocale()))
|
||||||
|
|
||||||
function t(key: string, params?: TranslateParams): string {
|
function t(key: string, params?: TranslateParams): string {
|
||||||
return translateFrom(messages(), key, params)
|
return translateFrom(messages(), key, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
globalMessages = messages()
|
const nextLocale = locale()
|
||||||
setGlobalRevision((value) => value + 1)
|
let cancelled = false
|
||||||
|
|
||||||
|
void loadLocaleMessages(nextLocale)
|
||||||
|
.then((loadedMessages) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localeMessagesCache.set(nextLocale, loadedMessages)
|
||||||
|
setResolvedLocale(nextLocale)
|
||||||
|
globalMessages = loadedMessages
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale("en")
|
||||||
|
globalMessages = enMessages
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
cancelled = true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
globalMessages = previousMessages
|
globalMessages = enMessages
|
||||||
setGlobalRevision((value) => value + 1)
|
setGlobalRevision((value) => value + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme"
|
|||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { I18nProvider } from "./lib/i18n"
|
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
|
||||||
import { storage } from "./lib/storage"
|
import { storage } from "./lib/storage"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
@@ -32,14 +32,18 @@ async function bootstrap() {
|
|||||||
try {
|
try {
|
||||||
const uiConfig = await storage.loadConfigOwner("ui")
|
const uiConfig = await storage.loadConfigOwner("ui")
|
||||||
const theme = (uiConfig as any)?.theme ?? "system"
|
const theme = (uiConfig as any)?.theme ?? "system"
|
||||||
|
const locale = (uiConfig as any)?.settings?.locale
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
document.documentElement.removeAttribute("data-theme")
|
document.documentElement.removeAttribute("data-theme")
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.setAttribute("data-theme", theme)
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await preloadLocaleMessages(typeof locale === "string" ? locale : undefined)
|
||||||
} catch {
|
} catch {
|
||||||
// If config fails to load, fall back to CSS defaults.
|
// If config fails to load, fall back to CSS defaults.
|
||||||
|
await preloadLocaleMessages(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user