From b41287af5a8a3270885c8ef1fb065efe5ea2a322 Mon Sep 17 00:00:00 2001 From: William Zeng <61915438+willzeng274@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:17:25 -0400 Subject: [PATCH] Fix on-window-detected floating rule causing brief tiling flash When exec-and-forget launches an app, multiple notifications fire rapidly (kAXWindowCreatedNotification, didActivateApplicationNotification, etc.), each calling scheduleRefreshSession(). Due to Swift cooperative cancellation, a session already running past checkCancellation() continues executing even after being "cancelled" by a newer session. Two sessions end up interleaved on @MainActor at every await suspension point. Race 1 (tiling flash): Session A places the new window in the tiling container (synchronous), then suspends inside tryOnWindowDetected() while evaluating rules via async AX calls. Session B gets CPU, runs layoutWorkspaces(), sees the window in the tiling container, and calls setAxFrame() -- producing a visible flash at the tiled position. Race 2 (sibling shift): layoutTiles() distributes tile space among all children by weight. While the new window sits in the tiling container awaiting rule evaluation, every concurrent layoutWorkspaces() call shrinks sibling windows to reserve a tile slot for it -- a slot that is never visually filled -- causing siblings to briefly shift. Fix: add isAwaitingOnWindowDetected flag to Window, set when the window enters allWindowsMap, cleared via defer after tryOnWindowDetected completes (covers both the normal path and the closedWindowsCache restore path). layoutWorkspaces() skips flagged windows entirely so no concurrent session can setAxFrame() the window while its rules are still being evaluated. layoutTiles() also excludes flagged windows from space allocation so siblings keep their full tile width during detection. Session A's own layoutWorkspaces() runs after the flag is cleared and positions the window correctly in its final container the first time. https://github.com/nikitabobko/AeroSpace/discussions/2016 --- Sources/AppBundle/layout/layoutRecursive.swift | 14 ++++++++++---- Sources/AppBundle/tree/MacWindow.swift | 2 ++ Sources/AppBundle/tree/Window.swift | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/AppBundle/layout/layoutRecursive.swift b/Sources/AppBundle/layout/layoutRecursive.swift index bc005b9b8..ed86f0a97 100644 --- a/Sources/AppBundle/layout/layoutRecursive.swift +++ b/Sources/AppBundle/layout/layoutRecursive.swift @@ -22,11 +22,13 @@ extension TreeNode { lastAppliedLayoutVirtualRect = virtual try await workspace.rootTilingContainer.layoutRecursive(point, width: width, height: height, virtual: virtual, context) for window in workspace.children.filterIsInstance(of: Window.self) { + if window.isAwaitingOnWindowDetected { continue } window.lastAppliedLayoutPhysicalRect = nil window.lastAppliedLayoutVirtualRect = nil try await window.layoutFloatingWindow(context) } case .window(let window): + if window.isAwaitingOnWindowDetected { break } if window.windowId != currentlyManipulatedWithMouseWindowId { lastAppliedLayoutVirtualRect = virtual if window.isFullscreen && window == context.workspace.rootTilingContainer.mostRecentWindowRecursive { @@ -108,11 +110,15 @@ extension TilingContainer { var point = point var virtualPoint = virtual.topLeftCorner - guard let delta = ((orientation == .h ? width : height) - CGFloat(children.sumOfDouble { $0.getWeight(orientation) })) - .div(children.count) else { return } + // Exclude windows awaiting on-window-detected from space allocation -- + // their slot would otherwise shrink siblings while they sit invisible. + let effectiveChildren = children.filter { ($0 as? Window)?.isAwaitingOnWindowDetected != true } - let lastIndex = children.indices.last - for (i, child) in children.enumerated() { + guard let delta = ((orientation == .h ? width : height) - CGFloat(effectiveChildren.sumOfDouble { $0.getWeight(orientation) })) + .div(effectiveChildren.count) else { return } + + let lastIndex = effectiveChildren.indices.last + for (i, child) in effectiveChildren.enumerated() { child.setWeight(orientation, child.getWeight(orientation) + delta) let rawGap = context.resolvedGaps.inner.get(orientation).toDouble() // Gaps. Consider 4 cases: diff --git a/Sources/AppBundle/tree/MacWindow.swift b/Sources/AppBundle/tree/MacWindow.swift index ac65852b1..1944e77b0 100644 --- a/Sources/AppBundle/tree/MacWindow.swift +++ b/Sources/AppBundle/tree/MacWindow.swift @@ -31,9 +31,11 @@ final class MacWindow: Window { // atomic synchronous section if let existing = allWindowsMap[windowId] { return existing } let window = MacWindow(windowId, macApp, lastFloatingSize: rect?.size, parent: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index) + window.isAwaitingOnWindowDetected = true allWindowsMap[windowId] = window try await debugWindowsIfRecording(window) + defer { window.isAwaitingOnWindowDetected = false } if try await !restoreClosedWindowsCacheIfNeeded(newlyDetectedWindow: window) { try await tryOnWindowDetected(window) } diff --git a/Sources/AppBundle/tree/Window.swift b/Sources/AppBundle/tree/Window.swift index 7ea98b9c1..b48d86795 100644 --- a/Sources/AppBundle/tree/Window.swift +++ b/Sources/AppBundle/tree/Window.swift @@ -5,6 +5,7 @@ open class Window: TreeNode, Hashable { let windowId: UInt32 let app: any AbstractApp var lastFloatingSize: CGSize? + var isAwaitingOnWindowDetected: Bool = false var isFullscreen: Bool = false var noOuterGapsInFullscreen: Bool = false var layoutReason: LayoutReason = .standard