feat: unify error handling + handle case of malformed config.yaml (#2058)
This commit is contained in:
+14
-11
@@ -7,7 +7,7 @@ import { useModel } from './components/settings/models/ModelContext';
|
|||||||
import { useRecentModels } from './components/settings/models/RecentModels';
|
import { useRecentModels } from './components/settings/models/RecentModels';
|
||||||
import { createSelectedModel } from './components/settings/models/utils';
|
import { createSelectedModel } from './components/settings/models/utils';
|
||||||
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
|
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 { ConfirmationModal } from './components/ui/ConfirmationModal';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import { toastService } from './toasts';
|
import { toastService } from './toasts';
|
||||||
@@ -29,7 +29,7 @@ import ProviderSettings from './components/settings_v2/providers/ProviderSetting
|
|||||||
import { useChat } from './hooks/useChat';
|
import { useChat } from './hooks/useChat';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
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';
|
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions';
|
||||||
|
|
||||||
// Views and their options
|
// Views and their options
|
||||||
@@ -64,7 +64,7 @@ export default function App() {
|
|||||||
view: 'welcome',
|
view: 'welcome',
|
||||||
viewOptions: {},
|
viewOptions: {},
|
||||||
});
|
});
|
||||||
const { getExtensions, addExtension, read, upsert } = useConfig();
|
const { getExtensions, addExtension, read } = useConfig();
|
||||||
const initAttemptedRef = useRef(false);
|
const initAttemptedRef = useRef(false);
|
||||||
|
|
||||||
// Utility function to extract the command from the link
|
// 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));
|
const model = config.GOOSE_MODEL ?? (await read('GOOSE_MODEL', false));
|
||||||
|
|
||||||
if (provider && model) {
|
if (provider && model) {
|
||||||
console.log(`Using provider: ${provider}, model: ${model}`);
|
|
||||||
setView('chat');
|
setView('chat');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -106,8 +105,14 @@ export default function App() {
|
|||||||
addExtension,
|
addExtension,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in alpha initialization:', error);
|
console.error('Error in initialization:', error);
|
||||||
setFatalError(`System initialization error: ${error.message || 'Unknown 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');
|
setView('welcome');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -115,8 +120,7 @@ export default function App() {
|
|||||||
setView('welcome');
|
setView('welcome');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in alpha config check:', error);
|
setFatalError(`${error.message || 'Unknown error'}`);
|
||||||
setFatalError(`Configuration error: ${error.message || 'Unknown error'}`);
|
|
||||||
setView('welcome');
|
setView('welcome');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +130,7 @@ export default function App() {
|
|||||||
|
|
||||||
initializeApp().catch((error) => {
|
initializeApp().catch((error) => {
|
||||||
console.error('Unhandled error in initialization:', 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) {
|
if (fatalError) {
|
||||||
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
|
return <ErrorUI error={new Error(fatalError)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoadingSession)
|
if (isLoadingSession)
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ interface ConfigProviderProps {
|
|||||||
children: React.ReactNode;
|
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<ConfigContextType | undefined>(undefined);
|
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||||
@@ -160,10 +168,20 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getExtensions = async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
const getExtensions = async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
||||||
|
// If a refresh is forced, or we don't have providers yet
|
||||||
if (forceRefresh || extensionsList.length === 0) {
|
if (forceRefresh || extensionsList.length === 0) {
|
||||||
// If a refresh is forced, or we don't have providers yet
|
const result = await apiGetExtensions();
|
||||||
const response = await apiGetExtensions();
|
|
||||||
const extensionResponse: ExtensionResponse = response.data;
|
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);
|
setExtensionsList(extensionResponse.extensions);
|
||||||
return extensionResponse.extensions;
|
return extensionResponse.extensions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
// Capture unhandled promise rejections
|
// Capture unhandled promise rejections
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
@@ -12,17 +14,48 @@ window.addEventListener('error', (event) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function ErrorUI({ error }) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 w-full h-full flex flex-col items-center justify-center gap-6 bg-background">
|
||||||
|
<div className="flex flex-col items-center gap-4 max-w-[600px] text-center px-6">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center mb-2">
|
||||||
|
<AlertTriangle className="w-8 h-8 text-destructive" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-2xl font-semibold text-foreground">
|
||||||
|
Honk!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-base text-textSubtle mb-2">
|
||||||
|
An error occurred.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre className="text-destructive text-sm p-4 bg-muted rounded-lg w-full overflow-auto border border-border">
|
||||||
|
{error.message}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 flex-1 justify-center text-white dark:text-textSubtle bg-black dark:bg-white hover:bg-subtle"
|
||||||
|
onClick={() => window.electron.reloadApp()}
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class ErrorBoundary extends React.Component<
|
export class ErrorBoundary extends React.Component<
|
||||||
{ children: React.ReactNode },
|
{ children: React.ReactNode },
|
||||||
{ hasError: boolean }
|
{ error: Error, hasError: boolean }
|
||||||
> {
|
> {
|
||||||
constructor(props: { children: React.ReactNode }) {
|
constructor(props: { children: React.ReactNode }) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false };
|
this.state = { hasError: false, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(_: Error) {
|
static getDerivedStateFromError(error: Error) {
|
||||||
return { hasError: true };
|
return { hasError: true, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
@@ -32,13 +65,8 @@ export class ErrorBoundary extends React.Component<
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return <ErrorUI error={this.state.error} />
|
||||||
<div className="fixed inset-0 w-full h-full flex items-center justify-center bg-background">
|
|
||||||
<h1 className="text-xl font-semibold text-foreground">Something went wrong.</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card } from './ui/card';
|
|
||||||
|
|
||||||
interface ErrorScreenProps {
|
|
||||||
error: string;
|
|
||||||
onReload: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, onReload }) => {
|
|
||||||
return (
|
|
||||||
<div className="chat-content flex flex-col w-screen h-screen bg-window-gradient dark:bg-dark-window-gradient items-center justify-center p-[10px]">
|
|
||||||
<div className="titlebar-drag-region" />
|
|
||||||
<div className="relative block h-[20px] w-screen" />
|
|
||||||
<Card className="flex flex-col flex-1 h-[calc(100vh-95px)] w-full bg-card-gradient dark:bg-dark-card-gradient mt-0 border-none shadow-xl rounded-2xl relative">
|
|
||||||
<div className="flex flex-col items-center justify-center h-full p-4">
|
|
||||||
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
|
|
||||||
{'Honk! Goose experienced a fatal error'}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="p-4 text-center text-splash-pills-text whitespace-nowrap cursor-pointer bg-prev-goose-gradient dark:bg-dark-prev-goose-gradient text-prev-goose-text dark:text-prev-goose-text-dark rounded-[14px] inline-block hover:scale-[1.02] transition-all duration-150"
|
|
||||||
onClick={onReload}
|
|
||||||
>
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorScreen;
|
|
||||||
@@ -627,9 +627,12 @@ app.whenReady().then(async () => {
|
|||||||
log.info('from renderer:', info);
|
log.info('from renderer:', info);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('reload-app', () => {
|
ipcMain.on('reload-app', (event) => {
|
||||||
app.relaunch();
|
// Get the window that sent the event
|
||||||
app.exit(0);
|
const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (window) {
|
||||||
|
window.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let powerSaveBlockerId: number | null = null;
|
let powerSaveBlockerId: number | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user