src/switcher/state/SelectionResolverSpecs.md
Line coverage:
SelectionResolver.swift93% · refreshed 2026-05-27 by/coverage-explore
SelectionResolver decides which tile is highlighted while the switcher is open. Every time the
window list changes (a window opens/closes, an app steals focus, a search query filters the list), the
switcher calls SelectionResolver.decide(_:) with a snapshot of the current state and gets back a
SelectionDecision enum; the wrapper (Windows) turns that into highlight redraws, scroll-to-visible,
target bookkeeping, and the preview. Pure data in, Equatable decision out — no globals, no AppKit.
Once the user moves the highlight, the selected window's id is remembered as the target. On every refresh the resolver tries to keep the highlight on that same window even as the list reorders — this is the #5665 fix (before it, a background app finishing launch could yank the highlight away mid-pick).
restoreDefaultOnSearchClear) takes precedence — re-runs the initial pick even with no visible windows.clearTargetAndHover.bestMatchOnSearchChange) → jump to the first visible (best-scored) window.selectedTarget == nil, first refresh) → "from scratch" initial pick.selectAt).selectedTarget means two different things, split by userPickedSelection: while the user hasn't moved the
selection it is merely where the DEFAULT landed, so step 4 re-derives it on every refresh; once the user
cycles or hovers it is a commitment and step 5 follows THAT window by id however the list reorders (#5665).
Conflating them was a bug: the switcher opens while the window set is still settling (tabs grouping, Spaces
settling), so the default locked onto whatever occupied the slot mid-churn and then trailed that window across
the list as things resolved — the highlight ending up on an unrelated tile.
Initial-pick rules: with the last-focused rule, pick the visible non-windowless window with the lowest
lastFocusOrder; the both-top-minimized edge lands on index 0; otherwise secondVisibleIndex — the SECOND
VISIBLE window (the one you were on before the current one), wrapping to the only visible window when there
is just one. It counts VISIBLE windows, not raw indices: hidden windows sit in the MRU too (a background tab
is fronted when discovered, then hidden once grouped), so index 0 can be hidden and index 1 be the CURRENT
window — counting indices then selected the current window itself. Windowless app entries and invisible
windows are skipped when scanning. findTarget only matches a target id that is currently visible.
secondVisibleIndex counts the MRU as of the summon: windows flagged appearedAfterSummon are stepped
over. The drawn list keeps showing the truth — a window created and focused behind the switcher takes tile 0
and pushes everything along — but "the window you were on before" is a question about the moment the shortcut
was pressed, and it does not change because something else appeared afterwards. The flag means ABSENT FROM THE LIST AT THE PRESS, not "focused since": a tab group re-electing a
different member of itself is a focus change with no newcomer, and a late read telling us who was already
frontmost only re-orders windows that were there all along, so the pick re-derives over both. Nothing is pinned: the answer is recomputed
every refresh, so a window that closes or stops being drawn drops out of it, and when stepping over leaves
nothing to land on the plain rule takes over.
Only an ARRIVAL is stepped over, never a REPLACEMENT. A newcomer can take the tile of a window that left
the drawn list in the same breath, and then nothing moved down for the pick to compensate for. Live case: two
Finder windows with tabs, switch a tab, summon — the incoming tab is a window the model had never tracked
(untracked inactive tabs are the norm), so it is a newcomer at tile 0, while the tab it replaced stops being
drawn. Tile 1 is still the other Finder window, and stepping over aimed one tile past it at an unrelated app.
The two are told apart by the length of the list: newcomers are stepped over only while there are more visible
windows than at the summon (visibleCountAtSummon), which is exactly how many of them arrived rather than
replaced. The count is measured on the summon's first selection pass — the same main-thread turn as the press,
so no event can land in between.
Mirrors SelectionResolverTests.swift 1:1. Groups: A initial pick · B preserve target (#5665) ·
C target removed · D search mode · E edge cases · plus direct helper-kernel checks.
selectedTarget == nil)clearTargetAndHover.resetThenSelect(0).lastFocusOrder.clearTargetAndHover.selectAt unchanged.selectedIndex → closest visible below.clearTargetAndHover.secondVisibleIndex: empty / single-visible (wraps to it) / multi; and a
HIDDEN window at index 0 must not shift the pick onto the CURRENT window (a background tab is fronted in the
MRU when discovered, then hidden once grouped, so index 0 can be hidden and index 1 IS the current window —
counting raw indices selected the current window itself).