From acaf16521993f7e90206337f24e05649688554e4 Mon Sep 17 00:00:00 2001
From: jbofill <74568881+jessebofill@users.noreply.github.com>
Date: Mon, 25 May 2026 17:43:17 -0600
Subject: Improved error screen (#841)

* improve the error screen visuals

* comment out placeholder buttons

* run formatter

* Refactor DeckyErrorBoundary styles and text

- Removed gray text class usage
- Removed styles reminiscent of Steam BPM
- Fixed typos

* Further refactor of DeckyErrorBoundary.tsx

- Change background/text of buttons to be closer to Steam Deck UI
- Make panel background not reliant on transparency and have a neutral gray
- Bold "likely occurred" text
- Make swipe prompt appear in the center of a horizontal bar, drawing more attention to it
- Make "An error occurred" text smaller, as it isn't helpful for troubleshooting
- Add text clarifying solutions are in recommended order and how to get more help
- Add "Retry the action or restart" to the left of Retry, Restart Steam, and Restart Decky buttons
- Move disabling Decky to beneath the Decky update checking

* Revert header boldness change

* add disable plugin buttons to error screen

* Set background to black

---------

Co-authored-by: EMERALD <info@eme.wtf>
---
 frontend/src/components/DeckyErrorBoundary.tsx | 499 ++++++++++++++++---------
 1 file changed, 332 insertions(+), 167 deletions(-)

(limited to 'frontend/src/components/DeckyErrorBoundary.tsx')

diff --git a/frontend/src/components/DeckyErrorBoundary.tsx b/frontend/src/components/DeckyErrorBoundary.tsx
index 7a8d2b31..29638521 100644
--- a/frontend/src/components/DeckyErrorBoundary.tsx
+++ b/frontend/src/components/DeckyErrorBoundary.tsx
@@ -1,7 +1,7 @@
-import { sleep } from '@decky/ui';
+import { joinClassNames, sleep } from '@decky/ui';
 import { FunctionComponent, useEffect, useReducer, useState } from 'react';
 
-import { uninstallPlugin } from '../plugin';
+import { disablePlugin, uninstallPlugin } from '../plugin';
 import { VerInfo, doRestart, doShutdown } from '../updater';
 import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
 import { useSetting } from '../utils/hooks/useSetting';
@@ -20,6 +20,26 @@ declare global {
   }
 }
 
+const classes = {
+  root: 'deckyErrorBoundary',
+  likelyOccurred: 'likely-occured-msg',
+  panel: 'panel-section',
+  panelHeader: 'panel-header',
+  trace: 'trace',
+  rowList: 'row-list',
+  rowItem: 'row-item',
+  buttonDescRow: 'button-description-row',
+  flexRowWGap: 'flex-row',
+  marginBottom: 'margin-bottom',
+  swipePrompt: 'swipe-prompt',
+};
+
+const vars = {
+  scrollBarwidth: '18px',
+  rootMarginLeft: '15px',
+  panelXPadding: '20px',
+};
+
 export const startSSH = DeckyBackend.callable('utilities/start_ssh');
 export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
 
