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');