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>
This commit is contained in:
jbofill
2026-05-25 17:43:17 -06:00
committed by GitHub
parent bef7ede91f
commit acaf165219
2 changed files with 346 additions and 171 deletions
+10
View File
@@ -82,6 +82,7 @@ class Utilities:
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.ws.add_route("utilities/enable_plugin", self.enable_plugin) context.ws.add_route("utilities/enable_plugin", self.enable_plugin)
context.ws.add_route("utilities/disable_plugin", self.disable_plugin) context.ws.add_route("utilities/disable_plugin", self.disable_plugin)
context.ws.add_route("utilities/set_all_plugins_disabled", self.set_all_plugins_disabled)
context.web_app.add_routes([ context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call) post("/methods/{method_name}", self._handle_legacy_server_method_call)
@@ -504,3 +505,12 @@ class Utilities:
await self.set_setting("disabled_plugins", disabled_plugins) await self.set_setting("disabled_plugins", disabled_plugins)
await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder) await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
async def set_all_plugins_disabled(self):
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
for name, _ in self.context.plugin_loader.plugins.items():
if name not in disabled_plugins:
disabled_plugins.append(name)
await self.set_setting("disabled_plugins", disabled_plugins)
+247 -82
View File
@@ -1,7 +1,7 @@
import { sleep } from '@decky/ui'; import { joinClassNames, sleep } from '@decky/ui';
import { FunctionComponent, useEffect, useReducer, useState } from 'react'; import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin'; import { disablePlugin, uninstallPlugin } from '../plugin';
import { VerInfo, doRestart, doShutdown } from '../updater'; import { VerInfo, doRestart, doShutdown } from '../updater';
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors'; import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
import { useSetting } from '../utils/hooks/useSetting'; 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 startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging'); export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
@@ -64,39 +84,131 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
<> <>
<style> <style>
{` {`
*:has(> .deckyErrorBoundary) { *:has(> .${classes.root}) {
margin-top: var(--basicui-header-height);
overflow: scroll !important; 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> </style>
<div <div className={classes.root}>
style={{ <div className={classes.marginBottom}>An error occurred while rendering this content.</div>
overflow: 'auto', <pre className={joinClassNames(classes.marginBottom)} style={{ marginTop: '0px' }}>
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={{}}>
<code> <code>
{identifier && `Error Reference: ${identifier}`} {identifier && `Error Reference: ${identifier}`}
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`} {versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code> </code>
</pre> </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 && ( {actionLog?.length > 0 && (
<pre> <pre>
<code> <code>
@@ -106,15 +218,20 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
</pre> </pre>
)} )}
{actionsEnabled && ( {actionsEnabled && (
<> <div className={classes.panel}>
<h3>Actions: </h3> <div className={classes.flexRowWGap} style={{ alignItems: 'center', marginBottom: '8px' }}>
<p>Use the touch screen.</p> <div className={classes.panelHeader}>Actions</div>
<div style={{ display: 'block', marginBottom: '5px' }}> <div style={{ fontSize: 'small', fontStyle: 'italic' }}>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}> Use the touch screen. Solutions are listed in the recommended order. If you are still experiencing
Retry issues, please post in the #loader-support channel at decky.xyz/discord.
</button> </div>
</div>
<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 <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={() => { onClick={() => {
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart(false);
@@ -122,10 +239,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
> >
Restart Steam Restart Steam
</button> </button>
</div>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setActionsEnabled(false); setActionsEnabled(false);
addLogLine('Restarting Decky...'); addLogLine('Restarting Decky...');
@@ -136,53 +250,80 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
> >
Restart Decky Restart Decky
</button> </button>
</div>
</div>
{wasCausedByPlugin && (
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Disable or uninstall the suspected plugin
<div className={classes.flexRowWGap}>
<button <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setActionsEnabled(false); setActionsEnabled(false);
addLogLine('Stopping Decky...'); addLogLine(`Disabling ${errorSource}...`);
doShutdown(); await disablePlugin(errorSource);
await sleep(5000); await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart(false);
}} }}
> >
Disable Decky until next boot Disable {errorSource}
</button> </button>
</div>
{debugAllowed && (
<div style={{ display: 'block', marginBottom: '5px' }}>
<button <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setDebugAllowed(false); setActionsEnabled(false);
addLogLine('Enabling CEF debugger forwarding...'); addLogLine(`Uninstalling ${errorSource}...`);
await starrCEFForwarding(); await uninstallPlugin(errorSource);
addLogLine('Enabling SSH...'); await DeckyPluginLoader.frozenPluginsService.invalidate();
await startSSH(); await DeckyPluginLoader.hiddenPluginsService.invalidate();
addLogLine('Ready for debugging!'); await sleep(1000);
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) { addLogLine('Restarting Decky...');
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip); doRestart();
addLogLine(`CEF Debugger: http://${ip}:8081`); await sleep(2000);
addLogLine(`SSH: deck@${ip}`); addLogLine('Restarting Steam...');
} await sleep(500);
SteamClient.User.StartRestart(false);
}} }}
> >
Allow remote debugging and SSH until next boot Uninstall {errorSource}
</button> </button>
</div> </div>
</div>
)} )}
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Disable all plugins
<button
onClick={async () => {
setActionsEnabled(false);
addLogLine(`Disabling plugins...`);
await DeckyBackend.call('utilities/set_all_plugins_disabled');
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart(false);
}}
>
Disable All Plugins
</button>
</div>
{ {
<div style={{ display: 'block', marginBottom: '5px' }}> <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
{updateProgress > -1 {updateProgress > -1
? 'Update in progress... ' + updateProgress + '%' ? 'Update in progress... ' + updateProgress + '%'
: updateProgress == -2 : updateProgress == -2
? 'Update complete. Restarting...' ? 'Update complete. Restarting...'
: 'Changing your Decky Loader branch and/or \n checking for updates might help!\n'} : 'Check for Decky updates'}
{
<div className={classes.flexRowWGap}>
{updateProgress == -1 && ( {updateProgress == -1 && (
<div style={{ height: '30px' }}> <>
<select <select
style={{ height: '100%' }}
onChange={async (e) => { onChange={async (e) => {
const branch = parseInt(e.target.value); const branch = parseInt(e.target.value);
setSelectedBranch(branch); setSelectedBranch(branch);
@@ -200,7 +341,6 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
</option> </option>
</select> </select>
<button <button
style={{ height: '100%' }}
disabled={updateProgress != -1 || isChecking} disabled={updateProgress != -1 || isChecking}
onClick={async () => { onClick={async () => {
if (versionToUpdateTo == '') { if (versionToUpdateTo == '') {
@@ -227,42 +367,66 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
? 'Update to ' + versionToUpdateTo ? 'Update to ' + versionToUpdateTo
: 'Check for updates'} : 'Check for updates'}
</button> </button>
</div> </>
)} )}
</div> </div>
} }
{wasCausedByPlugin && ( </div>
<div style={{ display: 'block', marginBottom: '5px' }}> }
{'\n'} <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Disable Decky until next boot
<button <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setActionsEnabled(false); setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`); addLogLine('Stopping Decky...');
await uninstallPlugin(errorSource); doShutdown();
await DeckyPluginLoader.frozenPluginsService.invalidate(); await sleep(5000);
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart(false);
}} }}
> >
Uninstall {errorSource} and restart Decky 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> </button>
</div> </div>
)} )}
</> </div>
</div>
)} )}
{actionsEnabled && (
<div className={classes.swipePrompt}>
<span>Swipe to scroll</span>
</div>
)}
<div className={classes.panel}>
<div className={classes.panelHeader}>Trace</div>
<pre <pre
style={{ style={{
marginTop: '15px', margin: `8px calc(-1 * ${vars.panelXPadding})`,
opacity: 0.7,
userSelect: 'auto', userSelect: 'auto',
overflowX: 'scroll',
padding: `0px ${vars.panelXPadding}`,
maskImage: `linear-gradient(to right, transparent, black ${vars.panelXPadding}, black calc(100% - ${vars.panelXPadding}), transparent)`,
}} }}
> >
<code> <code>
@@ -273,6 +437,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
</code> </code>
</pre> </pre>
</div> </div>
</div>
</> </>
); );
}; };