Claude Skill

desktop-multiwindow-electron

Multi-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_desktop-multiwindow-electron_skills_desktop-multiwindow-electron-3a51ef5.zip · 15 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-multiwindow-electron/skills/desktop-multiwindow-electron
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Electron Multi-Window Patterns

Quick Guide: Use BrowserWindow for single-view windows. Use BaseWindow + WebContentsView for multi-view layouts (tabs, split panes, panels). BrowserView is deprecated since Electron 30 -- migrate to WebContentsView. Track windows with a Map<string, BrowserWindow> registry. Communicate between windows via the main process or MessagePort for direct renderer-to-renderer channels. Persist window bounds manually or use the upcoming windowStatePersistence API. Always close webContents explicitly when using BaseWindow -- unlike BrowserWindow, it does not auto-cleanup.


<critical_requirements>

CRITICAL: Before Using This Skill

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:

  • Creating multi-view layouts (tabs, split panes, embedded panels) with BaseWindow + WebContentsView
  • Managing multiple BrowserWindow instances with a window registry
  • Migrating from deprecated BrowserView to WebContentsView
  • Setting up parent/child or modal windows
  • Communicating between windows (via main process relay or MessagePort)
  • Persisting and restoring window position, size, and display state
  • Placing windows on specific monitors using the screen API

When NOT to use:

  • Single-window apps with one view (BrowserWindow is sufficient on its own)
  • Choosing a UI framework for the renderer
  • IPC patterns between main and a single renderer (basic IPC is outside multi-window scope)
  • Styling or layout within a single renderer

Key patterns covered:

  • BaseWindow + WebContentsView for multi-view layouts
  • BrowserView to WebContentsView migration
  • Window lifecycle events (ready-to-show, close, closed)
  • Parent/child and modal windows
  • Window registry with Map-based tracking
  • Inter-window communication via main process and MessagePort
  • Window state persistence (bounds, maximized, fullscreen)
  • Multi-monitor placement with screen API



<decision_framework>

Decision Framework

Window Type Selection

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

Inter-Window Communication

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

Window Relationship

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>

RED FLAGS

Critical Issues:

  • Not closing webContents when destroying a BaseWindow -- causes memory leaks (BrowserWindow auto-cleans, BaseWindow does not)
  • Using deprecated BrowserView instead of WebContentsView -- deprecated since Electron 30
  • Direct renderer-to-renderer communication bypassing the main process -- violates process isolation
  • Restoring window bounds without checking if the target display still exists -- window appears off-screen

Architecture Issues:

  • Using BaseWindow for single-view windows -- unnecessary complexity, use BrowserWindow
  • Using BrowserView.setAutoResize() patterns with WebContentsView -- no equivalent exists, use manual resize listeners
  • Storing BrowserWindow objects as Map values without cleaning up on closed -- stale references
  • Creating child windows from the renderer process -- always create from main

Common Mistakes:

  • Expecting ready-to-show on BaseWindow -- it fires on BrowserWindow only; for BaseWindow, listen on view.webContents
  • Forgetting that WebContentsView defaults to white background (BrowserView defaulted to transparent) -- set "#00000000" explicitly
  • Using ipcRenderer.send() to transfer MessagePort -- only postMessage() can transfer ports
  • Placing windows using display.bounds instead of display.workArea -- window ends up behind taskbar/dock
  • Not handling display-removed event -- window references a disconnected monitor

Gotchas & Edge Cases:

  • Re-adding a child view with addChildView() moves it to the top of the z-order -- this is intentional, not a bug
  • setBounds() coordinates are relative to the parent view, not the screen
  • On macOS, modal windows display as sheets attached to the parent window
  • win.getBounds() returns the outer frame bounds on some platforms -- content area may differ
  • Each WebContentsView runs its own renderer process -- resource usage scales linearly with view count
  • MessagePortMain requires calling .start() before messages are delivered -- they queue until then

</red_flags>


<critical_reminders>

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>

