diff --git a/ui/desktop/index.html b/ui/desktop/index.html index f606653e47eb..fc995f1ba97e 100644 --- a/ui/desktop/index.html +++ b/ui/desktop/index.html @@ -8,20 +8,42 @@ // Initialize theme before any content loads (function() { function initializeTheme() { - const useSystemTheme = localStorage.getItem('use_system_theme') === 'true'; - const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - const savedTheme = localStorage.getItem('theme'); - const isDark = useSystemTheme ? systemPrefersDark : (savedTheme ? savedTheme === 'dark' : systemPrefersDark); - - if (isDark) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); + try { + if (window.localStorage) { + const useSystemTheme = localStorage.getItem('use_system_theme') === 'true'; + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const savedTheme = localStorage.getItem('theme'); + const isDark = useSystemTheme ? systemPrefersDark : (savedTheme ? savedTheme === 'dark' : systemPrefersDark); + + if (isDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } else { + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (systemPrefersDark) { + document.documentElement.classList.add('dark'); + } + } + } catch (error) { + console.warn('Failed to initialize theme from localStorage, using system preference:', error); + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (systemPrefersDark) { + document.documentElement.classList.add('dark'); + } } } // Run immediately initializeTheme(); + + // Retry after DOM is ready if initial attempt failed + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + setTimeout(initializeTheme, 50); + }); + } })(); diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 79c3ca219c7b..733d0d1d015c 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -40,6 +40,7 @@ import { validateConfig, } from './api/sdk.gen'; import PermissionSettingsView from './components/settings/permission/PermissionSetting'; +import { COST_TRACKING_ENABLED } from './updates'; import { type SessionDetails } from './sessions'; import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView'; @@ -855,53 +856,52 @@ export default function App() { const initializeApp = async () => { try { - // Initialize cost database early to pre-load pricing data - initializeCostDatabase().catch((error) => { - console.error('Failed to initialize cost database:', error); - }); + // Start cost database initialization early (non-blocking) - only if cost tracking is enabled + const costDbPromise = COST_TRACKING_ENABLED + ? initializeCostDatabase().catch((error) => { + console.error('Failed to initialize cost database:', error); + }) + : (() => { + console.log('Cost tracking disabled, skipping cost database initialization'); + return Promise.resolve(); + })(); await initConfig(); + try { await readAllConfig({ throwOnError: true }); } catch (error) { + console.warn('Initial config read failed, attempting recovery:', error); + const configVersion = localStorage.getItem('configVersion'); const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3; + if (shouldMigrateExtensions) { - await backupConfig({ throwOnError: true }); - await initConfig(); - } else { - // Config appears corrupted, try recovery - console.warn('Config file appears corrupted, attempting recovery...'); + console.log('Performing extension migration...'); try { - // First try to validate the config - try { - await validateConfig({ throwOnError: true }); - // Config is valid but readAllConfig failed for another reason - throw new Error('Unable to read config file, it may be malformed'); - } catch (validateError) { - console.log('Config validation failed, attempting recovery...'); - - // Try to recover the config - try { - const recoveryResult = await recoverConfig({ throwOnError: true }); - console.log('Config recovery result:', recoveryResult); - - // Try to read config again after recovery - try { - await readAllConfig({ throwOnError: true }); - console.log('Config successfully recovered and loaded'); - } catch (retryError) { - console.warn('Config still corrupted after recovery, reinitializing...'); - await initConfig(); - } - } catch (recoverError) { - console.warn('Config recovery failed, reinitializing...'); - await initConfig(); - } - } - } catch (recoveryError) { - console.error('Config recovery process failed:', recoveryError); - throw new Error('Unable to read config file, it may be malformed'); + await backupConfig({ throwOnError: true }); + await initConfig(); + } catch (migrationError) { + console.error('Migration failed:', migrationError); + // Continue with recovery attempts + } + } + + // Try recovery if migration didn't work or wasn't needed + console.log('Attempting config recovery...'); + try { + // Try to validate first (faster than recovery) + await validateConfig({ throwOnError: true }); + // If validation passes, try reading again + await readAllConfig({ throwOnError: true }); + } catch (validateError) { + console.log('Config validation failed, attempting recovery...'); + try { + await recoverConfig({ throwOnError: true }); + await readAllConfig({ throwOnError: true }); + } catch (recoverError) { + console.warn('Config recovery failed, reinitializing...'); + await initConfig(); } } } @@ -912,13 +912,21 @@ export default function App() { if (provider && model) { try { - await initializeSystem(provider as string, model as string, { - getExtensions, - addExtension, - }); + // Initialize system in parallel with cost database (if enabled) + const initPromises = [ + initializeSystem(provider as string, model as string, { + getExtensions, + addExtension, + }), + ]; + + if (COST_TRACKING_ENABLED) { + initPromises.push(costDbPromise); + } + + await Promise.all(initPromises); - // Check if we have a recipe config from a deeplink - // But skip navigation if we're ignoring recipe config changes (to prevent conflicts with new window creation) + const recipeConfig = window.appConfig.get('recipe'); if ( recipeConfig && typeof recipeConfig === 'object' && @@ -974,16 +982,16 @@ export default function App() { } } } catch (error) { - console.error('Error in initialization:', error); + console.error('Error in system initialization:', error); if (error instanceof MalformedConfigError) { throw error; } - // Navigate to welcome route - window.history.replaceState({}, '', '/welcome'); + window.location.hash = '#/welcome'; + window.history.replaceState({}, '', '#/welcome'); } } else { - // Navigate to welcome route - window.history.replaceState({}, '', '/welcome'); + window.location.hash = '#/welcome'; + window.history.replaceState({}, '', '#/welcome'); } } catch (error) { console.error('Fatal error during initialization:', error); diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index e1f9a7cba28a..053a1af0009b 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -672,34 +672,30 @@ const createChat = async ( // We need to wait for the window to load before we can access localStorage mainWindow.webContents.on('did-finish-load', () => { const configStr = JSON.stringify(windowConfig).replace(/'/g, "\\'"); - // Add error handling and retry logic for localStorage access mainWindow.webContents .executeJavaScript( ` - try { - if (typeof Storage !== 'undefined' && window.localStorage) { - localStorage.setItem('gooseConfig', '${configStr}'); - } else { - console.warn('localStorage not available, retrying in 100ms'); - setTimeout(() => { - try { + (function() { + function setConfig() { + try { + if (window.localStorage) { localStorage.setItem('gooseConfig', '${configStr}'); - } catch (e) { - console.error('Failed to set localStorage after retry:', e); + return true; + } + } catch (e) { + console.warn('localStorage access failed:', e); + } + return false; + } + + if (!setConfig()) { + setTimeout(() => { + if (!setConfig()) { + console.error('Failed to set localStorage after retry - continuing without localStorage config'); } }, 100); } - } catch (e) { - console.error('Failed to access localStorage:', e); - // Retry after a short delay - setTimeout(() => { - try { - localStorage.setItem('gooseConfig', '${configStr}'); - } catch (retryError) { - console.error('Failed to set localStorage after retry:', retryError); - } - }, 100); - } + })(); ` ) .catch((error) => { diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index cd701b0d2638..496aeb01507d 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -123,9 +123,11 @@ const electronAPI: ElectronAPI = { // Add fallback to localStorage if config from preload is empty or missing if (!config || Object.keys(config).length === 0) { try { - const storedConfig = localStorage.getItem('gooseConfig'); - if (storedConfig) { - return JSON.parse(storedConfig); + if (window.localStorage) { + const storedConfig = localStorage.getItem('gooseConfig'); + if (storedConfig) { + return JSON.parse(storedConfig); + } } } catch (e) { console.warn('Failed to parse stored config from localStorage:', e); diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index a6c089a0118d..bf43a96f710e 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -153,17 +153,20 @@ export const migrateExtensionsToSettingsV3 = async () => { console.error('Failed to parse user settings:', error); } + if (localStorageExtensions.length === 0) { + localStorage.setItem('configVersion', '3'); + console.log('No extensions to migrate. Config version set to 3.'); + return; + } + const migrationErrors: { name: string; error: unknown }[] = []; - for (const extension of localStorageExtensions) { - // NOTE: skip migrating builtin types since there was a format change - // instead we rely on initializeBundledExtensions & syncBundledExtensions - // to handle updating / creating the new builtins to the config.yaml - // For all other extension types we migrate them to config.yaml - if (extension.type !== 'builtin') { + // Process extensions in parallel for better performance + const migrationPromises = localStorageExtensions + .filter((extension) => extension.type !== 'builtin') // Skip builtins as before + .map(async (extension) => { console.log(`Migrating extension ${extension.name} to config.yaml`); try { - // manually import apiAddExtension to set throwOnError true const query: ExtensionQuery = { name: extension.name, config: extension, @@ -180,8 +183,9 @@ export const migrateExtensionsToSettingsV3 = async () => { error: `failed migration with ${JSON.stringify(err)}`, }); } - } - } + }); + + await Promise.allSettled(migrationPromises); if (migrationErrors.length === 0) { localStorage.setItem('configVersion', '3');