Skip to content
18 changes: 17 additions & 1 deletion apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { join } from "node:path";
import { workspaces, worktrees } from "@superset/local-db";
import { eq } from "drizzle-orm";
import type { BrowserWindow } from "electron";
import { app, Notification, nativeTheme } from "electron";
import { app, Notification, nativeTheme, webContents } from "electron";
import { createWindow } from "lib/electron-app/factories/windows/create";
import { createAppRouter } from "lib/trpc/routers";
import { localDb } from "main/lib/local-db";
Expand Down Expand Up @@ -282,6 +282,22 @@ export async function MainWindow() {
console.error(` Error:`, error);
});

// Handle mouse back/forward buttons for webview panes (Windows/Linux).
// `app-command` is not supported on macOS; macOS mouse buttons are handled
// via executeJavaScript injection in usePersistentWebview's dom-ready handler.
window.on("app-command", (_event, command) => {
const focusedGuest = webContents
.getAllWebContents()
.find((wc) => wc.getType() === "webview" && wc.isFocused());
if (!focusedGuest) return;

if (command === "browser-backward") {
focusedGuest.navigationHistory.goBack();
} else if (command === "browser-forward") {
focusedGuest.navigationHistory.goForward();
}
});

window.on("close", () => {
// Save window state first, before any cleanup
const isMaximized = window.isMaximized();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Outlet, useMatchRoute } from "@tanstack/react-router";
import { useEffect, useMemo, useRef, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { WorkspacePage } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page";

/**
* Replaces a plain <Outlet /> for workspace routes, keeping previously visited
* workspace pages mounted (but hidden) so that Electron <webview> elements
* inside BrowserPanes are never removed from the DOM.
*
* For non-workspace routes (settings, welcome, etc.) it renders the normal
* <Outlet />.
*
* Automatically evicts deleted workspaces from the keep-alive list by comparing
* visited IDs against the current workspace list from the database.
*/
export function KeepAliveWorkspaces() {
const matchRoute = useMatchRoute();
const workspaceMatch = matchRoute({
to: "/workspace/$workspaceId",
fuzzy: true,
});
const activeWorkspaceId =
workspaceMatch !== false ? workspaceMatch.workspaceId : null;

// Track every workspace that has been visited so we can keep them alive.
const [visitedIds, setVisitedIds] = useState<string[]>([]);
const visitedSetRef = useRef(new Set<string>());

useEffect(() => {
if (activeWorkspaceId && !visitedSetRef.current.has(activeWorkspaceId)) {
visitedSetRef.current.add(activeWorkspaceId);
setVisitedIds(Array.from(visitedSetRef.current));
}
}, [activeWorkspaceId]);

// Evict deleted workspaces: compare visited IDs against the live list.
const { data: workspaceGroups } =
electronTrpc.workspaces.getAllGrouped.useQuery();

const existingWorkspaceIds = useMemo(() => {
if (!workspaceGroups) return null;
const ids = new Set<string>();
for (const group of workspaceGroups) {
for (const ws of group.workspaces) {
ids.add(ws.id);
}
}
return ids;
}, [workspaceGroups]);

useEffect(() => {
if (!existingWorkspaceIds) return;
let changed = false;
for (const id of visitedSetRef.current) {
if (!existingWorkspaceIds.has(id)) {
visitedSetRef.current.delete(id);
changed = true;
}
}
if (changed) {
setVisitedIds(Array.from(visitedSetRef.current));
}
}, [existingWorkspaceIds]);

// Non-workspace route — fall through to the normal Outlet.
if (!activeWorkspaceId) {
return <Outlet />;
}

return (
<>
{visitedIds.map((id) => {
const isActive = id === activeWorkspaceId;
return (
<div
key={id}
className="flex flex-1 min-h-0 min-w-0"
style={
isActive
? undefined
: {
position: "fixed",
left: -9999,
top: -9999,
width: "100vw",
height: "100vh",
pointerEvents: "none",
}
}
>
<WorkspacePage workspaceIdOverride={id} isActive={isActive} />
</div>
);
})}
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { FEATURE_FLAGS } from "@superset/shared/constants";
import {
createFileRoute,
Outlet,
useMatchRoute,
useNavigate,
} from "@tanstack/react-router";
Expand All @@ -18,6 +17,7 @@ import {
MAX_WORKSPACE_SIDEBAR_WIDTH,
useWorkspaceSidebarStore,
} from "renderer/stores/workspace-sidebar-state";
import { KeepAliveWorkspaces } from "./components/KeepAliveWorkspaces";
import { TopBar } from "./components/TopBar";

export const Route = createFileRoute("/_authenticated/_dashboard")({
Expand Down Expand Up @@ -123,7 +123,7 @@ function DashboardLayout() {
</ResizablePanel>
)}
<div className="flex flex-1 min-h-0 min-w-0">
<Outlet />
<KeepAliveWorkspaces />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,33 @@ export const PRESET_HOTKEY_IDS: HotkeyId[] = [

export function usePresetHotkeys(
openTabWithPreset: (presetIndex: number) => void,
options?: { enabled?: boolean },
) {
useAppHotkey(PRESET_HOTKEY_IDS[0], () => openTabWithPreset(0), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[0], () => openTabWithPreset(0), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[1], () => openTabWithPreset(1), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[1], () => openTabWithPreset(1), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[2], () => openTabWithPreset(2), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[2], () => openTabWithPreset(2), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[3], () => openTabWithPreset(3), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[3], () => openTabWithPreset(3), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[4], () => openTabWithPreset(4), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[4], () => openTabWithPreset(4), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[5], () => openTabWithPreset(5), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[5], () => openTabWithPreset(5), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[6], () => openTabWithPreset(6), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[6], () => openTabWithPreset(6), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[7], () => openTabWithPreset(7), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[7], () => openTabWithPreset(7), options, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[8], () => openTabWithPreset(8), undefined, [
useAppHotkey(PRESET_HOTKEY_IDS[8], () => openTabWithPreset(8), options, [
openTabWithPreset,
]);
}
Loading