# How OpenWhispr Manages Its Dual Window Architecture: A Deep Dive into Electron Multi-Window Design

> Discover how OpenWhispr masterfully handles its dual window Electron architecture. Learn about its IPC state management and shared React codebase for seamless multi-window operation.

- Repository: [OpenWhispr/openwhispr](https://github.com/OpenWhispr/openwhispr)
- Tags: deep-dive
- Published: 2026-09-06

---

**OpenWhispr uses two specialized Electron windows—a minimal always-on-top dictation overlay and a full-featured control panel—that share a single React codebase while isolating window-specific behavior through centralized configuration and IPC-based state management.**

The OpenWhispr dual window architecture solves a common Electron challenge: how to maintain a lightweight, persistent UI for core functionality while providing access to complex settings without cluttering the primary experience. By splitting responsibilities between a **Main (Dictation) Window** and a **Control Panel Window**, the application achieves both minimal disruption during dictation and rich configuration capabilities when needed.

## The Two Windows: Purpose and Design

OpenWhispr's interface is deliberately bifurcated based on usage context.

### Main (Dictation) Window

The Main Window serves as the always-visible dictation interface. It is **frameless**, **transparent**, and **always-on-top**, displaying only the live dictation pill, recording status, and quick-paste UI. This minimal footprint ensures users can dictate without window management friction.

Key characteristics:
- **Small fixed dimensions** (approximately 400×80 pixels) determined by the `useMainWindowSizeOwner` React hook
- **Draggable positioning** anywhere on screen
- **Content protection** enabled via `window.setContentProtection(true)` to prevent sensitive dictation from appearing in screen captures or remote desktop streams

### Control Panel Window

The Control Panel Window provides the full desktop application experience: settings, transcription history, model management, and onboarding flows. Unlike the main window, it behaves as a standard resizable window with native menus and tabbed navigation.

Notable behaviors:
- **Lazily instantiated** on first access (settings or onboarding trigger)
- **Can be hidden to tray** independently of the main window
- **Shares React components** with the main window, differentiated only by router path (`/control-panel`)

## Window Creation and Lifecycle

Both windows are orchestrated through [`src/helpers/windowManager.js`](https://github.com/OpenWhispr/openwhispr/blob/main/src/helpers/windowManager.js), which centralizes creation, destruction, and visibility control.

### Creating the Main Window

```js
// src/helpers/windowManager.js
function createMainWindow() {
  const win = new BrowserWindow({
    width: 400,
    height: 80,
    alwaysOnTop: true,
    frame: false,
    transparent: true,
    webPreferences: { preload: PRELOAD_PATH },
  });
  win.loadURL(`file://${APP_ROOT}/index.html#main`);
  return win;
}

```

The `alwaysOnTop: true` and `frame: false` properties create the overlay aesthetic, while `transparent: true` enables the floating pill appearance. The window loads the same [`index.html`](https://github.com/OpenWhispr/openwhispr/blob/main/index.html) as the control panel but targets the `#main` hash route.

### Creating the Control Panel Window

```js
// src/helpers/windowManager.js
function createControlPanel() {
  const panel = new BrowserWindow({
    width: 1024,
    height: 768,
    show: false,
    webPreferences: { preload: PRELOAD_PATH },
  });
  panel.loadURL(`file://${APP_ROOT}/index.html#/control-panel`);
  panel.once('ready-to-show', () => panel.show());
  return panel;
}

```

The control panel starts hidden (`show: false`) to prevent visual flicker during React hydration, then reveals itself once content is ready. This matches standard Electron best practices for perceived performance.

## Centralized Window Configuration

Window-specific behaviors are abstracted into [`src/helpers/windowConfig.js`](https://github.com/OpenWhispr/openwhispr/blob/main/src/helpers/windowConfig.js), which exports a configuration object consumed by both the main process and renderer. This centralization prevents hardcoded window settings from scattering across the codebase and enables consistent behavior updates.

The configuration drives:
- Size constraints and position defaults
- Content protection flags
- Transparency and frame options
- Platform-specific adjustments

## Shared Codebase with Router-Based Differentiation

Despite their visual differences, both windows run identical React builds. Differentiation occurs at the routing layer in [`src/components/App.jsx`](https://github.com/OpenWhispr/openwhispr/blob/main/src/components/App.jsx):

- **Main Window**: Renders overlay components when URL contains `#main`
- **Control Panel Window**: Renders full application chrome when URL contains `#/control-panel`

This design eliminates code duplication while allowing each window to optimize for its specific context.

## IPC-Based Window Toggling

Inter-window communication uses Electron's IPC layer with context isolation enforced. The preload script safely exposes controlled APIs to the renderer:

**preload.js**

```js
contextBridge.exposeInMainWorld('api', {
  toggleControlPanel: () => ipcRenderer.send('toggle-control-panel')
});

```

**ipcHandlers.js**

```js
ipcMain.on('toggle-control-panel', () => {
  if (controlPanelWindow.isVisible()) controlPanelWindow.hide();
  else controlPanelWindow.show();
});

```

This pattern ensures the renderer cannot directly manipulate window objects—presentation logic remains strictly separated from privileged native code.

## State Synchronization Architecture

Global application state lives exclusively in the main process and is accessed by both windows via IPC. This includes:

- Hotkey registration state
- Audio manager status
- User settings

The OpenWhispr dual window architecture thereby avoids state duplication and synchronization complexity. Both windows remain consistent without implementing distributed state management in the renderer.

## Security and Process Isolation

The architecture implements Electron security best practices:

- **Context Isolation**: Enabled via `contextIsolation: true` (default) with explicit API exposure through [`preload.js`](https://github.com/OpenWhispr/openwhispr/blob/main/preload.js)
- **Node Integration Disabled**: Renderer processes cannot access Node.js APIs directly
- **Content Protection**: The main window's sensitive content is excluded from screen capture APIs

This isolation is particularly important for a dictation application handling potentially confidential audio and transcription data.

## Startup Sequence

Application bootstrapping in [`main.js`](https://github.com/OpenWhispr/openwhispr/blob/main/main.js) follows a deliberate initialization order:

1. `initializeCoreManagers()` spawns services (audio, settings, hotkeys)
2. `createMainWindow()` launches the dictation overlay immediately
3. Control panel creation is deferred until user action or first-run onboarding

This prioritizes core functionality availability while deferring heavier initialization costs.

## Summary

- **Two specialized windows**: A minimal always-on-top dictation overlay (Main Window) and a full-featured settings interface (Control Panel Window)
- **Centralized management**: Both created through [`src/helpers/windowManager.js`](https://github.com/OpenWhispr/openwhispr/blob/main/src/helpers/windowManager.js) with configuration from [`src/helpers/windowConfig.js`](https://github.com/OpenWhispr/openwhispr/blob/main/src/helpers/windowConfig.js)
- **Shared React codebase**: Router paths (`#main` vs `#/control-panel`) determine which UI hierarchy renders
- **IPC state sharing**: Global state lives in the main process, accessed by both windows through sanitized preload APIs
- **Lazy control panel loading**: The settings window instantiates only when needed, reducing startup overhead
- **Security-first design**: Context isolation, content protection, and disabled Node integration protect sensitive dictation data

## Frequently Asked Questions

### How does OpenWhispr keep both windows synchronized?

Both windows access shared state exclusively through IPC calls to the main process. Global state managers (hotkeys, audio, settings) reside in [`main.js`](https://github.com/OpenWhispr/openwhispr/blob/main/main.js) and its initialized core services, eliminating the need for renderer-to-renderer synchronization. When one window modifies state, the main process broadcasts updates to all subscribers.

### Can the control panel run without the main window?

No—the main window is the primary application entry point and always launches first. However, the control panel can be **hidden or closed** while the main window persists. The reverse (control panel without main window) is not supported by the current architecture, as dictation functionality requires the overlay interface.

### Why use two windows instead of a single resizable window?

The dual window model optimizes for conflicting usage patterns. The **Main Window** must remain persistently visible without consuming screen real estate or window focus—a frameless always-on-top overlay achieves this. A **single window approach** would force users to manage application visibility during dictation, disrupting workflow. The **Control Panel Window** requires standard desktop window behaviors (resizing, minimization, menu access) incompatible with overlay requirements.

### Where does the main window size logic reside?

Main window dimensions are controlled by the `useMainWindowSizeOwner` custom React hook in [`src/hooks/useMainWindowSizeOwner.tsx`](https://github.com/OpenWhispr/openwhispr/blob/main/src/hooks/useMainWindowSizeOwner.tsx). This implements a "window ladder" strategy that adjusts the overlay size based on content state (idle, recording, processing, error), ensuring the pill remains appropriately sized without manual intervention.