Files
CherryHQ-cherry-studio/docs/references/window-manager/window-manager-platform.md
fullex e666470794 refactor(sub-window): rename DetachedWindowManager to SubWindowService
Unify the "window detached from main" concept under the SubWindow name,
pairing it with MainWindow. Rename the service class, WindowType enum
value, HTML entry, renderer directory, React components, logger contexts,
and window-manager docs accordingly.

Scope of rename (noun-only):
- DetachedWindowManager -> SubWindowService
- DetachedWindowState -> SubWindowState
- WindowType.DetachedTab -> WindowType.SubWindow (value 'subWindow')
- detachedWindow.html / detachedWindow/ -> subWindow.html / subWindow/
- DetachedAppShell -> SubWindowAppShell
- DetachedTabApp -> SubWindowApp
- DETACHED_DEFAULT_WIDTH/HEIGHT -> SUB_WINDOW_DEFAULT_WIDTH/HEIGHT
- Logger contexts / sources and all "detached window" prose

Preserved (interaction-specific, not window-type nouns):
- IPC channels Tab_Detach / Tab_Attach / Tab_MoveWindow / Tab_TryAttach /
  Tab_DragEnd and their 'tab:*' literals - these describe the drag-tab
  interaction, not the SubWindow concept
- Component prop isDetached and useTabDrag.detachedCreated state
- The DevTools "detached" and Node spawn "detached" unrelated usages
2026-04-21 19:32:47 -07:00

10 KiB

Window Platform Configuration

WindowManager splits per-window configuration into three orthogonal layers:

  • windowOptions — Electron BrowserWindow constructor parameters (including platformOverrides for static per-OS differences).
  • behavior — cross-platform declarative WM behavior that Electron's constructor cannot express (blur-auto-hide, setAlwaysOnTop level, setVisibleOnAllWorkspaces options, Dock visibility). See README → Configuration Layers.
  • quirks — OS-specific hacks / workarounds applied via method-slot monkey-patches around hide() / show() / close().

Naming rule: any field effective on only one platform carries a mac / win / linux prefix, irrespective of layer (e.g. behavior.macShowInDock, quirks.macRestoreFocusOnHide).

OS Quirks

Some OS-specific behaviors are tedious to hand-roll at every call site (e.g. the macOS focus dance around hide()). WindowManager ships these as declarative opt-in flags under WindowTypeMetadata.quirks. When set, the manager transparently monkey-patches the corresponding BrowserWindow instance methods so business code continues calling window.hide() / window.show() as usual.

Available Quirks

Quirk Patches Behavior
macRestoreFocusOnHide: boolean hide(), close() Before invoking the native method, iterate every visible focusable BrowserWindow and setFocusable(false); restore them 50ms later. Prevents other windows from being brought to the front when this one disappears.
macClearHoverOnHide: boolean hide() After invoking the native hide(), send webContents.sendInputEvent({ type: 'mouseMove', x: -1, y: -1 }) to clear any residual hover state.
macReapplyAlwaysOnTop: boolean show(), showInactive() After invoking the native method, call setAlwaysOnTop(true, level, relativeLevel) with values read from behavior.alwaysOnTop (single source of truth). When behavior.alwaysOnTop.level is unset, falls back to 'floating'. Compensates for macOS level resets between hide/show.

All quirks are macOS-only: on other platforms the methods are left untouched, and window.hide === originalHide (identity preserved).

Example

[WindowType.SelectionToolbar]: {
  type: WindowType.SelectionToolbar,
  lifecycle: 'singleton',
  showMode: 'manual',
  windowOptions: { /* ... */ },
  behavior: {
    hideOnBlur: true,
    alwaysOnTop: { level: 'screen-saver' },  // level lives here, not in quirks
    visibleOnAllWorkspaces: { enabled: true, visibleOnFullScreen: true },
    macShowInDock: false
  },
  quirks: {
    macRestoreFocusOnHide: true,
    macClearHoverOnHide: true,
    macReapplyAlwaysOnTop: true              // boolean switch; reads level from behavior above
  }
}

With that in place, this.toolbarWindow.hide() from the domain service will:

  1. Snapshot every visible focusable window and call setFocusable(false) on them.
  2. Invoke the native hide().
  3. Send the synthetic mouseMove(-1, -1) to clear hover.
  4. Schedule setFocusable(true) restoration for the snapshot after 50ms.

The domain service carries none of this code.

Implementation Notes

  • w.hide.bind(w) captures the native method with this correctly bound, so Electron's C++ bindings continue to see the real BrowserWindow.
  • EventEmitter behavior (.on('hide', ...), .once('close', ...)) is untouched — the quirks patch only the method slots, not the emitter wiring.
  • Quirks run after onWindowCreated and after applyWindowBehavior fires. This ordering means the behavior layer's initial setter calls (e.g. first setAlwaysOnTop(true, level)) do not trigger the monkey-patched show/showInactive.
  • Quirks are applied per-window at creation time; there is no runtime toggle.

Declarative Behavior Layer

behavior captures configuration that's non-hacky, cross-platform, and needed beyond the Electron constructor. WindowManager applies these on window creation via applyWindowBehavior (in src/main/core/window/behavior.ts).

