discover_and_observe was sending every discovered window through
add_window_to_active without specifying a target workspace, so each
window landed in active_workspace_mut() — the workspace on the
monitor under the cursor at startup. Windows on other monitors got
inserted into the cursor monitor's BSP tree, then apply_layout asked
AX to move them across displays; some apps (Ghostty especially)
either reject or race that cross-display move and stay physically on
their original monitor while remaining logically in the wrong
workspace's tree. Visible to the user as 'two windows reachable via
stack-cycle but never joining the tile.'
Fix is local to the discovery loop: compute each window's monitor
from its center point and temporarily set focused_monitor to that
monitor for the duration of the add_window_to_active call. Restore
focused_monitor after the loop. add_window_to_active itself is
unchanged, so the rule path and runtime Created path keep working
exactly as before.
Refs memory observation #2542.
The reveal-offset path had a long-standing dead-code bug: the depth
counter never decremented past the active member because the active
case continued before the saturating_sub line could run, so windows
iterated after the active accumulated progressively larger origin
offsets and visibly drifted out of the tile.
The peek itself was a stylistic flourish; the user's report 'windows
in the stack are not sized and positioned as members of the stack'
matches removing it entirely. All stack members now occupy the full
padded tile; non-active windows sit fully behind the active in
z-order. Replaces the two reveal-direction tests with a single test
asserting all members share the tile rect across active positions.
Two paths can fail to detect a window close:
1. AXUIElementDestroyed fires with an element whose _AXUIElementGetWindow
call returns kAXErrorInvalidUIElement because the underlying window
has already been freed. The handler dropped the event silently and
the wid stayed in the BSP tree forever.
2. The 50ms workspace polling can race with workspace switches and
miss the brief window where a wid is gone from the on-screen list
but our tree still has it.
Fix (1) by falling back to a CFEqual lookup in ax_refs — we hold a
CFRetained on every tracked element, so equality survives destruction.
Fix (2) with a 1Hz reconcile against CGWindowList(all layers): any wid
in the registry that's missing from the WindowServer and not staged
off-screen gets reaped through on_window_closed. This is the
authoritative safety net so the tree never holds a dead wid, which
was the root cause of the 'closed window leaves a tile the surviving
sibling won't fill, and focus moves into empty space' bug.
Lots of app-defined modals (save dialogs, confirmation prompts,
inline preference panels) ship with subrole=AXStandardWindow and
relied on the human reading 'looks modal' to know not to tile them.
Tarmac was tiling them alongside the parent window. Detect AXModal
explicitly, and add AXSystemDialog to the subrole allowlist for the
ones that do declare it correctly.
is_manageable_window dropped windows with these subroles before they
could reach the auto-float path, so app-defined modals were either
silently ignored (sheets) or had nowhere to go. Accept them here and
let should_auto_float decide their fate.