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.
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,
useWindowInitDatahook
Reference Guides
- Warmup Mechanics — Shared warmup state machine (pooled two-axis model + singleton variant), config matrix, GC timer, suspend/resume,
WindowManager_ReusedIPC - Platform Configuration — Static
platformOverrides, declarativebehavior, and OSquirks(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
BrowserWindowusage 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:
| Layer | What it is | Mis-config consequence | Examples |
|---|---|---|---|
windowOptions |
Arguments to new BrowserWindow(...) — Electron-native constructor options |
Electron rejects the build or behaves wrong on construction | width, alwaysOnTop: true, frame: false, platformOverrides |
behavior |
Cross-platform, non-hacky declarative behavior that Electron's constructor cannot express | WindowManager behavior diverges from intent (e.g. no auto-hide on blur) | hideOnBlur, alwaysOnTop: { level, relativeLevel }, visibleOnAllWorkspaces, macShowInDock |
quirks |
OS-specific hacks / workarounds applied via monkey-patches | Sub-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:
| Window | What "pin" toggles |
|---|---|
| QuickAssistant | Suppress blur-auto-hide (alwaysOnTop stays true) |
| SelectionAction | Toggle alwaysOnTop (no blur auto-hide to suppress) |
| SelectionToolbar | No 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:
// 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:
- WM must maintain state — e.g.
hideOnBlurneeds an override map the blur listener reads;macShowInDockneeds a per-type override map the Dock predicate reads. - WM can derive parameters from the registry — e.g.
setAlwaysOnTopauto-fillslevel/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
| Situation | Do |
|---|---|
| Only want initial state on create | Declare in registry behavior.* |
| Single driver, runtime toggle | Use 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 call | Drive 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
levelargument onsetAlwaysOnTop), derive viaParameters<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):levelis ignored by Electron whenenabledis 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— seeMainWindowService.tsfor context. Consumers must guard this platform themselves; WM does not intervene.
Choosing the Right Lifecycle
| Mode | Instances | open() behavior |
close() behavior |
Use for |
|---|---|---|---|---|
default |
many | fresh create every call | destroys permanently | Windows that appear in parallel (e.g. sub windows) |
singleton |
at most one | creates, or shows + focuses the existing one | destroys by default; hides and later destroys when singletonConfig.retentionTime is set |
Unique windows (main, settings). See Warmup Mechanics → Singleton Variant for singletonConfig options. |
pooled |
many, reusable | pops an idle window, or creates fresh if empty | returns to the idle pool, or destroys if over cap | Frequently 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.
| Layer | Methods | Role |
|---|---|---|
| Consumer | open(type, args?), close(windowId) |
Lifecycle-aware; the only APIs business code should need |
| Internal | create(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 Choice | Why It's Wrong | Correct Choice |
|---|---|---|
Attaching listeners directly after wm.open() returns |
Reused windows (singleton reopen, pool recycle) accumulate duplicate listeners; forces you off open() onto create() |
Subscribe to onWindowCreatedByType(type, listener) |
Using wm.create() in business code |
Singleton uniqueness is already guaranteed by registry lifecycle; onWindowCreatedByType handles "run setup on fresh" |
Use wm.open() + onWindowCreatedByType |
Using wm.destroy() in business code |
On non-pooled windows, identical to close(). On pooled windows, bypasses pool — rarely desired |
Use wm.close(); for pool-wide shutdown, use suspendPool(type) |
Attaching resized / per-window closed listeners at the open() call site for a pooled window |
Pool recycle does not re-fire onWindowCreated, so reused windows miss them or double up on re-open |
Attach 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 path |
Use showMode: 'manual' + consumer-driven show(), or rely on the Reused payload to ensure data arrives before .show() |
Related Source Code
Core Infrastructure
src/main/core/window/WindowManager.ts— Service implementation; runtime behavior setters live onwm.behavior(seebehavior.ts)src/main/core/window/behavior.ts— InitialapplyWindowBehavior+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.ts—WindowType,WindowTypeMetadata,WindowBehavior,WindowQuirks,PoolConfig,SingletonConfig,WarmupMode,WarmupState,WarmupStateInit,ManagedWindowsrc/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.tsxthree-layer structure)src/renderer/hooks/useWindowInitData.ts— Canonical hook for init data consumptionsrc/shared/IpcChannel.ts—WindowManager_*IPC channel constants