@@ -64,39 +84,131 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
     <>
       <style>
         {`
-          *:has(> .deckyErrorBoundary) {
+          *:has(> .${classes.root}) {
+            margin-top: var(--basicui-header-height);
             overflow: scroll !important;
+            background: #000;
+          }
+          *:has(> .${classes.root})::-webkit-scrollbar {
+            display: initial !important;
+            width: ${vars.scrollBarwidth};
+            height: 0px;
+          }
+          *:has(> .${classes.root})::-webkit-scrollbar-thumb {
+            background: #4349535e;
+          }
+          .${classes.root} {
+            color: #93929e;
+            font-size: 15px;
+            margin: 10px 0px 40px ${vars.rootMarginLeft};
+            width: calc(100vw - ${vars.scrollBarwidth} - ${vars.rootMarginLeft});
+            overflow: visible;
+          }
+          .${classes.root} button,
+          .${classes.root} select {
+            border: none;
+            padding: 4px 16px !important;
+            background: #333;
+            color: #ddd;
+            font-size: 12px;
+            border-radius: 3px;
+            outline: none;
+            height: 28px;
+          }
+          .${classes.panel} {
+            background: #080808;
+            padding: 8px ${vars.panelXPadding};
+            border-radius: 3px;
+            /* box-shadow: 9px 9px 20px -5px rgb(0 0 0 / 89%); */
+          }
+          .${classes.panelHeader} {
+            font-size: 18px;
+            font-weight: bolder;
+            text-transform: uppercase;
+          }
+          .${classes.likelyOccurred} {
+            font-size: 22px;
+            font-weight: bold;
+            color: #588fb4;
+          }
+          .${classes.rowItem} {
+            position: relative;
+          }
+          .${classes.rowItem}:not(:last-child)::after {
+            content: '';
+            position: absolute;
+            bottom: -4.5px;
+            left: 5px;
+            right: 15px;
+            height: 0.5px;
+            background: #3c3c3c47;
+          }
+          .${classes.flexRowWGap},
+          .${classes.buttonDescRow},
+          .${classes.rowList},
+          .${classes.panel} {
+            display: flex;
+          }
+
+          .${classes.rowList},
+          .${classes.panel} {
+            flex-direction: column;
+          }
+          .${classes.flexRowWGap},
+          .${classes.rowList} {
+            gap: 8px;
+          }
+          .${classes.marginBottom} {
+            margin-bottom: 10px;
+          }
+          .${classes.buttonDescRow} {
+            justify-content: space-between;
+            align-items: center;
+          }
+
+          .${classes.swipePrompt} {
+            display: flex;
+            align-items: center;
+            text-align: center;
+            position: relative;
+            font-style: italic;
+            font-size: small;
+            margin: 16px 0;
+          }
+          .${classes.swipePrompt} span {
+            padding: 0 8px;
+            background-color: #000;
+            position: relative;
+            z-index: 1;
+          }
+          .${classes.swipePrompt}::before,
+          .${classes.swipePrompt}::after {
+            content: "";
+            flex-grow: 1;
+            border-bottom: 1px solid #474752;
+            top: 50%;
+          }
+          .${classes.swipePrompt}::before {
+            right: 50%;
+            margin-right: 8px;
+          }
+          .${classes.swipePrompt}::after {
+            left: 50%;
+            margin-left: 8px;
           }
         `}
       </style>
-      <div
-        style={{
-          overflow: 'auto',
-          marginLeft: '15px',
-          color: 'white',
-          fontSize: '16px',
-          userSelect: 'auto',
-          backgroundColor: 'black',
-          marginTop: '48px', // Incase this is a page
-        }}
-        className="deckyErrorBoundary"
-      >
-        <h1
-          style={{
-            fontSize: '20px',
-            display: 'inline-block',
-            userSelect: 'auto',
-          }}
-        >
-          ⚠️ An error occured while rendering this content.
-        </h1>
-        <pre style={{}}>
+      <div className={classes.root}>
+        <div className={classes.marginBottom}>An error occurred while rendering this content.</div>
+        <pre className={joinClassNames(classes.marginBottom)} style={{ marginTop: '0px' }}>
           <code>
             {identifier && `Error Reference: ${identifier}`}
             {versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
           </code>
         </pre>
-        <p>This error likely occured in {errorSource}.</p>
+        <div className={joinClassNames(classes.likelyOccurred, classes.marginBottom)}>
+          This error likely occurred in {errorSource}.
+        </div>
         {actionLog?.length > 0 && (
           <pre>
             <code>
@@ -106,142 +218,88 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
           </pre>
         )}
         {actionsEnabled && (
-          <>
-            <h3>Actions: </h3>
-            <p>Use the touch screen.</p>
-            <div style={{ display: 'block', marginBottom: '5px' }}>
-              <button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
-                Retry
-              </button>
-              <button
-                style={{ marginRight: '5px', padding: '5px' }}
-                onClick={() => {
-                  addLogLine('Restarting Steam...');
-                  SteamClient.User.StartRestart(false);
-                }}
-              >
-                Restart Steam
-              </button>
-            </div>
-            <div style={{ display: 'block', marginBottom: '5px' }}>
-              <button
-                style={{ marginRight: '5px', padding: '5px' }}
-                onClick={async () => {
-                  setActionsEnabled(false);
-                  addLogLine('Restarting Decky...');
-                  doRestart();
-                  await sleep(2000);
-                  addLogLine('Reloading UI...');
-                }}
-              >
-                Restart Decky
-              </button>
-              <button
-                style={{ marginRight: '5px', padding: '5px' }}
-                onClick={async () => {
-                  setActionsEnabled(false);
-                  addLogLine('Stopping Decky...');
-                  doShutdown();
-                  await sleep(5000);
-                  addLogLine('Restarting Steam...');
-                  SteamClient.User.StartRestart(false);
-                }}
-              >
-                Disable Decky until next boot
-              </button>
+          <div className={classes.panel}>
+            <div className={classes.flexRowWGap} style={{ alignItems: 'center', marginBottom: '8px' }}>
+              <div className={classes.panelHeader}>Actions</div>
+              <div style={{ fontSize: 'small', fontStyle: 'italic' }}>
+                Use the touch screen. Solutions are listed in the recommended order. If you are still experiencing
+                issues, please post in the #loader-support channel at decky.xyz/discord.
+              </div>
             </div>
-            {debugAllowed && (
-              <div style={{ display: 'block', marginBottom: '5px' }}>
-                <button
-                  style={{ marginRight: '5px', padding: '5px' }}
-                  onClick={async () => {
-                    setDebugAllowed(false);
-                    addLogLine('Enabling CEF debugger forwarding...');
-                    await starrCEFForwarding();
-                    addLogLine('Enabling SSH...');
-                    await startSSH();
-                    addLogLine('Ready for debugging!');
-                    if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
-                      const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
-                      addLogLine(`CEF Debugger: http://${ip}:8081`);
-                      addLogLine(`SSH: deck@${ip}`);
-                    }
-                  }}
-                >
-                  Allow remote debugging and SSH until next boot
-                </button>
+            <div className={classes.rowList}>
+              <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                Retry the action or restart
+                <div className={classes.flexRowWGap}>
+                  <button onClick={reset}>Retry</button>
+                  <button
+                    onClick={() => {
+                      addLogLine('Restarting Steam...');
+                      SteamClient.User.StartRestart(false);
+                    }}
+                  >
+                    Restart Steam
+                  </button>
+                  <button
+                    onClick={async () => {
+                      setActionsEnabled(false);
+                      addLogLine('Restarting Decky...');
+                      doRestart();
+                      await sleep(2000);
+                      addLogLine('Reloading UI...');
+                    }}
+                  >
+                    Restart Decky
+                  </button>
+                </div>
               </div>
-            )}
-            {
-              <div style={{ display: 'block', marginBottom: '5px' }}>
-                {updateProgress > -1
-                  ? 'Update in progress... ' + updateProgress + '%'
-                  : updateProgress == -2
-                    ? 'Update complete. Restarting...'
-                    : 'Changing your Decky Loader branch and/or \n checking for updates might help!\n'}
-                {updateProgress == -1 && (
-                  <div style={{ height: '30px' }}>
-                    <select
-                      style={{ height: '100%' }}
-                      onChange={async (e) => {
-                        const branch = parseInt(e.target.value);
-                        setSelectedBranch(branch);
-                        setSetVersionToUpdateTo('');
+              {wasCausedByPlugin && (
+                <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                  Disable or uninstall the suspected plugin
+                  <div className={classes.flexRowWGap}>
+                    <button
+                      onClick={async () => {
+                        setActionsEnabled(false);
+                        addLogLine(`Disabling ${errorSource}...`);
+                        await disablePlugin(errorSource);
+                        await sleep(1000);
+                        addLogLine('Restarting Decky...');
+                        doRestart();
+                        await sleep(2000);
+                        addLogLine('Restarting Steam...');
+                        await sleep(500);
+                        SteamClient.User.StartRestart(false);
                       }}
                     >
-                      <option value="0" selected={selectedBranch == UpdateBranch.Stable}>
-                        Stable
-                      </option>
-                      <option value="1" selected={selectedBranch == UpdateBranch.Prerelease}>
-                        Pre-Release
-                      </option>
-                      <option value="2" selected={selectedBranch == UpdateBranch.Testing}>
-                        Testing
-                      </option>
-                    </select>
+                      Disable {errorSource}
+                    </button>
                     <button
-                      style={{ height: '100%' }}
-                      disabled={updateProgress != -1 || isChecking}
                       onClick={async () => {
-                        if (versionToUpdateTo == '') {
-                          setIsChecking(true);
-                          const versionInfo = (await DeckyBackend.callable(
-                            'updater/check_for_updates',
-                          )()) as unknown as VerInfo;
-                          setIsChecking(false);
-                          if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
-                            setSetVersionToUpdateTo(versionInfo.remote.tag_name);
-                          } else {
-                            setSetVersionToUpdateTo('');
-                          }
-                        } else {
-                          DeckyBackend.callable('updater/do_update')();
-                          setUpdateProgress(0);
-                        }
+                        setActionsEnabled(false);
+                        addLogLine(`Uninstalling ${errorSource}...`);
+                        await uninstallPlugin(errorSource);
+                        await DeckyPluginLoader.frozenPluginsService.invalidate();
+                        await DeckyPluginLoader.hiddenPluginsService.invalidate();
+                        await sleep(1000);
+                        addLogLine('Restarting Decky...');
+                        doRestart();
+                        await sleep(2000);
+                        addLogLine('Restarting Steam...');
+                        await sleep(500);
+                        SteamClient.User.StartRestart(false);
                       }}
                     >
-                      {' '}
-                      {isChecking
-                        ? 'Checking for updates...'
-                        : versionToUpdateTo != ''
-                          ? 'Update to ' + versionToUpdateTo
-                          : 'Check for updates'}
+                      Uninstall {errorSource}
                     </button>
                   </div>
-                )}
-              </div>
-            }
-            {wasCausedByPlugin && (
-              <div style={{ display: 'block', marginBottom: '5px' }}>
-                {'\n'}
+                </div>
+              )}
+              <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                Disable all plugins
                 <button
-                  style={{ marginRight: '5px', padding: '5px' }}
                   onClick={async () => {
                     setActionsEnabled(false);
-                    addLogLine(`Uninstalling ${errorSource}...`);
-                    await uninstallPlugin(errorSource);
-                    await DeckyPluginLoader.frozenPluginsService.invalidate();
-                    await DeckyPluginLoader.hiddenPluginsService.invalidate();
+                    addLogLine(`Disabling plugins...`);
+                    await DeckyBackend.call('utilities/set_all_plugins_disabled');
                     await sleep(1000);
                     addLogLine('Restarting Decky...');
                     doRestart();
@@ -251,27 +309,134 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
                     SteamClient.User.StartRestart(false);
                   }}
                 >
-                  Uninstall {errorSource} and restart Decky
+                  Disable All Plugins
+                </button>
+              </div>
+              {
+                <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                  {updateProgress > -1
+                    ? 'Update in progress... ' + updateProgress + '%'
+                    : updateProgress == -2
+                      ? 'Update complete. Restarting...'
+                      : 'Check for Decky updates'}
+                  {
+                    <div className={classes.flexRowWGap}>
+                      {updateProgress == -1 && (
+                        <>
+                          <select
+                            onChange={async (e) => {
+                              const branch = parseInt(e.target.value);
+                              setSelectedBranch(branch);
+                              setSetVersionToUpdateTo('');
+                            }}
+                          >
+                            <option value="0" selected={selectedBranch == UpdateBranch.Stable}>
+                              Stable
+                            </option>
+                            <option value="1" selected={selectedBranch == UpdateBranch.Prerelease}>
+                              Pre-Release
+                            </option>
+                            <option value="2" selected={selectedBranch == UpdateBranch.Testing}>
+                              Testing
+                            </option>
+                          </select>
+                          <button
+                            disabled={updateProgress != -1 || isChecking}
+                            onClick={async () => {
+                              if (versionToUpdateTo == '') {
+                                setIsChecking(true);
+                                const versionInfo = (await DeckyBackend.callable(
+                                  'updater/check_for_updates',
+                                )()) as unknown as VerInfo;
+                                setIsChecking(false);
+                                if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
+                                  setSetVersionToUpdateTo(versionInfo.remote.tag_name);
+                                } else {
+                                  setSetVersionToUpdateTo('');
+                                }
+                              } else {
+                                DeckyBackend.callable('updater/do_update')();
+                                setUpdateProgress(0);
+                              }
+                            }}
+                          >
+                            {' '}
+                            {isChecking
+                              ? 'Checking for updates...'
+                              : versionToUpdateTo != ''
+                                ? 'Update to ' + versionToUpdateTo
+                                : 'Check for updates'}
+                          </button>
+                        </>
+                      )}
+                    </div>
+                  }
+                </div>
+              }
+              <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                Disable Decky until next boot
+                <button
+                  onClick={async () => {
+                    setActionsEnabled(false);
+                    addLogLine('Stopping Decky...');
+                    doShutdown();
+                    await sleep(5000);
+                    addLogLine('Restarting Steam...');
+                    SteamClient.User.StartRestart(false);
+                  }}
+                >
+                  Disable Decky
                 </button>
               </div>
-            )}
-          </>
+              {debugAllowed && (
+                <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
+                  Enable remote debugging and SSH until next boot (for developers)
+                  <button
+                    onClick={async () => {
+                      setDebugAllowed(false);
+                      addLogLine('Enabling CEF debugger forwarding...');
+                      await starrCEFForwarding();
+                      addLogLine('Enabling SSH...');
+                      await startSSH();
+                      addLogLine('Ready for debugging!');
+                      if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
+                        const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
+                        addLogLine(`CEF Debugger: http://${ip}:8081`);
+                        addLogLine(`SSH: deck@${ip}`);
+                      }
+                    }}
+                  >
+                    Enable
+                  </button>
+                </div>
+              )}
+            </div>
+          </div>
         )}
-
-        <pre
-          style={{
-            marginTop: '15px',
-            opacity: 0.7,
-            userSelect: 'auto',
-          }}
-        >
-          <code>
-            {error.error.stack}
-            {'\n\n'}
-            Component Stack:
-            {error.info.componentStack}
-          </code>
-        </pre>
+        {actionsEnabled && (
+          <div className={classes.swipePrompt}>
+            <span>Swipe to scroll</span>
+          </div>
+        )}
+        <div className={classes.panel}>
+          <div className={classes.panelHeader}>Trace</div>
+          <pre
+            style={{
+              margin: `8px calc(-1 * ${vars.panelXPadding})`,
+              userSelect: 'auto',
+              overflowX: 'scroll',
+              padding: `0px ${vars.panelXPadding}`,
+              maskImage: `linear-gradient(to right, transparent, black ${vars.panelXPadding}, black calc(100% - ${vars.panelXPadding}), transparent)`,
+            }}
+          >
+            <code>
+              {error.error.stack}
+              {'\n\n'}
+              Component Stack:
+              {error.info.componentStack}
+            </code>
+          </pre>
+        </div>
       </div>
     </>
   );
-- 
cgit v1.2.3

