Migrate window-bounds persistence off the electron-window-state library into a WindowManager built-in `rememberBounds` capability, backed by the main-process persist cache (`window.bounds` key — its first real consumer). - New `windowBoundsTracker` free-function module: validates the stored record (including displayBounds), restores onto the display the window was last on (clamping into its work area, never resetting to primary), and snapshots at teardown via getNormalBounds + isMaximized. - Singleton-only gate (dev warning for non-singleton types). Runtime toggle `wm.setRememberBounds` (orthogonal to the registry flag; OFF drops only that type's slot) plus `wm.peekWindowBounds`. - Persist at three teardown exits: native close (singletons), before window.destroy() in destroyWindow (programmatic destroys), and a new onStop so shutdown writes land before CacheService flushes its persist map. - Wire Main + QuickAssistant. Main re-applies maximize consumer-side on its own show schedule (tray-on-launch defers to first show); remove electron-window-state and its orphaned keepers/constants/comments. Fullscreen is not persisted and old *-state.json is not migrated (one-time reset, loseable). Adds tracker/integration/persist tests, extends the main CacheService mock with persist methods, and documents the capability plus a breaking-change note.
15 KiB
WindowManager API Reference
Full method reference for WindowManager. For conceptual guidance and when-to-use each group, see Usage Guide.
Open / Create / Close
Two layers: Consumer methods are the universal API and should be used by all business code. Internal methods are lower-level primitives for defensive assertions or pool-wide shutdown — consumer code should not reach for them. See Window API layers: consumer vs internal.
| Method | Layer | Signature | Description |
|---|---|---|---|
open |
Consumer | (type: WindowType, args?: OpenWindowArgs) => string |
Lifecycle-aware open: singleton reuse, pool recycle, or fresh create per registry lifecycle. Returns window ID. |
close |
Consumer | (windowId: string) => boolean |
Lifecycle-aware release: destroys default and singleton-without-config windows; hides pooled / singleton-with-retention windows into the warmup state machine (GC destroys per config). |
create |
Internal | (type: WindowType, args?: OpenWindowArgs) => string |
Force fresh creation; throws if a singleton of this type already exists. Use only as a defensive assertion — consumer code should use open() + onWindowCreatedByType instead. |
destroy |
Internal | (windowId: string) => boolean |
Force destroy via window.destroy(), which skips the close event — and therefore skips the pool's close interception, bypassing pool recycling. Non-pooled windows: identical to close(). Pooled windows: use suspendPool(type) for pool-wide shutdown instead of destroying individual pooled windows. |
Window Operations
| Method | Signature | Description |
|---|---|---|
show |
(windowId: string) => boolean |
Show a window. Does NOT change macOS Dock state — the Dock tracks window existence + per-type override, not visibility (matching native macOS: Cmd+W hiding a window does not remove the app from the Dock). |
hide |
(windowId: string) => boolean |
Hide a window. Does NOT change macOS Dock state — same reason as show. If callers want the Dock to disappear too (tray-mode UX), use wm.behavior.setMacShowInDockByType BEFORE hide. |
minimize |
(windowId: string) => boolean |
Minimize a window. |
maximize |
(windowId: string) => boolean |
Toggle maximize/unmaximize. |
restore |
(windowId: string) => boolean |
Restore a minimized window. |
focus |
(windowId: string) => boolean |
Focus a window. |
Behavior Runtime Setters
These operate on the declarative behavior layer per instance and are exposed on wm.behavior (a BehaviorController instance). See Platform Configuration → Declarative Behavior Layer for field semantics.
| Method | Signature | Description |
|---|---|---|
wm.behavior.setHideOnBlur |
(windowId: string, enabled: boolean) => void |
Override the declared behavior.hideOnBlur at runtime. enabled: true keeps auto-hide on; enabled: false suppresses (effectively "pinned"). No-op when the window type does not declare behavior.hideOnBlur (no listener to override). Override is cleared on window destroy and on pool releaseToPool. |
wm.behavior.setAlwaysOnTop |
(windowId: string, enabled: boolean) => void |
Toggle always-on-top using level / relativeLevel from behavior.alwaysOnTop (single source of truth). When neither is declared, setAlwaysOnTop(enabled) is called with no level — matching Electron's default. |
wm.behavior.setMacShowInDockByType |
(type: WindowType, value: boolean) => void |
Override behavior.macShowInDock for an entire type at runtime. Use this to express "app is entering / leaving tray mode": (Main, false) before window.hide() makes the Dock track the transition; (Main, true) before window.show() lifts the suppression. Keyed by type (not windowId) so it can be set BEFORE the first instance exists (e.g. tray-on-launch path). When multiple window types contribute (e.g. Main + SubWindow), the Dock stays visible as long as any contributing type is alive — wm.behavior.setMacShowInDockByType(Main, false) will not hide the Dock if a SubWindow is still present. |
No WM-level
setVisibleOnAllWorkspacesis provided: its options differ per call in real usage (e.g. SelectionAction's full-screen show sequence), and WM has no state to maintain. Consumers callwindow.setVisibleOnAllWorkspaces(enabled, options)directly on theBrowserWindowinstance. See README → When to Provide a Runtime Setter for the decision rule.
Bounds Persistence
Top-level primitives for the declarative rememberBounds capability (singleton-only). These are WindowManager methods, not part of wm.behavior. See README → Bounds Persistence.
| Method | Signature | Description |
|---|---|---|
setRememberBounds |
(type: WindowType, enabled: boolean) => void |
Runtime toggle for the rememberBounds capability, orthogonal to the registry flag. true persists position/size on teardown and restores on the next open; false stops persisting AND drops the saved record immediately, so the next open uses the registry default. Affects restore on the next open; the live window keeps its geometry. |
peekWindowBounds |
(type: WindowType) => WindowBoundsState | undefined |
Read a type's saved bounds without restoring them. Lets a consumer apply state WindowManager does not — e.g. MainWindowService re-applies the saved maximized flag on its own show schedule. undefined when nothing is saved. |
Queries
Naming convention: methods with Info in the name return serializable WindowInfo snapshots (safe across IPC); methods without it return live BrowserWindow instances.
| Method | Signature | Description |
|---|---|---|
getWindow |
(windowId: string) => BrowserWindow | undefined |
Get BrowserWindow instance by ID. |
getWindowInfo |
(windowId: string) => WindowInfo | undefined |
Get serializable window metadata. |
getWindowsByType |
(type: WindowType) => BrowserWindow[] |
Get all live window instances of a specific type (skips destroyed). |
getWindowInfosByType |
(type: WindowType) => WindowInfo[] |
Get serializable metadata for all windows of a specific type. |
getWindowId |
(window: BrowserWindow) => string | undefined |
Resolve window ID from BrowserWindow. |
getWindowIdByWebContents |
(wc: WebContents) => string | undefined |
Resolve window ID from WebContents (e.g., IPC event.sender). |
count |
(getter) |
Number of managed windows. |
Broadcast
| Method | Signature | Description |
|---|---|---|
broadcast |
(channel: string, ...args: unknown[]) => void |
Send IPC to all managed windows. Skips destroyed windows. |
broadcastToType |
(type: WindowType, channel: string, ...args: unknown[]) => void |
Send IPC to windows of a specific type. |
Init Data
| Method | Signature | Description |
|---|---|---|
open<T> |
(type: WindowType, args?: { initData?: T, options?: Partial<WindowOptions> }) => string |
When args.initData is supplied, written atomically to the store before the method returns; also pushed to the renderer as the WindowManager_Reused payload on reuse paths. |
create<T> |
(type: WindowType, args?: { initData?: T, options?: Partial<WindowOptions> }) => string |
Same atomicity as open, but never fires Reused (all create paths are fresh creation). |
setInitData |
(windowId: string, data: unknown) => void |
Low-level primitive. Prefer the open/create args form in new code. |
getInitData |
(windowId: string) => unknown | null |
Retrieve initialization data. Cleared on pool release; preserved on singleton hide. |
pushInitData<T> |
(windowId: string, data: T) => boolean |
Push fresh init data to an already-open window. Writes the store and fires WindowManager_Reused in one step. Returns false if the window is missing or destroyed. Main-process only. |
pushInitDataToType<T> |
(type: WindowType, data: T) => number |
Same as pushInitData but fans out to every live window of the given type. Returns the number of windows that received the event. Does not filter by visibility — idle pooled windows receive the payload too. |
Timing contract:
- Cold start (fresh creation):
createWindowwritesinitDatato the store synchronously before returning, so anygetInitDatainvoke from the renderer (after React mounts) sees the fresh value. The renderer should use theuseWindowInitDatahook — it handles the invoke on mount automatically. - Reuse (pool recycle / singleton reopen):
open()simultaneously writes to the store AND firesWindowManager_Reusedwith the same payload. TheuseWindowInitDatahook updates its state directly from the event payload — no round-trip. - No initData on a reuse call: the event is NOT fired. No "empty Reused" events — the hook therefore never needs a fallback invoke.
- Live update (already-open window): call
pushInitData/pushInitDataToTypefrom any main-process service. Both paths reuse theWindowManager_Reusedchannel, souseWindowInitDatapicks up the new payload in-place with no remount — useful for "swap the visible window's context withoutclose()+open()flicker". Unlike reuse, these methods forbidundefinedpayloads: pushing nothing has no meaningful semantics here.
webContents.send is fire-and-forget and does not buffer messages sent before the renderer registers listeners. This is exactly why fresh windows can't use PUSH — they still must PULL via getInitData on mount.
Pool Management
| Method | Signature | Description |
|---|---|---|
suspendPool |
(type: WindowType) => number |
Suspend pool: destroy idle windows, disable pool tracking. Returns count destroyed. |
resumePool |
(type: WindowType) => void |
Resume pool: restore lifecycle behavior, trigger eager warmup if configured. |
See Suspend / Resume for semantics while suspended.
Title Bar
| Method | Signature | Description |
|---|---|---|
setTitleBarOverlay |
(options: TitleBarOverlayOptions) => void |
Update title bar overlay on all windows with overlay configured. |
Renderer IPC Surface
All methods above are main-process APIs. WindowManager also exposes an IPC surface so the renderer can drive window operations for itself. Channel constants live in src/shared/IpcChannel.ts; handlers are registered in WindowManager.registerIpcHandlers().
Preload only wraps getInitData as window.api.windowManager.getInitData(). The other channels are invoked directly via window.electron.ipcRenderer.invoke(IpcChannel.WindowManager_*, ...). WindowManager_Reused is a push-only channel (main → renderer) — see Warmup Mechanics → WindowManager_Reused IPC.
| Channel | Direction | Args | Effect |
|---|---|---|---|
WindowManager_Open |
renderer → main | (type, initData?) |
wm.open(type, { initData }). Returns window ID. Throws if type is not registered. |
WindowManager_GetInitData |
renderer → main | — | wm.getInitData(senderWindowId). Returns stored init data or null. |
WindowManager_Close |
renderer → main | (type?) |
wm.close(resolveTargetWindowId(sender, type)). Returns boolean. |
WindowManager_Show |
renderer → main | (type?) |
wm.show(...). |
WindowManager_Hide |
renderer → main | (type?) |
wm.hide(...). |
WindowManager_Minimize |
renderer → main | (type?) |
wm.minimize(...). |
WindowManager_Maximize |
renderer → main | (type?) |
wm.maximize(...). |
WindowManager_Focus |
renderer → main | (type?) |
wm.focus(...). |
WindowManager_Reused |
main → renderer (push) | (payload) |
Fires on pool recycle or singleton reopen when the caller supplied initData. |
Target resolution for the optional type argument (Close / Show / Hide / Minimize / Maximize / Focus):
- No
type: the target is the sender's own window, resolved viagetWindowIdByWebContents(event.sender). This is the common case — a window acting on itself. - With
type: the target must be a singleton — the first (and only) window of that type.defaultandpooledlifecycles are not supported for cross-window targeting via IPC; the call silently returnsfalseand the operation is a no-op.
The bare renderer consumption pattern for Reused uses ipcRenderer.on(IpcChannel.WindowManager_Reused, ...) — but most renderer code should prefer the useWindowInitData hook, which encapsulates both cold-start getInitData invoke and reuse payload delivery.
Events
Pooled windows traverse a four-stage conceptual lifecycle, but only the endpoints have dedicated events:
Created ──▶ [Released ──▶ Recycled ──▶ Released ──▶ ...] ──▶ Destroyed
For non-pooled windows, the same two endpoints apply without any intermediate stages.
| Event | Type | Description |
|---|---|---|
onWindowCreated |
Event<ManagedWindow> |
Fires when a new window is created (before content loads). Fresh-path only for pooled windows. |
onWindowDestroyed |
Event<ManagedWindow> |
Fires when a window is truly destroyed (not on pool release). |
onWindowCreatedByType(type, listener) |
(type, listener) => Disposable |
Convenience variant of onWindowCreated that filters to a single WindowType. Equivalent to onWindowCreated + an inline if (managed.type === type) guard, but avoids the boilerplate at every call site. Prefer this for single-type subscriptions (the typical consumer case). |
onWindowDestroyedByType(type, listener) |
(type, listener) => Disposable |
Type-filtered counterpart to onWindowDestroyed. Same filtering semantics as onWindowCreatedByType. |
The intermediate Released and Recycled stages have no dedicated events — side effects on hide / close / show should be expressed as declarative Platform Quirks, and per-session data on recycle is delivered via the WindowManager_Reused IPC payload (see Init Data).
Usage notes for pooled windows:
- Do NOT set
paintWhenInitiallyHidden: falseon pooled windows — it suppresses the nativeready-to-showevent, breaking the pool's fresh-window auto-show path (showMode === 'auto'listens forready-to-show). It is NOT an acceptable workaround for "show only when content ready" — useshowMode: 'manual'+ consumer-driven show for that, or rely on the reuse-pathReusedpayload to ensure the renderer has data before.show()is called. - macOS focus / hover / always-on-top workarounds are declarative — see Platform Quirks.