Skip to content

ui: add root and tab error boundaries with retry fallback#173

Merged
Mehdi-Bl merged 1 commit into
mainfrom
feat/ui-01-error-boundaries
Feb 19, 2026
Merged

ui: add root and tab error boundaries with retry fallback#173
Mehdi-Bl merged 1 commit into
mainfrom
feat/ui-01-error-boundaries

Conversation

@Mehdi-Bl

@Mehdi-Bl Mehdi-Bl commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a reusable ErrorBoundary component for renderer crash containment
  • wrap the full renderer provider tree with a root error boundary
  • wrap each tab panel (config, source, processed) with tab-level boundaries and retry action
  • add localized fallback strings for EN/ES/FR/DE
  • add unit coverage for ErrorBoundary behavior and update test catalog

Validation

  • npm run lint
  • npm test -- --runInBand
  • npm run qa:screenshot
  • xvfb-run -a npm run e2e:playwright:ci
  • make sonar

Backlog Item

  • UI-01: Add root and tab-level error boundaries with actionable fallback UX

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Mehdi-Bl, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Add root and tab-level error boundaries with retry fallback

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add reusable ErrorBoundary component for renderer crash containment
• Wrap root app and all tab panels with error boundaries and retry actions
• Add localized fallback strings for EN/ES/FR/DE languages
• Add comprehensive unit test coverage for ErrorBoundary behavior

Grey Divider

File Changes

1. src/renderer/components/ErrorBoundary.tsx ✨ Enhancement +85/-0

New ErrorBoundary component with retry mechanism

• New reusable error boundary component using React class component pattern
• Captures child render errors and displays fallback UI with title, message, and retry button
• Implements reset key comparison to auto-recover when dependencies change
• Logs errors to console for debugging purposes

src/renderer/components/ErrorBoundary.tsx


2. src/renderer/components/App.tsx ✨ Enhancement +58/-25

Integrate error boundaries at root and tab levels

• Import and integrate ErrorBoundary component
• Wrap root app with error boundary for renderer-level crash containment
• Wrap each tab panel (ConfigTab, SourceTab, ProcessedTab) with tab-level error boundaries
• Add localized fallback strings using i18n translation keys
• Configure reset keys for each boundary to trigger recovery on state changes

src/renderer/components/App.tsx


3. src/renderer/i18n/locales/en/common.json 📝 Documentation +6/-1

Add English error boundary localization strings

• Add rendererRootCrashedTitle and rendererRootCrashedDescription for root-level errors
• Add tabCrashedTitle and tabCrashedDescription for tab-level errors
• Add retryRender label for retry button

src/renderer/i18n/locales/en/common.json


View more (5)
4. src/renderer/i18n/locales/es/common.json 📝 Documentation +6/-1

Add Spanish error boundary localization strings

• Add Spanish translations for root and tab-level error messages
• Add Spanish retry button label

src/renderer/i18n/locales/es/common.json


5. src/renderer/i18n/locales/fr/common.json 📝 Documentation +6/-1

Add French error boundary localization strings

• Add French translations for root and tab-level error messages
• Add French retry button label

src/renderer/i18n/locales/fr/common.json


6. src/renderer/i18n/locales/de/common.json 📝 Documentation +6/-1

Add German error boundary localization strings

• Add German translations for root and tab-level error messages
• Add German retry button label

src/renderer/i18n/locales/de/common.json


7. tests/unit/components/error-boundary.test.tsx 🧪 Tests +97/-0

Add ErrorBoundary unit test coverage

• New test file with four test cases covering error boundary behavior
• Tests include: normal child rendering, fallback UI display, reset key recovery, and retry callback
 invocation
• Uses test harness pattern to verify reset key change triggers recovery
• Mocks console.error to suppress error logs during tests

tests/unit/components/error-boundary.test.tsx


8. tests/catalog.md 📝 Documentation +1/-0

Update test catalog with ErrorBoundary tests

• Add entry for new error-boundary.test.tsx test file
• Document test coverage for child render failure capture, fallback rendering, reset-key recovery,
 and retry callback behavior

tests/catalog.md


Grey Divider

Qodo Logo

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @Mehdi-Bl, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's resilience by introducing robust error handling mechanisms. It integrates a new, reusable ErrorBoundary component at both the application root and individual tab levels, providing a more stable user experience by preventing entire application crashes due to isolated component failures. Users will now encounter actionable fallback UIs with retry options, improving recovery from unexpected rendering issues.

