mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat: implement React Error Boundaries for improved error handling
- Created ErrorBoundary component with comprehensive fallback UI - Displays error details and component stack trace - Provides "Try Again" and "Reload App" recovery options - Clean, user-friendly error display with Lucide icons - Wrapped entire app in ErrorBoundary at main.tsx entry point - Added granular error boundaries around major sections: - SessionList (left sidebar) - Center workspace (TerminalOutput/InputArea) - RightPanel (right sidebar) - Prevents component crashes from taking down entire application - Each section can now fail independently without affecting others - Build verified successfully Related to housekeeping task #21
This commit is contained in:
@@ -22,6 +22,7 @@ import { CreateGroupModal } from './components/CreateGroupModal';
|
||||
import { RenameSessionModal } from './components/RenameSessionModal';
|
||||
import { RenameGroupModal } from './components/RenameGroupModal';
|
||||
import { ConfirmModal } from './components/ConfirmModal';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
|
||||
// Import custom hooks
|
||||
import { useSettings, useSessionManager, useFileExplorer } from './hooks';
|
||||
@@ -1468,42 +1469,44 @@ export default function MaestroConsole() {
|
||||
)}
|
||||
|
||||
{/* --- LEFT SIDEBAR --- */}
|
||||
<SessionList
|
||||
theme={theme}
|
||||
sessions={sessions}
|
||||
groups={groups}
|
||||
sortedSessions={sortedSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
leftSidebarOpen={leftSidebarOpen}
|
||||
leftSidebarWidthState={leftSidebarWidthState}
|
||||
activeFocus={activeFocus}
|
||||
selectedSidebarIndex={selectedSidebarIndex}
|
||||
editingGroupId={editingGroupId}
|
||||
editingSessionId={editingSessionId}
|
||||
draggingSessionId={draggingSessionId}
|
||||
anyTunnelActive={anyTunnelActive}
|
||||
shortcuts={shortcuts}
|
||||
setActiveFocus={setActiveFocus}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
setLeftSidebarOpen={setLeftSidebarOpen}
|
||||
setLeftSidebarWidthState={setLeftSidebarWidthState}
|
||||
setShortcutsHelpOpen={setShortcutsHelpOpen}
|
||||
setSettingsModalOpen={setSettingsModalOpen}
|
||||
setSettingsTab={setSettingsTab}
|
||||
toggleGroup={toggleGroup}
|
||||
handleDragStart={handleDragStart}
|
||||
handleDragOver={handleDragOver}
|
||||
handleDropOnGroup={handleDropOnGroup}
|
||||
handleDropOnUngrouped={handleDropOnUngrouped}
|
||||
finishRenamingGroup={finishRenamingGroup}
|
||||
finishRenamingSession={finishRenamingSession}
|
||||
startRenamingGroup={startRenamingGroup}
|
||||
startRenamingSession={startRenamingSession}
|
||||
showConfirmation={showConfirmation}
|
||||
setGroups={setGroups}
|
||||
createNewGroup={createNewGroup}
|
||||
addNewSession={addNewSession}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<SessionList
|
||||
theme={theme}
|
||||
sessions={sessions}
|
||||
groups={groups}
|
||||
sortedSessions={sortedSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
leftSidebarOpen={leftSidebarOpen}
|
||||
leftSidebarWidthState={leftSidebarWidthState}
|
||||
activeFocus={activeFocus}
|
||||
selectedSidebarIndex={selectedSidebarIndex}
|
||||
editingGroupId={editingGroupId}
|
||||
editingSessionId={editingSessionId}
|
||||
draggingSessionId={draggingSessionId}
|
||||
anyTunnelActive={anyTunnelActive}
|
||||
shortcuts={shortcuts}
|
||||
setActiveFocus={setActiveFocus}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
setLeftSidebarOpen={setLeftSidebarOpen}
|
||||
setLeftSidebarWidthState={setLeftSidebarWidthState}
|
||||
setShortcutsHelpOpen={setShortcutsHelpOpen}
|
||||
setSettingsModalOpen={setSettingsModalOpen}
|
||||
setSettingsTab={setSettingsTab}
|
||||
toggleGroup={toggleGroup}
|
||||
handleDragStart={handleDragStart}
|
||||
handleDragOver={handleDragOver}
|
||||
handleDropOnGroup={handleDropOnGroup}
|
||||
handleDropOnUngrouped={handleDropOnUngrouped}
|
||||
finishRenamingGroup={finishRenamingGroup}
|
||||
finishRenamingSession={finishRenamingSession}
|
||||
startRenamingGroup={startRenamingGroup}
|
||||
startRenamingSession={startRenamingSession}
|
||||
showConfirmation={showConfirmation}
|
||||
setGroups={setGroups}
|
||||
createNewGroup={createNewGroup}
|
||||
addNewSession={addNewSession}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* --- CENTER WORKSPACE --- */}
|
||||
{!activeSession ? (
|
||||
@@ -1522,11 +1525,12 @@ export default function MaestroConsole() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex flex-col min-w-0 relative ${activeFocus === 'main' ? 'ring-1 ring-inset z-10' : ''}`}
|
||||
style={{ backgroundColor: theme.colors.bgMain, ringColor: theme.colors.accent }}
|
||||
onClick={() => setActiveFocus('main')}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<div
|
||||
className={`flex-1 flex flex-col min-w-0 relative ${activeFocus === 'main' ? 'ring-1 ring-inset z-10' : ''}`}
|
||||
style={{ backgroundColor: theme.colors.bgMain, ringColor: theme.colors.accent }}
|
||||
onClick={() => setActiveFocus('main')}
|
||||
>
|
||||
{/* Top Bar */}
|
||||
<div className="h-16 border-b flex items-center justify-between px-6 shrink-0" style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgSidebar }}>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -1643,38 +1647,41 @@ export default function MaestroConsole() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* --- RIGHT PANEL --- */}
|
||||
<RightPanel
|
||||
session={activeSession}
|
||||
theme={theme}
|
||||
shortcuts={shortcuts}
|
||||
rightPanelOpen={rightPanelOpen}
|
||||
setRightPanelOpen={setRightPanelOpen}
|
||||
rightPanelWidth={rightPanelWidthState}
|
||||
setRightPanelWidthState={setRightPanelWidthState}
|
||||
activeRightTab={activeRightTab}
|
||||
setActiveRightTab={setActiveRightTab}
|
||||
activeFocus={activeFocus}
|
||||
setActiveFocus={setActiveFocus}
|
||||
fileTreeFilter={fileTreeFilter}
|
||||
setFileTreeFilter={setFileTreeFilter}
|
||||
fileTreeFilterOpen={fileTreeFilterOpen}
|
||||
setFileTreeFilterOpen={setFileTreeFilterOpen}
|
||||
filteredFileTree={filteredFileTree}
|
||||
selectedFileIndex={selectedFileIndex}
|
||||
setSelectedFileIndex={setSelectedFileIndex}
|
||||
previewFile={previewFile}
|
||||
fileTreeContainerRef={fileTreeContainerRef}
|
||||
toggleFolder={toggleFolder}
|
||||
handleFileClick={handleFileClick}
|
||||
expandAllFolders={expandAllFolders}
|
||||
collapseAllFolders={collapseAllFolders}
|
||||
updateSessionWorkingDirectory={updateSessionWorkingDirectory}
|
||||
setSessions={setSessions}
|
||||
updateScratchPad={updateScratchPad}
|
||||
updateScratchPadState={updateScratchPadState}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RightPanel
|
||||
session={activeSession}
|
||||
theme={theme}
|
||||
shortcuts={shortcuts}
|
||||
rightPanelOpen={rightPanelOpen}
|
||||
setRightPanelOpen={setRightPanelOpen}
|
||||
rightPanelWidth={rightPanelWidthState}
|
||||
setRightPanelWidthState={setRightPanelWidthState}
|
||||
activeRightTab={activeRightTab}
|
||||
setActiveRightTab={setActiveRightTab}
|
||||
activeFocus={activeFocus}
|
||||
setActiveFocus={setActiveFocus}
|
||||
fileTreeFilter={fileTreeFilter}
|
||||
setFileTreeFilter={setFileTreeFilter}
|
||||
fileTreeFilterOpen={fileTreeFilterOpen}
|
||||
setFileTreeFilterOpen={setFileTreeFilterOpen}
|
||||
filteredFileTree={filteredFileTree}
|
||||
selectedFileIndex={selectedFileIndex}
|
||||
setSelectedFileIndex={setSelectedFileIndex}
|
||||
previewFile={previewFile}
|
||||
fileTreeContainerRef={fileTreeContainerRef}
|
||||
toggleFolder={toggleFolder}
|
||||
handleFileClick={handleFileClick}
|
||||
expandAllFolders={expandAllFolders}
|
||||
collapseAllFolders={collapseAllFolders}
|
||||
updateSessionWorkingDirectory={updateSessionWorkingDirectory}
|
||||
setSessions={setSessions}
|
||||
updateScratchPad={updateScratchPad}
|
||||
updateScratchPadState={updateScratchPadState}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
148
src/renderer/components/ErrorBoundary.tsx
Normal file
148
src/renderer/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallbackComponent?: ReactNode;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: React.ErrorInfo | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorBoundary component catches JavaScript errors anywhere in the child component tree,
|
||||
* logs those errors, and displays a fallback UI instead of crashing the entire app.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <ErrorBoundary>
|
||||
* <YourComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
// Update state so the next render will show the fallback UI
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
// Log error details to console for debugging
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
|
||||
// Update state with error info
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
// Reset error state
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
});
|
||||
|
||||
// Call optional reset handler
|
||||
if (this.props.onReset) {
|
||||
this.props.onReset();
|
||||
}
|
||||
};
|
||||
|
||||
handleReload = () => {
|
||||
// Reload the entire app
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// If a custom fallback is provided, use it
|
||||
if (this.props.fallbackComponent) {
|
||||
return this.props.fallbackComponent;
|
||||
}
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-gray-100 p-6">
|
||||
<div className="max-w-2xl w-full bg-gray-800 rounded-lg shadow-xl p-8">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0 bg-red-500/10 p-3 rounded-lg">
|
||||
<AlertTriangle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold mb-2 text-red-400">
|
||||
Something went wrong
|
||||
</h1>
|
||||
<p className="text-gray-300">
|
||||
An unexpected error occurred in the application. You can try to recover or reload the app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.error && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-semibold text-gray-400 mb-2">Error Details:</h2>
|
||||
<div className="bg-gray-900 rounded p-4 overflow-auto max-h-40">
|
||||
<pre className="text-xs text-red-300 font-mono whitespace-pre-wrap">
|
||||
{this.state.error.toString()}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.errorInfo && (
|
||||
<details className="mb-6">
|
||||
<summary className="text-sm font-semibold text-gray-400 cursor-pointer hover:text-gray-300">
|
||||
Component Stack Trace
|
||||
</summary>
|
||||
<div className="bg-gray-900 rounded p-4 overflow-auto max-h-60 mt-2">
|
||||
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap">
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Reload App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import MaestroConsole from './App';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<MaestroConsole />
|
||||
<ErrorBoundary>
|
||||
<MaestroConsole />
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user