src/skills/desktop-multiwindow-electron/SKILL.md
Multi-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence
npx skillsauth add agents-inc/skills desktop-multiwindow-electronInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
BrowserWindowfor single-view windows. UseBaseWindow+WebContentsViewfor multi-view layouts (tabs, split panes, panels).BrowserViewis deprecated since Electron 30 -- migrate toWebContentsView. Track windows with aMap<string, BrowserWindow>registry. Communicate between windows via the main process orMessagePortfor direct renderer-to-renderer channels. Persist window bounds manually or use the upcomingwindowStatePersistenceAPI. Always closewebContentsexplicitly when usingBaseWindow-- unlikeBrowserWindow, it does not auto-cleanup.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST close webContents explicitly when destroying a BaseWindow -- it does not auto-cleanup like BrowserWindow, causing memory leaks)
(You MUST use WebContentsView instead of BrowserView -- BrowserView is deprecated since Electron 30)
(You MUST route all inter-window communication through the main process or MessagePort -- never access another window's renderer directly)
(You MUST validate that saved window bounds are on a visible display before restoring -- monitors may disconnect between sessions)
</critical_requirements>
Auto-detection: multi-window, BaseWindow, WebContentsView, BrowserView migration, contentView, addChildView, removeChildView, parent window, child window, modal window, window registry, MessagePort, MessageChannelMain, window state persistence, screen API, workArea, getAllDisplays, split view, tabs, panels, window lifecycle, ready-to-show, window-all-closed
When to use:
When NOT to use:
BrowserWindow is sufficient on its own)Key patterns covered:
Electron's window model has two tiers. BrowserWindow is the simple path: one window, one web view, automatic lifecycle management. BaseWindow + WebContentsView is the flexible path: one window shell containing multiple independently managed web views, each with its own renderer process and preload script.
The key architectural decision: use BrowserWindow for single-view windows, BaseWindow for multi-view layouts. BaseWindow trades convenience for control -- you manage view lifecycle, bounds, and cleanup explicitly.
When to use BaseWindow + WebContentsView:
When NOT to use BaseWindow:
BaseWindow is the window shell; WebContentsView instances are the content. Each view has its own renderer process and preload script.
const { BaseWindow, WebContentsView } = require("electron");
const win = new BaseWindow({ width: 1200, height: 800 });
const sidebar = new WebContentsView({
webPreferences: { preload: path.join(__dirname, "preload.js") },
});
const main = new WebContentsView({
webPreferences: { preload: path.join(__dirname, "preload.js") },
});
win.contentView.addChildView(sidebar);
win.contentView.addChildView(main);
const SIDEBAR_WIDTH = 250;
sidebar.setBounds({ x: 0, y: 0, width: SIDEBAR_WIDTH, height: 800 });
main.setBounds({ x: SIDEBAR_WIDTH, y: 0, width: 950, height: 800 });
sidebar.webContents.loadFile("sidebar.html");
main.webContents.loadFile("main.html");
Key point: Each WebContentsView needs its own webPreferences and preload script. BaseWindow has no webContents of its own. See examples/core.md for complete split-view and tab examples with resize handling.
BrowserView is deprecated since Electron 30. Migration is straightforward -- constructors have the same shape.
| Deprecated (BrowserView) | Replacement (WebContentsView) |
| ---------------------------------------- | --------------------------------------------------------- |
| new BrowserView(opts) | new WebContentsView(opts) |
| win.addBrowserView(view) | win.contentView.addChildView(view) |
| win.removeBrowserView(view) | win.contentView.removeChildView(view) |
| win.getBrowserViews() | win.contentView.children |
| win.setTopBrowserView(view) | win.contentView.addChildView(view) (re-adding reorders) |
| view.setAutoResize({ vertical: true }) | Manual resize via win.on("resize", ...) + setBounds() |
Gotcha: WebContentsView defaults to a white background; BrowserView defaulted to transparent. Set view.setBackgroundColor("#00000000") for transparency.
See examples/core.md for the complete migration pattern with auto-resize replacement.
Window events fire in a predictable order. Use ready-to-show to prevent visual flash, close to intercept (confirmations, state saving), closed for final cleanup.
const win = new BrowserWindow({ show: false });
// Prevent white flash -- show only after first paint
win.once("ready-to-show", () => {
win.show();
});
// Intercept close for unsaved changes
win.on("close", (event) => {
if (hasUnsavedChanges()) {
event.preventDefault();
promptSaveDialog(win);
}
});
// Final cleanup after window is gone
win.on("closed", () => {
windowRegistry.delete(win.id);
});
Gotcha: ready-to-show fires on BrowserWindow (which owns a webContents) but NOT on BaseWindow (which has no webContents). For BaseWindow, listen on the individual WebContentsView's webContents instead: view.webContents.once("ready-to-show", ...).
See examples/core.md for the full lifecycle sequence and BaseWindow workaround.
Child windows always appear above their parent. Modal windows additionally disable the parent until closed.
const parent = new BrowserWindow({ width: 800, height: 600 });
// Child window: always on top of parent, non-blocking
const child = new BrowserWindow({
parent,
width: 400,
height: 300,
});
// Modal window: blocks parent interaction
const modal = new BrowserWindow({
parent,
modal: true,
show: false,
width: 500,
height: 400,
});
modal.once("ready-to-show", () => modal.show());
Platform behavior: On macOS, modal child windows display as sheets attached to the parent. On Windows/Linux, they display as separate windows with the parent disabled.
See examples/core.md for confirmation dialogs and settings windows.
Track all open windows with a Map for reliable lookup, messaging, and cleanup.
const windowRegistry = new Map();
function createWindow(id, options) {
const win = new BrowserWindow(options);
windowRegistry.set(id, win);
win.on("closed", () => {
windowRegistry.delete(id);
});
return win;
}
// Find and focus a window by ID
function focusWindow(id) {
const win = windowRegistry.get(id);
if (!win) return;
if (win.isMinimized()) win.restore();
win.focus();
}
Key point: Use string IDs (not window objects) as keys. Clean up on closed event. The registry enables "show existing or create new" patterns for settings, about, and preferences windows.
See examples/core.md for the full singleton window pattern.
Two patterns: main process relay for simple messages, MessagePort for direct high-frequency renderer-to-renderer channels.
// Pattern A: Main process relay
ipcMain.on("message-to-window", (_event, targetId, channel, data) => {
const target = windowRegistry.get(targetId);
if (target) target.webContents.send(channel, data);
});
// Pattern B: MessagePort -- direct renderer-to-renderer
const { MessageChannelMain } = require("electron");
const { port1, port2 } = new MessageChannelMain();
window1.webContents.postMessage("port", null, [port1]);
window2.webContents.postMessage("port", null, [port2]);
Key point: Main process relay is simpler but adds latency. MessagePort creates a direct channel after initial setup. Use postMessage (not send) to transfer ports.
See examples/inter-window-communication.md for complete examples of both patterns.
Save and restore window bounds, maximized state, and display information across sessions.
function saveWindowState(win, stateFile) {
const bounds = win.getBounds();
const state = {
bounds,
isMaximized: win.isMaximized(),
isFullScreen: win.isFullScreen(),
displayId: screen.getDisplayMatching(bounds).id,
};
fs.writeFileSync(stateFile, JSON.stringify(state));
}
Key point: Always validate restored bounds against current displays -- a monitor may have been disconnected. Fall back to the primary display's work area if the saved display is unavailable.
See examples/core.md for the complete save/restore cycle with multi-monitor validation.
Use the screen API to enumerate displays, find work areas, and place windows on specific monitors.
const { screen } = require("electron");
const displays = screen.getAllDisplays();
const externalDisplay = displays.find(
(d) => d.bounds.x !== 0 || d.bounds.y !== 0,
);
if (externalDisplay) {
const win = new BrowserWindow({
x: externalDisplay.bounds.x,
y: externalDisplay.bounds.y,
width: 800,
height: 600,
});
}
Key point: Use workArea (not bounds) to avoid placing windows behind taskbars/docks. Listen for display-added, display-removed, and display-metrics-changed events to react to monitor changes at runtime.
See examples/core.md for display enumeration and safe placement.
</patterns><decision_framework>
How many web views does this window need?
+-- One full-size view?
| +-- BrowserWindow (simpler, automatic lifecycle)
+-- Multiple views (tabs, split pane, sidebar + content)?
| +-- BaseWindow + WebContentsView
+-- Frameless window with custom layout?
+-- One view? -> BrowserWindow with frame: false
+-- Multiple views? -> BaseWindow with frame: false
How should windows communicate?
+-- Simple, infrequent messages?
| +-- Main process relay (ipcMain/webContents.send)
+-- High-frequency or streaming data?
| +-- MessagePort (direct renderer-to-renderer after setup)
+-- Shared state across windows?
+-- Main process as single source of truth, push updates via IPC
What is the relationship between windows?
+-- Independent (editor, browser tabs)?
| +-- Separate BrowserWindow instances, window registry
+-- Always above parent (inspector, palette)?
| +-- Child window: { parent: parentWin }
+-- Blocks parent (save dialog, settings confirmation)?
+-- Modal window: { parent: parentWin, modal: true }
</decision_framework>
Detailed resources:
<red_flags>
Critical Issues:
webContents when destroying a BaseWindow -- causes memory leaks (BrowserWindow auto-cleans, BaseWindow does not)BrowserView instead of WebContentsView -- deprecated since Electron 30Architecture Issues:
BaseWindow for single-view windows -- unnecessary complexity, use BrowserWindowBrowserView.setAutoResize() patterns with WebContentsView -- no equivalent exists, use manual resize listenersBrowserWindow objects as Map values without cleaning up on closed -- stale referencesCommon Mistakes:
ready-to-show on BaseWindow -- it fires on BrowserWindow only; for BaseWindow, listen on view.webContentsWebContentsView defaults to white background (BrowserView defaulted to transparent) -- set "#00000000" explicitlyipcRenderer.send() to transfer MessagePort -- only postMessage() can transfer portsdisplay.bounds instead of display.workArea -- window ends up behind taskbar/dockdisplay-removed event -- window references a disconnected monitorGotchas & Edge Cases:
addChildView() moves it to the top of the z-order -- this is intentional, not a bugsetBounds() coordinates are relative to the parent view, not the screenwin.getBounds() returns the outer frame bounds on some platforms -- content area may differWebContentsView runs its own renderer process -- resource usage scales linearly with view countMessagePortMain requires calling .start() before messages are delivered -- they queue until then</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST close webContents explicitly when destroying a BaseWindow -- it does not auto-cleanup like BrowserWindow, causing memory leaks)
(You MUST use WebContentsView instead of BrowserView -- BrowserView is deprecated since Electron 30)
(You MUST route all inter-window communication through the main process or MessagePort -- never access another window's renderer directly)
(You MUST validate that saved window bounds are on a visible display before restoring -- monitors may disconnect between sessions)
Failure to follow these rules will cause memory leaks, deprecated API warnings, broken inter-process communication, or off-screen windows.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events