Fix native macOS tabs being treated as separate windows#2027
Closed
EdenRochmanSharabi wants to merge 2 commits intonikitabobko:mainfrom
Closed
Fix native macOS tabs being treated as separate windows#2027EdenRochmanSharabi wants to merge 2 commits intonikitabobko:mainfrom
EdenRochmanSharabi wants to merge 2 commits intonikitabobko:mainfrom
Conversation
added 2 commits
March 26, 2026 18:43
macOS accessibility API reports native tabs as separate AXWindow elements, causing AeroSpace to tile each tab as its own window. This uses CGWindowListCopyWindowInfo (public API) to detect inactive tabs: if a window is not on-screen but another window from the same app is, it's likely a background native tab. Changes: - windowLevelCache.swift: extended CG window info cache with bounds and PID tracking; added isLikelyNativeTab() detection function - MacWindow.swift: check for native tabs before tiling new windows - normalizeLayoutReason.swift: demote tiled windows that become inactive tabs to popup container; prevent tab windows from being promoted back Works with Terminal.app, Finder, Safari, and any app using native macOS tabs. No private APIs used.
- Add appWindowCount safety check: only consider windows as tabs if the app has 2+ windows known to AeroSpace, preventing false positives when CGWindowList is slow to update - Simplify normalization: split into validatePopups() and demoteInactiveTabs() for clearer logic - Refresh CG cache once per pass for consistency - Add refreshNativeTabDetection() and windowCountForApp() helpers
Owner
|
The PR is a workaround and not a complete fix. Also please make sure to read all the discussions around this issue A complete fix is much more complicated and I don't expect 3rd party contributors to fix this issue |
Author
|
I am well aware that it is a workaround and not perfect. It introduced a smaller bug: when you navigate between tabs in the same app while you have three apps open vertically, sometimes the order of the apps swaps. However, I think that bug is less frustrating than the current one. I understand it is a workaround, but I thought it was a good temporary solution. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #68. macOS accessibility API reports native tabs as separate
AXWindowelements, causing AeroSpace to tile each tab as its own window (e.g., Terminal with 3 tabs creates 3 tiled windows, 2 of which are empty).This PR detects inactive native tabs using
CGWindowListCopyWindowInfo(public API, no private APIs): if a window detected by the AX API is not on-screen but another window from the same app is on-screen, it's an inactive background tab and should not be tiled.Changes
windowLevelCache.swift: Extended the CG window info cache to track bounds and owner PID alongside window level. AddedisLikelyNativeTab(windowId:appPid:)function.MacWindow.swift: CheckisLikelyNativeTabbefore binding new windows — detected tabs go to the popup container instead of tiling.normalizeLayoutReason.swift: AddeddemoteNativeTabsToPopup()to move tiled windows that become inactive tabs (e.g., after the user opens a new tab) to the popup container. ModifiedvalidateStillPopups()to skip promoting windows that are still native tabs.How it works
CGWindowListCopyWindowInfo(.optionOnScreenOnly)doesn't include the window but does include another window from the same PID → it's a background tab → popup container.demoteNativeTabsToPopup()moves it to popup. The newly active tab either gets detected as new or gets promoted from popup viavalidateStillPopups().Tested with
Test plan