How OpenWhispr Manages Its Dual Window Architecture: A Deep Dive into Electron Multi-Window Design
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
useMainWindowSizeOwnerReact 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, which centralizes creation, destruction, and visibility control.
Creating the Main Window
// 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 as the control panel but targets the #main hash route.
Creating the Control Panel Window
// 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, 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:
- 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
contextBridge.exposeInMainWorld('api', {
toggleControlPanel: () => ipcRenderer.send('toggle-control-panel')
});
ipcHandlers.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 throughpreload.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 follows a deliberate initialization order:
initializeCoreManagers()spawns services (audio, settings, hotkeys)createMainWindow()launches the dictation overlay immediately- 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.jswith configuration fromsrc/helpers/windowConfig.js - Shared React codebase: Router paths (
#mainvs#/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 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →