feat(panes)!: dress the renderer's pane window, don't create it

Follows the core change: a pane is now the plugin's REAL panel element,
moved into the pop-out window and still running the plugin's own code
(got-feedback/feedback#928).

That forces one thing here, and it is worth being loud about it:

  WE MUST NOT CREATE THE PANE WINDOW.

To move a live DOM node into another window, the renderer needs a handle on
that window's document. A BrowserWindow we construct in the main process
gives it no such handle. So the renderer opens the window itself with
window.open(), Electron's setWindowOpenHandler turns that into a real
BrowserWindow anyway, and we recognise it in did-create-window by the frame
name the renderer gave it (`fbpane-<paneId>`) and attach the OS behaviour:
remembered bounds, off the taskbar, minimize-to-tray, listed in the tray.

Create the window here instead and the whole feature collapses back into
"reimplement the panel in the pop-out and sync it over IPC" — which is
exactly what we just deleted.

The IPC surface shrinks to two channels, because main never creates or
destroys a pane window and never looks inside one:

  pane:sync    renderer → main   the registry, so the tray can list panes
  pane:toggle  main → renderer   the tray asking for a pane; only the
                                 renderer knows what opening one means (it
                                 might belong in the dock, and its element
                                 lives there)

Gone: pane:open, pane:close, pane:focus, pane:setAlwaysOnTop, pane:closed.
The renderer holds the WindowProxy for a window it opened, so it already
knows when the user closes it — and it has to, because its element is inside
and must be brought home.

Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-12 18:14:49 -04:00
parent 39251f3d12
commit 938ddded60
4 changed files with 130 additions and 228 deletions
+14 -13
View File
@@ -126,7 +126,7 @@ import * as updateManager from './update-manager';
import type { UpdateChannel } from './update-manager';
import { installAppMenu } from './app-menu';
import { sanitizeWindowBounds, MIN_WIDTH, MIN_HEIGHT } from './window-bounds';
import { initPaneHosts, closeAllPanes } from './pane-hosts';
import { initPaneHosts, closeAllPanes, adoptPaneWindow, paneIdFromFrameName } from './pane-hosts';
import { initTray, destroyTray } from './pane-tray';
// Linux: enable Chromium's PipeWire capturer feature so getUserMedia can see
@@ -781,8 +781,15 @@ function createWindow(port: number): void {
wc.setWindowOpenHandler(rendererWindowOpenHandler);
wc.on('did-create-window', (nestedWin) => wirePopupGuards(nestedWin.webContents));
}
mainWindow.webContents.on('did-create-window', (popupWin) => {
mainWindow.webContents.on('did-create-window', (popupWin, details) => {
wirePopupGuards(popupWin.webContents);
// A pane pop-out. The RENDERER opened it (window.open) because it moves a
// live DOM node into it and needs a handle on the new document to do that —
// see pane-hosts.ts. We recognise it by the frame name it was opened with
// and give it the OS behaviour a pane should have: remembered bounds, off
// the taskbar, minimize-to-tray, listed in the tray menu.
const paneId = paneIdFromFrameName(details.frameName || '');
if (paneId) adoptPaneWindow(popupWin, paneId);
});
mainWindow.on('closed', () => {
@@ -1176,17 +1183,11 @@ async function startup(): Promise<void> {
// Create the main window
createWindow(port);
// Detachable panes: real BrowserWindows for popped-out panes, plus the tray
// that lists them. Must come after createWindow — the pane host and the tray
// both reach the renderer through mainWindow, and Tray requires a ready app.
// The origin predicate is the same one the navigation guards use, so a pane
// window can only ever load OUR renderer, never arbitrary web content with
// the preload bridge attached.
initPaneHosts({
getMainWindow: () => mainWindow,
isRendererOrigin: makeRendererOriginPredicate(port),
webPreferences: rendererWebPreferences,
});
// Detachable panes: the tray that lists them, and the OS behaviour applied to
// each pane window as the renderer opens it (see did-create-window above).
// Must come after createWindow — both reach the renderer through mainWindow,
// and Tray requires a ready app.
initPaneHosts({ getMainWindow: () => mainWindow });
initTray({ getMainWindow: () => mainWindow });
// Install our application menu (replaces Electron's default so View →