Experimental option to open some editors (Event Sheets, Extensions...) into separate windows#8492
Merged
Experimental option to open some editors (Event Sheets, Extensions...) into separate windows#8492
Conversation
Add the ability to pop out any closable editor tab (including Events Sheet) into a separate browser/Electron window via right-click context menu. Uses React portals to render the editor content in the new window while keeping all app logic and state in the main React tree. - New WindowPortal component that opens a browser window, copies styles (including MUI injected styles via MutationObserver), and renders children through ReactDOM.createPortal - Added "Pop out in a separate window" context menu option to ClosableTabs - Closing the external window closes the editor tab and vice versa - Full theme support in the popped-out window via FullThemeProvider https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
Add a PortalContainerContext that provides the DOM container element where Material-UI portals should render. In the main window this is undefined (default MUI behavior), but in a popped-out window it points to the external window's container div. Two-layer approach for maximum coverage: 1. Global: FullThemeProvider injects portal container into MUI theme default props (MuiModal, MuiPopover, MuiMenu, MuiDrawer, MuiTooltip) so ALL MUI overlay components automatically render in the correct window. 2. Explicit: Key components also read PortalContainerContext directly and pass `container` props as a safety net: - Dialog.js, LoaderModal.js (MUI Dialog/Modal) - ContextMenu.js (MUI Menu + Drawer) - InlinePopover.js (MUI Popper) - InlineParameterEditor.js (MUI Drawer) - InstructionEditorMenu.js (MUI Popover) - ExpressionAutocompletionsDisplayer.js (MUI Popper) - MaterialUIMenuImplementation.js (submenu MUI Menu) - ColorPicker.js (MUI Popover) https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
- WindowPortal: Provide external window's document.body (instead of
container div) as PortalContainerContext value. This fixes:
- ContextMenu not appearing (MUI needs body for anchorPosition positioning)
- InlinePopover not closing on click-away (ClickAwayListener needs
correct ownerDocument to detect clicks in the external window)
- EditorTabsPane: Render a full Toolbar inside each popped-out WindowPortal
with its own ref. Route the editor's setToolbar callback to the
popped-out Toolbar when isPoppedOut is true, so toolbar buttons properly
interact with the editor in the separate window.
https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
The inline ref callback on the popped-out Toolbar was calling updateToolbar() on every render, which set state in the Toolbar, triggering a re-render, creating a new callback identity, causing React to re-invoke the ref — producing an infinite loop. Track which popped-out toolbars have been initialized so updateToolbar is only called once per pop-out lifecycle. https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
The inline ref callback was clearing the initialized-tracking set on null (React's cleanup between re-renders), which reset the guard and allowed updateToolbar() to fire again on the next render — sustaining the infinite loop. Now: ignore null entirely in the ref callback. Cleanup of both the ref and the tracking set is handled by onPopInTab (called on close and pop-back-in). https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
MUI v4 uses JSS to inject <style> elements into the main window's document.head. When components render in a separate window via WindowPortal, their styles were injected into the wrong document. The MutationObserver approach (copying styles from main to child) had race conditions and missed updates to existing style elements. Fix: create a dedicated JSS instance targeting the child window's document when FullThemeProvider detects a portal container from a different window. This makes JSS inject styles directly into the correct document. Also disconnect the MutationObserver on cleanup to prevent leaks. https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
Three fixes for the separate-window (WindowPortal) support: 1. ElementWithMenu: `instanceof HTMLElement` fails across windows because each window has its own constructor. Use `nodeType === 1` instead, which is the DOM-standard way to check for Element nodes. 2. SemiControlledAutoComplete: MUI Autocomplete's internal Popper renders to the main document.body by default. Pass a custom PopperComponent with the portal container so the dropdown renders in the correct window. 3. InlinePopover: `instanceof MouseEvent` also fails across windows. Use `event.type` string check instead. https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
MUI's Popover uses ownerWindow(anchorEl) to get viewport dimensions for bounds-checking. With anchorReference="anchorPosition" and no anchorEl, it falls back to the main window, causing menus to use the wrong innerWidth/innerHeight for repositioning. Fix: pass portalContainer as anchorEl when in a separate window. Positioning is still driven by anchorPosition (not anchorEl), but MUI now derives the correct window for viewport calculations. https://claude.ai/code/session_01BNeruZtX6hSfXJ2pqjkGAR
- Add originalPaneIdentifier field to EditorTab to track where a tab came from when popped out - Add 'external' pane to EditorTabsState initial state - Add popOutTab/popInTab functions in EditorTabsHandler that move tabs between their source pane and the external pane - Add getExternalEditors helper to retrieve popped-out tabs - Handle openEditorTab finding a tab in the external pane by popping it back in first - Create ExternalEditorWindow component that renders one popped-out editor in a WindowPortal with its own Toolbar, FullThemeProvider, and ResizeObserver-based SpecificDimensionsWindowSizeProvider - Create ExternalEditorWindows container that maps external tabs to ExternalEditorWindow instances - Remove all pop-out state/logic from EditorTabsPane (~100 lines): poppedOutTabKeys, poppedOutToolbarRefs, initializedPoppedOutToolbars, isPoppedOut checks, WindowPortal rendering block - Wire up onPopOutTab/onPopInTab/onExternalWindowClose in MainFrame - Render ExternalEditorWindows alongside PanesContainer https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
…te import - Add React.Node return type annotations to ExternalEditorWindow and ExternalEditorWindows components - Pass editorTabs through to ExternalEditorWindow via sharedProps spread - Restructure popOutTab/popInTab to build panes in two steps, avoiding Flow's invalid-computed-prop error with explicit + computed keys - Remove duplicate EditorTab type import in MainFrame/index.js https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
The ResizeObserver was set up in useEffect with [] deps, but the container div lives inside a WindowPortal which creates its window asynchronously. On first render containerRef.current was null, the effect never re-ran, dimensions stayed null, and SpecificDimensionsWindowSizeProvider rendered nothing. Switch to a callback ref so the ResizeObserver attaches as soon as the DOM node actually mounts inside the portal window. https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
… toolbar - Add popOutEnabled prop to ClosableTab/DraggableClosableTab to gray out the "Pop out in a separate window" context menu item for scene editor tabs (layout, external layout, custom object kinds) - Add customLeftContent prop to MainFrame Toolbar, rendered on the left when showProjectButtons is false - In ExternalEditorWindow, set showProjectButtons to false and add a "Pop back into main window" IconButton using the Restore icon https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
- In Electron main.js, allow blank-URL pop-outs in setWindowOpenHandler instead of denying all new windows. Child windows get standard title bar, nodeIntegration, and contextIsolation disabled. - Add did-create-window listener to enable @electron/remote on child windows, block navigation, and prevent further window nesting. - Make Window.setUpContextMenu accept an optional targetWindow param so it can set up context menus in pop-out windows (both Electron and web), not just the main window. - Call Window.setUpContextMenu(externalWindow) in WindowPortal after creating the external window. https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
remote.getCurrentWindow() always returns the main window, even when the right-click happens in a pop-out. Use BrowserWindow.getFocusedWindow() (which is the window the user just clicked in) with a fallback to getCurrentWindow(). Applied in both ElectronMenuImplementation.showMenu and Window.setUpContextMenu. https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
Root cause: MUI's ClickAwayListener uses ReactDOM.findDOMNode on its child to determine which document to attach listeners to. Because the child is a Popper (which portals its content), findDOMNode returns null and the listener falls back to the main window's `document`. Clicks in the external window never bubble to the main document, so onClickAway never fires. Fix: Add a useEffect that, when the popover is in an external window (portalContainer differs from document), attaches a mouseup listener directly on the external window's document. Clicks outside the popper content trigger onApply, matching the existing ClickAwayListener behavior. A ref wrapper div on the Popper content allows contains() checks. https://claude.ai/code/session_01G1DsG5SdZKcoix6z4MNJ5j
…nal windows External (popped-out) editor windows now have their own keyboard shortcut listeners and command palette instances, so shortcuts like Ctrl+S and Ctrl+P work correctly in external windows on both web and Electron. Changes: - useKeyboardShortcuts: accept targetDocument and ignoreHandledByElectron params - isUserTyping/isDialogOpen: accept optional targetDocument param - WindowPortal: add onWindowReady callback to expose the external window ref - ExternalEditorWindow: wire up local shortcuts, command palette, and compute adaptive window dimensions based on originalPaneIdentifier https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
- Rename getPopOutDimensions return keys to popOutWidth/popOutHeight - Pass props.previewDebuggerServer instead of null so in-game editor shortcuts are handled in external windows too - Rename WindowPortal width/height props to initialWidth/initialHeight to clarify they are only used on mount https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
…ired These props are always passed by ExternalEditorWindow, the only consumer. Remove optional markers and default values, and simplify null guards. https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
The isDialogOpen() check looks for a div.main-frame element in the document to determine if focus is inside the main content area. External windows didn't have this class, so when EventsSheet took focus (via tabIndex and _ensureFocused), isDialogOpen() returned true and blocked all shortcuts. Add className="main-frame" to the external window's container div so the check works correctly. https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
The isDialogOpen() check looks for a div.main-frame element to determine if focus is inside the main content area. External windows didn't have such an element, so when EventsSheet took focus (via tabIndex and _ensureFocused), isDialogOpen() returned true and blocked all shortcuts. Add className="popped-out-frame" to the external window's container and update isDialogOpen() to also check for this class. https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
Monaco editor uses CSSStyleSheet.insertRule() to inject CSS rules dynamically rather than setting style element textContent. The _copyStyles function in WindowPortal only copied textContent, so Monaco's layout-critical CSS rules were never copied to the external window, causing all dimension computations to fail (0 widths, no content rendered). Fix _copyStyles to serialize CSSOM rules from style elements when textContent is empty. Also add a delayed editor.layout() call in CodeEditor to handle any remaining timing issues between CSS copying and Monaco's initial layout computation. https://claude.ai/code/session_01Y7C4smFZcx6ZfJZq5QTzdG
beforeunload fires before the user answers the browser's "leave page?" confirmation dialog (from CloseConfirmDialog). If the user cancels, the popped-out windows were already closed. Switch to the unload event which only fires after the user confirms the close, so cancelling preserves the popped-out windows. https://claude.ai/code/session_01FrBtMXstqs8qMyZppXUb7U
…r-popout-editor-toolbar-0wHDo
…th ESM and AMD The old patch-package patch only modified the ESM clipboard module, so the Electron paste fallback didn't work in popped-out windows which load Monaco via the AMD loader. Move the fix to a runtime patch in MonacoSetup.js (applyElectronClipboardPatch) that is called after editor creation in both the main editor and PoppedOutMonacoEditor, keeping the fix in one place. https://claude.ai/code/session_01BxFngmmzdFM16i1rY9Me3s
Override the clipboard paste action's run method directly so the Electron fallback covers all paste triggers (keyboard shortcuts, context menu, programmatic), not just Ctrl+V and Shift+Insert keybindings. https://claude.ai/code/session_01BxFngmmzdFM16i1rY9Me3s
When popping out an editor tab, the main frame now selects the tab just before the popped-out tab instead of always falling back to the first tab (home). https://claude.ai/code/session_01AKW6eYtaZub2D4oDWnzCrE
…imental-build/popped-editors
…ertMessage Window.showConfirmDialog uses the native browser confirm dialog which always appears in the main window, breaking the UX when called from a PoppedOutEditorContainerWindow. Switch to useAlertDialog's showConfirmation which renders an in-app React dialog that respects the current window context. https://claude.ai/code/session_01NW9VQGvvtg7mjv9hWAc5SY
…esourcesEditor Use useAlertDialog hook in InstructionParametersEditor and AlertContext with static contextType in ResourcesEditor (class component) to show in-app confirmation dialogs instead of native browser dialogs that always appear in the main window. https://claude.ai/code/session_01NW9VQGvvtg7mjv9hWAc5SY
The "ResizeObserver loop completed with undelivered notifications" error is a non-fatal browser warning (per W3C spec) that occurs when a ResizeObserver callback triggers layout changes producing additional observations that can't be delivered in the same frame. This happens when resizing react-mosaic panels in a popped-out editor window because WindowPortal's ResizeObserver calls setWindowSize(), triggering a React re-render that causes further resize observations. In development, react-error-overlay treats this as a fatal error and shows a red overlay. The fix suppresses this specific error on both the main window and the external popped-out window by calling stopImmediatePropagation before react-error-overlay's handler runs. https://claude.ai/code/session_016EQE821byiGiwsXZg2pcH3
Log a message explaining the silenced error is a benign W3C spec warning (observations deferred to next frame). Throttle the log to at most once per second to avoid console spam during continuous resizing. https://claude.ai/code/session_016EQE821byiGiwsXZg2pcH3
…eObserverError Extract the shared logic into a reusable function so both index.js (main window) and WindowPortal.js (popped-out window) call the same helper instead of duplicating the handler. https://claude.ai/code/session_016EQE821byiGiwsXZg2pcH3
Move the silenceBenignResizeObserverError(window) call below all imports. ES module hoisting guarantees the import is resolved before any code runs, so the listener is still registered before react-error-overlay. https://claude.ai/code/session_016EQE821byiGiwsXZg2pcH3
…k bundles CRA's react-error-overlay is injected as a webpack entry point that runs before application code, so the listener in index.js was too late to call stopImmediatePropagation before the overlay handler. Moving the listener to an inline <script> in index.html ensures it is registered before any bundle code loads. The Utils/SilenceBenignResizeObserverError.js helper is kept for popped-out windows created by WindowPortal (which open as about:blank and don't share the main window's inline scripts). https://claude.ai/code/session_016EQE821byiGiwsXZg2pcH3
When an EventsSheet editor was in an external/popped-out window and the user clicked its scene in the project manager, openEditorTab() would detect the tab in the 'external' pane and call popInTab() to move it back to the main window. Instead, treat it the same as any other already-open tab: leave it where it is and just update the focus. The explicit "Pop In" button still works as before since it calls popInTab() directly. https://claude.ai/code/session_011LRZD5VVzJwizPY8GNedm3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.