perf(ui): trim hidden session and bootstrap work

This commit is contained in:
Pascal André
2026-03-14 11:50:56 +01:00
parent 695c3fa954
commit 6f15ba2051
6 changed files with 151 additions and 64 deletions

View File

@@ -36,7 +36,6 @@ import { useI18n } from "../../lib/i18n"
import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances"
import SessionSidebar from "./shell/SessionSidebar"
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
import RightPanel from "./shell/right-panel/RightPanel"
import { useDrawerChrome } from "./shell/useDrawerChrome"
import { getSessionStatus } from "../../stores/session-status"
import { Maximize2, ShieldAlert } from "lucide-solid"
@@ -64,6 +63,11 @@ const LazyBackgroundProcessOutputDialog = lazy(() =>
import("../background-process-output-dialog").then((module) => ({ default: module.BackgroundProcessOutputDialog })),
)
const LazyPermissionApprovalModal = lazy(() => import("../permission-approval-modal"))
const LazyRightPanel = lazy(() => import("./shell/right-panel/RightPanel"))
function RightPanelFallback() {
return <div class="flex-1 min-h-0" />
}
interface InstanceShellProps {
instance: Instance
@@ -499,28 +503,30 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
role="presentation"
aria-hidden="true"
/>
<RightPanel
t={t}
instanceId={props.instance.id}
instance={props.instance}
activeSessionId={activeSessionIdForInstance}
activeSession={activeSessionForInstance}
activeSessionDiffs={activeSessionDiffs}
latestTodoState={latestTodoState}
backgroundProcessList={backgroundProcessList}
onOpenBackgroundOutput={openBackgroundOutput}
onStopBackgroundProcess={stopBackgroundProcess}
onTerminateBackgroundProcess={terminateBackgroundProcess}
isPhoneLayout={isPhoneLayout}
rightDrawerWidth={rightDrawerWidth}
rightDrawerWidthInitialized={rightDrawerWidthInitialized}
rightDrawerState={rightDrawerState}
rightPinned={rightPinned}
onCloseRightDrawer={closeRightDrawer}
onPinRightDrawer={pinRightDrawer}
onUnpinRightDrawer={unpinRightDrawer}
setContentEl={setRightDrawerContentEl}
/>
<Suspense fallback={<RightPanelFallback />}>
<LazyRightPanel
t={t}
instanceId={props.instance.id}
instance={props.instance}
activeSessionId={activeSessionIdForInstance}
activeSession={activeSessionForInstance}
activeSessionDiffs={activeSessionDiffs}
latestTodoState={latestTodoState}
backgroundProcessList={backgroundProcessList}
onOpenBackgroundOutput={openBackgroundOutput}
onStopBackgroundProcess={stopBackgroundProcess}
onTerminateBackgroundProcess={terminateBackgroundProcess}
isPhoneLayout={isPhoneLayout}
rightDrawerWidth={rightDrawerWidth}
rightDrawerWidthInitialized={rightDrawerWidthInitialized}
rightDrawerState={rightDrawerState}
rightPinned={rightPinned}
onCloseRightDrawer={closeRightDrawer}
onPinRightDrawer={pinRightDrawer}
onUnpinRightDrawer={unpinRightDrawer}
setContentEl={setRightDrawerContentEl}
/>
</Suspense>
</Box>
)
}
@@ -560,28 +566,32 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
aria-hidden="true"
/>
</Show>
<RightPanel
t={t}
instanceId={props.instance.id}
instance={props.instance}
activeSessionId={activeSessionIdForInstance}
activeSession={activeSessionForInstance}
activeSessionDiffs={activeSessionDiffs}
latestTodoState={latestTodoState}
backgroundProcessList={backgroundProcessList}
onOpenBackgroundOutput={openBackgroundOutput}
onStopBackgroundProcess={stopBackgroundProcess}
onTerminateBackgroundProcess={terminateBackgroundProcess}
isPhoneLayout={isPhoneLayout}
rightDrawerWidth={rightDrawerWidth}
rightDrawerWidthInitialized={rightDrawerWidthInitialized}
rightDrawerState={rightDrawerState}
rightPinned={rightPinned}
onCloseRightDrawer={closeRightDrawer}
onPinRightDrawer={pinRightDrawer}
onUnpinRightDrawer={unpinRightDrawer}
setContentEl={setRightDrawerContentEl}
/>
<Show when={rightOpen() || rightPinned()} fallback={<RightPanelFallback />}>
<Suspense fallback={<RightPanelFallback />}>
<LazyRightPanel
t={t}
instanceId={props.instance.id}
instance={props.instance}
activeSessionId={activeSessionIdForInstance}
activeSession={activeSessionForInstance}
activeSessionDiffs={activeSessionDiffs}
latestTodoState={latestTodoState}
backgroundProcessList={backgroundProcessList}
onOpenBackgroundOutput={openBackgroundOutput}
onStopBackgroundProcess={stopBackgroundProcess}
onTerminateBackgroundProcess={terminateBackgroundProcess}
isPhoneLayout={isPhoneLayout}
rightDrawerWidth={rightDrawerWidth}
rightDrawerWidthInitialized={rightDrawerWidthInitialized}
rightDrawerState={rightDrawerState}
rightPinned={rightPinned}
onCloseRightDrawer={closeRightDrawer}
onPinRightDrawer={pinRightDrawer}
onUnpinRightDrawer={unpinRightDrawer}
setContentEl={setRightDrawerContentEl}
/>
</Suspense>
</Show>
</Drawer>
)