Highlights

  • Reusable ErrorBoundary Component: Added a new, reusable React class component named ErrorBoundary to gracefully catch and handle JavaScript errors in the UI component tree, preventing full application crashes.
  • Root-Level Error Handling: Implemented a root-level error boundary around the entire application's main content (AppContent) to provide a global fallback UI for unhandled rendering errors.
  • Tab-Specific Error Boundaries: Wrapped individual tab components (ConfigTab, SourceTab, ProcessedTab) with dedicated error boundaries, allowing for isolated error recovery and retry functionality for each tab.
  • Localized Fallback Messages: Introduced new localized strings for error titles, descriptions, and retry actions in English, Spanish, French, and German to provide a user-friendly experience across different languages.
  • Comprehensive Unit Testing: Included a new unit test file (error-boundary.test.tsx) with tests covering child rendering, fallback UI display, error state reset on key changes, and retry callback invocation, and updated the test catalog.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/renderer/components/App.tsx
    • Imported the new ErrorBoundary component.
    • Added translation keys for tab and root error messages.
    • Wrapped the main AppContent with a root ErrorBoundary.
    • Wrapped ConfigTab, SourceTab, and ProcessedTab components with individual ErrorBoundary instances, configured with specific resetKeys for each tab.
  • src/renderer/components/ErrorBoundary.tsx
    • Added a new React class component implementing error boundary logic.
    • Included getDerivedStateFromError to update state on error.
    • Implemented componentDidCatch for logging errors.
    • Added componentDidUpdate to reset error state when resetKeys change.
    • Provided a render method to display children or a fallback UI with a retry button.
    • Defined types for ErrorBoundaryProps and ErrorBoundaryState.
  • src/renderer/i18n/locales/de/common.json
    • Added German translations for rendererRootCrashedTitle, rendererRootCrashedDescription, tabCrashedTitle, tabCrashedDescription, and retryRender.
  • src/renderer/i18n/locales/en/common.json
    • Added English translations for rendererRootCrashedTitle, rendererRootCrashedDescription, tabCrashedTitle, tabCrashedDescription, and retryRender.
  • src/renderer/i18n/locales/es/common.json
    • Added Spanish translations for rendererRootCrashedTitle, rendererRootCrashedDescription, tabCrashedTitle, tabCrashedDescription, and retryRender.
  • src/renderer/i18n/locales/fr/common.json
    • Added French translations for rendererRootCrashedTitle, rendererRootCrashedDescription, tabCrashedTitle, tabCrashedDescription, and retryRender.
  • tests/catalog.md
    • Updated the test catalog to include the new error-boundary.test.tsx entry.
  • tests/unit/components/error-boundary.test.tsx
    • Added a new unit test file for the ErrorBoundary component.
    • Included tests for rendering children without errors, displaying fallback UI on error, resetting on resetKeys change, and calling onReset callback.
