perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing cache, synchronization, and persistence correctness across runtimes, projects, directories, and worktrees. - prioritize selected and visible sessions during bootstrap and defer non-critical enrichment work - reduce redundant message loading, event processing, store publication, and hidden sidebar work - prevent stale session and message requests from overwriting newer authoritative state - preserve existing data when authoritative fetches fail instead of treating failures as successful empty responses - scope session materialization, messages, drafts, queues, todos, pins, permissions, folders, tabs, Git state, and pull request data by runtime and directory identity - harden runtime switching, reconnect, cleanup, mutation reconciliation, and persisted-state ordering - preserve live subagent Task linkage when metadata arrives after an older message request or while streaming parts are suspended - coalesce overlapping tail refreshes without losing newer refresh demand - improve cold-session loading by moving deferrable work out of the critical bootstrap path - isolate URL authentication, mobile credentials, native secrets, and other runtime-owned state across endpoint changes - bound long-lived caches and remove avoidable allocations from event and rendering hot paths - limit virtualization to archive collections where it improves rendering without disrupting active sidebar layout - stabilize session folders, pin ordering, expanded state, and persisted sidebar behavior - open skill files through the same secure editor and outside-workspace grant flow used by file navigation, including worktree sessions - expand regression coverage for stale completions, runtime collisions, reconnect behavior, persistence races, authoritative empty results, and subagent refresh ordering - document the updated synchronization, cache ownership, performance, and runtime-isolation invariants
This commit is contained in:
committed by
GitHub
parent
485efc7117
commit
85400459e9
@@ -2,7 +2,13 @@ import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { createProjectIdFromPath } from '@/lib/projectId';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions';
|
||||
import { isFollowUpBehavior, normalizeFollowUpBehavior, useMessageQueueStore, type FollowUpBehavior } from '@/stores/messageQueueStore';
|
||||
import {
|
||||
DEFAULT_FOLLOW_UP_BEHAVIOR,
|
||||
isFollowUpBehavior,
|
||||
normalizeFollowUpBehavior,
|
||||
useMessageQueueStore,
|
||||
type FollowUpBehavior,
|
||||
} from '@/stores/messageQueueStore';
|
||||
import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
|
||||
@@ -12,6 +18,8 @@ import { normalizeMobileKeyboardMode, setStoredMobileKeyboardMode } from '@/lib/
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { isTerminalShell } from '@/lib/terminalShell';
|
||||
import { getRuntimeKey, subscribeRuntimeEndpointChanged, subscribeRuntimeEndpointWillChange } from '@/lib/runtime-switch';
|
||||
import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID } from '@/lib/theme/themes';
|
||||
import { DEFAULT_OPEN_IN_APP_ID } from '@/lib/openInApps';
|
||||
|
||||
export const applyPersistedHomeDirectoryToWindow = (homeDirectory: string): void => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -28,32 +36,80 @@ export const applyPersistedHomeDirectoryToWindow = (homeDirectory: string): void
|
||||
}
|
||||
};
|
||||
|
||||
const SETTINGS_MIRROR_INDEX_KEY = 'openchamber.settingsMirror.v2.index';
|
||||
const SETTINGS_MIRROR_KEY_PREFIX = 'openchamber.settingsMirror.v2:';
|
||||
const MAX_SETTINGS_MIRROR_RUNTIMES = 5;
|
||||
|
||||
export const getRuntimeSettingsMirrorStorageKey = (runtimeKey: string): string =>
|
||||
`${SETTINGS_MIRROR_KEY_PREFIX}${encodeURIComponent(runtimeKey)}`;
|
||||
|
||||
const setOrRemoveLocalStorage = (key: string, value: string | null): void => {
|
||||
if (value === null) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
const persistRuntimeSettingsMirror = (settings: DesktopSettings, runtimeKey: string): void => {
|
||||
const mirror = {
|
||||
themeId: settings.themeId,
|
||||
themeVariant: settings.themeVariant,
|
||||
lightThemeId: settings.lightThemeId,
|
||||
darkThemeId: settings.darkThemeId,
|
||||
useSystemTheme: settings.useSystemTheme,
|
||||
lastDirectory: settings.lastDirectory,
|
||||
homeDirectory: settings.homeDirectory,
|
||||
projects: settings.projects,
|
||||
activeProjectId: settings.activeProjectId,
|
||||
pinnedDirectories: settings.pinnedDirectories,
|
||||
gitmojiEnabled: settings.gitmojiEnabled,
|
||||
directoryShowHidden: settings.directoryShowHidden,
|
||||
filesViewShowGitignored: settings.filesViewShowGitignored,
|
||||
openInAppId: settings.openInAppId,
|
||||
pwaAppName: settings.pwaAppName,
|
||||
mobileKeyboardMode: settings.mobileKeyboardMode,
|
||||
openCodeUpdateToastDismissedVersion: settings.openCodeUpdateToastDismissedVersion,
|
||||
dictationEnabled: settings.dictationEnabled,
|
||||
sttProvider: settings.sttProvider,
|
||||
sttServerUrl: settings.sttServerUrl,
|
||||
sttModel: settings.sttModel,
|
||||
sttLocalModel: settings.sttLocalModel,
|
||||
sttLanguage: settings.sttLanguage,
|
||||
};
|
||||
localStorage.setItem(getRuntimeSettingsMirrorStorageKey(runtimeKey), JSON.stringify(mirror));
|
||||
|
||||
let previous: string[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(SETTINGS_MIRROR_INDEX_KEY) ?? '[]') as unknown;
|
||||
if (Array.isArray(parsed)) previous = parsed.filter((entry): entry is string => typeof entry === 'string');
|
||||
} catch {
|
||||
previous = [];
|
||||
}
|
||||
const runtimes = [runtimeKey, ...previous.filter((entry) => entry !== runtimeKey)].slice(0, MAX_SETTINGS_MIRROR_RUNTIMES);
|
||||
for (const staleRuntime of previous) {
|
||||
if (!runtimes.includes(staleRuntime)) localStorage.removeItem(getRuntimeSettingsMirrorStorageKey(staleRuntime));
|
||||
}
|
||||
localStorage.setItem(SETTINGS_MIRROR_INDEX_KEY, JSON.stringify(runtimes));
|
||||
};
|
||||
|
||||
const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.themeId) {
|
||||
localStorage.setItem('selectedThemeId', settings.themeId);
|
||||
}
|
||||
if (settings.themeVariant) {
|
||||
localStorage.setItem('selectedThemeVariant', settings.themeVariant);
|
||||
}
|
||||
if (settings.lightThemeId) {
|
||||
localStorage.setItem('lightThemeId', settings.lightThemeId);
|
||||
}
|
||||
if (settings.darkThemeId) {
|
||||
localStorage.setItem('darkThemeId', settings.darkThemeId);
|
||||
}
|
||||
if (typeof settings.useSystemTheme === 'boolean') {
|
||||
localStorage.setItem('useSystemTheme', String(settings.useSystemTheme));
|
||||
}
|
||||
if (settings.lastDirectory) {
|
||||
localStorage.setItem('lastDirectory', settings.lastDirectory);
|
||||
}
|
||||
persistRuntimeSettingsMirror(settings, getRuntimeKey());
|
||||
setOrRemoveLocalStorage('selectedThemeId', settings.themeId || null);
|
||||
setOrRemoveLocalStorage('selectedThemeVariant', settings.themeVariant || null);
|
||||
setOrRemoveLocalStorage('lightThemeId', settings.lightThemeId || null);
|
||||
setOrRemoveLocalStorage('darkThemeId', settings.darkThemeId || null);
|
||||
setOrRemoveLocalStorage('useSystemTheme', typeof settings.useSystemTheme === 'boolean' ? String(settings.useSystemTheme) : null);
|
||||
setOrRemoveLocalStorage('lastDirectory', settings.lastDirectory || null);
|
||||
if (settings.homeDirectory) {
|
||||
localStorage.setItem('homeDirectory', settings.homeDirectory);
|
||||
applyPersistedHomeDirectoryToWindow(settings.homeDirectory);
|
||||
} else {
|
||||
localStorage.removeItem('homeDirectory');
|
||||
}
|
||||
if (Array.isArray(settings.projects) && settings.projects.length > 0) {
|
||||
localStorage.setItem('projects', JSON.stringify(settings.projects));
|
||||
@@ -81,6 +137,8 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
} else {
|
||||
localStorage.removeItem('oc.sessions.projectCollapse');
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('oc.sessions.projectCollapse');
|
||||
}
|
||||
if (typeof settings.gitmojiEnabled === 'boolean') {
|
||||
localStorage.setItem('gitmojiEnabled', String(settings.gitmojiEnabled));
|
||||
@@ -89,13 +147,15 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
}
|
||||
if (typeof settings.directoryShowHidden === 'boolean') {
|
||||
localStorage.setItem('directoryTreeShowHidden', settings.directoryShowHidden ? 'true' : 'false');
|
||||
} else {
|
||||
localStorage.removeItem('directoryTreeShowHidden');
|
||||
}
|
||||
if (typeof settings.filesViewShowGitignored === 'boolean') {
|
||||
localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false');
|
||||
} else {
|
||||
localStorage.removeItem('filesViewShowGitignored');
|
||||
}
|
||||
if (typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0) {
|
||||
localStorage.setItem('openInAppId', settings.openInAppId);
|
||||
}
|
||||
setOrRemoveLocalStorage('openInAppId', typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0 ? settings.openInAppId : null);
|
||||
if (typeof settings.pwaAppName === 'string') {
|
||||
const normalized = settings.pwaAppName.trim().replace(/\s+/g, ' ').slice(0, 64);
|
||||
if (normalized.length > 0) {
|
||||
@@ -103,10 +163,10 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
} else {
|
||||
localStorage.removeItem('openchamber.pwaName');
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('openchamber.pwaName');
|
||||
}
|
||||
if (typeof settings.mobileKeyboardMode === 'string') {
|
||||
setStoredMobileKeyboardMode(settings.mobileKeyboardMode);
|
||||
}
|
||||
setStoredMobileKeyboardMode(settings.mobileKeyboardMode);
|
||||
if (typeof settings.openCodeUpdateToastDismissedVersion === 'string') {
|
||||
const version = settings.openCodeUpdateToastDismissedVersion.trim();
|
||||
if (version) {
|
||||
@@ -114,25 +174,23 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
} else {
|
||||
localStorage.removeItem('opencode-update-toast-dismissed-version');
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('opencode-update-toast-dismissed-version');
|
||||
}
|
||||
if (typeof settings.dictationEnabled === 'boolean') {
|
||||
localStorage.setItem('dictationEnabled', String(settings.dictationEnabled));
|
||||
} else {
|
||||
localStorage.removeItem('dictationEnabled');
|
||||
}
|
||||
if (settings.sttProvider === 'local' || settings.sttProvider === 'openai-compatible') {
|
||||
localStorage.setItem('sttProvider', settings.sttProvider);
|
||||
} else {
|
||||
localStorage.removeItem('sttProvider');
|
||||
}
|
||||
if (typeof settings.sttServerUrl === 'string') {
|
||||
localStorage.setItem('sttServerUrl', settings.sttServerUrl);
|
||||
}
|
||||
if (typeof settings.sttModel === 'string') {
|
||||
localStorage.setItem('sttModel', settings.sttModel);
|
||||
}
|
||||
if (typeof settings.sttLocalModel === 'string') {
|
||||
localStorage.setItem('sttLocalModel', settings.sttLocalModel);
|
||||
}
|
||||
if (typeof settings.sttLanguage === 'string') {
|
||||
localStorage.setItem('sttLanguage', settings.sttLanguage);
|
||||
}
|
||||
setOrRemoveLocalStorage('sttServerUrl', typeof settings.sttServerUrl === 'string' ? settings.sttServerUrl : null);
|
||||
setOrRemoveLocalStorage('sttModel', typeof settings.sttModel === 'string' ? settings.sttModel : null);
|
||||
setOrRemoveLocalStorage('sttLocalModel', typeof settings.sttLocalModel === 'string' ? settings.sttLocalModel : null);
|
||||
setOrRemoveLocalStorage('sttLanguage', typeof settings.sttLanguage === 'string' ? settings.sttLanguage : null);
|
||||
};
|
||||
|
||||
const dispatchSettingsSynced = (settings: DesktopSettings): void => {
|
||||
@@ -457,6 +515,91 @@ const getPersistApi = (): PersistApi | undefined => {
|
||||
|
||||
const getRuntimeSettingsAPI = () => getRegisteredRuntimeAPIs()?.settings ?? null;
|
||||
|
||||
const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopSettings => {
|
||||
const defaults = useUIStore.getInitialState();
|
||||
|
||||
return {
|
||||
useSystemTheme: true,
|
||||
lightThemeId: DEFAULT_LIGHT_THEME_ID,
|
||||
darkThemeId: DEFAULT_DARK_THEME_ID,
|
||||
openInAppId: DEFAULT_OPEN_IN_APP_ID,
|
||||
showReasoningTraces: defaults.showReasoningTraces,
|
||||
sessionRecapEnabled: defaults.sessionRecapEnabled,
|
||||
sessionSuggestionEnabled: defaults.sessionSuggestionEnabled,
|
||||
sessionGoalEnabled: defaults.sessionGoalEnabled,
|
||||
sessionGoalDefaultBudgetEnabled: defaults.sessionGoalDefaultBudgetEnabled,
|
||||
sessionGoalDefaultBudget: defaults.sessionGoalDefaultBudget,
|
||||
collapsibleThinkingBlocks: defaults.collapsibleThinkingBlocks,
|
||||
autoDeleteEnabled: defaults.autoDeleteEnabled,
|
||||
autoDeleteAfterDays: defaults.autoDeleteAfterDays,
|
||||
sessionRetentionAction: defaults.sessionRetentionAction,
|
||||
followUpBehavior: DEFAULT_FOLLOW_UP_BEHAVIOR,
|
||||
showDeletionDialog: defaults.showDeletionDialog,
|
||||
nativeNotificationsEnabled: defaults.nativeNotificationsEnabled,
|
||||
notificationMode: defaults.notificationMode,
|
||||
notifyOnSubtasks: defaults.notifyOnSubtasks,
|
||||
notifyOnCompletion: defaults.notifyOnCompletion,
|
||||
notifyOnError: defaults.notifyOnError,
|
||||
notifyOnQuestion: defaults.notifyOnQuestion,
|
||||
notificationTemplates: defaults.notificationTemplates,
|
||||
summarizeLastMessage: defaults.summarizeLastMessage,
|
||||
summaryThreshold: defaults.summaryThreshold,
|
||||
summaryLength: defaults.summaryLength,
|
||||
maxLastMessageLength: defaults.maxLastMessageLength,
|
||||
inputSpellcheckEnabled: defaults.inputSpellcheckEnabled,
|
||||
showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications,
|
||||
showToolFileIcons: defaults.showToolFileIcons,
|
||||
codeBlockLineWrap: defaults.codeBlockLineWrap,
|
||||
showTurnChangedFiles: defaults.showTurnChangedFiles,
|
||||
showExpandedBashTools: defaults.showExpandedBashTools,
|
||||
showExpandedEditTools: defaults.showExpandedEditTools,
|
||||
timeFormatPreference: defaults.timeFormatPreference,
|
||||
weekStartPreference: defaults.weekStartPreference,
|
||||
desktopWindowControlsPosition: defaults.desktopWindowControlsPosition,
|
||||
chatRenderMode: defaults.chatRenderMode,
|
||||
activityRenderMode: defaults.activityRenderMode,
|
||||
mermaidRenderingMode: defaults.mermaidRenderingMode,
|
||||
userMessageRenderingMode: defaults.userMessageRenderingMode,
|
||||
collapsibleUserMessages: defaults.collapsibleUserMessages,
|
||||
messageStreamTransport: 'auto',
|
||||
stickyUserHeader: defaults.stickyUserHeader,
|
||||
promptNavigatorEnabled: defaults.promptNavigatorEnabled,
|
||||
expandedEditorToolbar: defaults.expandedEditorToolbar,
|
||||
wideChatLayoutEnabled: defaults.wideChatLayoutEnabled,
|
||||
showSplitAssistantMessageActions: defaults.showSplitAssistantMessageActions,
|
||||
reportUsage: defaults.reportUsage,
|
||||
fontSize: defaults.fontSize,
|
||||
terminalFontSize: defaults.terminalFontSize,
|
||||
terminalShell: defaults.terminalShell,
|
||||
terminalLoginShells: defaults.terminalLoginShells,
|
||||
editorFontSize: defaults.editorFontSize,
|
||||
uiFont: defaults.uiFont,
|
||||
monoFont: defaults.monoFont,
|
||||
padding: defaults.padding,
|
||||
cornerRadius: defaults.cornerRadius,
|
||||
inputBarOffset: defaults.inputBarOffset,
|
||||
shortcutOverrides: defaults.shortcutOverrides,
|
||||
mobileKeyboardMode: 'resize-content',
|
||||
favoriteModels: defaults.favoriteModels,
|
||||
hiddenModels: defaults.hiddenModels,
|
||||
collapsedModelProviders: defaults.collapsedModelProviders,
|
||||
recentModels: defaults.recentModels,
|
||||
recentAgents: defaults.recentAgents,
|
||||
recentEfforts: defaults.recentEfforts,
|
||||
diffLayoutPreference: defaults.diffLayoutPreference,
|
||||
gitChangesViewMode: defaults.gitChangesViewMode,
|
||||
directoryShowHidden: true,
|
||||
filesViewShowGitignored: false,
|
||||
dictationEnabled: true,
|
||||
sttProvider: 'local',
|
||||
sttServerUrl: 'http://localhost:8001/v1',
|
||||
sttModel: 'deepdml/faster-whisper-large-v3-turbo-ct2',
|
||||
sttLocalModel: 'parakeet-tdt-0.6b-v2-int8',
|
||||
sttLanguage: '',
|
||||
...settings,
|
||||
};
|
||||
};
|
||||
|
||||
const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
||||
const store = useUIStore.getState();
|
||||
const configStore = typeof window !== 'undefined'
|
||||
@@ -1215,6 +1358,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.promptNavigatorEnabled === 'boolean') {
|
||||
result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
|
||||
}
|
||||
if (typeof candidate.expandedEditorToolbar === 'boolean') {
|
||||
result.expandedEditorToolbar = candidate.expandedEditorToolbar;
|
||||
}
|
||||
if (typeof candidate.wideChatLayoutEnabled === 'boolean') {
|
||||
result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled;
|
||||
}
|
||||
@@ -1524,6 +1670,7 @@ export const syncDesktopSettings = async (): Promise<void> => {
|
||||
const applySettings = async (settings: DesktopSettings) => {
|
||||
if (!isSettingsRuntimeContextCurrent(context)) return;
|
||||
const shouldPersistCraftGoalMigration = settings.draftStartersCraftGoalAdded !== true;
|
||||
const authoritativeSettings = materializeAuthoritativeUiSettings(settings);
|
||||
try {
|
||||
persistToLocalStorage(settings);
|
||||
} catch (error) {
|
||||
@@ -1531,20 +1678,23 @@ export const syncDesktopSettings = async (): Promise<void> => {
|
||||
}
|
||||
await waitForHydration();
|
||||
if (!isSettingsRuntimeContextCurrent(context)) return;
|
||||
if (settings.draftStarters === undefined) {
|
||||
useUIStore.setState({ globalDraftStarters: null });
|
||||
}
|
||||
try {
|
||||
applyDesktopUiPreferences(settings);
|
||||
applyDesktopUiPreferences(authoritativeSettings);
|
||||
} catch (error) {
|
||||
console.warn('applyDesktopUiPreferences failed:', error);
|
||||
}
|
||||
if (shouldPersistCraftGoalMigration) {
|
||||
await updateDesktopSettings({
|
||||
...(settings.draftStarters ? { draftStarters: settings.draftStarters } : {}),
|
||||
...(authoritativeSettings.draftStarters ? { draftStarters: authoritativeSettings.draftStarters } : {}),
|
||||
draftStartersCraftGoalAdded: true,
|
||||
});
|
||||
if (!isSettingsRuntimeContextCurrent(context)) return;
|
||||
}
|
||||
|
||||
dispatchSettingsSynced(settings);
|
||||
dispatchSettingsSynced(authoritativeSettings);
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1579,7 +1729,6 @@ async function _flushSettingsUpdate(): Promise<void> {
|
||||
const updated = await runtimeSettings.save(changes);
|
||||
if (!isSettingsRuntimeContextCurrent(context)) return;
|
||||
if (updated) {
|
||||
persistToLocalStorage(updated);
|
||||
applyDesktopUiPreferences(updated);
|
||||
dispatchSettingsSynced(updated);
|
||||
_settingsCache = null;
|
||||
@@ -1613,7 +1762,6 @@ async function _flushSettingsUpdate(): Promise<void> {
|
||||
const updated = (await response.json().catch(() => null)) as DesktopSettings | null;
|
||||
if (!isSettingsRuntimeContextCurrent(context)) return;
|
||||
if (updated) {
|
||||
persistToLocalStorage(updated);
|
||||
applyDesktopUiPreferences(updated);
|
||||
dispatchSettingsSynced(updated);
|
||||
dispatchSettingsSaveState('saved');
|
||||
|
||||
Reference in New Issue
Block a user