docs/references/window-manager/window-manager-overview.md
Architecture, lifecycle modes, and event timing contract for WindowManager.
WindowManager is an @Injectable() service (Phase.WhenReady, priority 5) registered in the lifecycle system. Window configurations live in windowRegistry.ts; WindowManager consumes them at runtime.
WindowType (enum)
└─ WindowTypeMetadata (discriminated union on `lifecycle`)
├─ { lifecycle: 'default' }
├─ { lifecycle: 'singleton', singletonConfig?: SingletonConfig }
└─ { lifecycle: 'pooled', poolConfig: PoolConfig }
WindowManager
├─ windows: Map<windowId, ManagedWindow> ── all tracked windows
├─ windowsByType: Map<WindowType, Set<windowId>> ── type index
├─ warmupStates: Map<WindowType, WarmupState> ── per-type warmup state (pool + singleton)
└─ initDataStore: Map<windowId, unknown> ── one-shot init data
┌────────── open() ──────────┐
│ │
│ ┌─────────────────────┐ │
│ │ lifecycle check │ │
│ └────────┬────────────┘ │
│ ┌────┼────┐ │
│ ▼ ▼ ▼ │
│ default singleton pooled│
│ │ │ │ │
│ │ existing? idle? │
│ │ ┌──┴──┐ ┌──┴──┐ │
│ │ Y N Y N │
│ │ │ │ │ │ │
│ │ show() │ recycle │ │
│ │ focus() │ │ │ │
│ │ │ ▼ │ ▼ │
│ └─────┼─ create() ──┘ │
│ │ │ │
│ ▼ ▼ │
│ return windowId │
└─────────────────────────────┘
default — Create on Open, Destroy on CloseMulti-instance mode. Every open() call creates a fresh window. close() destroys it permanently.
Use for: windows that appear many times simultaneously (e.g., sub windows).
// windowRegistry.ts
WINDOW_TYPE_REGISTRY[WindowType.SubWindow] = {
type: WindowType.SubWindow,
lifecycle: 'default',
htmlPath: 'sub-window.html',
windowOptions: { ...DEFAULT_WINDOW_CONFIG },
}
// Usage — each call creates a new window
const tab1 = wm.open(WindowType.SubWindow)
const tab2 = wm.open(WindowType.SubWindow)
wm.close(tab1) // destroyed
singleton — At Most One Instance, Reuse on OpenOnly one instance can exist at a time. open() shows and focuses the existing window if present; creates one if absent. create() throws if one already exists.
Use for: windows that should never have duplicates (e.g., main window, settings).
WINDOW_TYPE_REGISTRY[WindowType.Main] = {
type: WindowType.Main,
lifecycle: 'singleton',
htmlPath: 'index.html',
windowOptions: { ...DEFAULT_WINDOW_CONFIG, minWidth: 350, minHeight: 400 },
}
// First call creates; second call shows + focuses the existing window
const id1 = wm.open(WindowType.Main) // creates
const id2 = wm.open(WindowType.Main) // shows + focuses, id2 === id1
Optional singletonConfig: enable eager pre-warm and/or close→hide with delayed destroy. See Warmup Mechanics → Singleton Variant.
pooled — Two-Axis Pool with Active Standby + Passive RecycleWindows are reused rather than destroyed. The pool has two orthogonal axes:
standbySize): Pre-warmed spares are always maintained in the idle queue, actively replenished on every open() via setImmediate. Guarantees zero-wait for the next caller regardless of concurrent usage.recycleMinSize / recycleMaxSize): On close(), windows are pushed back to the idle queue (bounded by recycleMaxSize) for reuse. recycleMinSize is a passive decay floor.Both axes are independently enabled via config. open() pops an idle window (sending the window.reused IpcApi event when initData is provided) or creates fresh if empty. close() either recycles or destroys depending on the recycle config.
Use for: frequently opened windows where creation cost is high (selection actions, screenshot overlays).
// Example: SelectionAction — hybrid (standby + recycle).
WINDOW_TYPE_REGISTRY[WindowType.SelectionAction] = {
type: WindowType.SelectionAction,
lifecycle: 'pooled',
htmlPath: 'selectionAction.html',
poolConfig: {
standbySize: 1, // always keep 1 pre-warmed spare
recycleMaxSize: 3, // recycle up to 3 windows for burst handling
decayInterval: 60, // decay one excess idle per minute
inactivityTimeout: 300, // after 5min idle, trim back to standbySize
warmup: 'eager'
},
windowOptions: { ...DEFAULT_WINDOW_CONFIG, width: 400, height: 300 },
}
See Warmup Mechanics for the full pool configuration matrix, GC timer behavior, warmup strategies, and suspend/resume semantics. Note that the inactivity timer resets on both open() and close() (via lastActivityAt), so a long-held-then-closed window does not immediately trigger a trim.
| Feature | Description |
|---|---|
| Lifecycle modes | default, singleton, pooled — covers all window patterns |
Window lifecycle hooks (onWindowCreated / onWindowDestroyed, plus type-filtered onWindowCreatedByType / onWindowDestroyedByType) | Domain services inject behavior at creation and clean up on destruction via typed Emitter<ManagedWindow> events |
broadcast() / broadcastToType() | IPC fan-out to all or type-filtered windows |
open({ initData }) / create({ initData }) / setInitData() / getInitData() | Init payload passed atomically on open/create; automatically pushed to renderer via window.reused on reuse paths |
suspendPool() / resumePool() | Pause pool tracking without destroying in-use windows |
| macOS Dock visibility management | Existence-based: Dock is visible while any window with behavior.macShowInDock !== false is alive (not destroyed). Services express tray-mode intent via wm.behavior.setMacShowInDockByType(type, value) to temporarily opt a type out of Dock contribution. Matches native macOS semantics where Cmd+W does not remove the app from the Dock. |
Bounds persistence (rememberBounds) | Singleton-only opt-in to persist & restore a window's position/size across launches (onto its last display), backed by the main persist cache. Runtime-toggleable via wm.setRememberBounds. See README → Bounds Persistence. |
The createWindow() method follows a strict 5-step execution order:
1. new BrowserWindow(config) ── native window exists
2. setupWindowListeners() ── close/closed/show/hide handlers attached
3. windows.set() / windowsByType ── window is queryable
4. _onWindowCreated.fire() ── domain services inject behavior (sync)
5. loadWindowContent() ── HTML loads, ready-to-show may fire
getWindow(), getWindowInfo(), etc. inside the onWindowCreated callback.ready-to-show, did-finish-load, and other content-dependent listeners with the guarantee that content has not started loading yet.onWindowCreated fires exactly once per window, synchronously.metadata.htmlPath is empty — the domain service is responsible for loading content.onWindowCreated fires only on fresh creation — recycled opens do NOT re-fire, because the BrowserWindow is already created and tracked. Per-instance listeners (e.g. resized, per-window closed cleanup) must therefore be attached inside onWindowCreated, not at the open() call site — otherwise a recycled window would either miss the listener on first reuse or accumulate duplicates across successive opens.