Files
CherryHQ-cherry-studio/docs/references/window-manager/window-manager-api-reference.md
fullex 32e8ef273c feat(window-manager): add declarative rememberBounds persistence
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.
2026-06-26 22:39:52 -07:00

152 lines
15 KiB
Markdown

# WindowManager API Reference
Full method reference for `WindowManager`. For conceptual guidance and when-to-use each group, see [Usage Guide](./window-manager-usage.md).
## 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](./window-manager-usage.md#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](./window-manager-platform.md#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 `setVisibleOnAllWorkspaces` is 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 call `window.setVisibleOnAllWorkspaces(enabled, options)` directly on the `BrowserWindow` instance. See [README → When to Provide a Runtime Setter](./README.md#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](./README.md#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): `createWindow` writes `initData` to the store synchronously before returning, so any `getInitData` invoke from the renderer (after React mounts) sees the fresh value. The renderer should use the [`useWindowInitData` hook](./window-manager-usage.md#renderer-usewindowinitdata-hook) — it handles the invoke on mount automatically.
- **Reuse** (pool recycle / singleton reopen): `open()` simultaneously writes to the store AND fires `WindowManager_Reused` with the same payload. The `useWindowInitData` hook 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` / `pushInitDataToType` from any main-process service. Both paths reuse the `WindowManager_Reused` channel, so `useWindowInitData` picks up the new payload in-place with no remount — useful for "swap the visible window's context without `close()`+`open()` flicker". Unlike reuse, these methods forbid `undefined` payloads: 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](./window-manager-warmup-mechanics.md#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](./window-manager-warmup-mechanics.md#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 via `getWindowIdByWebContents(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. `default` and `pooled` lifecycles are **not supported** for cross-window targeting via IPC; the call silently returns `false` and 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](./window-manager-usage.md#renderer-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](./window-manager-platform.md#platform-quirks), and per-session data on recycle is delivered via the `WindowManager_Reused` IPC payload (see [Init Data](#init-data)).
**Usage notes for pooled windows:**
- **Do NOT set `paintWhenInitiallyHidden: false`** on pooled windows — it suppresses the native `ready-to-show` event, breaking the pool's fresh-window auto-show path (`showMode === 'auto'` listens for `ready-to-show`). It is NOT an acceptable workaround for "show only when content ready" — use `showMode: 'manual'` + consumer-driven show for that, or rely on the reuse-path `Reused` payload to ensure the renderer has data before `.show()` is called.
- **macOS focus / hover / always-on-top workarounds** are declarative — see [Platform Quirks](./window-manager-platform.md#platform-quirks).