{showInitView ? (
@@ -646,6 +667,7 @@ function WorkspacePage() {
/>
) : (
+
);
}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx
index 4bdae7e68c7..94257f6ac6f 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx
@@ -1,5 +1,5 @@
-import { useParams } from "@tanstack/react-router";
import { electronTrpc } from "renderer/lib/electron-trpc";
+import { useWorkspaceId } from "renderer/screens/main/components/WorkspaceView/WorkspaceIdContext";
import { useGitChangesStatus } from "renderer/screens/main/hooks/useGitChangesStatus";
import {
RightSidebarTab,
@@ -8,7 +8,7 @@ import {
import { InfiniteScrollView } from "./components/InfiniteScrollView";
export function ChangesContent() {
- const { workspaceId } = useParams({ strict: false });
+ const workspaceId = useWorkspaceId();
const isChangesSidebarVisible = useSidebarStore(
(s) => s.isSidebarOpen && s.rightSidebarTab === RightSidebarTab.Changes,
);
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx
index e6b3e785714..10e0e076684 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx
@@ -1,10 +1,10 @@
import { Alert, AlertDescription, AlertTitle } from "@superset/ui/alert";
import { Button } from "@superset/ui/button";
import { Collapsible, CollapsibleContent } from "@superset/ui/collapsible";
-import { useParams } from "@tanstack/react-router";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuFileCode, LuLoader } from "react-icons/lu";
import { electronTrpc } from "renderer/lib/electron-trpc";
+import { useWorkspaceId } from "renderer/screens/main/components/WorkspaceView/WorkspaceIdContext";
import { CodeEditor } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor";
import { FileSaveConflictDialog } from "renderer/screens/main/components/WorkspaceView/components/FileSaveConflictDialog";
import { useChangesStore } from "renderer/stores/changes";
@@ -79,7 +79,7 @@ export function FileDiffSection({
onDiscard,
isActioning = false,
}: FileDiffSectionProps) {
- const { workspaceId } = useParams({ strict: false });
+ const workspaceId = useWorkspaceId();
const sectionRef = useRef
(null);
const copyTimeoutRef = useRef | null>(null);
const {
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/EmptyTabView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/EmptyTabView.tsx
index ffd4a619a7d..619910a45e1 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/EmptyTabView.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/EmptyTabView.tsx
@@ -1,5 +1,4 @@
import type { ExternalApp } from "@superset/local-db";
-import { useParams } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import type { IconType } from "react-icons";
import { BsTerminalPlus } from "react-icons/bs";
@@ -8,6 +7,7 @@ import { TbMessageCirclePlus, TbWorld } from "react-icons/tb";
import { getAppOption } from "renderer/components/OpenInExternalDropdown";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useWorkspaceDeleteHandler } from "renderer/react-query/workspaces";
+import { useWorkspaceId } from "renderer/screens/main/components/WorkspaceView/WorkspaceIdContext";
import { DeleteWorkspaceDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog";
import { useHotkeyDisplay } from "renderer/stores/hotkeys";
import { useTabsStore } from "renderer/stores/tabs/store";
@@ -35,9 +35,7 @@ export function EmptyTabView({
onOpenInApp,
onOpenQuickOpen,
}: EmptyTabViewProps) {
- const { workspaceId } = useParams({
- from: "/_authenticated/_dashboard/workspace/$workspaceId/",
- });
+ const workspaceId = useWorkspaceId();
const addChatTab = useTabsStore((s) => s.addChatTab);
const addBrowserTab = useTabsStore((s) => s.addBrowserTab);
const activeTheme = useTheme();
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx
index 3296797fc8f..ab19a9838ae 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx
@@ -88,7 +88,9 @@ export function GroupItem({
return {
type: MosaicDragType.WINDOW,
item: {
- mosaicId: canDropOntoActiveTab ? MOSAIC_ID : TAB_DRAG_NO_MATCH_ID,
+ mosaicId: canDropOntoActiveTab
+ ? `${MOSAIC_ID}-${activeTabId}`
+ : TAB_DRAG_NO_MATCH_ID,
hideTimer: 0,
tabId: tab.id,
index,
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
index 9f6a84e3195..a39be6d099e 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
@@ -1,7 +1,7 @@
import type { TerminalPreset } from "@superset/local-db";
import { eq, or } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
-import { useNavigate, useParams } from "@tanstack/react-router";
+import { useNavigate } from "@tanstack/react-router";
import {
useCallback,
useEffect,
@@ -12,6 +12,7 @@ import {
} from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { usePresets } from "renderer/react-query/presets";
+import { useWorkspaceId } from "renderer/screens/main/components/WorkspaceView/WorkspaceIdContext";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { requestTabClose } from "renderer/stores/editor-state/editorCoordinator";
import { useTabsStore } from "renderer/stores/tabs/store";
@@ -31,7 +32,7 @@ import { GroupItem } from "./GroupItem";
const NO_WORKSPACE_MATCH = "__no_workspace__";
export function GroupStrip() {
- const { workspaceId: activeWorkspaceId } = useParams({ strict: false });
+ const activeWorkspaceId = useWorkspaceId();
const allTabs = useTabsStore((s) => s.tabs);
const panes = useTabsStore((s) => s.panes);
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx
new file mode 100644
index 00000000000..5fbe6c2b4d3
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx
@@ -0,0 +1,71 @@
+import { useMemo } from "react";
+import { useTabsStore } from "renderer/stores/tabs/store";
+import type { Tab } from "renderer/stores/tabs/types";
+import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils";
+import { TabView } from "./TabView";
+
+interface PersistentTabRendererProps {
+ tabs: Tab[];
+ activeTabId: string | null;
+}
+
+/**
+ * Renders workspace tabs, keeping only those that contain a browser (webview)
+ * pane mounted when inactive. Tabs without webviews are unmounted normally.
+ *
+ * Electron's tag reloads its content whenever it is reparented in the
+ * DOM. By keeping webview-containing tabs mounted (but off-screen), webview
+ * elements stay in their original DOM parent and never reparent, eliminating
+ * the reload. Non-webview tabs (terminals, chat, files) can safely unmount and
+ * remount without data loss.
+ */
+export function PersistentTabRenderer({
+ tabs,
+ activeTabId,
+}: PersistentTabRendererProps) {
+ const panes = useTabsStore((s) => s.panes);
+
+ const tabsWithWebview = useMemo(() => {
+ const ids = new Set();
+ for (const tab of tabs) {
+ const paneIds = extractPaneIdsFromLayout(tab.layout);
+ if (paneIds.some((id) => panes[id]?.type === "webview")) {
+ ids.add(tab.id);
+ }
+ }
+ return ids;
+ }, [tabs, panes]);
+
+ return (
+ <>
+ {tabs.map((tab) => {
+ const isActive = tab.id === activeTabId;
+ const hasWebview = tabsWithWebview.has(tab.id);
+
+ // Tabs without webviews: only render when active (original behavior)
+ if (!hasWebview && !isActive) return null;
+
+ return (
+
+
+
+ );
+ })}
+ >
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
index cd9363802dc..1412808a396 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
@@ -1,12 +1,27 @@
import { useCallback, useEffect, useRef } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useTabsStore } from "renderer/stores/tabs/store";
+import { PLATFORM } from "shared/constants";
// ---------------------------------------------------------------------------
// Module-level singletons
// ---------------------------------------------------------------------------
const webviewRegistry = new Map();
+/**
+ * A persistent wrapper div per pane that ALWAYS contains its webview.
+ *
+ * Electron's tag reloads its content whenever the element is
+ * reparented (moved from one parent to another). The previous approach moved
+ * the webview itself between a visible container and a hidden one — each move
+ * was a reparent that triggered a reload.
+ *
+ * By wrapping the webview in a persistent div and only ever moving that
+ * wrapper, the webview's parentNode never changes, so Electron never sees a
+ * reparent. The wrapper moves between React's container div (visible) and a
+ * hidden parking container, but the webview inside is untouched.
+ */
+const wrapperRegistry = new Map();
/** Tracks paneId → last-registered webContentsId so we can re-register if it changes. */
const registeredWebContentsIds = new Map();
let hiddenContainer: HTMLDivElement | null = null;
@@ -58,6 +73,11 @@ window.addEventListener("drop", () => setWebviewsDragPassthrough(false), true);
/** Call from useBrowserLifecycle when a pane is removed. */
export function destroyPersistentWebview(paneId: string): void {
+ const wrapper = wrapperRegistry.get(paneId);
+ if (wrapper) {
+ wrapper.remove();
+ wrapperRegistry.delete(paneId);
+ }
const webview = webviewRegistry.get(paneId);
if (webview) {
webview.remove();
@@ -171,19 +191,27 @@ export function usePersistentWebview({
[paneId],
);
- // Main lifecycle effect: create or reclaim webview, attach events, park on unmount
+ // Main lifecycle effect: create or reclaim wrapper+webview, attach events, park on unmount
useEffect(() => {
const container = containerRef.current;
if (!container) return;
+ let wrapper = wrapperRegistry.get(paneId);
let webview = webviewRegistry.get(paneId);
- if (webview) {
- // Reclaim from hidden container
- container.appendChild(webview);
+ if (wrapper && webview) {
+ // Reclaim: move the wrapper (with webview inside) into React's container.
+ // The webview's parentNode stays as `wrapper` — no reparent, no reload.
+ container.appendChild(wrapper);
syncStoreFromWebview(webview);
} else {
- // Create new webview
+ // First time: create a persistent wrapper div and a webview inside it.
+ wrapper = document.createElement("div");
+ wrapper.style.display = "flex";
+ wrapper.style.flex = "1";
+ wrapper.style.width = "100%";
+ wrapper.style.height = "100%";
+
webview = document.createElement("webview") as Electron.WebviewTag;
webview.setAttribute("partition", "persist:superset");
webview.setAttribute("allowpopups", "");
@@ -193,8 +221,11 @@ export function usePersistentWebview({
webview.style.height = "100%";
webview.style.border = "none";
+ // webview goes into wrapper, wrapper goes into container
+ wrapper.appendChild(webview);
+ wrapperRegistry.set(paneId, wrapper);
webviewRegistry.set(paneId, webview);
- container.appendChild(webview);
+ container.appendChild(wrapper);
const finalUrl = sanitizeUrl(initialUrlRef.current);
webview.src = finalUrl;
@@ -207,11 +238,28 @@ export function usePersistentWebview({
const handleDomReady = () => {
const webContentsId = wv.getWebContentsId();
const previousId = registeredWebContentsIds.get(paneId);
- // Register on first load, or re-register if webContentsId changed (e.g. after DOM reparenting)
+ // Register on first load, or re-register if webContentsId changed
if (previousId !== webContentsId) {
registeredWebContentsIds.set(paneId, webContentsId);
registerBrowser({ paneId, webContentsId });
}
+
+ // Inject mouse back/forward button support into the guest page.
+ // Electron's consumes mouse events in the guest process,
+ // so the host renderer never sees button 3/4 (back/forward).
+ // Only needed on macOS — Windows/Linux use the `app-command` event
+ // handler in the main process instead.
+ if (PLATFORM.IS_MAC) {
+ wv.executeJavaScript(`
+ if (!window.__supersetMouseNavInstalled) {
+ window.__supersetMouseNavInstalled = true;
+ window.addEventListener('mouseup', function(e) {
+ if (e.button === 3) { e.preventDefault(); history.back(); }
+ if (e.button === 4) { e.preventDefault(); history.forward(); }
+ }, true);
+ }
+ `).catch(() => {});
+ }
};
const handleDidStartLoading = () => {
@@ -340,7 +388,7 @@ export function usePersistentWebview({
);
wv.addEventListener("did-fail-load", handleDidFailLoad as EventListener);
- // -- Cleanup: park in hidden container -----------------------------
+ // -- Cleanup: park the wrapper (not the webview) in hidden container -
return () => {
wv.removeEventListener("dom-ready", handleDomReady);
@@ -367,7 +415,13 @@ export function usePersistentWebview({
handleDidFailLoad as EventListener,
);
- getHiddenContainer().appendChild(wv);
+ // Park the WRAPPER (which contains the webview) in the hidden
+ // container. The webview's parentNode remains `wrapper` throughout
+ // — no reparent, no reload.
+ const w = wrapperRegistry.get(paneId);
+ if (w) {
+ getHiddenContainer().appendChild(w);
+ }
};
// paneId is stable for the lifetime of a pane; initialUrlRef only used on first create.
}, [paneId, registerBrowser, syncStoreFromWebview, upsertHistory]);
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
index c3ed1de0ea7..cad87dec889 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
@@ -1,10 +1,10 @@
import { Alert, AlertDescription, AlertTitle } from "@superset/ui/alert";
import { Button } from "@superset/ui/button";
-import { useParams } from "@tanstack/react-router";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { MosaicBranch } from "react-mosaic-component";
import type { MarkdownEditorAdapter } from "renderer/components/MarkdownRenderer";
import { electronTrpc } from "renderer/lib/electron-trpc";
+import { useWorkspaceId } from "renderer/screens/main/components/WorkspaceView/WorkspaceIdContext";
import { FileSaveConflictDialog } from "renderer/screens/main/components/WorkspaceView/components/FileSaveConflictDialog";
import { useWorkspaceFileEvents } from "renderer/screens/main/components/WorkspaceView/hooks/useWorkspaceFileEvents";
import { useChangesStore } from "renderer/stores/changes";
@@ -127,7 +127,7 @@ export function FileViewerPane({
onMoveToTab,
onMoveToNewTab,
}: FileViewerPaneProps) {
- const { workspaceId } = useParams({ strict: false });
+ const workspaceId = useWorkspaceId();
const normalizedWorkspaceId = workspaceId ?? worktreePath;
const fileViewer = useTabsStore((s) => s.panes[paneId]?.fileViewer);
const isFocused = useTabsStore((s) => s.focusedPaneIds[tabId] === paneId);
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
index 7dd16399845..bf03841d4ea 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
@@ -295,7 +295,7 @@ export function TabView({ tab }: TabViewProps) {
return (
- mosaicId={MOSAIC_ID}
+ mosaicId={`${MOSAIC_ID}-${tab.id}`}
renderTile={renderPane}
value={cleanedLayout}
onChange={handleLayoutChange}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
index 77ecd4c0183..8b0bab01c77 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
@@ -1,23 +1,23 @@
import type { ExternalApp } from "@superset/local-db";
-import { useParams } from "@tanstack/react-router";
import { useEffect, useMemo, useRef } from "react";
import { useTabsStore } from "renderer/stores/tabs/store";
import { resolveActiveTabIdForWorkspace } from "renderer/stores/tabs/utils";
import { EmptyTabView } from "./EmptyTabView";
-import { TabView } from "./TabView";
+import { PersistentTabRenderer } from "./PersistentTabRenderer";
interface TabsContentProps {
+ workspaceId: string;
defaultExternalApp?: ExternalApp | null;
onOpenInApp: () => void;
onOpenQuickOpen: () => void;
}
export function TabsContent({
+ workspaceId: activeWorkspaceId,
defaultExternalApp,
onOpenInApp,
onOpenQuickOpen,
}: TabsContentProps) {
- const { workspaceId: activeWorkspaceId } = useParams({ strict: false });
const allTabs = useTabsStore((s) => s.tabs);
const activeTabIds = useTabsStore((s) => s.activeTabIds);
const tabHistoryStacks = useTabsStore((s) => s.tabHistoryStacks);
@@ -47,10 +47,13 @@ export function TabsContent({
return resolvedActiveTabId;
}, [activeWorkspaceId, activeTabIds, allTabs, tabHistoryStacks]);
- const tabToRender = useMemo(() => {
- if (!activeTabId) return null;
- return allTabs.find((tab) => tab.id === activeTabId) || null;
- }, [activeTabId, allTabs]);
+ const workspaceTabs = useMemo(
+ () =>
+ activeWorkspaceId
+ ? allTabs.filter((t) => t.workspaceId === activeWorkspaceId)
+ : [],
+ [activeWorkspaceId, allTabs],
+ );
useEffect(() => {
const nextWorkspaceId = activeWorkspaceId ?? null;
@@ -89,8 +92,11 @@ export function TabsContent({
return (
- {tabToRender ? (
-
+ {workspaceTabs.length > 0 ? (
+
) : (
void;
onOpenQuickOpen: () => void;
}
export function ContentView({
+ workspaceId,
defaultExternalApp,
onOpenInApp,
onOpenQuickOpen,
@@ -31,6 +33,7 @@ export function ContentView({
{showPresetsBar && }
(null);
+
+export const WorkspaceIdProvider = WorkspaceIdContext.Provider;
+
+export function useWorkspaceId(): string {
+ const id = useContext(WorkspaceIdContext);
+ if (!id) {
+ throw new Error("useWorkspaceId must be used within a WorkspaceIdProvider");
+ }
+ return id;
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx
index 94f8b5dabea..ef4b4684ceb 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx
@@ -13,12 +13,14 @@ import { useBrowserLifecycle } from "../hooks/useBrowserLifecycle";
import { RightSidebar } from "../RightSidebar";
interface WorkspaceLayoutProps {
+ workspaceId: string;
defaultExternalApp?: ExternalApp | null;
onOpenInApp: () => void;
onOpenQuickOpen: () => void;
}
export function WorkspaceLayout({
+ workspaceId,
defaultExternalApp,
onOpenInApp,
onOpenQuickOpen,
@@ -40,6 +42,7 @@ export function WorkspaceLayout({
) : (