Repository: vercel/next.js
Pull request: [devtools] replace the old error overlay with the issues tab view

Closes https://linear.app/vercel/issue/NEXT-4573/

This PR replaces the Error Overlay with the Issues Tab view of the panel UI.

https://github.com/user-attachments/assets/b5b75a29-e581-4b9a-9023-789c40969214

Confirmed opening on SSR error replay:

https://github.com/user-attachments/assets/f7d032a3-3b8c-45ec-b468-8949173f9c0a

Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/devtools-indicator.tsx
patch: @@ -10,11 +10,10 @@ import {
 } from '../errors/dev-tools-indicator/utils'
 import {
   ACTION_DEVTOOLS_PANEL_TOGGLE,
-  ACTION_ERROR_OVERLAY_TOGGLE,
-  ACTION_ERROR_OVERLAY_CLOSE,
   STORAGE_KEY_POSITION,
-  ACTION_DEVTOOLS_PANEL_CLOSE,
   ACTION_DEVTOOLS_POSITION,
+  ACTION_DEVTOOLS_PANEL_OPEN,
+  ACTION_ERROR_OVERLAY_OPEN,
 } from '../../shared'
 import { Draggable } from '../errors/dev-tools-indicator/draggable'
 
@@ -35,13 +34,13 @@ export function DevToolsIndicator({
 
   const [vertical, horizontal] = state.devToolsPosition.split('-', 2)
 
-  const toggleErrorOverlay = () => {
-    dispatch({ type: ACTION_DEVTOOLS_PANEL_CLOSE })
-    dispatch({ type: ACTION_ERROR_OVERLAY_TOGGLE })
+  const enableErrorOverlayMode = () => {
+    dispatch({ type: ACTION_ERROR_OVERLAY_OPEN })
+    // Open the DevTools panel to view as error overlay mode.
+    dispatch({ type: ACTION_DEVTOOLS_PANEL_OPEN })
   }
 
   const toggleDevToolsPanel = () => {
-    dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE })
     dispatch({ type: ACTION_DEVTOOLS_PANEL_TOGGLE })
   }
 
@@ -56,7 +55,10 @@ export function DevToolsIndicator({
           zIndex: 2147483647,
           [vertical]: `${INDICATOR_PADDING}px`,
           [horizontal]: `${INDICATOR_PADDING}px`,
-          visibility: state.isDevToolsPanelOpen ? 'hidden' : 'visible',
+          visibility:
+            state.isDevToolsPanelOpen || state.isErrorOverlayOpen
+              ? 'hidden'
+              : 'visible',
         } as CSSProperties
       }
     >
@@ -82,7 +84,7 @@ export function DevToolsIndicator({
           disabled={state.disableDevIndicator}
           issueCount={errorCount}
           onTriggerClick={toggleDevToolsPanel}
-          toggleErrorOverlay={toggleErrorOverlay}
+          toggleErrorOverlay={enableErrorOverlayMode}
           isDevBuilding={state.buildingIndicator}
           isDevRendering={state.renderingIndicator}
           isBuildError={isBuildError}

filename: packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel.tsx
patch: @@ -14,6 +14,7 @@ import {
   ACTION_DEVTOOLS_SCALE,
   STORAGE_KEY_SCALE,
   STORAGE_KEY_POSITION,
+  ACTION_ERROR_OVERLAY_CLOSE,
 } from '../../shared'
 import { css } from '../../utils/css'
 import { OverlayBackdrop } from '../overlay'
@@ -40,10 +41,20 @@ export function DevToolsPanel({
 }) {
   const [activeTab, setActiveTab] = useState<DevToolsPanelTabType>('issues')
   const [isFullscreen, setIsFullscreen] = useState(false)
+  const [prevIsErrorOverlayOpen, setPrevIsErrorOverlayOpen] = useState(false)
+
+  if (state.isErrorOverlayOpen !== prevIsErrorOverlayOpen) {
+    if (state.isErrorOverlayOpen) {
+      setIsFullscreen(true)
+    }
+    setPrevIsErrorOverlayOpen(state.isErrorOverlayOpen)
+  }
+
   const [vertical, horizontal] = state.devToolsPosition.split('-', 2)
 
   const onCloseDevToolsPanel = () => {
     dispatch({ type: ACTION_DEVTOOLS_PANEL_CLOSE })
+    dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE })
   }
 
   const handlePositionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -64,6 +75,7 @@ export function DevToolsPanel({
 
   const handleFullscreenToggle = () => {
     setIsFullscreen((prev) => !prev)
+    dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE })
   }
 
   return (

filename: packages/next/src/next-devtools/dev-overlay/dev-overlay.tsx
patch: @@ -1,4 +1,11 @@
-import type { OverlayDispatch, OverlayState } from './shared'
+import {
+  ACTION_DEVTOOLS_PANEL_OPEN,
+  ACTION_ERROR_OVERLAY_OPEN,
+  type OverlayDispatch,
+  type OverlayState,
+} from './shared'
+
+import { useState } from 'react'
 
 import { ShadowPortal } from './components/shadow-portal'
 import { Base } from './styles/base'
@@ -24,6 +31,23 @@ export function DevOverlay({
   getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
 }) {
   const [scale, setScale] = useDevToolsScale()
+  const [isPrevBuildError, setIsPrevBuildError] = useState(false)
+
+  const isBuildError = state.buildError !== null
+
+  if (
+    process.env.__NEXT_DEVTOOL_NEW_PANEL_UI &&
+    isBuildError !== isPrevBuildError
+  ) {
+    // If the build error is set, enable the devtools panel as the error overlay mode,
+    // and the rest actions (close, minimize, fullscreen) can be handled by the user.
+    if (isBuildError) {
+      dispatch({ type: ACTION_DEVTOOLS_PANEL_OPEN })
+      dispatch({ type: ACTION_ERROR_OVERLAY_OPEN })
+    }
+    setIsPrevBuildError(isBuildError)
+  }
+
   return (
     <ShadowPortal>
       <CssReset />
@@ -36,7 +60,6 @@ export function DevOverlay({
 
       <RenderError state={state} isAppDir={true}>
         {({ runtimeErrors, totalErrorCount }) => {
-          const isBuildError = state.buildError !== null
           return (
             <>
               {state.showIndicator &&
@@ -49,7 +72,8 @@ export function DevOverlay({
                       isBuildError={isBuildError}
                     />
 
-                    {state.isDevToolsPanelOpen && (
+                    {(state.isDevToolsPanelOpen ||
+                      state.isErrorOverlayOpen) && (
                       <DevToolsPanel
                         state={state}
                         dispatch={dispatch}
@@ -62,25 +86,27 @@ export function DevOverlay({
                     )}
                   </>
                 ) : (
-                  <DevToolsIndicator
-                    scale={scale}
-                    setScale={setScale}
-                    state={state}
-                    dispatch={dispatch}
-                    errorCount={totalErrorCount}
-                    isBuildError={isBuildError}
-                  />
-                ))}
+                  <>
+                    <DevToolsIndicator
+                      scale={scale}
+                      setScale={setScale}
+                      state={state}
+                      dispatch={dispatch}
+                      errorCount={totalErrorCount}
+                      isBuildError={isBuildError}
+                    />
 
-              <ErrorOverlay
-                state={state}
-                dispatch={dispatch}
-                getSquashedHydrationErrorDetails={
-                  getSquashedHydrationErrorDetails
-                }
-                runtimeErrors={runtimeErrors}
-                errorCount={totalErrorCount}
-              />
+                    <ErrorOverlay
+                      state={state}
+                      dispatch={dispatch}
+                      getSquashedHydrationErrorDetails={
+                        getSquashedHydrationErrorDetails
+                      }
+                      runtimeErrors={runtimeErrors}
+                      errorCount={totalErrorCount}
+                    />
+                  </>
+                ))}
             </>
           )
         }}

filename: packages/next/src/next-devtools/dev-overlay/shared.ts
patch: @@ -38,6 +38,10 @@ export interface OverlayState {
   disableDevIndicator: boolean
   debugInfo: DebugInfo
   routerType: 'pages' | 'app'
+  /** This flag is used to handle the Error Overlay state in the "old" overlay.
+   *  In the DevTools panel, this value will used for the "Error Overlay Mode"
+   *  which is viewing the "Issues Tab" as a fullscreen.
+   */
   isErrorOverlayOpen: boolean
   isDevToolsPanelOpen: boolean
   devToolsPosition: Corners

filename: packages/next/src/next-devtools/dev-overlay/storybook/use-overlay-reducer.ts
patch: @@ -10,6 +10,7 @@ import {
   ACTION_ERROR_OVERLAY_TOGGLE,
   ACTION_DEVTOOLS_SCALE,
   INITIAL_OVERLAY_STATE,
+  ACTION_DEVTOOLS_PANEL_OPEN,
 } from '../shared'
 
 export const storybookDefaultOverlayState: OverlayState = {
@@ -42,6 +43,9 @@ export function useStorybookOverlayReducer(initialState?: OverlayState) {
         case ACTION_DEVTOOLS_PANEL_CLOSE: {
           return { ...state, isDevToolsPanelOpen: false }
         }
+        case ACTION_DEVTOOLS_PANEL_OPEN: {
+          return { ...state, isDevToolsPanelOpen: true }
+        }
         case ACTION_DEVTOOLS_POSITION: {
           return { ...state, devToolsPosition: action.devToolsPosition }
         }