Insert replaced native-tab windows into their old tree slot - #59
Merged
Conversation
Apps that fold native tabs into one titlebar (Finder, Ghostty, Fork) keep the old tab's AX object alive after you switch tabs, but drop it from AXWindows. AeroSpace kept tracking the stale window and treated the newly focused tab as a brand new window, so the layout gained a phantom tile and the visible tab landed wherever on-window-detected/MRU placement put it. Detect that the previously focused window has disappeared from the app's live AXWindows list (in getFocusedWindow, and again in the periodic window refresh so it can't flicker), retire it, and splice the replacement into the exact tree slot the old one occupied - parent, index, weight, floating size, fullscreen state and cached layout geometry. Ports nikitabobko#2225 Fork adaptations: the fork's `getFocusedWindow()` / `runInLoop` take no CancellationMode and guard on `threadGuardedOrNil` (AX-destroy-race hardening from fork PR #43), so the new logic is nested inside those guards and uses the unwrapped `axApp`; `debugWindowsIfRecording` and `getAxRect` take no CancellationMode here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBcwRe3CktWZGGcHJHP4ds
vitorebatista
added a commit
that referenced
this pull request
Aug 27, 2026
…te sync state Sync state moves to upstream main @ c548c7f / 59 open PRs, reviewed 2026-08-27. Adds the newly ported upstream PRs (2244, 2232, 2211, 2228, 2225) to the already-backported list, and records this cycle's skips: focus-follows-mouse PRs (nikitabobko#2238/nikitabobko#2245, feature absent in the fork), nikitabobko#2220 (duplicate of the already-ported nikitabobko#2024), nikitabobko#2213 (deletes lastNativeFocusedWindowId, which fork PR #59 depends on), nikitabobko#2206 (competing implementation of nikitabobko#2225), and six deferred feature PRs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBcwRe3CktWZGGcHJHP4ds
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.
Apps that fold native tabs into a single titlebar (Finder, Ghostty, Fork) keep the old tab's AX object alive after you switch tabs, but drop it from
AXWindows. AeroSpace went on tracking the stale window and treated the newly focused tab as a brand-new window — so the layout gained a phantom tile and the visible tab was placed byon-window-detected/ MRU rather than staying where the tab group was.This detects that the previously focused window has vanished from the app's live
AXWindowslist — ingetFocusedWindow, and again in the periodic window refresh so the stale tab can't be reported alive and flicker the layout — retires it, and splices the replacement into the exact tree slot the old one occupied: parent, index, weight, floating size, fullscreen state and cached layout geometry.Ports nikitabobko#2225 (author: @jeajar).
Both checks are gated on
!isLeftMouseButtonDown, matching the existing tab-drag-out safeguard, and scoped to the just-focused window only (which is guaranteed to have been on the active Space — a blanketAXWindows/tracked-set intersection would not be).Fork adaptations (this fork's
MacAppdiverges from upstream):getFocusedWindow()andrunInLooptake noCancellationModehere, and both closures guard onthreadGuardedOrNil(the AX-destroy-race hardening from fork PR MacApp: fix AX-object destroy race + wipPids spin in getOrRegister #43) — the new logic is nested inside those guards and reads through the unwrapped localaxAppinstead ofaxApp.threadGuarded.debugWindowsIfRecordingandMacApp.getAxRecttake noCancellationMode.MacWindow.getOrRegisteralso carries the fork'sisAwaitingOnWindowDetectedflag andpreventNewWindowFlickerIfNeeded; both are preserved. The replacement path deliberately bypasseson-window-detected— a replaced tab is not a newly opened window.Note: upstream has a competing PR for the same bug (nikitabobko#2206, by @trentkm), which takes an app-bundle-id-list approach. Neither is merged upstream yet; nikitabobko#2225 was chosen as the more general fix.
Tests: the upstream
NativeTabWindowReplacementTest(4 cases) applies unchanged and passes — it coverstakeNativeTabReplacementBinding: slot/weight preservation in a nested container, nil stale window, already-unbound stale window, and that exactly the given window is unbound. The AX-side detection is runtime Accessibility-API behavior and is not unit-testable here; no fake test was added for it../build-debug.sh -Xswiftc -warnings-as-errors✅ ·./swift-test.sh✅Docs: none needed (no command/flag/config change).