Skip to content

Add a hover badge and switcher for stacked windows#1795

Draft
MyronKoch wants to merge 9 commits into
rxhanson:mainfrom
MyronKoch:feat/stack-badge
Draft

Add a hover badge and switcher for stacked windows#1795
MyronKoch wants to merge 9 commits into
rxhanson:mainfrom
MyronKoch:feat/stack-badge

Conversation

@MyronKoch

@MyronKoch MyronKoch commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Follow-on to #1762 (grid-position overlap offset). On that PR you mentioned looking forward to the hover count badge idea as a follow-on — here it is.

Dark mode

Screenshot 2026-07-21 at 3 35 56 PM (3)

Light mode

Screenshot 2026-07-21 at 3 37 48 PM (3)

What it does

When the overlap offset stacks multiple windows at the same grid position, resting the cursor on the stack's top-left corner shows a small count badge and a list of the stacked windows — each with its app icon and title. Click a name to bring that window forward.

It turns overlap from something you can't see into a lightweight switcher for that grid slot. On a laptop or small screen where you can't spread windows out, a stack of tiles plus this becomes a de-facto tab bar for the spot.

Opt-in and unobtrusive

  • Off by default, behind a new stackBadge hidden default, with a checkbox ("Show stacked window badge on hover") in the General-tab additional-settings popover next to the overlap-offset toggle, and an entry in TerminalCommands.md.
  • Hover-only — nothing on screen at rest.
  • The badge is click-through and sits below the title-bar band, so the window's traffic lights stay clickable.

How it works (stateless)

No window state is cached, so nothing can go stale across window moves, closes, Spaces, Stage Manager, or sleep/wake. A lightweight timer reads the cursor position; only a brief dwell on a grid corner (computed from screen geometry alone) triggers a single window-server query via CGWindowListCopyWindowInfo, which can't block on an unresponsive app. Accessibility is touched only to read the matched windows' titles, on a background queue with a messaging timeout.

Testing

  • Unit tests for the grid-corner geometry, including negative-origin (secondary-display) screens.
  • Multi-model adversarial code review across correctness, concurrency, and view lifecycle.
  • Manually verified in Light and Dark mode and across multiple displays; soak-tested over ~2 weeks of daily use on 2 separate Macs.

Screenshots

Light mode

(drag stack-badge-light.png here)

Dark mode

(drag stack-badge-dark.png here)

MyronKoch and others added 9 commits July 16, 2026 16:40
When multiple windows are stacked at the same grid position, resting
the cursor on that position's top-left corner shows a small count
badge and a list of the stacked window names. Clicking a name brings
that window forward. Off by default (stackBadge), with a checkbox in
the additional settings popover.

Detection is stateless: a 200ms timer reads the mouse position, and
only a 250ms dwell on a computed grid corner triggers one fresh
window-server query (CGWindowList, which cannot block on unresponsive
apps). AX is touched only to read the titles of matched windows, off
the main thread with a messaging timeout, and discarded if the cursor
has moved on. No window state is cached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Window gaps place a cell's window up to a full gap away from the
geometric grid corner, and the cascade offset - applied in AppKit
coordinates - converts to an upward move in AX space, so the original
corner-anchored, downward-only match never saw a complete stack.

Candidates are now gathered from a gap-widened box, clustered around
the leftmost origin with a y-symmetric cascade range (extracted into
StackBadgeGeometry for testability), and the badge anchors on the
stack's visual top-left extremity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Bump the request generation on every dismiss so an in-flight title
  fetch can't resurrect the badge after the feature is disabled or the
  screens change
- Resolve click-to-focus by pid on the background queue with AX
  messaging timeouts, instead of going through the unsynchronized
  shared window-list cache
- Recompute corner points per dwell instead of caching them:
  adjustedVisibleFrame depends on inputs (Todo mode, Stage Manager,
  edge-gap defaults) that don't post screen-change notifications
- Pick the densest cascade cluster instead of anchoring on the
  leftmost candidate, so an unrelated left neighbor can't mask a stack
- Fetch AX titles once per application rather than once per window
- Restart or stop the badge timer when a config import changes the
  stackBadge default
- Clamp the name list to the screen so every row stays clickable
- Align the settings checkbox with its neighbors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Enlarge the hover catch zone and shorten the dwell for a snappier,
  easier-to-hit trigger
- Show each stacked window's app icon beside its name, and drop the now
  redundant app-name prefix from the title
- Highlight rows with the system selection color and white text, like a
  native menu
- Enlarge the count badge and size it to its contents
- Drop the badge and list below the title-bar band so the front
  window's traffic lights stay clickable
- Add a keep-alive corridor from the trigger down to the list so moving
  onto it doesn't dismiss it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-model review (GPT adversarial + Opus architecture) findings:

- Route row clicks reliably: override hitTest so clicks on the icon and
  title subviews reach the row instead of being swallowed, and override
  acceptsFirstMouse so the first click on the background non-activating
  panel focuses the window instead of being eaten as an activation click.
- Bound the keep-alive corridor to the gap between the peek and the list's
  top edge, so a list clamped to the screen bottom no longer turns the
  corridor into a tall sticky column that keeps the overlay alive far from
  the actual UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the badge read as a native element per macOS HIG:
- The count badge is now a vibrancy capsule using the same .hudWindow
  material as the list, with an SF Symbol stack glyph (guarded for
  macOS 11+ like the menu icons) instead of a Unicode character.
- Continuous (squircle) corner curves on the badge, list, and row
  highlights; SF Pro throughout.
- Count and symbol use semantic labelColor so they stay legible in both
  Light and Dark mode (the .hudWindow material follows the appearance).
- Open the list below the badge with a real gap so they never overlap,
  and increase the title-bar clearance so both clear the traffic lights.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A tall stack pinned near the screen bottom clamped the list's origin
upward, which could push its top back over the badge. Pin the list top
below the badge and cap its height to the space above the screen bottom
so it only ever grows downward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bigger pill (28pt), count (15pt), and stack symbol (14pt) so the badge
fills the space above the list and reads as a deliberate anchor rather
than floating low, and clears the extra title-bar clearance cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking a row focuses that window but no longer dismisses the list, so
you can click through the stacked windows one after another without
re-opening it. The list still dismisses when the cursor leaves the
overlay; clicks keep working while another app is frontmost thanks to
the rows' first-mouse handling and the non-activating panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rxhanson

Copy link
Copy Markdown
Owner

This looks awesome! Looking forward to testing this out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants