diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts
index 5ac810ec67f..65e833ea801 100644
--- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts
+++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts
@@ -71,6 +71,11 @@ export async function makeAppSetup(
PLATFORM.IS_LINUX && app.disableHardwareAcceleration();
+// macOS Sequoia+: occluded window throttling can corrupt GPU compositor layers
+if (PLATFORM.IS_MAC) {
+ app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
+}
+
PLATFORM.IS_WINDOWS &&
app.setAppUserModelId(
env.NODE_ENV === "development" ? process.execPath : makeAppId(),
diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts
index a5f2fb7aaaf..b993b1edd71 100644
--- a/apps/desktop/src/main/windows/main.ts
+++ b/apps/desktop/src/main/windows/main.ts
@@ -2,11 +2,11 @@ import { join } from "node:path";
import { workspaces, worktrees } from "@superset/local-db";
import { eq } from "drizzle-orm";
import type { BrowserWindow } from "electron";
-import { Notification } from "electron";
+import { app, Notification, nativeTheme } from "electron";
import { createWindow } from "lib/electron-app/factories/windows/create";
import { createAppRouter } from "lib/trpc/routers";
import { localDb } from "main/lib/local-db";
-import { NOTIFICATION_EVENTS, PORTS } from "shared/constants";
+import { NOTIFICATION_EVENTS, PLATFORM, PORTS } from "shared/constants";
import { createIPCHandler } from "trpc-electron/main";
import { productName } from "~/package.json";
import { appState } from "../lib/app-state";
@@ -61,6 +61,28 @@ let currentWindow: BrowserWindow | null = null;
// Getter for routers to access current window without stale references
const getWindow = () => currentWindow;
+// invalidate() alone may not rebuild corrupted GPU layers — a tiny resize
+// forces Chromium to reconstruct the compositor layer tree.
+const forceRepaint = (win: BrowserWindow) => {
+ if (win.isDestroyed()) return;
+ win.webContents.invalidate();
+ if (win.isMaximized() || win.isFullScreen()) return;
+ const [width, height] = win.getSize();
+ win.setSize(width + 1, height);
+ setTimeout(() => {
+ if (!win.isDestroyed()) win.setSize(width, height);
+ }, 32);
+};
+
+// GPU process restarts don't repaint existing compositor layers automatically.
+app.on("child-process-gone", (_event, details) => {
+ if (details.type === "GPU") {
+ console.warn("[main-window] GPU process gone:", details.reason);
+ const win = getWindow();
+ if (win) forceRepaint(win);
+ }
+});
+
export async function MainWindow() {
const savedWindowState = loadWindowState();
const initialBounds = getInitialWindowBounds(savedWindowState);
@@ -75,6 +97,7 @@ export async function MainWindow() {
minWidth: 400,
minHeight: 400,
show: false,
+ backgroundColor: nativeTheme.shouldUseDarkColors ? "#252525" : "#ffffff",
center: initialBounds.center,
movable: true,
resizable: true,
@@ -97,6 +120,11 @@ export async function MainWindow() {
currentWindow = window;
+ // macOS Sequoia+: background throttling can corrupt GPU compositor layers
+ if (PLATFORM.IS_MAC) {
+ window.webContents.setBackgroundThrottling(false);
+ }
+
if (ipcHandler) {
ipcHandler.attachWindow(window);
} else {
@@ -204,6 +232,16 @@ export async function MainWindow() {
},
);
+ // macOS Sequoia+: occluded/minimized windows can lose compositor layers
+ if (PLATFORM.IS_MAC) {
+ window.on("restore", () => {
+ window.webContents.invalidate();
+ });
+ window.on("show", () => {
+ window.webContents.invalidate();
+ });
+ }
+
window.webContents.on("did-finish-load", async () => {
console.log("[main-window] Renderer loaded successfully");
// Restore maximized state if it was saved
diff --git a/apps/desktop/src/renderer/components/BootErrorBoundary/BootErrorBoundary.tsx b/apps/desktop/src/renderer/components/BootErrorBoundary/BootErrorBoundary.tsx
new file mode 100644
index 00000000000..15c347bdba4
--- /dev/null
+++ b/apps/desktop/src/renderer/components/BootErrorBoundary/BootErrorBoundary.tsx
@@ -0,0 +1,76 @@
+import type { ReactNode } from "react";
+import { Component } from "react";
+
+export interface BootErrorBoundaryProps {
+ children: ReactNode;
+ onError?: (error: Error) => void;
+}
+
+interface BootErrorBoundaryState {
+ hasError: boolean;
+ error?: Error;
+}
+
+export class BootErrorBoundary extends Component<
+ BootErrorBoundaryProps,
+ BootErrorBoundaryState
+> {
+ state: BootErrorBoundaryState = { hasError: false };
+
+ static getDerivedStateFromError(error: Error): BootErrorBoundaryState {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error): void {
+ console.error("[renderer] Boot error boundary caught:", error);
+ this.props.onError?.(error);
+ }
+
+ render() {
+ if (!this.state.hasError) {
+ return this.props.children;
+ }
+
+ return (
+
+
+
+ Superset failed to start
+
+
+ The renderer crashed during startup. Please check logs for details.
+
+
+
+
+
+ );
+ }
+}
diff --git a/apps/desktop/src/renderer/components/BootErrorBoundary/index.ts b/apps/desktop/src/renderer/components/BootErrorBoundary/index.ts
new file mode 100644
index 00000000000..6e8ab7d5dae
--- /dev/null
+++ b/apps/desktop/src/renderer/components/BootErrorBoundary/index.ts
@@ -0,0 +1 @@
+export { BootErrorBoundary } from "./BootErrorBoundary";
diff --git a/apps/desktop/src/renderer/index.tsx b/apps/desktop/src/renderer/index.tsx
index 0354ef032dc..7bbcf4bf404 100644
--- a/apps/desktop/src/renderer/index.tsx
+++ b/apps/desktop/src/renderer/index.tsx
@@ -4,6 +4,14 @@ initSentry();
import { createRouter, RouterProvider } from "@tanstack/react-router";
import ReactDom from "react-dom/client";
+import { BootErrorBoundary } from "./components/BootErrorBoundary";
+import {
+ cleanupBootErrorHandling,
+ initBootErrorHandling,
+ isBootErrorReported,
+ markBootMounted,
+ reportBootError,
+} from "./lib/boot-errors";
import { persistentHistory } from "./lib/persistent-hash-history";
import { posthog } from "./lib/posthog";
import { electronQueryClient } from "./providers/ElectronTRPCProvider";
@@ -11,6 +19,9 @@ import { routeTree } from "./routeTree.gen";
import "./globals.css";
+const rootElement = document.querySelector("app");
+initBootErrorHandling(rootElement);
+
const router = createRouter({
routeTree,
history: persistentHistory,
@@ -30,12 +41,19 @@ const handleDeepLink = (path: string) => {
console.log("[deep-link] Navigating to:", path);
router.navigate({ to: path });
};
-window.ipcRenderer.on("deep-link-navigate", handleDeepLink);
+if (window.ipcRenderer?.on && window.ipcRenderer?.off) {
+ window.ipcRenderer.on("deep-link-navigate", handleDeepLink);
+} else {
+ reportBootError(
+ "Renderer preload not available (window.ipcRenderer missing)",
+ );
+}
if (import.meta.hot) {
import.meta.hot.dispose(() => {
unsubscribe();
- window.ipcRenderer.off("deep-link-navigate", handleDeepLink);
+ window.ipcRenderer?.off?.("deep-link-navigate", handleDeepLink);
+ cleanupBootErrorHandling();
});
}
@@ -45,6 +63,15 @@ declare module "@tanstack/react-router" {
}
}
-ReactDom.createRoot(document.querySelector("app") as HTMLElement).render(
- ,
-);
+if (!rootElement) {
+ reportBootError("Missing root element");
+} else if (!isBootErrorReported()) {
+ ReactDom.createRoot(rootElement).render(
+ reportBootError("Render failed", error)}
+ >
+
+ ,
+ );
+ markBootMounted();
+}
diff --git a/apps/desktop/src/renderer/lib/boot-errors.ts b/apps/desktop/src/renderer/lib/boot-errors.ts
new file mode 100644
index 00000000000..5a62dd0fb05
--- /dev/null
+++ b/apps/desktop/src/renderer/lib/boot-errors.ts
@@ -0,0 +1,99 @@
+let bootErrorReported = false;
+let hasMounted = false;
+let rootElement: Element | null = null;
+let listenersAttached = false;
+
+const renderBootError = (message: string, error?: unknown) => {
+ if (bootErrorReported) return;
+ bootErrorReported = true;
+
+ const container = rootElement ?? document.body;
+ const wrapper = document.createElement("div");
+ wrapper.style.display = "flex";
+ wrapper.style.height = "100vh";
+ wrapper.style.alignItems = "center";
+ wrapper.style.justifyContent = "center";
+ wrapper.style.background = "#0f0f0f";
+ wrapper.style.color = "#e5e5e5";
+ wrapper.style.fontFamily = "system-ui, sans-serif";
+ wrapper.style.padding = "24px";
+ wrapper.style.textAlign = "center";
+
+ const inner = document.createElement("div");
+ inner.style.maxWidth = "520px";
+
+ const title = document.createElement("div");
+ title.textContent = "Superset failed to start";
+ title.style.fontSize = "18px";
+ title.style.marginBottom = "8px";
+
+ const detail = document.createElement("div");
+ detail.textContent = message;
+ detail.style.fontSize = "14px";
+ detail.style.opacity = "0.8";
+
+ inner.appendChild(title);
+ inner.appendChild(detail);
+
+ if (error) {
+ let errorText = "Unknown error";
+ if (error instanceof Error) {
+ errorText = error.message;
+ } else if (typeof error === "string") {
+ errorText = error;
+ } else {
+ try {
+ errorText = JSON.stringify(error);
+ } catch {
+ errorText = String(error);
+ }
+ }
+ const pre = document.createElement("pre");
+ pre.textContent = errorText;
+ pre.style.marginTop = "12px";
+ pre.style.fontSize = "12px";
+ pre.style.opacity = "0.7";
+ pre.style.whiteSpace = "pre-wrap";
+ inner.appendChild(pre);
+ }
+
+ wrapper.appendChild(inner);
+ container.replaceChildren(wrapper);
+};
+
+export const reportBootError = (message: string, error?: unknown) => {
+ console.error("[renderer] Boot error:", message, error);
+ if (hasMounted) return;
+ renderBootError(message, error);
+};
+
+const handleGlobalError = (event: ErrorEvent) => {
+ if (hasMounted) return;
+ reportBootError(event.message || "Unhandled error", event.error);
+};
+
+const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
+ if (hasMounted) return;
+ reportBootError("Unhandled promise rejection", event.reason);
+};
+
+export const initBootErrorHandling = (root: Element | null) => {
+ rootElement = root;
+ if (listenersAttached) return;
+ listenersAttached = true;
+ window.addEventListener("error", handleGlobalError);
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
+};
+
+export const cleanupBootErrorHandling = () => {
+ if (!listenersAttached) return;
+ listenersAttached = false;
+ window.removeEventListener("error", handleGlobalError);
+ window.removeEventListener("unhandledrejection", handleUnhandledRejection);
+};
+
+export const markBootMounted = () => {
+ hasMounted = true;
+};
+
+export const isBootErrorReported = () => bootErrorReported;
diff --git a/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx b/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx
index ca9e7528368..985421d5e7c 100644
--- a/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx
+++ b/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx
@@ -11,12 +11,16 @@ export function PostHogProvider({ children }: PostHogProviderProps) {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
- initPostHog();
- posthog.capture("desktop_opened");
- setIsInitialized(true);
+ try {
+ initPostHog();
+ posthog.capture("desktop_opened");
+ } catch (error) {
+ console.error("[posthog] Failed to initialize:", error);
+ } finally {
+ setIsInitialized(true);
+ }
}, []);
- // Don't render children until PostHog is initialized
if (!isInitialized) {
return null;
}