diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 014d422f..b24e15e7 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -7,7 +7,7 @@ import { useModel } from './components/settings/models/ModelContext'; import { useRecentModels } from './components/settings/models/RecentModels'; import { createSelectedModel } from './components/settings/models/utils'; import { getDefaultModel } from './components/settings/models/hardcoded_stuff'; -import ErrorScreen from './components/ErrorScreen'; +import { ErrorUI } from './components/ErrorBoundary'; import { ConfirmationModal } from './components/ui/ConfirmationModal'; import { ToastContainer } from 'react-toastify'; import { toastService } from './toasts'; @@ -29,7 +29,7 @@ import ProviderSettings from './components/settings_v2/providers/ProviderSetting import { useChat } from './hooks/useChat'; import 'react-toastify/dist/ReactToastify.css'; -import { useConfig } from './components/ConfigContext'; +import { useConfig, MalformedConfigError } from './components/ConfigContext'; import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions'; // Views and their options @@ -64,7 +64,7 @@ export default function App() { view: 'welcome', viewOptions: {}, }); - const { getExtensions, addExtension, read, upsert } = useConfig(); + const { getExtensions, addExtension, read } = useConfig(); const initAttemptedRef = useRef(false); // Utility function to extract the command from the link @@ -97,7 +97,6 @@ export default function App() { const model = config.GOOSE_MODEL ?? (await read('GOOSE_MODEL', false)); if (provider && model) { - console.log(`Using provider: ${provider}, model: ${model}`); setView('chat'); try { @@ -106,8 +105,14 @@ export default function App() { addExtension, }); } catch (error) { - console.error('Error in alpha initialization:', error); - setFatalError(`System initialization error: ${error.message || 'Unknown error'}`); + console.error('Error in initialization:', error); + + // propagate the error upward so the global ErrorUI shows in cases + // where going through welcome/onboarding wouldn't address the issue + if (error instanceof MalformedConfigError) { + throw error; + } + setView('welcome'); } } else { @@ -115,8 +120,7 @@ export default function App() { setView('welcome'); } } catch (error) { - console.error('Error in alpha config check:', error); - setFatalError(`Configuration error: ${error.message || 'Unknown error'}`); + setFatalError(`${error.message || 'Unknown error'}`); setView('welcome'); } @@ -126,7 +130,7 @@ export default function App() { initializeApp().catch((error) => { console.error('Unhandled error in initialization:', error); - setFatalError(`Initialization error: ${error.message || 'Unknown error'}`); + setFatalError(`${error.message || 'Unknown error'}`); }); }, []); @@ -364,9 +368,8 @@ export default function App() { }); }, []); - // keep if (fatalError) { - return window.electron.reloadApp()} />; + return ; } if (isLoadingSession) diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 81b8a0d6..9d81c5e2 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -55,6 +55,14 @@ interface ConfigProviderProps { children: React.ReactNode; } +export class MalformedConfigError extends Error { + constructor() { + super('Check contents of ~/.config/goose/config.yaml'); + this.name = 'MalformedConfigError'; + Object.setPrototypeOf(this, MalformedConfigError.prototype); + } +} + const ConfigContext = createContext(undefined); export const ConfigProvider: React.FC = ({ children }) => { @@ -160,10 +168,20 @@ export const ConfigProvider: React.FC = ({ children }) => { }; const getExtensions = async (forceRefresh = false): Promise => { + // If a refresh is forced, or we don't have providers yet if (forceRefresh || extensionsList.length === 0) { - // If a refresh is forced, or we don't have providers yet - const response = await apiGetExtensions(); - const extensionResponse: ExtensionResponse = response.data; + const result = await apiGetExtensions(); + + if (result.response.status === 422) { + throw new MalformedConfigError(); + } + + if (result.error && !result.data) { + console.log(result.error); + return; + } + + const extensionResponse: ExtensionResponse = result.data; setExtensionsList(extensionResponse.extensions); return extensionResponse.extensions; } diff --git a/ui/desktop/src/components/ErrorBoundary.tsx b/ui/desktop/src/components/ErrorBoundary.tsx index 9ec1df1b..9d5c4827 100644 --- a/ui/desktop/src/components/ErrorBoundary.tsx +++ b/ui/desktop/src/components/ErrorBoundary.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { Button } from './ui/button'; +import { AlertTriangle } from 'lucide-react'; // Capture unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { @@ -12,17 +14,48 @@ window.addEventListener('error', (event) => { ); }); +export function ErrorUI({ error }) { + return ( +
+
+
+ +
+ +

+ Honk! +

+ +

+ An error occurred. +

+ +
+          {error.message}
+        
+ + +
+
+ ); +} + export class ErrorBoundary extends React.Component< { children: React.ReactNode }, - { hasError: boolean } + { error: Error, hasError: boolean } > { constructor(props: { children: React.ReactNode }) { super(props); - this.state = { hasError: false }; + this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(_: Error) { - return { hasError: true }; + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { @@ -32,13 +65,8 @@ export class ErrorBoundary extends React.Component< render() { if (this.state.hasError) { - return ( -
-

Something went wrong.

-
- ); + return } - return this.props.children; } } diff --git a/ui/desktop/src/components/ErrorScreen.tsx b/ui/desktop/src/components/ErrorScreen.tsx deleted file mode 100644 index fba2e3ff..00000000 --- a/ui/desktop/src/components/ErrorScreen.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Card } from './ui/card'; - -interface ErrorScreenProps { - error: string; - onReload: () => void; -} - -const ErrorScreen: React.FC = ({ error, onReload }) => { - return ( -
-
-
- -
-
- {'Honk! Goose experienced a fatal error'} -
-
- Reload -
-
-
-
- ); -}; - -export default ErrorScreen; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 4495e536..068352dc 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -627,9 +627,12 @@ app.whenReady().then(async () => { log.info('from renderer:', info); }); - ipcMain.on('reload-app', () => { - app.relaunch(); - app.exit(0); + ipcMain.on('reload-app', (event) => { + // Get the window that sent the event + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + window.reload(); + } }); let powerSaveBlockerId: number | null = null;