Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/desktop/src/lib/electron-app/factories/app/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
42 changes: 40 additions & 2 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div
style={{
display: "flex",
height: "100vh",
alignItems: "center",
justifyContent: "center",
background: "#0f0f0f",
color: "#e5e5e5",
fontFamily: "system-ui, sans-serif",
padding: "24px",
textAlign: "center",
}}
>
<div style={{ maxWidth: "520px" }}>
<h1 style={{ fontSize: "18px", marginBottom: "8px" }}>
Superset failed to start
</h1>
<p style={{ fontSize: "14px", opacity: 0.8 }}>
The renderer crashed during startup. Please check logs for details.
</p>

<button
type="button"
onClick={() => window.location.reload()}
style={{
marginTop: "16px",
padding: "8px 20px",
fontSize: "14px",
background: "#333",
color: "#e5e5e5",
border: "1px solid #555",
borderRadius: "6px",
cursor: "pointer",
}}
>
Reload
</button>
</div>
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BootErrorBoundary } from "./BootErrorBoundary";
37 changes: 32 additions & 5 deletions apps/desktop/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ 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";
import { routeTree } from "./routeTree.gen";

import "./globals.css";

const rootElement = document.querySelector("app");
initBootErrorHandling(rootElement);

const router = createRouter({
routeTree,
history: persistentHistory,
Expand All @@ -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)",
);
}
Comment on lines +44 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the file and its context
cat -n apps/desktop/src/renderer/index.tsx | head -60

Repository: superset-sh/superset

Length of output: 2077


🏁 Script executed:

# Check for tRPC setup in the codebase
fd -t f "trpc" apps/desktop/src --type f -x basename {} \;

Repository: superset-sh/superset

Length of output: 155


🏁 Script executed:

# Search for window.ipcRenderer usage in renderer
rg "window\.ipcRenderer" apps/desktop/src/renderer --type ts --type tsx -n

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Look for deep-link handling elsewhere
rg "deep-link" apps/desktop/src -n

Repository: superset-sh/superset

Length of output: 452


🏁 Script executed:

# Check the shared IPC channels definition
cat -n apps/desktop/src/shared/ipc-channels.ts 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the electron-trpc.ts to see how tRPC is configured
cat -n apps/desktop/src/lib/trpc/electron-trpc.ts

Repository: superset-sh/superset

Length of output: 138


🏁 Script executed:

# Check ElectronTRPCProvider
cat -n apps/desktop/src/renderer/providers/ElectronTRPCProvider.tsx

Repository: superset-sh/superset

Length of output: 156


🏁 Script executed:

# Search for tRPC router definition
fd -t f "router\|.*procedure\|.*subscription" apps/desktop/src/lib/trpc -e ts -e tsx

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check main/lib to see IPC handlers
fd -t f ".*ipc" apps/desktop/src/main/lib -e ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if there are other subscription patterns in the codebase
rg "\.subscription\(" apps/desktop/src/lib/trpc -A 3

Repository: superset-sh/superset

Length of output: 3569


🏁 Script executed:

# List the trpc directory structure
find apps/desktop/src/lib/trpc -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: superset-sh/superset

Length of output: 1204


🏁 Script executed:

# Find ElectronTRPCProvider
find apps/desktop/src -name "*ElectronTRPC*" -o -name "*TRPC*Provider*"

Repository: superset-sh/superset

Length of output: 202


🏁 Script executed:

# Check the main trpc index or router
cat -n apps/desktop/src/lib/trpc/index.ts 2>/dev/null || cat -n apps/desktop/src/lib/trpc/router.ts 2>/dev/null || ls -la apps/desktop/src/lib/trpc/

Repository: superset-sh/superset

Length of output: 1642


🏁 Script executed:

# Look for where deep-link might be handled in main process
cat -n apps/desktop/src/main/index.ts | grep -A 5 -B 5 "deep-link"

Repository: superset-sh/superset

Length of output: 446


🏁 Script executed:

# Check if there's a router definition for navigation or deep-link
rg "navigation\|deepLink\|deep.link" apps/desktop/src/lib/trpc -t ts -i

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the ElectronTRPCProvider to understand how tRPC subscriptions work
cat -n apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx

Repository: superset-sh/superset

Length of output: 1272


🏁 Script executed:

# Check the menu router to understand the subscription pattern for events
cat -n apps/desktop/src/lib/trpc/routers/menu.ts | head -80

Repository: superset-sh/superset

Length of output: 1352


🏁 Script executed:

# Look for the routers index to see what's exported
cat -n apps/desktop/src/lib/trpc/routers/index.ts

Repository: superset-sh/superset

Length of output: 2277


🏁 Script executed:

# Check the app router to see all available procedures
cat -n apps/desktop/src/lib/trpc/routers/app.ts 2>/dev/null || cat -n apps/desktop/src/lib/trpc/routers.ts

Repository: superset-sh/superset

Length of output: 132


🏁 Script executed:

# Check if there's any deep-link related event emitter
find apps/desktop/src/main -name "*deep*" -o -name "*navigation*" -o -name "*events*"

Repository: superset-sh/superset

Length of output: 148


🏁 Script executed:

# Check the main/lib directory structure
ls -la apps/desktop/src/main/lib/

Repository: superset-sh/superset

Length of output: 2057


🏁 Script executed:

# Search for EventEmitter patterns in main/lib
rg "EventEmitter\|emitter\|\.on\(" apps/desktop/src/main/lib -t ts --max-count=5

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the menu-events pattern to understand how events are exposed
cat -n apps/desktop/src/main/lib/menu-events.ts

Repository: superset-sh/superset

Length of output: 541


Route deep-link navigation through a tRPC subscription instead of window.ipcRenderer.

Direct ipcRenderer access bypasses the tRPC IPC layer established throughout the codebase. Create a deep-link-events.ts emitter in src/main/lib/ and expose deep-link navigation as a tRPC subscription in the app router (following the pattern used in menu.ts), then consume it through the tRPC client in the renderer instead.

🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/index.tsx` around lines 44 - 50, Current code
directly uses window.ipcRenderer and handleDeepLink in the renderer; instead
create a deep-link-events.ts emitter under src/main/lib (mirroring menu.ts
emitter pattern), add a tRPC subscription in the app router that forwards
emitted deep-link events, then remove the window.ipcRenderer usage and subscribe
to that tRPC subscription from the renderer via the trpc client and call
handleDeepLink from the subscription handler; reference the existing symbols
handleDeepLink, window.ipcRenderer, menu.ts pattern, and the new
deep-link-events.ts and the app router subscription when making these changes.


if (import.meta.hot) {
import.meta.hot.dispose(() => {
unsubscribe();
window.ipcRenderer.off("deep-link-navigate", handleDeepLink);
window.ipcRenderer?.off?.("deep-link-navigate", handleDeepLink);
cleanupBootErrorHandling();
});
}

Expand All @@ -45,6 +63,15 @@ declare module "@tanstack/react-router" {
}
}

ReactDom.createRoot(document.querySelector("app") as HTMLElement).render(
<RouterProvider router={router} />,
);
if (!rootElement) {
reportBootError("Missing <app> root element");
} else if (!isBootErrorReported()) {
ReactDom.createRoot(rootElement).render(
<BootErrorBoundary
onError={(error) => reportBootError("Render failed", error)}
>
<RouterProvider router={router} />
</BootErrorBoundary>,
);
markBootMounted();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
99 changes: 99 additions & 0 deletions apps/desktop/src/renderer/lib/boot-errors.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading