src/windowserver/ActivationFocusResolverSpecs.md
Pure decisions for MRU focus around an app activation, extracted from WindowServerEvents after two
regressions in a row (#5596). The recorded ground truth (TextEdit and iTerm, via Cmd+Tab, clicks, and
AltTab-initiated focus): on activation macOS emits 808s for the app's on-Space windows — the first is the
genuinely focused window; the rest, when there is a storm at all, are raises front-to-back; sometimes there is
no storm, just the single focus 808 (iTerm). Three rules follow:
isActive guard
(NSRunningApplication.isActive is a separate clock and can still read false at that instant).bumpFocusOnActivation, for activations that emit no 808) is the weak
signal: it races the app's internal focus update and can return the previous window (iTerm with panes).
It yields once the activation's focus 808 has spoken (focusBumped), checked at apply time since the read
is async and can land after the 808.ActivationEntry is the per-pid state: the snapshot wids (only windows the storm can raise — the adapter
excludes minimized and inactive tabs), the until expiry (0.5s; generous because the 808s queue behind
AltTab's own activation work), focusBumped, and raiseTail.
raiseTail is wids as it was built, never drained. The 808 path spends wids (a wid still in it is a
raise), which leaves the set useless to the 815 path in WindowEventReducer: a tail's order-ins arrive
after its focus events emptied it, so an empty set is not "the storm is over". That path therefore reads
raiseTail — "was this wid ever a candidate of this activation" — bounded by until. An AltTab-initiated
activation snapshots nothing, so its tail is empty and the user's own Cmd+` inside the 0.5s is a real focus
(#5875, where the plain time gate swallowed it and the next alt-tab landed back on the window they left).
onFocusEvent(entry, wid, now, wasJustCreated, appIsActive) -> FocusDecision — decide one 808:
bump + the entry state to store back. Expired entry ⇒ pruned (nil) and plain rules apply. Brand-new
window ⇒ always bump. First 808 of a live activation ⇒ bump, mark focusBumped, consume the wid.
In-snapshot 808 after that ⇒ raise, swallow, consume. Otherwise ⇒ bump iff appIsActive.onActivation(snapshotWids, until, altTabTarget) -> (entry, bumpWid) — build the activation entry.
A known AltTab-initiated target (switcher selection / CLI focus) is bumped directly with focusBumped set
(AX backstop yields) — with no 808 and a stale AX read, the freshly-focused window's bump was otherwise
lost — and with an EMPTY snapshot, because AltTab raises exactly one window rather than fronting the app's
stack, so that activation has no raise tail to swallow (#5785's log: three alt-tabs into a 3-window Chrome,
one 808 each, always the target's). No target ⇒ plain entry; the first 808 or the AX backstop decides.altTabIntentToRecord(wid, pid, frontmostPid, at) -> AltTabFocusIntent? — is AltTab's own focus worth
recording, i.e. is an activation actually COMING? Not when the app is already frontmost: nothing would
consume the record, and a leftover intent hands the next activation of that app a stale target AND "no raise
tail to mute", so the real burst re-fronts each window (#5596). The in-app case needs nothing from it — its
focus arrives as an ordinary 808, or an order-in for Cmd+`.altTabIntentApplies(intent, activatedPid, now) -> Bool — does a recorded intent belong to THIS
activation? Same app, and within altTabIntentFailsafe (1s), which only exists because an activation that
never comes must not leave the record live forever.axBackstopShouldApply(entry) -> Bool — false only when a live entry has focusBumped (the real
808 already spoke); nil/expired/pre-focus ⇒ true.Mirrors ActivationFocusResolverTests.swift 1:1.
isActive still
false (the iTerm #5596 case) → bump; focusBumped set; wid consumed.until → pruned to nil; plain isActive rule.testAltTabInitiatedActivationBumpsKnownTarget — known target → bumped directly; entry starts
focusBumped (the AX backstop yields) with an empty snapshot.
testAltTabInitiatedActivationMutesNothing — that empty snapshot means a later 808 for ANOTHER window of the same app bumps: the second alt-tab of #5785's stuck switcher.
testRestoringAMinimizedTargetMutesTheDeminiaturizeTail — a MINIMIZED target is the exception: AltTab deminiaturizes before focusing, restoring a window stirs the app's others, so this activation does have a tail and the snapshot is kept. Live I-11: the sibling's 808 landed 38ms in and took the front off the window just restored (#5439).
testRestoringAMinimizedTargetStillBumpsTheTargetsOwnFocus — the target is subtracted from that tail, so its own 808 is never read as a raise. A GUARD, not a pin: it passes with the fix removed too (an empty tail contains nothing either), and exists to catch a future change that widens the tail to include the target. The two tests around it are teeth-verified — both fail when the subtraction is reverted.
testRestoringAMinimizedTargetMutesEachSiblingOnlyOnce — the entry consumes per wid, so a genuine later switch to that sibling still bumps; the muting is one event deep, not a 0.5s blackout.
testExternalActivationWaitsForFocusSignal — no target (Cmd+Tab, click) → plain entry; the first 808 (or the AX backstop when none arrives) decides.
wids; raiseTail still names
both, which is the only thing left for the order-ins that arrive after them (#5596).focusBumped → yield (the stale-AX race, #5596).