Skip to content

ewiner/backstage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Backstage

A macOS menu-bar utility that augments Apple's Stage Manager.

Stage Manager parks your off-stage app groups as small thumbnails in a strip on the screen edge — cramped and hard to tell apart. Backstage adds previews, click-to-raise, and persistent labels to that strip.

Features

  • Fan-out previews — hover a multi-window group to fan out a bright, live thumbnail per window, each with a crisp app-icon badge, app name, and full window title. (Single-window groups don't fan — their strip thumbnail already says it all.)
  • Click-to-raise — click a thumbnail to bring that window (and its stage) to the front.
  • Group labels — every group gets an automatic name from its apps ("Claude", "Claude + Slack", "Claude + 2 others"), pinned to its upper-left in the strip. Hover a group and click the to replace it with your own name + optional emoji (the emoji overlays the thumbnail) — it autosaves, and erasing a custom name falls back to the automatic one. Custom labels follow a group as its window membership drifts (fuzzy-matched on the app set).
  • Drag-to-merge — drag a thumbnail out of the hover fan and drop it on the stage: that window leaves its group and joins what you're working on, right where you dropped it. Works for any window in the fan; non-front windows take a brief automated raise → switch-back → drag dance (~3s) since Stage Manager will only hand over a group's front window.
  • Merge new windows into the last group (menu toggle) — when a freshly opened window boots your working group into the strip, Backstage brings the group back and pulls the new window into it, where it first appeared. Waits for your mouse to rest before borrowing the cursor.
  • Drop a window into a strip group — drag an on-stage window onto a group's strip thumbnail to file it there. A precise drop is merged by macOS itself; Backstage detects near-misses (stashed as a lone group instead) and finishes the merge, returning you to the stage you were on.
  • Merge / new-group buttons — for when the native drag-a-window-onto-the-strip gesture refuses to cooperate (a well-known Stage Manager bug: the strip auto-hides instead of becoming a drop target). Hover a group and click the on its right edge to pull the current on-stage window into that group and switch to it. Click the persistent below a display's strip to file the current window into a brand-new group. Both avoid the flaky native drag — ⏎ and the lone-window ⊕ use the same click-to-stash + strip→stage choreography as the merge features above. (Peeling one window off a multi-window stage into a new group still needs the native window→strip drag under the hood, so that case is best-effort: if Stage Manager eats the drop, the window's frame is restored and nothing moves.)
  • Menu bar — enable/disable detection, Show window previews and Merge new windows toggles, live stage count, permission status, and Quit.

How it works

Stage Manager exposes no public API, so detection is geometric: when a window goes off-stage, the WindowManager process repositions the real app window into a small rect near a screen edge and pairs it with a WindowManager-owned container of identical bounds:

158    WindowManager   (16,744,129,108)
39782  Claude          (16,744,129,108)   ← the real app window: windowID + pid

CGWindowListCopyWindowInfo (no permission needed) yields each off-stage window's windowID (for ScreenCaptureKit thumbnails) and pid (for Accessibility control). Backstage finds those edge-hugging pairs and clusters overlapping thumbnails into stages.

File Role
main.swift / AppDelegate.swift .accessory agent; periodic scan, wires hover → previews / labels / raise / merges
WindowEnumerator.swift CGWindowList scan → strip slots clustered into [Stage], plus on-stage windows
Stage.swift / StripSlot.swift models — per-window slot (with z-order); group with app-set signature + liveKey
Geometry.swift CG (top-left) ⇄ AppKit (bottom-left) coordinate conversion
ThumbnailCapturer.swift ScreenCaptureKit capture for a stage's windows, refreshed while hovered
HoverMonitor.swift cursor poll → hovered group (for the ✎) + multi-window fan trigger
PreviewPanel.swift the floating, non-activating fan; click a node to raise, drag one out to merge
DragSynthesizer.swift HID-level synthetic clicks/drags on the strip (Stage Manager honors them)
StageChoreographer.swift multi-step merge sequences built from raise/click/drag, verified at each step
NewWindowMerger.swift detects a brand-new window landing alone on stage (merge-into-last-group cue)
WindowDropMonitor.swift detects an on-stage window dropped onto a strip group (drop-into-group cue)
StripBadges.swift persistent name pills + emoji overlays + hover ✎/⏎ buttons + per-display ⊕ button
LabelEditorController.swift inline label-editor popover (emoji picker + name, autosave)
GroupLabelStore.swift label persistence; fuzzy + unambiguous-only resolution
AXControl.swift windowIDAXUIElement, AXRaise + activate
MenuBarController.swift / Permissions.swift status-item menu; permission preflight/request

Build & run

Requires macOS 14+ (built on macOS 26, Apple Silicon), the Xcode toolchain, and Stage Manager enabled.

bash scripts/bundle.sh     # swift build -c release → Backstage.app
open Backstage.app         # menu-bar agent (rectangle-stack icon); no Dock icon

Permissions

  • Screen Recording — for live thumbnails (without it, previews fall back to app icons).
  • Accessibility — for click-to-raise and all merge features (they post synthetic mouse events).
  • Strip detection and labeling need neither.
  • Ad-hoc signing changes the app's identity each build, resetting both grants. To keep them sticky, sign with a stable identity: CODESIGN_ID="Apple Development: you@…" bash scripts/bundle.sh.

Notes

  • Off-stage windows render at strip resolution, so an enlarged preview is a touch soft.
  • Detection is heuristic (Stage Manager internals are private); tuned for the standard edge strip across multiple displays.
  • Two groups with identical app composition can't be told apart across relaunches, so a colliding label won't auto-restore (rather than mislabel).
  • A strip drag only ever extracts a group's front window — the grab point within the cluster is ignored. Every merge choreography is built around that constraint.
  • A crowded strip hides overflow groups but still reports their windows at small "phantom" rects that swallow clicks and drags. Choreographed drags verify the window actually moved (one retry, then an honest abort); a stage's thumbnail beyond the first ~3 also isn't rendered, so very large groups show partial fans.

Roadmap

  • Possibly: themes, adjustable preview size / hover delay, launch-at-login.

Layout

Package.swift
Sources/Backstage/*.swift     # the app
scripts/bundle.sh             # build → Backstage.app  (CODESIGN_ID=… for sticky permissions)
probe/*.swift                 # standalone diagnostic scripts used during development

About

Improvements to MacOS Stage Manager

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors