Back to Cherry Studio

WindowManager Reference

docs/references/window-manager/README.md

2.0.012.7 KB
Original Source

WindowManager Reference

This is the main entry point for Cherry Studio's WindowManager documentation. WindowManager is a lifecycle-managed service that creates, tracks, and reuses Electron BrowserWindow instances with three lifecycle modes (default / singleton / pooled), IPC broadcast, domain-service event hooks, and elastic pool reuse.

Quick Navigation

System Overview (Architecture)

  • Overview — Core types, three lifecycle modes, event timing contract

Usage Guide (Code Examples)

  • Usage Guide — Quick Start, domain-service integration pattern, consumer-vs-internal API layering, anti-patterns, useWindowInitData hook

Reference Guides

  • Warmup Mechanics — Shared warmup state machine (pooled two-axis model + singleton variant), config matrix, GC timer, suspend/resume, WindowManager_Reused IPC
  • Platform Configuration — Static platformOverrides, declarative behavior, and OS quirks (macOS focus / hover / always-on-top)
  • API Reference — Full method tables: open/close/create/destroy, window ops, queries, broadcast, init data, pool management, runtime setters, events
  • Migration Guide — Converting direct BrowserWindow usage to WindowManager

Configuration Layers (windowOptions / behavior / quirks)

Per-type metadata in windowRegistry.ts is split into three layers. Each field belongs to exactly one — choose by what goes wrong if you misconfigure it:

LayerWhat it isMis-config consequenceExamples
windowOptionsArguments to new BrowserWindow(...) — Electron-native constructor optionsElectron rejects the build or behaves wrong on constructionwidth, alwaysOnTop: true, frame: false, platformOverrides
behaviorCross-platform, non-hacky declarative behavior that Electron's constructor cannot expressWindowManager behavior diverges from intent (e.g. no auto-hide on blur)hideOnBlur, alwaysOnTop: { level, relativeLevel }, visibleOnAllWorkspaces, macShowInDock
quirksOS-specific hacks / workarounds applied via monkey-patchesSub-par UX on the specific OS (focus steal, Dock flicker, level demotion)macRestoreFocusOnHide, macClearHoverOnHide, macReapplyAlwaysOnTop

Naming rule (orthogonal to layering): any field that is effective only on one platform carries a mac / win / linux prefix — regardless of layer. behavior.macShowInDock is a behavior field but its mac prefix signals the platform scope; quirks.macRestoreFocusOnHide is a hack with the same prefix.


Bounds Persistence

rememberBounds is a per-type metadata flag — a sibling of showMode / lifecycle, not part of the three configuration layers above. When set, WindowManager persists the window's position/size on teardown and restores them on the next open, onto the display it was last on (clamping into that display's work area if a monitor was removed or resized — never resetting to the primary display). It is backed by the main-process persist cache (window.bounds key); old geometry is loseable, so there is no migration.

Singleton-only. Bounds answer "where does this window reopen?", which has a unique answer only when window identity equals window type — i.e. a single instance. The flag is ignored (with a dev warning) for non-singleton types. Multi-instance types would need a per-instance content identity (e.g. a tab id) plus reopen-restore and stale-id GC; that is a documented future extension.

Maximize stays consumer-side. The maximized flag is persisted, but re-applying maximize() is left to the consumer (read it back via peekWindowBounds). Geometry is injected declaratively at construction (x/y/width/height are BrowserWindow constructor options), but Electron has no maximized constructor option — so maximize is a post-construction imperative call whose correct timing is coupled to each window's show choreography (e.g. Main defers it to first show when launching to tray). The split: WindowManager owns saving + restoring geometry; the consumer owns re-applying maximize.

Runtime toggle. wm.setRememberBounds(type, enabled) overrides the registry flag at runtime and is orthogonal to it — it can disable a flag-on type or enable a type that declares no flag. Switching it off also drops the saved record immediately, so the next open uses the registry default. Fullscreen is neither persisted nor restored.


WM Does Not Know "Pin"

Cherry Studio windows do not share a single "pin" concept — the three pinnable windows each mean something different by it:

WindowWhat "pin" toggles
QuickAssistantSuppress blur-auto-hide (alwaysOnTop stays true)
SelectionActionToggle alwaysOnTop (no blur auto-hide to suppress)
SelectionToolbarNo pin concept (always hide on blur)

Plus SelectionAction has an independent auto_close user preference that drives blur-auto-hide on its own axis — so all four {hideOnBlur, alwaysOnTop} quadrants are reachable.

WindowManager therefore exposes orthogonal primitives, not a pin abstraction. Consumers compose pin semantics in their own service layer:

typescript
// QuickAssistant (pin = suppress blur-hide only)
wm.behavior.setHideOnBlur(id, !isPinned)

// SelectionAction (pin = toggle alwaysOnTop only)
wm.behavior.setAlwaysOnTop(id, isPinned)

// SelectionAction (auto_close + pin composed in renderer)
wm.behavior.setHideOnBlur(id, isAutoClose && !isPinned)

When to Provide a Runtime Setter

Runtime setters for the declarative behavior layer live on wm.behavior (the {@link BehaviorController} instance). WindowManager provides setHideOnBlur, setAlwaysOnTop, and setMacShowInDockByType there but deliberately does not provide setVisibleOnAllWorkspaces. A behavior field deserves a runtime setter only when at least one of:

  1. WM must maintain state — e.g. hideOnBlur needs an override map the blur listener reads; macShowInDock needs a per-type override map the Dock predicate reads.
  2. WM can derive parameters from the registry — e.g. setAlwaysOnTop auto-fills level / relativeLevel.

visibleOnAllWorkspaces satisfies neither (no state; options differ per call, as in SelectionAction's full-screen show sequence) — consumers drive it directly on the BrowserWindow instance.

Note on wm.behavior.setMacShowInDockByType: uniquely keyed by window TYPE (not windowId), because Dock visibility is an app-level UI decision — two instances of the same type should contribute identically, and services routinely need to flip the override BEFORE any instance exists (e.g. tray-on-launch calls wm.behavior.setMacShowInDockByType(Main, false) before the first open(Main)). See Platform → Declarative Behavior Layer for semantics.

Consumer Decision Guide

SituationDo
Only want initial state on createDeclare in registry behavior.*
Single driver, runtime toggleUse wm.behavior.setHideOnBlur / wm.behavior.setAlwaysOnTop (or window.* if no setter exists)
Multiple independent drivers (pin + auto_close)Compute final target state on the consumer side, then call setters once. Do NOT store intermediate state in WM.
Call-specific options that differ per callDrive directly on BrowserWindow (e.g. SelectionAction's show sequence)

Type Derivation Convention

  • When Electron exports a named type (e.g. VisibleOnAllWorkspacesOptions), import it directly.
  • When it exposes only an inline union (e.g. the level argument on setAlwaysOnTop), derive via Parameters<BrowserWindow['setAlwaysOnTop']>[1].
  • Never re-declare Electron argument unions by hand.
  • Caveat: if Electron adds method overloads, Parameters<> resolves against the last overload only — re-verify after Electron upgrades.

Electron Edge Cases to Watch

  • setAlwaysOnTop(false, level): level is ignored by Electron when enabled is false. Safe, but document the intent at the call site.
  • setVisibleOnAllWorkspaces: both options (visibleOnFullScreen, skipTransformProcessType) are @platform darwin. Electron silently ignores them elsewhere.
  • Linux / KDE Wayland has a "phantom popup" bug with setVisibleOnAllWorkspaces — see MainWindowService.ts for context. Consumers must guard this platform themselves; WM does not intervene.

Choosing the Right Lifecycle

ModeInstancesopen() behaviorclose() behaviorUse for
defaultmanyfresh create every calldestroys permanentlyWindows that appear in parallel (e.g. sub windows)
singletonat most onecreates, or shows + focuses the existing onedestroys by default; hides and later destroys when singletonConfig.retentionTime is setUnique windows (main, settings). See Warmup Mechanics → Singleton Variant for singletonConfig options.
pooledmany, reusablepops an idle window, or creates fresh if emptyreturns to the idle pool, or destroys if over capFrequently opened windows where creation cost matters (selection actions)

Full mode semantics and registry examples: Overview → Three Lifecycle Modes.


Consumer vs Internal APIs

WindowManager's lifecycle methods are arranged in two layers. Consumer code should only ever call open() and close() — the registry's lifecycle declaration tells them how to behave for each window type.

LayerMethodsRole
Consumeropen(type, args?), close(windowId)Lifecycle-aware; the only APIs business code should need
Internalcreate(type, args?), destroy(windowId)Defensive / escape-hatch primitives; prefer open() + onWindowCreatedByType instead

Behavioral injection goes through onWindowCreated (or its type-filtered convenience variant onWindowCreatedByType for single-type subscriptions) — see Usage → Injecting behavior.


Common Anti-patterns

Wrong ChoiceWhy It's WrongCorrect Choice
Attaching listeners directly after wm.open() returnsReused windows (singleton reopen, pool recycle) accumulate duplicate listeners; forces you off open() onto create()Subscribe to onWindowCreatedByType(type, listener)
Using wm.create() in business codeSingleton uniqueness is already guaranteed by registry lifecycle; onWindowCreatedByType handles "run setup on fresh"Use wm.open() + onWindowCreatedByType
Using wm.destroy() in business codeOn non-pooled windows, identical to close(). On pooled windows, bypasses pool — rarely desiredUse wm.close(); for pool-wide shutdown, use suspendPool(type)
Attaching resized / per-window closed listeners at the open() call site for a pooled windowPool recycle does not re-fire onWindowCreated, so reused windows miss them or double up on re-openAttach inside onWindowCreatedByType — it fires exactly once per BrowserWindow instance
Setting paintWhenInitiallyHidden: false on a pooled window to "delay show until content is ready"Suppresses native ready-to-show, breaking the fresh-window auto-show pathUse showMode: 'manual' + consumer-driven show(), or rely on the Reused payload to ensure data arrives before .show()

Core Infrastructure

  • src/main/core/window/WindowManager.ts — Service implementation; runtime behavior setters live on wm.behavior (see behavior.ts)
  • src/main/core/window/behavior.ts — Initial applyWindowBehavior + BehaviorController (runtime setters: setHideOnBlur, setAlwaysOnTop, setMacShowInDockByType)
  • src/main/core/window/windowRegistry.ts — Per-type metadata (lifecycle, pool config, windowOptions, behavior, quirks, platform overrides)
  • src/main/core/window/types.tsWindowType, WindowTypeMetadata, WindowBehavior, WindowQuirks, PoolConfig, SingletonConfig, WarmupMode, WarmupState, WarmupStateInit, ManagedWindow
  • src/main/core/window/quirks.ts — macOS method-slot monkey-patches

Renderer Integration

  • src/renderer/windows/README.md — Renderer window entry-point convention (entryPoint.tsx + XxxApp.tsx three-layer structure)
  • src/renderer/hooks/useWindowInitData.ts — Canonical hook for init data consumption
  • src/shared/IpcChannel.tsWindowManager_* IPC channel constants