Field Type What it does
hideOnBlur boolean Installs a blur listener that calls window.hide() (with optional runtime override via wm.behavior.setHideOnBlur(id, enabled)).
alwaysOnTop { level?: AlwaysOnTopLevel, relativeLevel?: number } Supplies the level / relativeLevel to setAlwaysOnTop calls — the single source of truth, read by: (1) the initial application after create (when windowOptions.alwaysOnTop is true), (2) wm.behavior.setAlwaysOnTop(id, enabled) runtime calls, (3) the macReapplyAlwaysOnTop quirk.
visibleOnAllWorkspaces { enabled: boolean } & VisibleOnAllWorkspacesOptions Runs window.setVisibleOnAllWorkspaces(enabled, options) once on create. Windows whose true/false options differ per call should not declare this (e.g. SelectionAction) — drive directly on BrowserWindow instead.
macShowInDock boolean macOS-only default for whether a window of this type CONTRIBUTES to Dock visibility (Dock shown iff any alive window contributes). Existence-based, not visibility-based: hiding a contributing window does NOT hide the Dock (Cmd+W semantics). When omitted, defaults to true. false is for helper windows (floating panels, menu-bar style overlays) that should never affect the Dock. Runtime override via wm.behavior.setMacShowInDockByType(type, value) — set it to false before window.hide() to enter tray mode, true before window.show() to leave. No-op on Windows/Linux.

Runtime Setters

Runtime setters for the behavior layer live on wm.behavior (a BehaviorController instance defined in src/main/core/window/behavior.ts). Grouping them under this sub-namespace mirrors the three-layer windowOptions / behavior / quirks split at the API surface.

Setter Purpose
wm.behavior.setHideOnBlur(id, enabled) Override the declared behavior.hideOnBlur per instance. Cleared on destroy and on pool releaseToPool — pool consumers that need a non-default value must re-apply after open() / reuse. No-op when the window does not declare behavior.hideOnBlur.
wm.behavior.setAlwaysOnTop(id, enabled) Toggle always-on-top using the level / relativeLevel declared in behavior.alwaysOnTop. When neither is declared, calls setAlwaysOnTop(enabled) with no level.
wm.behavior.setMacShowInDockByType(type, value) Override behavior.macShowInDock for an entire window type (not a single instance). Use for app-level tray-mode transitions: (Main, false) then hide() pulls the Dock icon down; (Main, true) then show() brings it back. Keyed by type so it can be set BEFORE the first instance exists (tray-on-launch). Multi-window safe: with Main + SubWindow both contributing, a wm.behavior.setMacShowInDockByType(Main, false) alone does NOT hide the Dock while any SubWindow is alive.

setVisibleOnAllWorkspaces intentionally has no WM-level setter — consumers call it directly on the BrowserWindow when needed. See README → When to Provide a Runtime Setter.

Platform Overrides

Static BrowserWindowConstructorOptions that differ per OS go in windowOptions.platformOverrides. Only the branch matching the current runtime is deep-merged into the final config; unmatched branches are discarded, and the platformOverrides field itself is stripped before reaching new BrowserWindow(...).

windowOptions: {
  width: 350, height: 43,
  frame: false, transparent: true,
  platformOverrides: {
    mac: { type: 'panel', hiddenInMissionControl: true, acceptFirstMouse: true },
    win: { type: 'toolbar', focusable: false },
    linux: { type: 'toolbar' } // focusable is set at runtime by the domain service
  },
  webPreferences: { /* ... */ }
}

Precedence (later wins) when merging inside mergeWindowOptions:

  1. baseOptions (registry windowOptions)
  2. baseOptions.platformOverrides[currentPlatform]
  3. Caller-provided overrides (via wm.open(type, { options }))
  4. Caller-provided overrides.platformOverrides[currentPlatform]

webPreferences is deep-merged in the same order.

When to Use Which Layer

Situation Layer
BrowserWindow constructor can accept it directly windowOptions
Only a subset of OSes need a different static value windowOptions.platformOverrides[mac/win/linux]
Cross-platform, non-hacky declarative behavior (auto-hide on blur, initial setAlwaysOnTop level, dock visibility, initial setVisibleOnAllWorkspaces) behavior
OS-specific bug workaround requiring a hide/show/close hook quirks

The layers are composable: Selection's toolbar uses all three (windowOptions.platformOverrides for static per-OS differences, behavior.hideOnBlur / behavior.alwaysOnTop / behavior.visibleOnAllWorkspaces / behavior.macShowInDock for declarative behavior, and quirks.* for the macOS hide/show hacks).

Electron Edge Cases

  • setAlwaysOnTop(false, level) — Electron ignores level when enabled is false. The WM wm.behavior.setAlwaysOnTop(id, false) preserves the registry-declared level arg only for signature symmetry; the effect is identical.
  • VisibleOnAllWorkspacesOptions — both visibleOnFullScreen and skipTransformProcessType are documented as @platform darwin in Electron. They are silently ignored on Windows / Linux.
  • Linux Wayland "phantom popup" bugsetVisibleOnAllWorkspaces can put windows into a broken "floating popup" state on KDE Wayland. See MainWindowService.ts:573 for context. WM does not intervene; consumers using behavior.visibleOnAllWorkspaces on Linux should guard via runtime display-protocol checks if they see the regression.
  • Parameters<> type derivationAlwaysOnTopLevel is derived from Parameters<BrowserWindow['setAlwaysOnTop']>[1]. If Electron adds method overloads to setAlwaysOnTop, this derivation resolves against the last overload only and may silently narrow. Re-verify after Electron upgrades.