diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 57c1c683dfc0..fd97bca068ce 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -19,7 +19,6 @@ import type { ExtensionConfig, } from '../api'; import { removeShims } from './settings/extensions/utils'; -import { ensureClientInitialized } from '../utils'; export type { ExtensionConfig } from '../api/types.gen'; @@ -183,7 +182,6 @@ export const ConfigProvider: React.FC = ({ children }) => { useEffect(() => { // Load all configuration data and providers on mount (async () => { - await ensureClientInitialized(); // Load config const configResponse = await readAllConfig(); setConfig(configResponse.data?.config || {}); diff --git a/ui/desktop/src/components/ModelAndProviderContext.tsx b/ui/desktop/src/components/ModelAndProviderContext.tsx index a8aadbb2caa2..27d3164b4793 100644 --- a/ui/desktop/src/components/ModelAndProviderContext.tsx +++ b/ui/desktop/src/components/ModelAndProviderContext.tsx @@ -8,7 +8,6 @@ import { getModelDisplayName, getProviderDisplayName, } from './settings/models/predefinedModelsUtils'; -import { ensureClientInitialized } from '../utils'; // titles export const UNKNOWN_PROVIDER_TITLE = 'Provider name lookup'; @@ -171,7 +170,6 @@ export const ModelAndProviderProvider: React.FC = const refreshCurrentModelAndProvider = useCallback(async () => { try { - await ensureClientInitialized(); const { model, provider } = await getCurrentModelAndProvider(); setCurrentModel(model); setCurrentProvider(provider); diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 7f4e3fc416c7..3c88e806e833 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -6,7 +6,6 @@ import { startOpenRouterSetup } from '../utils/openRouterSetup'; import WelcomeGooseLogo from './WelcomeGooseLogo'; import { initializeSystem } from '../utils/providerUtils'; import { toastService } from '../toasts'; -import { ensureClientInitialized } from '../utils'; interface ProviderGuardProps { children: React.ReactNode; @@ -96,8 +95,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { useEffect(() => { const checkProvider = async () => { try { - await ensureClientInitialized(); - const config = window.electron.getConfig(); console.log('ProviderGuard - Full config:', config); diff --git a/ui/desktop/src/contexts/ClientInitializationContext.tsx b/ui/desktop/src/contexts/ClientInitializationContext.tsx new file mode 100644 index 000000000000..02bbce5a41a3 --- /dev/null +++ b/ui/desktop/src/contexts/ClientInitializationContext.tsx @@ -0,0 +1,84 @@ +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { client } from '../api/client.gen'; + +interface ClientInitializationContextType { + isInitialized: boolean; + initializationError: Error | null; +} + +// Track if client has been initialized to avoid duplicate initialization +let clientInitialized = false; + +async function ensureClientInitialized() { + if (clientInitialized) return; + client.setConfig({ + baseUrl: window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'), + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': await window.electron.getSecretKey(), + }, + }); + clientInitialized = true; +} + +const ClientInitializationContext = createContext( + undefined +); + +interface ClientInitializationProviderProps { + children: ReactNode; +} + +export const ClientInitializationProvider: React.FC = ({ + children, +}) => { + const [isInitialized, setIsInitialized] = useState(false); + const [initializationError, setInitializationError] = useState(null); + + useEffect(() => { + const initializeClient = async () => { + try { + await ensureClientInitialized(); + setIsInitialized(true); + } catch (error) { + console.error('Failed to initialize API client:', error); + setInitializationError(error instanceof Error ? error : new Error('Unknown error')); + } + }; + + initializeClient(); + }, []); + + return ( + + {children} + + ); +}; + +export const useClientInitialization = () => { + const context = useContext(ClientInitializationContext); + if (context === undefined) { + throw new Error('useClientInitialization must be used within a ClientInitializationProvider'); + } + return context; +}; + +// Helper component to ensure initialization before rendering children +export const RequireClientInitialization: React.FC<{ children: ReactNode }> = ({ children }) => { + const { isInitialized, initializationError } = useClientInitialization(); + + if (initializationError) { + throw initializationError; + } + + if (!isInitialized) { + return ( +
+
+
+ ); + } + + return <>{children}; +}; diff --git a/ui/desktop/src/hooks/useChat.ts b/ui/desktop/src/hooks/useChat.ts index b7d09ea58841..7db74e5a6e65 100644 --- a/ui/desktop/src/hooks/useChat.ts +++ b/ui/desktop/src/hooks/useChat.ts @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import { ChatType } from '../types/chat'; import { fetchSessionDetails, generateSessionId } from '../sessions'; -import { ensureClientInitialized } from '../utils'; import { View, ViewOptions } from '../App'; type UseChatArgs = { @@ -30,7 +29,6 @@ export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatAr setIsLoadingSession(true); try { - await ensureClientInitialized(); const sessionDetails = await fetchSessionDetails(resumeSessionId); // Only set view if we have valid session details diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 71a90179f19d..8f8694e82e03 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -1,6 +1,10 @@ import React, { Suspense, lazy } from 'react'; import ReactDOM from 'react-dom/client'; import { ConfigProvider } from './components/ConfigContext'; +import { + ClientInitializationProvider, + RequireClientInitialization, +} from './contexts/ClientInitializationContext'; import { ErrorBoundary } from './components/ErrorBoundary'; import { patchConsoleLogging } from './utils'; import SuspenseLoader from './suspense-loader'; @@ -10,13 +14,17 @@ patchConsoleLogging(); const App = lazy(() => import('./App')); ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - - - + + + + + + + + + + + + + ); diff --git a/ui/desktop/src/utils.ts b/ui/desktop/src/utils.ts index 01557f0d9d8d..922519dd7b09 100644 --- a/ui/desktop/src/utils.ts +++ b/ui/desktop/src/utils.ts @@ -1,6 +1,5 @@ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; -import { client } from './api/client.gen'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -17,19 +16,3 @@ export function patchConsoleLogging() { // Intercept console methods return; } - -// This needs to be called before any API calls are made, but since we're using the client -// in multiple useEffect locations, we can't be sure who goes first. -let clientInitialized = false; - -export async function ensureClientInitialized() { - if (clientInitialized) return; - client.setConfig({ - baseUrl: window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'), - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - }); - clientInitialized = true; -}