From 7ce21b2da6fe78d65dc4eb6c9064f8d8ea954b01 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 22 Aug 2025 12:56:03 -0700 Subject: [PATCH 1/4] added exponential backoff retries to avoid accessing local storage before its available --- ui/desktop/src/main.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 7fd5821715c6..ecf6685c8f45 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -659,25 +659,42 @@ const createChat = async ( .executeJavaScript( ` (function() { + let retryCount = 0; + const maxRetries = 5; + const baseDelay = 100; + function setConfig() { try { - if (window.localStorage) { + if (window.localStorage && typeof window.localStorage.setItem === 'function') { localStorage.setItem('gooseConfig', '${configStr}'); + console.log('[Renderer] Successfully set localStorage config'); return true; + } else { + console.warn('[Renderer] localStorage not available or setItem not a function'); } } catch (e) { - console.warn('localStorage access failed:', e); + console.warn('[Renderer] localStorage access failed:', e); } return false; } - if (!setConfig()) { - setTimeout(() => { - if (!setConfig()) { - console.error('Failed to set localStorage after retry - continuing without localStorage config'); - } - }, 100); + function retrySetConfig() { + if (setConfig()) { + return; // Success, no need to retry + } + + retryCount++; + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + console.log(\`[Renderer] Retrying localStorage config set (attempt \${retryCount + 1}/\${maxRetries}) in \${delay}ms\`); + setTimeout(retrySetConfig, delay); + } else { + console.error('[Renderer] Failed to set localStorage after all retries - continuing without localStorage config'); + } } + + // Initial attempt + retrySetConfig(); })(); ` ) From 7c625833b5a4c74f19784d8e8953dcc6a61bc7fc Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 22 Aug 2025 12:57:28 -0700 Subject: [PATCH 2/4] added direct call to react router navigation with fallback to hash routing to avoid protocol errors in electron --- ui/desktop/src/App.tsx | 120 ++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index edac767f575c..361808f23be1 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,6 +1,13 @@ import { useEffect, useRef, useState } from 'react'; import { IpcRendererEvent } from 'electron'; -import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; +import { + HashRouter, + Routes, + Route, + useNavigate, + useLocation, + NavigateFunction, +} from 'react-router-dom'; import { ErrorUI } from './components/ErrorBoundary'; import { ConfirmationModal } from './components/ui/ConfirmationModal'; import { ToastContainer } from 'react-toastify'; @@ -350,6 +357,21 @@ const ExtensionsRoute = () => { ); }; +// Component to capture navigate function and provide it via callback +const NavigateCapture = ({ + onNavigateReady, +}: { + onNavigateReady: (navigate: NavigateFunction) => void; +}) => { + const navigate = useNavigate(); + + useEffect(() => { + onNavigateReady(navigate); + }, [navigate, onNavigateReady]); + + return null; +}; + export default function App() { const [fatalError, setFatalError] = useState(null); const [modalVisible, setModalVisible] = useState(false); @@ -373,50 +395,59 @@ export default function App() { const { getExtensions, addExtension, read } = useConfig(); const initAttemptedRef = useRef(false); - // Create a setView function for useChat hook - we'll use window.history instead of navigate + // Create a setView function for useChat hook + const navigateRef = useRef(null); const setView = (view: View, viewOptions: ViewOptions = {}) => { console.log(`Setting view to: ${view}`, viewOptions); console.trace('setView called from:'); // This will show the call stack - // Convert view to route navigation using hash routing - switch (view) { - case 'chat': - window.location.hash = '#/'; - break; - case 'pair': - window.location.hash = '#/pair'; - break; - case 'settings': - window.location.hash = '#/settings'; - break; - case 'extensions': - window.location.hash = '#/extensions'; - break; - case 'sessions': - window.location.hash = '#/sessions'; - break; - case 'schedules': - window.location.hash = '#/schedules'; - break; - case 'recipes': - window.location.hash = '#/recipes'; - break; - case 'permission': - window.location.hash = '#/permission'; - break; - case 'ConfigureProviders': - window.location.hash = '#/configure-providers'; - break; - case 'recipeEditor': - window.location.hash = '#/recipe-editor'; - break; - case 'welcome': - window.location.hash = '#/welcome'; - break; - default: - console.error(`Unknown view: ${view}, not navigating anywhere. This is likely a bug.`); - console.trace('Invalid setView call stack:'); - // Don't navigate anywhere for unknown views to avoid unexpected redirects - break; + + // Use React Router navigation if available, otherwise fallback to hash manipulation + if (navigateRef.current) { + const navigationHandler = createNavigationHandler(navigateRef.current); + navigationHandler(view, viewOptions); + } else { + // Fallback to hash manipulation for cases where navigate isn't available yet + console.warn('Navigate function not available, using hash fallback'); + switch (view) { + case 'chat': + window.location.hash = '#/'; + break; + case 'pair': + window.location.hash = '#/pair'; + break; + case 'settings': + window.location.hash = '#/settings'; + break; + case 'extensions': + window.location.hash = '#/extensions'; + break; + case 'sessions': + window.location.hash = '#/sessions'; + break; + case 'schedules': + window.location.hash = '#/schedules'; + break; + case 'recipes': + window.location.hash = '#/recipes'; + break; + case 'permission': + window.location.hash = '#/permission'; + break; + case 'ConfigureProviders': + window.location.hash = '#/configure-providers'; + break; + case 'recipeEditor': + window.location.hash = '#/recipe-editor'; + break; + case 'welcome': + window.location.hash = '#/welcome'; + break; + default: + console.error(`Unknown view: ${view}, not navigating anywhere. This is likely a bug.`); + console.trace('Invalid setView call stack:'); + // Don't navigate anywhere for unknown views to avoid unexpected redirects + break; + } } }; @@ -834,6 +865,11 @@ export default function App() { onCancel={handleCancel} /> )} + { + navigateRef.current = navigate; + }} + />
From 68f50bb5902bf8138db0d167d7599d28b370ae24 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 25 Aug 2025 09:46:15 -0700 Subject: [PATCH 3/4] added comments about initialization order and navigation fallback --- ui/desktop/src/App.tsx | 8 ++++++++ ui/desktop/src/main.ts | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 361808f23be1..052407ea1bd4 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -402,11 +402,19 @@ export default function App() { console.trace('setView called from:'); // This will show the call stack // Use React Router navigation if available, otherwise fallback to hash manipulation + // navigateRef.current may not be available during: + // - Initial app startup before React Router is fully initialized + // - Component unmounting/remounting cycles + // - Error boundary scenarios where React Router context is lost + // - When rendered outside of Router context (though this shouldn't happen in our app) + // - Race conditions during rapid navigation or window creation if (navigateRef.current) { const navigationHandler = createNavigationHandler(navigateRef.current); navigationHandler(view, viewOptions); } else { // Fallback to hash manipulation for cases where navigate isn't available yet + // This is a legacy implementation that directly manipulates the URL hash + // as a backup when React Router navigation is not available console.warn('Navigate function not available, using hash fallback'); switch (view) { case 'chat': diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index ecf6685c8f45..c527a1ddde07 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -655,6 +655,15 @@ 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, "\\'"); + + // This JavaScript is injected into the renderer process to set localStorage with retry logic. + // The retry mechanism is necessary because localStorage may not be immediately available + // during Electron renderer initialization, especially on slower systems or during heavy load. + // We use executeJavaScript from the main process because: + // 1. The main process needs to pass configuration data to the renderer process + // 2. This happens during window initialization before the renderer's React app is ready + // 3. The timing is critical - we need to set config before React components try to read it + // 4. Direct IPC communication would require the renderer to be fully loaded first mainWindow.webContents .executeJavaScript( ` From 3ade8bb6c0422f61fe2ef707b70f65ba46ea1220 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 25 Aug 2025 10:07:57 -0700 Subject: [PATCH 4/4] move localhost storage injection script to own util file for better maintenance --- ui/desktop/src/main.ts | 51 ++----------------- .../src/utils/localStorageInjectionScript.ts | 50 ++++++++++++++++++ 2 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 ui/desktop/src/utils/localStorageInjectionScript.ts diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index c527a1ddde07..83f6908dbd94 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -36,6 +36,7 @@ import { updateEnvironmentVariables, updateSchedulingEngineEnvironment, } from './utils/settings'; +import localStorageInjectionScript from './utils/localStorageInjectionScript'; import * as crypto from 'crypto'; // import electron from "electron"; import * as yaml from 'yaml'; @@ -664,52 +665,10 @@ const createChat = async ( // 2. This happens during window initialization before the renderer's React app is ready // 3. The timing is critical - we need to set config before React components try to read it // 4. Direct IPC communication would require the renderer to be fully loaded first - mainWindow.webContents - .executeJavaScript( - ` - (function() { - let retryCount = 0; - const maxRetries = 5; - const baseDelay = 100; - - function setConfig() { - try { - if (window.localStorage && typeof window.localStorage.setItem === 'function') { - localStorage.setItem('gooseConfig', '${configStr}'); - console.log('[Renderer] Successfully set localStorage config'); - return true; - } else { - console.warn('[Renderer] localStorage not available or setItem not a function'); - } - } catch (e) { - console.warn('[Renderer] localStorage access failed:', e); - } - return false; - } - - function retrySetConfig() { - if (setConfig()) { - return; // Success, no need to retry - } - - retryCount++; - if (retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff - console.log(\`[Renderer] Retrying localStorage config set (attempt \${retryCount + 1}/\${maxRetries}) in \${delay}ms\`); - setTimeout(retrySetConfig, delay); - } else { - console.error('[Renderer] Failed to set localStorage after all retries - continuing without localStorage config'); - } - } - - // Initial attempt - retrySetConfig(); - })(); - ` - ) - .catch((error) => { - console.error('Failed to execute localStorage script:', error); - }); + const injectionScript = localStorageInjectionScript('gooseConfig', configStr); + mainWindow.webContents.executeJavaScript(injectionScript).catch((error) => { + console.error('Failed to execute localStorage script:', error); + }); }); // Handle new window creation for links diff --git a/ui/desktop/src/utils/localStorageInjectionScript.ts b/ui/desktop/src/utils/localStorageInjectionScript.ts new file mode 100644 index 000000000000..479b7bf0e8bf --- /dev/null +++ b/ui/desktop/src/utils/localStorageInjectionScript.ts @@ -0,0 +1,50 @@ +/** + * Generates the JavaScript code to inject into the renderer process for setting localStorage + * with retry logic. This is used by the main process to inject configuration. + * + * @param key The localStorage key to set + * @param value The value to store (will be JSON stringified if not already a string) + * @returns JavaScript code as a string + */ +export default function generateLocalStorageInjectionScript(key: string, value: string): string { + return ` + (function() { + let retryCount = 0; + const maxRetries = 5; + const baseDelay = 100; + + function setConfig() { + try { + if (window.localStorage && typeof window.localStorage.setItem === 'function') { + localStorage.setItem('${key}', '${value}'); + console.log('[Renderer] Successfully set localStorage ${key}'); + return true; + } else { + console.warn('[Renderer] localStorage not available or setItem not a function'); + } + } catch (e) { + console.warn('[Renderer] localStorage access failed:', e); + } + return false; + } + + function retrySetConfig() { + if (setConfig()) { + return; // Success, no need to retry + } + + retryCount++; + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + console.log(\`[Renderer] Retrying localStorage ${key} set (attempt \${retryCount + 1}/\${maxRetries}) in \${delay}ms\`); + setTimeout(retrySetConfig, delay); + } else { + console.error('[Renderer] Failed to set localStorage ${key} after all retries - continuing without localStorage config'); + } + } + + // Initial attempt + retrySetConfig(); + })(); + `; +}