From 4506d00efffbee33f288e3f1d82cebf89aa22f1d Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 9 Apr 2026 01:07:06 +1000 Subject: [PATCH] fix: error scenario when checking provider (#8394) --- ui/desktop/src/components/ConfigContext.tsx | 7 +- .../components/onboarding/OnboardingGuard.tsx | 67 +++++++++++++++---- ui/desktop/src/i18n/messages/en.json | 9 +++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 03d90023..c4987814 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -33,7 +33,7 @@ interface ConfigContextType { extensionsList: FixedExtensionEntry[]; extensionWarnings: string[]; upsert: (key: string, value: unknown, is_secret: boolean) => Promise; - read: (key: string, is_secret: boolean) => Promise; + read: (key: string, is_secret: boolean, options?: { throwOnError?: boolean }) => Promise; remove: (key: string, is_secret: boolean) => Promise; addExtension: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; toggleExtension: (name: string) => Promise; @@ -88,11 +88,14 @@ export const ConfigProvider: React.FC = ({ children }) => { [reloadConfig] ); - const read = useCallback(async (key: string, is_secret: boolean = false) => { + const read = useCallback(async (key: string, is_secret: boolean = false, options?: { throwOnError?: boolean }) => { const query: ConfigKeyQuery = { key: key, is_secret: is_secret }; const response = await readConfig({ body: query, }); + if (options?.throwOnError && response.error) { + throw response.error; + } return response.data; }, []); diff --git a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx index 8c1c8dd2..cb709818 100644 --- a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx +++ b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { useConfig } from '../ConfigContext'; import { useModelAndProvider } from '../ModelAndProviderContext'; import { Goose } from '../icons'; +import { Button } from '../ui/button'; import ProviderSelector from './ProviderSelector'; import OnboardingSuccess from './OnboardingSuccess'; import { @@ -23,6 +24,18 @@ const i18n = defineMessages({ id: 'onboardingGuard.welcomeDescription', defaultMessage: 'Your local AI agent. Connect an AI model provider to get started.', }, + checkProviderErrorTitle: { + id: 'onboardingGuard.checkProviderErrorTitle', + defaultMessage: 'Unable to connect to Goose server', + }, + checkProviderErrorDescription: { + id: 'onboardingGuard.checkProviderErrorDescription', + defaultMessage: 'The server may be starting up or temporarily unavailable.', + }, + retry: { + id: 'onboardingGuard.retry', + defaultMessage: 'Retry', + }, }); const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED'; @@ -39,6 +52,7 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { const [isCheckingProvider, setIsCheckingProvider] = useState(true); const [hasProvider, setHasProvider] = useState(false); + const [checkProviderError, setCheckProviderError] = useState(false); const [hasSelection, setHasSelection] = useState(false); const [configuredProvider, setConfiguredProvider] = useState(null); const [configuredProviderDisplayName, setConfiguredProviderDisplayName] = useState( @@ -47,27 +61,37 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { const [configuredModel, setConfiguredModel] = useState(null); const hasTrackedOnboardingStart = useRef(false); - useEffect(() => { - const checkProvider = async () => { + const checkProvider = async (retries = 3, delay = 1000) => { + setIsCheckingProvider(true); + setCheckProviderError(false); + for (let attempt = 0; attempt <= retries; attempt++) { try { - const provider = ((await read('GOOSE_PROVIDER', false)) as string) || ''; - setHasProvider(provider.trim() !== ''); - } catch (error) { - console.error('Error checking provider:', error); - setHasProvider(false); - } finally { + const provider = (await read('GOOSE_PROVIDER', false, { throwOnError: true })) as string | null; + setHasProvider(!!provider?.trim()); setIsCheckingProvider(false); + return; + } catch (error) { + console.error(`Error checking provider (attempt ${attempt + 1}/${retries + 1}):`, error); + if (attempt < retries) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } } - }; - checkProvider(); - }, [read]); + } + setCheckProviderError(true); + setIsCheckingProvider(false); + }; useEffect(() => { - if (!isCheckingProvider && !hasProvider && !hasTrackedOnboardingStart.current) { + checkProvider(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!isCheckingProvider && !hasProvider && !checkProviderError && !hasTrackedOnboardingStart.current) { trackOnboardingStarted(); hasTrackedOnboardingStart.current = true; } - }, [isCheckingProvider, hasProvider]); + }, [isCheckingProvider, hasProvider, checkProviderError]); const handleConfigured = async (providerName: string, modelId?: string) => { trackOnboardingProviderSelected({ provider: providerName }); @@ -107,6 +131,23 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { return null; } + if (checkProviderError) { + return ( +
+
+
+ +
+

{intl.formatMessage(i18n.checkProviderErrorTitle)}

+

{intl.formatMessage(i18n.checkProviderErrorDescription)}

+ +
+
+ ); + } + if (hasProvider) { return <>{children}; } diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 04b50388..7dfb8cc6 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -2387,6 +2387,15 @@ "navigationStyleSelector.tileLabel": { "defaultMessage": "Tile" }, + "onboardingGuard.checkProviderErrorDescription": { + "defaultMessage": "The server may be starting up or temporarily unavailable." + }, + "onboardingGuard.checkProviderErrorTitle": { + "defaultMessage": "Unable to connect to Goose server" + }, + "onboardingGuard.retry": { + "defaultMessage": "Retry" + }, "onboardingGuard.welcomeDescription": { "defaultMessage": "Your local AI agent. Connect an AI model provider to get started." },