Files (skills)
  • examples
    • core.md 16.1 KB
      # Electron Multi-Window - Core Patterns
      
      > BaseWindow + WebContentsView layouts, window lifecycle, parent/child windows, window registry, state persistence, multi-monitor. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [inter-window-communication.md](inter-window-communication.md) for IPC between windows.
      
      ---
      
      ## BaseWindow with Split View
      
      Two WebContentsView instances side by side in a single BaseWindow, with resize handling.
      
      ```javascript
      const { app, BaseWindow, WebContentsView } = require("electron");
      const path = require("node:path");
      
      const SIDEBAR_WIDTH = 300;
      const MIN_MAIN_WIDTH = 400;
      
      function createSplitWindow() {
        const win = new BaseWindow({ width: 1200, height: 800, show: false });
      
        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);
      
        // Initial layout
        const { width, height } = win.getBounds();
        sidebar.setBounds({ x: 0, y: 0, width: SIDEBAR_WIDTH, height });
        main.setBounds({
          x: SIDEBAR_WIDTH,
          y: 0,
          width: width - SIDEBAR_WIDTH,
          height,
        });
      
        // Re-layout on resize (WebContentsView has no setAutoResize)
        win.on("resize", () => {
          const { width: w, height: h } = win.getBounds();
          sidebar.setBounds({ x: 0, y: 0, width: SIDEBAR_WIDTH, height: h });
          main.setBounds({
            x: SIDEBAR_WIDTH,
            y: 0,
            width: w - SIDEBAR_WIDTH,
            height: h,
          });
        });
      
        sidebar.webContents.loadFile("sidebar.html");
        main.webContents.loadFile("main.html");
      
        // BaseWindow does not fire ready-to-show -- listen on a view's webContents
        main.webContents.once("ready-to-show", () => {
          win.show();
        });
      
        // CRITICAL: explicitly destroy webContents on close (BaseWindow does not auto-cleanup)
        win.on("closed", () => {
          sidebar.webContents.close();
          main.webContents.close();
        });
      
        return win;
      }
      
      app.whenReady().then(createSplitWindow);
      ```
      
      **Why good:** Named constants for layout dimensions, manual resize replaces deprecated setAutoResize, explicit webContents cleanup prevents memory leaks, ready-to-show workaround for BaseWindow
      
      ---
      
      ## Tab Management with WebContentsView
      
      Multiple tabs where only the active tab view is visible, with a fixed tab bar.
      
      ```javascript
      const { BaseWindow, WebContentsView } = require("electron");
      const path = require("node:path");
      
      const TAB_BAR_HEIGHT = 40;
      
      class TabbedWindow {
        constructor() {
          this.win = new BaseWindow({ width: 1000, height: 700, show: false });
          this.tabs = new Map(); // tabId -> WebContentsView
          this.activeTabId = null;
      
          // Tab bar is itself a WebContentsView
          this.tabBar = new WebContentsView({
            webPreferences: { preload: path.join(__dirname, "tab-bar-preload.js") },
          });
          this.win.contentView.addChildView(this.tabBar);
          this.tabBar.webContents.loadFile("tab-bar.html");
      
          this.layoutViews();
          this.win.on("resize", () => this.layoutViews());
      
          // Cleanup all views on close
          this.win.on("closed", () => {
            this.tabBar.webContents.close();
            for (const view of this.tabs.values()) {
              view.webContents.close();
            }
            this.tabs.clear();
          });
        }
      
        addTab(tabId, url) {
          const view = new WebContentsView({
            webPreferences: { preload: path.join(__dirname, "preload.js") },
          });
          this.tabs.set(tabId, view);
          this.win.contentView.addChildView(view);
          view.webContents.loadURL(url);
      
          // Hide by setting zero bounds until activated
          view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
          this.activateTab(tabId);
        }
      
        activateTab(tabId) {
          const { width, height } = this.win.getBounds();
          const contentHeight = height - TAB_BAR_HEIGHT;
      
          // Hide current active tab
          if (this.activeTabId && this.tabs.has(this.activeTabId)) {
            this.tabs
              .get(this.activeTabId)
              .setBounds({ x: 0, y: 0, width: 0, height: 0 });
          }
      
          // Show new active tab
          const view = this.tabs.get(tabId);
          if (view) {
            view.setBounds({ x: 0, y: TAB_BAR_HEIGHT, width, height: contentHeight });
            this.activeTabId = tabId;
          }
        }
      
        removeTab(tabId) {
          const view = this.tabs.get(tabId);
          if (!view) return;
      
          this.win.contentView.removeChildView(view);
          view.webContents.close(); // Prevent memory leak
          this.tabs.delete(tabId);
      
          // Activate another tab if we removed the active one
          if (this.activeTabId === tabId) {
            const nextId = this.tabs.keys().next().value;
            if (nextId) this.activateTab(nextId);
          }
        }
      
        layoutViews() {
          const { width, height } = this.win.getBounds();
          this.tabBar.setBounds({ x: 0, y: 0, width, height: TAB_BAR_HEIGHT });
      
          if (this.activeTabId && this.tabs.has(this.activeTabId)) {
            const contentHeight = height - TAB_BAR_HEIGHT;
            this.tabs.get(this.activeTabId).setBounds({
              x: 0,
              y: TAB_BAR_HEIGHT,
              width,
              height: contentHeight,
            });
          }
        }
      }
      ```
      
      **Why good:** Each tab is a separate WebContentsView with independent renderer, hidden tabs have zero bounds (lightweight), explicit cleanup on close and on tab removal
      
      ---
      
      ## BrowserView to WebContentsView Migration
      
      Before/after showing the key API changes.
      
      ```javascript
      // BEFORE (deprecated BrowserView)
      const { BrowserView, BrowserWindow } = require("electron");
      
      const win = new BrowserWindow({ width: 800, height: 600 });
      const view = new BrowserView({
        webPreferences: { preload: path.join(__dirname, "preload.js") },
      });
      
      win.addBrowserView(view);
      view.setBounds({ x: 0, y: 0, width: 800, height: 600 });
      view.setAutoResize({ width: true, height: true });
      view.webContents.loadURL("https://example.com");
      ```
      
      ```javascript
      // AFTER (WebContentsView)
      const { BrowserWindow, WebContentsView } = require("electron");
      
      const win = new BrowserWindow({ width: 800, height: 600 });
      const view = new WebContentsView({
        webPreferences: { preload: path.join(__dirname, "preload.js") },
      });
      
      win.contentView.addChildView(view);
      view.setBounds({ x: 0, y: 0, width: 800, height: 600 });
      // No setAutoResize -- use manual resize handler
      win.on("resize", () => {
        const { width, height } = win.getBounds();
        view.setBounds({ x: 0, y: 0, width, height });
      });
      view.webContents.loadURL("https://example.com");
      
      // Default background is white -- set transparent if needed
      view.setBackgroundColor("#00000000");
      ```
      
      **Why good:** Minimal API change, explicit resize handling replaces magic auto-resize, background color explicitly set to match previous behavior
      
      ---
      
      ## Window Lifecycle Events
      
      The full sequence of events and where to hook into each.
      
      ```javascript
      const { BrowserWindow } = require("electron");
      
      function createManagedWindow() {
        const win = new BrowserWindow({
          show: false, // Start hidden to prevent flash
          width: 800,
          height: 600,
          webPreferences: {
            preload: path.join(__dirname, "preload.js"),
          },
        });
      
        // 1. Show after first paint (prevents white flash)
        win.once("ready-to-show", () => {
          win.show();
        });
      
        // 2. Intercept close -- save state, confirm unsaved changes
        win.on("close", (event) => {
          saveWindowState(win);
      
          if (hasUnsavedChanges(win.id)) {
            event.preventDefault();
            // Show confirmation dialog, then call win.destroy() to force close
            showSaveConfirmation(win);
          }
        });
      
        // 3. Cleanup after window is fully gone
        win.on("closed", () => {
          windowRegistry.delete(win.id);
        });
      
        win.loadFile("index.html");
        return win;
      }
      ```
      
      **Event order:** `close` (preventable) -> window closes -> `closed` (cleanup, non-preventable)
      
      **BaseWindow workaround:** Since `BaseWindow` does not have a `webContents`, `ready-to-show` does not fire. Listen on the view:
      
      ```javascript
      const win = new BaseWindow({ show: false, width: 800, height: 600 });
      const view = new WebContentsView({
        /* ... */
      });
      win.contentView.addChildView(view);
      view.webContents.loadFile("index.html");
      
      // Listen on the view's webContents, not on the BaseWindow
      view.webContents.once("ready-to-show", () => {
        win.show();
      });
      ```
      
      ---
      
      ## Parent/Child and Modal Windows
      
      ```javascript
      const { BrowserWindow } = require("electron");
      
      function openSettingsWindow(parentWin) {
        const settings = new BrowserWindow({
          parent: parentWin,
          modal: true,
          show: false,
          width: 600,
          height: 500,
          // macOS: display as sheet
          // Windows/Linux: separate window with parent disabled
          webPreferences: {
            preload: path.join(__dirname, "preload.js"),
          },
        });
      
        settings.once("ready-to-show", () => {
          settings.show();
        });
      
        settings.loadFile("settings.html");
        return settings;
      }
      
      // Non-modal child: always above parent but does not block it
      function openInspectorWindow(parentWin) {
        const inspector = new BrowserWindow({
          parent: parentWin,
          width: 400,
          height: 600,
        });
      
        inspector.loadFile("inspector.html");
        return inspector;
      }
      ```
      
      **Key point:** `modal: true` requires a `parent`. On macOS, modals appear as sheets. On Windows/Linux, the parent is disabled until the modal closes.
      
      ---
      
      ## Window Registry
      
      A Map-based registry for tracking, looking up, and messaging windows.
      
      ```javascript
      const { BrowserWindow } = require("electron");
      
      const windowRegistry = new Map();
      
      function createTrackedWindow(id, options) {
        // Singleton pattern: focus existing window instead of creating duplicate
        const existing = windowRegistry.get(id);
        if (existing) {
          if (existing.isMinimized()) existing.restore();
          existing.focus();
          return existing;
        }
      
        const win = new BrowserWindow(options);
        windowRegistry.set(id, win);
      
        win.on("closed", () => {
          windowRegistry.delete(id);
        });
      
        return win;
      }
      
      // Send message to a specific window by ID
      function sendToWindow(id, channel, data) {
        const win = windowRegistry.get(id);
        if (win && !win.isDestroyed()) {
          win.webContents.send(channel, data);
        }
      }
      
      // Broadcast to all windows
      function broadcastToAll(channel, data) {
        for (const win of windowRegistry.values()) {
          if (!win.isDestroyed()) {
            win.webContents.send(channel, data);
          }
        }
      }
      ```
      
      **Why good:** Singleton pattern prevents duplicate windows, `isDestroyed()` check avoids sending to dead windows, `closed` event keeps the registry clean
      
      ---
      
      ## Window State Persistence
      
      Save bounds, maximized/fullscreen state, and display ID. Validate on restore.
      
      ```javascript
      const { screen } = require("electron");
      const fs = require("node:fs");
      const path = require("node:path");
      
      const DEFAULT_WIDTH = 1200;
      const DEFAULT_HEIGHT = 800;
      
      function getStateFilePath(windowName) {
        return path.join(app.getPath("userData"), `window-state-${windowName}.json`);
      }
      
      function saveWindowState(win, windowName) {
        // Don't save bounds if maximized/fullscreen -- save the restored bounds instead
        if (win.isMaximized() || win.isFullScreen()) {
          const state = loadWindowState(windowName) || {};
          state.isMaximized = win.isMaximized();
          state.isFullScreen = win.isFullScreen();
          fs.writeFileSync(getStateFilePath(windowName), JSON.stringify(state));
          return;
        }
      
        const bounds = win.getBounds();
        const display = screen.getDisplayMatching(bounds);
        const state = {
          bounds,
          isMaximized: false,
          isFullScreen: false,
          displayId: display.id,
          workArea: display.workArea,
        };
        fs.writeFileSync(getStateFilePath(windowName), JSON.stringify(state));
      }
      
      function loadWindowState(windowName) {
        try {
          const raw = fs.readFileSync(getStateFilePath(windowName), "utf-8");
          return JSON.parse(raw);
        } catch {
          return null;
        }
      }
      
      function restoreWindowState(windowName) {
        const state = loadWindowState(windowName);
        if (!state || !state.bounds) {
          return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
        }
      
        // Validate: is the saved display still connected?
        const displays = screen.getAllDisplays();
        const targetDisplay = displays.find((d) => d.id === state.displayId);
      
        if (!targetDisplay) {
          // Display disconnected -- use primary display
          const primary = screen.getPrimaryDisplay();
          return {
            x: primary.workArea.x,
            y: primary.workArea.y,
            width: Math.min(state.bounds.width, primary.workArea.width),
            height: Math.min(state.bounds.height, primary.workArea.height),
          };
        }
      
        // Validate: are saved bounds within the display's work area?
        const { workArea } = targetDisplay;
        const x = Math.max(
          workArea.x,
          Math.min(state.bounds.x, workArea.x + workArea.width - state.bounds.width),
        );
        const y = Math.max(
          workArea.y,
          Math.min(
            state.bounds.y,
            workArea.y + workArea.height - state.bounds.height,
          ),
        );
      
        return {
          x,
          y,
          width: Math.min(state.bounds.width, workArea.width),
          height: Math.min(state.bounds.height, workArea.height),
          isMaximized: state.isMaximized,
          isFullScreen: state.isFullScreen,
        };
      }
      
      // Usage in window creation
      function createWindowWithState(windowName) {
        const restored = restoreWindowState(windowName);
        const win = new BrowserWindow({
          ...restored,
          show: false,
          webPreferences: { preload: path.join(__dirname, "preload.js") },
        });
      
        if (restored.isMaximized) win.maximize();
        if (restored.isFullScreen) win.setFullScreen(true);
      
        win.on("close", () => saveWindowState(win, windowName));
        win.once("ready-to-show", () => win.show());
      
        return win;
      }
      ```
      
      **Why good:** Validates saved display still exists, clamps bounds to work area (not display bounds -- avoids taskbar), saves on close, falls back to primary display
      
      ---
      
      ## Multi-Monitor Placement
      
      ```javascript
      const { screen, BrowserWindow } = require("electron");
      
      // Find a specific display
      function getExternalDisplay() {
        const displays = screen.getAllDisplays();
        return displays.find((d) => d.bounds.x !== 0 || d.bounds.y !== 0);
      }
      
      // Create window centered on a specific display's work area
      function createWindowOnDisplay(display) {
        const { workArea } = display;
        const WIDTH = 800;
        const HEIGHT = 600;
      
        return new BrowserWindow({
          x: workArea.x + Math.floor((workArea.width - WIDTH) / 2),
          y: workArea.y + Math.floor((workArea.height - HEIGHT) / 2),
          width: WIDTH,
          height: HEIGHT,
        });
      }
      
      // React to display changes
      screen.on("display-added", (event, newDisplay) => {
        // A new monitor was connected
      });
      
      screen.on("display-removed", (event, oldDisplay) => {
        // A monitor was disconnected -- move any windows on it to primary
        const primary = screen.getPrimaryDisplay();
        for (const win of BrowserWindow.getAllWindows()) {
          const winBounds = win.getBounds();
          const winDisplay = screen.getDisplayMatching(winBounds);
          if (winDisplay.id === oldDisplay.id) {
            win.setBounds({
              x: primary.workArea.x,
              y: primary.workArea.y,
              width: winBounds.width,
              height: winBounds.height,
            });
          }
        }
      });
      ```
      
      **Why good:** Uses `workArea` (excludes taskbar/dock), centers window properly, handles display disconnect by relocating affected windows
      
      ---
      
      ## BaseWindow Cleanup Pattern
      
      Critical pattern to prevent memory leaks with BaseWindow.
      
      ```javascript
      function createBaseWindowWithViews() {
        const win = new BaseWindow({ width: 1000, height: 700 });
        const views = [];
      
        function addView(url) {
          const view = new WebContentsView({
            webPreferences: { preload: path.join(__dirname, "preload.js") },
          });
          win.contentView.addChildView(view);
          view.webContents.loadURL(url);
          views.push(view);
          return view;
        }
      
        // CRITICAL: clean up ALL views when window closes
        win.on("closed", () => {
          for (const view of views) {
            if (!view.webContents.isDestroyed()) {
              view.webContents.close();
            }
          }
          views.length = 0;
        });
      
        return { win, addView };
      }
      ```
      
      **Why good:** Tracks all views, checks `isDestroyed()` before closing (avoids double-close errors), clears the array to release references
      
      ```javascript
      // BAD: forgetting to close webContents
      win.on("closed", () => {
        // views still hold live webContents -- MEMORY LEAK
        windowRegistry.delete(win.id);
      });
      ```
      
      **Why bad:** BaseWindow does not auto-destroy webContents on close. Each orphaned webContents keeps its renderer process alive, consuming memory indefinitely.
      
      ---
      
      See [inter-window-communication.md](inter-window-communication.md) for MessagePort and main process relay patterns.
      
    • inter-window-communication.md 7.7 KB
      # Electron Multi-Window - Inter-Window Communication
      
      > Main process relay, MessagePort for direct renderer-to-renderer, typed channels. See [core.md](core.md) for window registry and lifecycle patterns. See [SKILL.md](../SKILL.md) for decision frameworks.
      
      ---
      
      ## Main Process Relay
      
      The simplest pattern: renderer A sends to main, main forwards to renderer B.
      
      ```javascript
      // main.js
      const { ipcMain } = require("electron");
      
      // Route messages between windows using the registry
      ipcMain.on("relay-to-window", (event, targetWindowId, channel, data) => {
        const targetWin = windowRegistry.get(targetWindowId);
        if (!targetWin || targetWin.isDestroyed()) return;
      
        targetWin.webContents.send(channel, {
          ...data,
          sourceWindowId: getWindowId(event.sender), // track origin
        });
      });
      
      // Broadcast to all windows except sender
      ipcMain.on("broadcast", (event, channel, data) => {
        const senderId = event.sender.id;
        for (const win of windowRegistry.values()) {
          if (!win.isDestroyed() && win.webContents.id !== senderId) {
            win.webContents.send(channel, data);
          }
        }
      });
      
      // Helper: find window ID by webContents
      function getWindowId(webContents) {
        for (const [id, win] of windowRegistry.entries()) {
          if (win.webContents.id === webContents.id) return id;
        }
        return null;
      }
      ```
      
      ```javascript
      // preload.js -- expose relay and broadcast
      const { contextBridge, ipcRenderer } = require("electron/renderer");
      
      contextBridge.exposeInMainWorld("windowAPI", {
        sendToWindow: (targetId, channel, data) =>
          ipcRenderer.send("relay-to-window", targetId, channel, data),
        broadcast: (channel, data) => ipcRenderer.send("broadcast", channel, data),
        onMessage: (channel, callback) => {
          ipcRenderer.on(channel, (_event, data) => callback(data));
        },
        removeListener: (channel) => {
          ipcRenderer.removeAllListeners(channel);
        },
      });
      ```
      
      ```javascript
      // renderer.js (window A)
      window.windowAPI.sendToWindow("editor", "file-changed", { path: "/app.js" });
      
      // renderer.js (window B -- "editor")
      window.windowAPI.onMessage("file-changed", (data) => {
        reloadFile(data.path);
      });
      ```
      
      **Why good:** Simple setup, uses existing IPC infrastructure, main process can validate/transform messages, works with any number of windows
      
      **When to use:** Infrequent messages (settings changes, file updates, status notifications) where slight latency from the main process roundtrip is acceptable.
      
      ---
      
      ## MessagePort -- Direct Renderer-to-Renderer
      
      For high-frequency communication (collaborative editing, streaming data), establish a direct channel that bypasses the main process after setup.
      
      ```javascript
      // main.js -- create and distribute ports
      const { MessageChannelMain, ipcMain } = require("electron");
      
      function connectWindows(window1, window2) {
        const { port1, port2 } = new MessageChannelMain();
      
        // Transfer ports to each renderer -- MUST use postMessage, not send
        window1.webContents.postMessage("connect-port", { peerId: "window2" }, [
          port1,
        ]);
        window2.webContents.postMessage("connect-port", { peerId: "window1" }, [
          port2,
        ]);
      }
      
      // Example: connect windows when both are ready
      ipcMain.handle("request-connection", (event, targetWindowId) => {
        const sourceWin = BrowserWindow.fromWebContents(event.sender);
        const targetWin = windowRegistry.get(targetWindowId);
        if (sourceWin && targetWin) {
          connectWindows(sourceWin, targetWin);
        }
      });
      ```
      
      ```javascript
      // preload.js -- receive and expose port
      const { contextBridge, ipcRenderer } = require("electron/renderer");
      
      let messagePort = null;
      
      ipcRenderer.on("connect-port", (event) => {
        // Port arrives via event.ports, not event args
        const [port] = event.ports;
        messagePort = port;
      
        // CRITICAL: start() must be called to begin receiving queued messages
        port.start();
      
        // Forward port messages to the renderer world
        port.onmessage = (msgEvent) => {
          window.postMessage({ type: "peer-message", data: msgEvent.data }, "*");
        };
      });
      
      contextBridge.exposeInMainWorld("portAPI", {
        sendToPeer: (data) => {
          if (messagePort) messagePort.postMessage(data);
        },
        onPeerMessage: (callback) => {
          window.addEventListener("message", (event) => {
            if (event.data?.type === "peer-message") {
              callback(event.data.data);
            }
          });
        },
        requestConnection: (targetWindowId) =>
          ipcRenderer.invoke("request-connection", targetWindowId),
      });
      ```
      
      ```javascript
      // renderer.js (window 1)
      await window.portAPI.requestConnection("editor");
      
      window.portAPI.onPeerMessage((data) => {
        console.log("Received from peer:", data);
      });
      
      window.portAPI.sendToPeer({
        action: "cursor-move",
        position: { line: 10, col: 5 },
      });
      ```
      
      **Why good:** After setup, messages flow directly between renderers without main process overhead. Ideal for real-time collaboration, streaming updates, or high-frequency events.
      
      **Key gotchas:**
      
      - Ports transfer via `postMessage`, not `send` or `invoke`
      - `port.start()` must be called in preload -- messages queue until then
      - Port arrives in `event.ports`, not as a regular argument
      - With `contextIsolation`, the preload must relay port messages to the renderer world via `window.postMessage`
      
      ---
      
      ## Typed IPC Channels
      
      For TypeScript projects, define channel types for type-safe inter-window messaging.
      
      ```typescript
      // shared/ipc-channels.ts
      interface WindowMessages {
        "file-changed": { path: string; content: string };
        "theme-changed": { theme: "light" | "dark" };
        "selection-changed": { start: number; end: number; windowId: string };
      }
      
      type WindowChannel = keyof WindowMessages;
      
      // Preload exposes typed methods
      interface WindowAPI {
        sendToWindow: <C extends WindowChannel>(
          targetId: string,
          channel: C,
          data: WindowMessages[C],
        ) => void;
        onMessage: <C extends WindowChannel>(
          channel: C,
          callback: (data: WindowMessages[C]) => void,
        ) => void;
        removeListener: (channel: WindowChannel) => void;
      }
      ```
      
      ```typescript
      // global.d.ts
      declare global {
        interface Window {
          windowAPI: WindowAPI;
        }
      }
      ```
      
      ```typescript
      // renderer.ts -- type-safe usage
      window.windowAPI.sendToWindow("editor", "file-changed", {
        path: "/app.js",
        content: "// updated",
      });
      
      // Type error: { wrong: "data" } does not match FileChangedPayload
      window.windowAPI.sendToWindow("editor", "file-changed", { wrong: "data" });
      ```
      
      **Why good:** Compile-time safety for inter-window messages, channel names and payloads are centrally defined, type errors catch mismatched messages
      
      ---
      
      ## Shared State via Main Process
      
      When multiple windows need the same state, keep a single source of truth in main and push updates.
      
      ```javascript
      // main.js -- main process as state owner
      const { ipcMain } = require("electron");
      
      let appState = {
        theme: "light",
        recentFiles: [],
        user: null,
      };
      
      ipcMain.handle("get-state", () => {
        return appState;
      });
      
      ipcMain.handle("update-state", (_event, patch) => {
        appState = { ...appState, ...patch };
      
        // Push updated state to ALL windows
        for (const win of windowRegistry.values()) {
          if (!win.isDestroyed()) {
            win.webContents.send("state-updated", appState);
          }
        }
      
        return appState;
      });
      ```
      
      ```javascript
      // preload.js
      contextBridge.exposeInMainWorld("stateAPI", {
        getState: () => ipcRenderer.invoke("get-state"),
        updateState: (patch) => ipcRenderer.invoke("update-state", patch),
        onStateUpdate: (callback) => {
          ipcRenderer.on("state-updated", (_event, state) => callback(state));
        },
      });
      ```
      
      **Why good:** Single source of truth prevents desync, all windows receive updates, state changes are serialized through main process (no race conditions)
      
      **When to use:** Application-wide settings (theme, user preferences, authentication state) that all windows must reflect consistently.
      
      ---
      
      See [core.md](core.md) for window registry implementation and lifecycle patterns.
      
  • reference.md 7 KB
    # Electron Multi-Window Reference
    
    > Quick-lookup tables, migration checklist, event order. See [SKILL.md](SKILL.md) for decision frameworks and red flags. See [examples/](examples/) for full code examples.
    
    ---
    
    ## Window Type Comparison
    
    | Feature               | BrowserWindow              | BaseWindow + WebContentsView         |
    | --------------------- | -------------------------- | ------------------------------------ |
    | Web views             | 1 (built-in)               | 0+ (added manually)                  |
    | Has `webContents`     | Yes (automatic)            | No (each view has its own)           |
    | `ready-to-show`       | Yes                        | No (listen on view's webContents)    |
    | Auto-cleanup on close | Yes (destroys webContents) | No (must close webContents manually) |
    | Preload scripts       | 1 (in webPreferences)      | 1 per WebContentsView                |
    | Resize handling       | Automatic for content      | Manual via `resize` event            |
    | Use case              | Single-view windows        | Multi-view layouts (tabs, panels)    |
    
    ---
    
    ## BrowserView to WebContentsView Migration Checklist
    
    - [ ] Replace `new BrowserView(opts)` with `new WebContentsView(opts)`
    - [ ] Replace `win.addBrowserView(view)` with `win.contentView.addChildView(view)`
    - [ ] Replace `win.removeBrowserView(view)` with `win.contentView.removeChildView(view)`
    - [ ] Replace `win.getBrowserViews()` with `win.contentView.children`
    - [ ] Replace `win.setTopBrowserView(view)` with `win.contentView.addChildView(view)` (re-adding reorders)
    - [ ] Replace `view.setAutoResize(...)` with manual `win.on("resize", ...)` + `view.setBounds(...)`
    - [ ] Set `view.setBackgroundColor("#00000000")` if transparency is needed (WebContentsView defaults to white)
    - [ ] Add explicit `view.webContents.close()` in window `closed` handler (no auto-cleanup)
    
    ---
    
    ## View API Quick Reference
    
    | Method / Property        | Class            | Description                                      |
    | ------------------------ | ---------------- | ------------------------------------------------ |
    | `addChildView(view, i?)` | View             | Add child view; optional index for z-order       |
    | `removeChildView(view)`  | View             | Remove child view (no-op if not a child)         |
    | `children`               | View (read-only) | Array of child View objects                      |
    | `setBounds(rect)`        | View             | Set position and size relative to parent         |
    | `getBounds()`            | View             | Get position and size relative to parent         |
    | `setBackgroundColor(c)`  | View             | Set background (hex, RGB, RGBA, HSL, CSS names)  |
    | `setBorderRadius(r)`     | View             | Set border radius in pixels                      |
    | `setVisible(bool)`       | View             | Show or hide the view                            |
    | `getVisible()`           | View             | Whether the view should be drawn                 |
    | `webContents`            | WebContentsView  | Read-only reference to the displayed WebContents |
    
    ---
    
    ## Window Lifecycle Events (Ordered)
    
    | Event           | Preventable? | When                                      | Typical Use                      |
    | --------------- | ------------ | ----------------------------------------- | -------------------------------- |
    | `ready-to-show` | No           | First paint complete (BrowserWindow only) | Show window without flash        |
    | `close`         | Yes          | Window is about to close                  | Save state, confirm unsaved work |
    | `closed`        | No           | Window has been destroyed                 | Clean up registry, release refs  |
    
    **Note:** `will-close` does not exist as a documented event. Use `close` with `event.preventDefault()` to intercept.
    
    ---
    
    ## Screen API Quick Reference
    
    | Method / Event                         | Returns / Fires | Description                                    |
    | -------------------------------------- | --------------- | ---------------------------------------------- |
    | `screen.getAllDisplays()`              | `Display[]`     | All connected displays                         |
    | `screen.getPrimaryDisplay()`           | `Display`       | The primary/main display                       |
    | `screen.getDisplayNearestPoint(point)` | `Display`       | Display closest to a screen coordinate         |
    | `screen.getDisplayMatching(rect)`      | `Display`       | Display that most overlaps with the given rect |
    | `display-added`                        | Event           | New display connected                          |
    | `display-removed`                      | Event           | Display disconnected                           |
    | `display-metrics-changed`              | Event           | Display properties changed (resolution, etc.)  |
    
    ### Display Object Properties
    
    | Property      | Type        | Description                                   |
    | ------------- | ----------- | --------------------------------------------- |
    | `id`          | `number`    | Unique display identifier                     |
    | `bounds`      | `Rectangle` | Full display area including taskbar           |
    | `workArea`    | `Rectangle` | Usable area excluding taskbar/dock            |
    | `scaleFactor` | `number`    | DPI scale factor (1 = standard, 2 = retina)   |
    | `rotation`    | `number`    | Display rotation in degrees (0, 90, 180, 270) |
    
    **Key distinction:** Use `workArea` (not `bounds`) for window placement to avoid positioning behind taskbars or docks.
    
    ---
    
    ## Inter-Window Communication Methods
    
    | Pattern            | Setup Complexity | Latency | Use Case                        |
    | ------------------ | ---------------- | ------- | ------------------------------- |
    | Main process relay | Low              | Medium  | Infrequent messages, state sync |
    | MessagePort        | Medium           | Low     | High-frequency, streaming data  |
    | Shared main state  | Low              | Medium  | App-wide settings, preferences  |
    
    ---
    
    ## Parent/Child Window Behavior by Platform
    
    | Behavior                     | macOS                    | Windows / Linux |
    | ---------------------------- | ------------------------ | --------------- |
    | Modal display                | Sheet attached to parent | Separate window |
    | Modal blocks parent          | Yes                      | Yes             |
    | Child always above parent    | Yes                      | Yes             |
    | Parent close closes children | Yes                      | Yes             |
    | Child can outlive parent     | No                       | No              |
    
    ---
    
    ## See Also
    
    - [Electron BaseWindow API](https://www.electronjs.org/docs/latest/api/base-window)
    - [Electron WebContentsView API](https://www.electronjs.org/docs/latest/api/web-contents-view)
    - [Electron View API](https://www.electronjs.org/docs/latest/api/view)
    - [Electron Screen API](https://www.electronjs.org/docs/latest/api/screen)
    - [BrowserView to WebContentsView Migration Guide](https://www.electronjs.org/blog/migrate-to-webcontentsview)
    - [MessagePorts in Electron](https://www.electronjs.org/docs/latest/tutorial/message-ports)
    
  • SKILL.md 16 KB
    ---
    name: desktop-multiwindow-electron
    description: Multi-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence
    ---
    
    # Electron Multi-Window Patterns
    
    > **Quick Guide:** Use `BrowserWindow` for single-view windows. Use `BaseWindow` + `WebContentsView` for multi-view layouts (tabs, split panes, panels). `BrowserView` is deprecated since Electron 30 -- migrate to `WebContentsView`. Track windows with a `Map<string, BrowserWindow>` registry. Communicate between windows via the main process or `MessagePort` for direct renderer-to-renderer channels. Persist window bounds manually or use the upcoming `windowStatePersistence` API. Always close `webContents` explicitly when using `BaseWindow` -- unlike `BrowserWindow`, it does not auto-cleanup.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **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:**
    
    - Creating multi-view layouts (tabs, split panes, embedded panels) with BaseWindow + WebContentsView
    - Managing multiple BrowserWindow instances with a window registry
    - Migrating from deprecated BrowserView to WebContentsView
    - Setting up parent/child or modal windows
    - Communicating between windows (via main process relay or MessagePort)
    - Persisting and restoring window position, size, and display state
    - Placing windows on specific monitors using the screen API
    
    **When NOT to use:**
    
    - Single-window apps with one view (`BrowserWindow` is sufficient on its own)
    - Choosing a UI framework for the renderer
    - IPC patterns between main and a single renderer (basic IPC is outside multi-window scope)
    - Styling or layout within a single renderer
    
    **Key patterns covered:**
    
    - BaseWindow + WebContentsView for multi-view layouts
    - BrowserView to WebContentsView migration
    - Window lifecycle events (ready-to-show, close, closed)
    - Parent/child and modal windows
    - Window registry with Map-based tracking
    - Inter-window communication via main process and MessagePort
    - Window state persistence (bounds, maximized, fullscreen)
    - Multi-monitor placement with screen API
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    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:**
    
    - Tab bars, split editors, preview panels, embedded browser views
    - Any layout where multiple independent web pages share one OS window
    - Applications migrating from deprecated BrowserView
    
    **When NOT to use BaseWindow:**
    
    - Single-view windows (BrowserWindow is simpler and handles cleanup automatically)
    - Windows that only need a toolbar or status bar (a single BrowserWindow with HTML layout is sufficient)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: BaseWindow with WebContentsView
    
    BaseWindow is the window shell; WebContentsView instances are the content. Each view has its own renderer process and preload script.
    
    ```javascript
    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](examples/core.md) for complete split-view and tab examples with resize handling.
    
    ---
    
    ### Pattern 2: BrowserView to WebContentsView Migration
    
    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](examples/core.md) for the complete migration pattern with auto-resize replacement.
    
    ---
    
    ### Pattern 3: Window Lifecycle Events
    
    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.
    
    ```javascript
    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](examples/core.md) for the full lifecycle sequence and BaseWindow workaround.
    
    ---
    
    ### Pattern 4: Parent/Child and Modal Windows
    
    Child windows always appear above their parent. Modal windows additionally disable the parent until closed.
    
    ```javascript
    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](examples/core.md) for confirmation dialogs and settings windows.
    
    ---
    
    ### Pattern 5: Window Registry
    
    Track all open windows with a Map for reliable lookup, messaging, and cleanup.
    
    ```javascript
    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](examples/core.md) for the full singleton window pattern.
    
    ---
    
    ### Pattern 6: Inter-Window Communication
    
    Two patterns: **main process relay** for simple messages, **MessagePort** for direct high-frequency renderer-to-renderer channels.
    
    ```javascript
    // 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](examples/inter-window-communication.md) for complete examples of both patterns.
    
    ---
    
    ### Pattern 7: Window State Persistence
    
    Save and restore window bounds, maximized state, and display information across sessions.
    
    ```javascript
    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](examples/core.md) for the complete save/restore cycle with multi-monitor validation.
    
    ---
    
    ### Pattern 8: Multi-Monitor Placement
    
    Use the `screen` API to enumerate displays, find work areas, and place windows on specific monitors.
    
    ```javascript
    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](examples/core.md) for display enumeration and safe placement.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Window Type Selection
    
    ```
    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
    ```
    
    ### Inter-Window Communication
    
    ```
    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
    ```
    
    ### Window Relationship
    
    ```
    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:**
    
    - [examples/core.md](examples/core.md) - BaseWindow + WebContentsView, lifecycle, registry, state persistence, multi-monitor
    - [examples/inter-window-communication.md](examples/inter-window-communication.md) - Main process relay, MessagePort, typed channels
    - [reference.md](reference.md) - API quick-reference tables, migration checklist, event order
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **Critical Issues:**
    
    - Not closing `webContents` when destroying a `BaseWindow` -- causes memory leaks (BrowserWindow auto-cleans, BaseWindow does not)
    - Using deprecated `BrowserView` instead of `WebContentsView` -- deprecated since Electron 30
    - Direct renderer-to-renderer communication bypassing the main process -- violates process isolation
    - Restoring window bounds without checking if the target display still exists -- window appears off-screen
    
    **Architecture Issues:**
    
    - Using `BaseWindow` for single-view windows -- unnecessary complexity, use `BrowserWindow`
    - Using `BrowserView.setAutoResize()` patterns with `WebContentsView` -- no equivalent exists, use manual resize listeners
    - Storing `BrowserWindow` objects as Map values without cleaning up on `closed` -- stale references
    - Creating child windows from the renderer process -- always create from main
    
    **Common Mistakes:**
    
    - Expecting `ready-to-show` on `BaseWindow` -- it fires on `BrowserWindow` only; for `BaseWindow`, listen on `view.webContents`
    - Forgetting that `WebContentsView` defaults to white background (BrowserView defaulted to transparent) -- set `"#00000000"` explicitly
    - Using `ipcRenderer.send()` to transfer `MessagePort` -- only `postMessage()` can transfer ports
    - Placing windows using `display.bounds` instead of `display.workArea` -- window ends up behind taskbar/dock
    - Not handling `display-removed` event -- window references a disconnected monitor
    
    **Gotchas & Edge Cases:**
    
    - Re-adding a child view with `addChildView()` moves it to the top of the z-order -- this is intentional, not a bug
    - `setBounds()` coordinates are relative to the parent view, not the screen
    - On macOS, modal windows display as sheets attached to the parent window
    - `win.getBounds()` returns the outer frame bounds on some platforms -- content area may differ
    - Each `WebContentsView` runs its own renderer process -- resource usage scales linearly with view count
    - `MessagePortMain` requires calling `.start()` before messages are delivered -- they queue until then
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## 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>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related