View File

@@ -2,10 +2,30 @@ import { createEffect, createSignal, type Accessor } from "solid-js"
import { messageStoreBus } from "../../../stores/message-v2/bus"
import { clearSessionRenderCache } from "../../message-block"
import { getLogger } from "../../../lib/logger"
import { runtimeEnv } from "../../../lib/runtime-env"
const log = getLogger("session")
const SESSION_CACHE_LIMIT = 5
function getSessionCacheLimit() {
if (runtimeEnv.platform === "mobile") {
return 2
}
if (runtimeEnv.host === "tauri") {
return 3
}
if (typeof navigator !== "undefined") {
const deviceMemory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory
if (typeof deviceMemory === "number" && deviceMemory <= 4) {
return 3
}
}
return 5
}
const SESSION_CACHE_LIMIT = getSessionCacheLimit()
type SessionCacheOptions = {
instanceId: Accessor<string>

View File

@@ -131,9 +131,15 @@ export const SessionView: Component<SessionViewProps> = (props) => {
createEffect(() => {
const currentSession = session()
if (currentSession) {
loadMessages(props.instanceId, currentSession.id).catch((error) => log.error("Failed to load messages", error))
if (!currentSession) {
return
}
if (props.isActive === false) {
return
}
loadMessages(props.instanceId, currentSession.id).catch((error) => log.error("Failed to load messages", error))
})
function registerPromptInputApi(api: PromptInputApi) {

View File

@@ -0,0 +1,48 @@
export type UiBootstrapTheme = "light" | "dark" | "system"
export interface UiBootstrapCacheSnapshot {
theme?: UiBootstrapTheme
locale?: string
}
const UI_BOOTSTRAP_CACHE_KEY = "codenomad:ui-bootstrap"
export function readUiBootstrapCache(): UiBootstrapCacheSnapshot {
if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
return {}
}
try {
const raw = window.localStorage.getItem(UI_BOOTSTRAP_CACHE_KEY)
if (!raw) {
return {}
}
const parsed = JSON.parse(raw) as UiBootstrapCacheSnapshot
if (!parsed || typeof parsed !== "object") {
return {}
}
return {
theme:
parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system"
? parsed.theme
: undefined,
locale: typeof parsed.locale === "string" ? parsed.locale : undefined,
}
} catch {
return {}
}
}
export function writeUiBootstrapCache(snapshot: UiBootstrapCacheSnapshot) {
if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
return
}
try {
window.localStorage.setItem(UI_BOOTSTRAP_CACHE_KEY, JSON.stringify(snapshot))
} catch {
/* noop */
}
}

View File

@@ -5,7 +5,7 @@ import { ConfigProvider } from "./stores/preferences"
import { InstanceConfigProvider } from "./stores/instance-config"
import { runtimeEnv } from "./lib/runtime-env"
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
import { storage } from "./lib/storage"
import { readUiBootstrapCache } from "./lib/ui-bootstrap-cache"
import "./index.css"
import "@git-diff-view/solid/styles/diff-view-pure.css"
@@ -29,22 +29,16 @@ async function bootstrap() {
// (and then refine once persisted config loads).
document.documentElement.removeAttribute("data-theme")
try {
const uiConfig = await storage.loadConfigOwner("ui")
const theme = (uiConfig as any)?.theme ?? "system"
const locale = (uiConfig as any)?.settings?.locale
const bootstrapCache = readUiBootstrapCache()
const theme = bootstrapCache.theme ?? "system"
if (theme === "system") {
document.documentElement.removeAttribute("data-theme")
} else {
document.documentElement.setAttribute("data-theme", theme)
}
await preloadLocaleMessages(typeof locale === "string" ? locale : undefined)
} catch {
// If config fails to load, fall back to CSS defaults.
await preloadLocaleMessages(undefined)
if (theme === "system") {
document.documentElement.removeAttribute("data-theme")
} else {
document.documentElement.setAttribute("data-theme", theme)
}
await preloadLocaleMessages(bootstrapCache.locale)
}
render(

View File

@@ -1,6 +1,7 @@
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from "solid-js"
import type { Accessor, ParentComponent } from "solid-js"
import { storage, type OwnerBucket } from "../lib/storage"
import { writeUiBootstrapCache } from "../lib/ui-bootstrap-cache"
import {
ensureInstanceConfigLoaded,
getInstanceConfig,
@@ -598,6 +599,14 @@ const configContextValue: ConfigContextValue = {
}
export const ConfigProvider: ParentComponent = (props) => {
createEffect(() => {
const bucket = uiConfigBucket()
writeUiBootstrapCache({
theme: bucket.theme,
locale: bucket.settings?.locale,
})
})
onMount(() => {
ensureLoaded().catch((error: unknown) => {
log.error("Failed to initialize settings", error)