Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions ui/desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
})();
</script>
<link href="./src/styles/main.css" rel="stylesheet" />
Expand Down
106 changes: 57 additions & 49 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
}
}
Expand All @@ -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' &&
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 17 additions & 21 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
8 changes: 5 additions & 3 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 13 additions & 9 deletions ui/desktop/src/utils/providerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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');
Expand Down
Loading