Activity
  • No specific activity (comments, reviews, or progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable ErrorBoundary component to improve the application's robustness by containing renderer crashes. The implementation correctly wraps the root application and individual tabs, providing a fallback UI and a retry mechanism. The new component is well-designed and comes with good unit test coverage. The addition of localized strings for error messages is also a nice touch. My review includes one suggestion to improve the reliability of the retry mechanism for one of the tabs.

fallbackTitle={tabFallbackTitle}
fallbackMessage={tabFallbackMessage}
resetLabel={retryLabel}
resetKeys={[activeTab, rootPath, selectedFiles.size, selectedFolders.size]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using selectedFiles.size and selectedFolders.size as reset keys for the error boundary is not robust. If a user replaces one file with another in the selection, the size might remain the same, but the content of the selection has changed. If the previous selection caused a crash, the error boundary would not reset on this new selection, preventing the component from recovering and re-rendering with the valid selection.

To fix this, the reset key should depend on the content of the selection sets, not just their size. A simple way to create a stable key from the sets is to convert them to a sorted string.

              resetKeys={[activeTab, rootPath, [...selectedFiles].sort().join(','), [...selectedFolders].sort().join(',')]}

@sonarqubecloud

Copy link
Copy Markdown

@Mehdi-Bl Mehdi-Bl merged commit ae16268 into main Feb 19, 2026
27 checks passed
@Mehdi-Bl Mehdi-Bl deleted the feat/ui-01-error-boundaries branch February 19, 2026 06:57
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. activeTab in resetKeys auto-retries crashed tabs 🐞 Bug ✓ Correctness
Description
All three tab error boundaries include activeTab as the first resetKey. Since all tabs stay
permanently mounted (hidden via CSS), every tab switch changes activeTab for all three boundaries
simultaneously. Any boundary in error state will auto-reset and re-render its still-broken child on
every tab switch, causing repeated throws, noisy console output, and fallback UI flickering.
Code

src/renderer/components/App.tsx[145]

+              resetKeys={[activeTab, configContent]}
Evidence
All three tab boundaries include activeTab as their first resetKey. The tabs are always mounted
(hidden via CSS), so all three ErrorBoundary instances are always live. When activeTab changes
(e.g. 'source' → 'config'), haveResetKeysChanged returns true for all three boundaries via
Object.is comparison, triggering setState({ hasError: false }) on any boundary currently in
error state — forcing a re-render of the still-broken child.

src/renderer/components/App.tsx[145-145]
src/renderer/components/App.tsx[161-161]
src/renderer/components/App.tsx[190-190]
src/renderer/components/App.tsx[139-139]
src/renderer/components/ErrorBoundary.tsx[51-53]
src/renderer/components/ErrorBoundary.tsx[24-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
All three tab `ErrorBoundary` instances include `activeTab` as the first element of their `resetKeys`. Because all tabs remain permanently mounted (hidden via CSS), every tab switch changes `activeTab` for all three boundaries simultaneously. Any boundary currently in error state will auto-reset and immediately re-render its still-broken child, causing repeated throws, noisy `console.error` output, and fallback UI flickering on every tab switch.

## Issue Context
The tabs are kept mounted via a CSS `hidden` class (not unmounted), so all three `ErrorBoundary` instances are always live and receive prop updates. `haveResetKeysChanged` uses `Object.is` comparison — a tab switch from `&#x27;source&#x27;` to `&#x27;config&#x27;` is detected as a changed key for all three boundaries, triggering `setState({ hasError: false })` on any that are in error state.

## Fix Focus Areas
Remove `activeTab` from all three `resetKeys` arrays, keeping only the content-specific keys:
- `src/renderer/components/App.tsx[145-145]` → change to `resetKeys={[configContent]}`
- `src/renderer/components/App.tsx[161-161]` → change to `resetKeys={[rootPath, selectedFiles.size, selectedFolders.size]}`
- `src/renderer/components/App.tsx[190-190]` → change to `resetKeys={[processedResult?.content]}`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. componentDidUpdate missing previousState guard 🐞 Bug ✓ Correctness
Description
The componentDidUpdate override only accepts previousProps, omitting previousState. After
getDerivedStateFromError transitions hasError to true, React commits the fallback UI and then
calls componentDidUpdate with previousState.hasError === false. Without checking
previousState.hasError, if any resetKey also changed in the same render cycle, the just-caught
error is immediately cleared, the broken child re-renders, throws again, and the cycle repeats.
Code

src/renderer/components/ErrorBoundary.tsx[R46-54]

+  componentDidUpdate(previousProps: ErrorBoundaryProps) {
+    if (!this.state.hasError) {
+      return;
+    }
+
+    if (haveResetKeysChanged(previousProps.resetKeys, this.props.resetKeys)) {
+      this.setState({ hasError: false });
+    }
+  }
Evidence
The method signature at line 46 only receives previousProps. React's lifecycle guarantees that
after getDerivedStateFromError sets hasError: true, componentDidUpdate is called with
previousState.hasError === false. The existing guard at line 47 (`if (!this.state.hasError)
return) does NOT prevent the reset — this.state.hasError is already true` at that point. Without
checking previousState.hasError, if haveResetKeysChanged returns true (which it does on every
tab switch due to activeTab being in all resetKeys), setState({ hasError: false }) immediately
clears the freshly-caught error, causing a rapid error/reset loop. The well-known
react-error-boundary library explicitly guards this with if (!prevState.hasError) return.

src/renderer/components/ErrorBoundary.tsx[46-54]
src/renderer/components/ErrorBoundary.tsx[38-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`componentDidUpdate` only receives `previousProps`, so there is no way to distinguish between &quot;we just entered error state this cycle&quot; and &quot;we were already in error state from a previous cycle&quot;. The existing guard `if (!this.state.hasError) return` checks the *current* state (which is already `true` after `getDerivedStateFromError` ran), not the *previous* state. This means if any resetKey changed in the same render cycle the error was caught, the error is immediately cleared, the broken child re-renders, throws again, and the cycle repeats.

## Issue Context
React&#x27;s lifecycle sequence when a child throws:
1. `getDerivedStateFromError` → sets `hasError: true` (previousState.hasError was `false`)
2. React renders fallback UI and commits to DOM
3. `componentDidUpdate(previousProps, previousState)` is called — `previousState.hasError === false`, `this.state.hasError === true`

The guard must check `previousState.hasError` to detect this transition and skip the reset logic during the initial error-catch cycle.

## Fix Focus Areas
- `src/renderer/components/ErrorBoundary.tsx[46-54]`

Change the method to:
```typescript
componentDidUpdate(previousProps: ErrorBoundaryProps, previousState: ErrorBoundaryState) {
 // Only auto-reset if we were ALREADY in error state, not when we just entered it.
 if (!previousState.hasError) {
   return;
 }

 if (haveResetKeysChanged(previousProps.resetKeys, this.props.resetKeys)) {
   this.setState({ hasError: false });
 }
}
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

fallbackTitle={tabFallbackTitle}
fallbackMessage={tabFallbackMessage}
resetLabel={retryLabel}
resetKeys={[activeTab, configContent]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Activetab in resetkeys auto-retries crashed tabs 🐞 Bug ✓ Correctness

All three tab error boundaries include activeTab as the first resetKey. Since all tabs stay
permanently mounted (hidden via CSS), every tab switch changes activeTab for all three boundaries
simultaneously. Any boundary in error state will auto-reset and re-render its still-broken child on
every tab switch, causing repeated throws, noisy console output, and fallback UI flickering.
Agent Prompt
## Issue description
All three tab `ErrorBoundary` instances include `activeTab` as the first element of their `resetKeys`. Because all tabs remain permanently mounted (hidden via CSS), every tab switch changes `activeTab` for all three boundaries simultaneously. Any boundary currently in error state will auto-reset and immediately re-render its still-broken child, causing repeated throws, noisy `console.error` output, and fallback UI flickering on every tab switch.

## Issue Context
The tabs are kept mounted via a CSS `hidden` class (not unmounted), so all three `ErrorBoundary` instances are always live and receive prop updates. `haveResetKeysChanged` uses `Object.is` comparison — a tab switch from `'source'` to `'config'` is detected as a changed key for all three boundaries, triggering `setState({ hasError: false })` on any that are in error state.

## Fix Focus Areas
Remove `activeTab` from all three `resetKeys` arrays, keeping only the content-specific keys:
- `src/renderer/components/App.tsx[145-145]` → change to `resetKeys={[configContent]}`
- `src/renderer/components/App.tsx[161-161]` → change to `resetKeys={[rootPath, selectedFiles.size, selectedFolders.size]}`
- `src/renderer/components/App.tsx[190-190]` → change to `resetKeys={[processedResult?.content]}`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +46 to +54
componentDidUpdate(previousProps: ErrorBoundaryProps) {
if (!this.state.hasError) {
return;
}

if (haveResetKeysChanged(previousProps.resetKeys, this.props.resetKeys)) {
this.setState({ hasError: false });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Componentdidupdate missing previousstate guard 🐞 Bug ✓ Correctness

The componentDidUpdate override only accepts previousProps, omitting previousState. After
getDerivedStateFromError transitions hasError to true, React commits the fallback UI and then
calls componentDidUpdate with previousState.hasError === false. Without checking
previousState.hasError, if any resetKey also changed in the same render cycle, the just-caught
error is immediately cleared, the broken child re-renders, throws again, and the cycle repeats.
Agent Prompt
## Issue description
`componentDidUpdate` only receives `previousProps`, so there is no way to distinguish between "we just entered error state this cycle" and "we were already in error state from a previous cycle". The existing guard `if (!this.state.hasError) return` checks the *current* state (which is already `true` after `getDerivedStateFromError` ran), not the *previous* state. This means if any resetKey changed in the same render cycle the error was caught, the error is immediately cleared, the broken child re-renders, throws again, and the cycle repeats.

## Issue Context
React's lifecycle sequence when a child throws:
1. `getDerivedStateFromError` → sets `hasError: true` (previousState.hasError was `false`)
2. React renders fallback UI and commits to DOM
3. `componentDidUpdate(previousProps, previousState)` is called — `previousState.hasError === false`, `this.state.hasError === true`

The guard must check `previousState.hasError` to detect this transition and skip the reset logic during the initial error-catch cycle.

## Fix Focus Areas
- `src/renderer/components/ErrorBoundary.tsx[46-54]`

Change the method to:
```typescript
componentDidUpdate(previousProps: ErrorBoundaryProps, previousState: ErrorBoundaryState) {
  // Only auto-reset if we were ALREADY in error state, not when we just entered it.
  if (!previousState.hasError) {
    return;
  }

  if (haveResetKeysChanged(previousProps.resetKeys, this.props.resetKeys)) {
    this.setState({ hasError: false });
  }
}
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant