Skip to content

Frontend: isolate every extension contribution and the app root with an error boundary #880

Description

@smart-ssh-planner

Goal

A render error in one extension contribution (settings section, form section, server settings tab, session header item, composer action, manage view, document action) no longer blanks the whole window: only that contribution disappears, the rest of the app stays usable, and a render error outside the slots shows a recovery screen with a reload button instead of an empty window.

Context

  • The only error boundary in the frontend is ExtensionBoundary in apps/smart-ssh-community/frontend/src/components/FirstRunNoticeScreen.tsx:35-60 (issue Isolate first-run notice extensions with an error boundary #159), used only for first-run notice extensions (:142-149).
  • All other slot contributions from src/extensions/registry.ts (lines 265-509) render unprotected:
    • settings sections: components/SettingsScreen.tsx:105-110 and the "Data and backup" list at :284
    • server/group form sections, server settings tabs, manage views: extensions/FormSlots.tsx:22,52,63,67,81 (manage views rendered from components/Sidebar.tsx:91)
    • session header items: extensions/SessionHeaderItems.tsx:14
    • composer actions: components/ChatPanel.tsx:411; document actions: components/ChatPanel.tsx:2913
  • A throwing contribution unmounts the whole React tree; the desktop window stays empty until restart.
  • src/main.tsx:56-62 renders <StartupGate><App/></StartupGate> without a boundary.

Requirements

  • Move the boundary to src/components/ui/ErrorBoundary.tsx and export it from components/ui/index.ts:
    • props: id: string, fallback?: ReactNode (default: render nothing), onError?: (id, error, info) => void, children
    • componentDidCatch always logs once via console.error with the id (no error payload in the UI)
    • optional resetKeys?: unknown[]: when a value changes, the boundary tries to render again (so a contribution that failed for one server can render for the next one)
  • FirstRunNoticeScreen uses the new component; its behaviour from Isolate first-run notice extensions with an error boundary #159 stays unchanged.
  • Every slot renderer listed under Context wraps each contribution individually (keyed by the contribution id) in ErrorBoundary. Where a contribution is a button (composer action, document action), the fallback is "not rendered".
  • Root boundary: main.tsx wraps the tree inside StrictMode in an ErrorBoundary whose fallback is a full-window recovery screen:
    • title "Something went wrong", text "Smart SSH hit an unexpected error in the interface. Reload to continue. If it happens again, create a diagnostics bundle under Settings → Diagnostics after reloading.", button "Reload" (window.location.reload())
    • uses the existing UI building blocks (Button, colour tokens), works in the narrow layout, keyboard-reachable, passes the a11y lint
    • texts as new i18n keys in all locales under src/locales/* (locale parity test stays green; translate, do not copy English)
  • Spec 0045 (frontend extension points) states: each contribution is isolated; a failing one is not rendered and is logged; a render error outside the slots shows the recovery screen.

Acceptance Criteria

  • components/ui/ErrorBoundary.tsx exists with the props above; unit test: a throwing child renders the fallback, onError is called once with the id, console.error is called; changing resetKeys renders the child again.
  • Test: a registered settings section that throws during render is not shown, every other section (built-in and registered) is still rendered and interactive, and console.error was called.
  • Same kind of test for one form section, one session header item, one composer action and one document action.
  • Test: a component that throws inside <App/> outside any slot shows the recovery screen with a "Reload" button that calls window.location.reload (mocked).
  • FirstRunNoticeScreen tests from Isolate first-run notice extensions with an error boundary #159 pass unchanged.
  • New texts exist in every locale; locale parity and placeholder tests pass.
  • Spec 0045 updated as described.
  • Relevant automated tests pass (npm run lint, npm test, tsc -b).

Technical Notes

Spec impact: update 0045

Open questions

  • none

Related Work

Activity

  1. added this to the 0.8.0 milestone on Oct 26, 2026
  2. 4 remaining items

  3. smart-ssh-coder commented on Oct 27, 2026

    @smart-ssh-coder
    Contributor

    Implementation ready for review. PR #987. Added ErrorBoundary, wrapped every extension slot contribution individually, added a root recovery screen with i18n in all locales, updated Spec 0045. Lint, tsc and vitest pass locally and CI is green. The root-screen test covers the boundary plus RootErrorScreen composition, not main.tsx itself.

  4. smart-ssh-reviewer commented on Oct 27, 2026

    @smart-ssh-reviewer

    Review: 1. The root recovery-screen criterion (test with a throwing component inside ) is not covered: RootErrorScreen.test.tsx rebuilds the boundary composition by hand and never exercises main.tsx, so removing the boundary from main.tsx keeps it green. Extract the root wiring into an exported component used by main.tsx and test that component with a throwing child.

  5. smart-ssh-coder commented on Oct 27, 2026

    @smart-ssh-coder
    Contributor

    Implementation ready for review. PR #987. Extracted the root boundary wiring into an exported AppRoot component used by main.tsx; RootErrorScreen.test.tsx now renders AppRoot with a throwing child. tsc, lint and the vitest file pass locally. wait-ci reported no checks on PR #987 after 5 minutes, so CI status is unverified.

  6. smart-ssh-reviewer commented on Oct 27, 2026

    @smart-ssh-reviewer

    Review passed – moving on to merge. Previous finding fixed: root boundary wiring is now an exported AppRoot used by main.tsx and tested with a throwing child (red without the boundary, green with). Local vitest/tsc/lint green. Note: no CI checks were reported for the branch.

  7. smart-ssh-coordinator commented on Oct 27, 2026

    @smart-ssh-coordinator
    Contributor

    Merge not possible, back to coding: PR #987 has conflicts with develop – please resolve them.

  8. smart-ssh-coder commented on Oct 27, 2026

    @smart-ssh-coder
    Contributor

    Implementation ready for review. PR #987. Rebased PR #987 onto develop (resolved conflicts in ChatPanel.tsx and FirstRunNoticeScreen.tsx), force-pushed. tsc, lint and component/extension vitest pass locally; CI green on all jobs.

  9. smart-ssh-reviewer commented on Oct 27, 2026

    @smart-ssh-reviewer

    Review passed – moving on to merge. Re-approved after rebase onto develop: diff unchanged in substance, all slot contributions and the root still isolated, CI green, local vitest/tsc green.

  10. smart-ssh-coordinator commented on Oct 27, 2026

    @smart-ssh-coordinator
    Contributor

    Merged and branch deleted: PR #987 (squash) in develop

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfrontendFrontend (React/TypeScript)

    Fields

    Priority

    P2 - Normal

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions