From 4fee9efb5567ef1825687b29f4c721668d47540f Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:32:56 -0700 Subject: [PATCH 1/9] test: allow fullscreen tiling for ordinary cmux windows --- cmuxTests/AppDelegateShortcutRoutingTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 0d14f88c343c..b7870cc33b3f 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -1020,7 +1020,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertEqual(workspace.panels.count, initialPanelCount, "Unmatched chord suffix must not trigger the action") } - func testCreateMainWindowDisallowsFullScreenTilingByDefault() { + func testCreateMainWindowAllowsFullScreenTilingByDefault() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared") return @@ -1036,9 +1036,9 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { return } - XCTAssertTrue( + XCTAssertFalse( window.collectionBehavior.contains(.fullScreenDisallowsTiling), - "Main windows should opt out of macOS Full Screen Tile so native fullscreen does not trap Space navigation" + "Main windows should allow macOS Full Screen Tile unless they are spawned from a native fullscreen source" ) } From ecc41a2906a9213fb9e232809e35b7ebaf530b0c Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:35:26 -0700 Subject: [PATCH 2/9] fix: restore fullscreen window tiling --- Sources/App/CmuxMainWindow.swift | 11 +++--- Sources/AppDelegate.swift | 35 +++++++++++++++++++ .../AppDelegateShortcutRoutingTests.swift | 35 +++++++++++++++++++ ...xMainWindowFullScreenCapabilityTests.swift | 11 ++---- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/Sources/App/CmuxMainWindow.swift b/Sources/App/CmuxMainWindow.swift index 30869b7a78c5..b92b35a28859 100644 --- a/Sources/App/CmuxMainWindow.swift +++ b/Sources/App/CmuxMainWindow.swift @@ -318,9 +318,8 @@ final class CmuxMainWindow: NSWindow { /// /// Declaring `.fullScreenPrimary` here makes native fullscreen reachable /// regardless of the OS's implicit default. It is idempotent where AppKit - /// would have granted it anyway. `.fullScreenDisallowsTiling` is also set - /// permanently so macOS Full Screen Tile does not trap cmux in a managed - /// tile Space that breaks Mission Control and horizontal Space swipes. + /// would have granted it anyway. Fullscreen tiling is controlled by the + /// window creation path when a window is spawned from native fullscreen. override init( contentRect: NSRect, styleMask: NSWindow.StyleMask, @@ -343,9 +342,8 @@ final class CmuxMainWindow: NSWindow { /// Returns `base` guaranteed to carry `.fullScreenPrimary` (and never /// `.fullScreenNone`) so a cmux main window can always enter a native - /// fullscreen Space, plus `.fullScreenDisallowsTiling` so AppKit does not - /// route the window into macOS Full Screen Tile. Pure and `nonisolated` so - /// it can be unit-tested without constructing a window; see + /// fullscreen Space. Pure and `nonisolated` so it can be unit-tested + /// without constructing a window; see /// ``init(contentRect:styleMask:backing:defer:)`` for why declaring the /// capability explicitly is required. nonisolated static func canonicalCollectionBehavior( @@ -357,7 +355,6 @@ final class CmuxMainWindow: NSWindow { // suppressed. behavior.remove(.fullScreenNone) behavior.insert(.fullScreenPrimary) - behavior.insert(.fullScreenDisallowsTiling) return behavior } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 814ec969ca7b..e259c5495f5b 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1214,6 +1214,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent /// instead of spawning the bundled `cmux diff` CLI, so shortcut-dispatch tests can /// assert routing without launching a subprocess. var debugOpenDiffViewerHandler: (() -> Void)? + var debugCreateMainWindowSourceIsNativeFullScreenOverride: Bool? // Keep debug-only windows alive when tests intentionally inject key mismatches. private var debugDetachedContextWindows: [NSWindow] = [] @@ -10489,6 +10490,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let sourceWindow = resolvedMainWindowSource(preferredSourceWindow) ?? sourceContext.flatMap { resolvedWindow(for: $0) } let existingFrame = sourceWindow?.frame + let sourceWindowIsNativeFullScreen: Bool = { +#if DEBUG + if let debugCreateMainWindowSourceIsNativeFullScreenOverride { + return debugCreateMainWindowSourceIsNativeFullScreenOverride + } +#endif + return sourceWindow?.styleMask.contains(.fullScreen) == true + }() + let shouldTemporarilyDisallowFullScreenTiling = + sessionWindowSnapshot == nil && sourceWindowIsNativeFullScreen let restoredFrame = resolvedWindowFrame(from: sessionWindowSnapshot) let persistedGeometryFrame = (restoredFrame == nil && sourceWindow == nil) ? resolvedPersistedWindowGeometryFrame() @@ -10514,6 +10525,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent window.minSize = minimumWindowSize window.contentMinSize = minimumWindowSize window.animationBehavior = .none + // A new window opened from a native fullscreen source must not be + // placed into that fullscreen Space while it is being presented. + // Remove the opt-out on the next run-loop turns so ordinary windows + // continue to support macOS Full Screen Tile with other apps. + if shouldTemporarilyDisallowFullScreenTiling { + window.collectionBehavior.insert(.fullScreenDisallowsTiling) + } window.title = "" window.titleVisibility = .hidden window.titlebarAppearsTransparent = true @@ -10644,6 +10662,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent respectActivationSuppression: false ) } + if shouldTemporarilyDisallowFullScreenTiling { + let clearFullScreenTilingOptOut: () -> Void = { [weak window] in + guard let window else { return } + window.collectionBehavior.remove(.fullScreenDisallowsTiling) + if window.collectionBehavior.contains(.fullScreenDisallowsTiling) { + var behavior = window.collectionBehavior + behavior.remove(.fullScreenDisallowsTiling) + window.collectionBehavior = behavior + } + } + RunLoop.main.perform { + clearFullScreenTilingOptOut() + } + DispatchQueue.main.async { + clearFullScreenTilingOptOut() + } + } if let explicitInitialFrame { window.setFrame(explicitInitialFrame, display: true) #if DEBUG diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index b7870cc33b3f..2683153c83ff 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -207,6 +207,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { } AppDelegate.shared?.shortcutLayoutCharacterProvider = KeyboardLayout.character(forKeyCode:modifierFlags:) AppDelegate.shared?.debugCloseMainWindowConfirmationHandler = nil + AppDelegate.shared?.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil if AppDelegate.shared?.dismissNotificationsPopoverIfShown() == true { RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) } @@ -1042,6 +1043,40 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { ) } + func testCreateMainWindowTemporarilyDisallowsFullScreenTilingFromFullscreenSource() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = true + + let newWindowId = appDelegate.createMainWindow() + defer { + closeWindow(withId: newWindowId) + } + + guard let newWindow = window(withId: newWindowId) else { + XCTFail("Expected new window") + return + } + + XCTAssertTrue( + newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), + "A window spawned from native fullscreen should opt out while it is being presented" + ) + + appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil + waitUntil(timeout: 1.0) { + !newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling) + } + + XCTAssertFalse( + newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), + "The fullscreen tiling opt-out should be cleared after initial presentation" + ) + } + func testAddWorkspaceInPreferredMainWindowIgnoresStaleTabManagerPointer() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared") diff --git a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift index 7824ee6a777e..a9a9e9fd2c5d 100644 --- a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift +++ b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift @@ -22,8 +22,8 @@ struct CmuxMainWindowFullScreenCapabilityTests { // // A CmuxMainWindow must therefore *declare* `.fullScreenPrimary` itself so // native fullscreen is reachable regardless of the OS's implicit default. - // It must also opt out of Full Screen Tile so Mission Control and Space - // navigation remain available while cmux is fullscreen. + // Full Screen Tile remains available so other apps, such as Simulator, + // can be tiled with a fullscreen cmux window. @Test func mainWindowDeclaresFullScreenPrimaryCapability() { let window = CmuxMainWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), @@ -45,10 +45,6 @@ struct CmuxMainWindowFullScreenCapabilityTests { !window.collectionBehavior.contains(.fullScreenNone), "Main window must never carry .fullScreenNone, which suppresses native fullscreen" ) - #expect( - window.collectionBehavior.contains(.fullScreenDisallowsTiling), - "Main window must disallow Full Screen Tile so native fullscreen does not trap Space navigation" - ) } // The capability decision is a pure, screen-agnostic transform so it runs @@ -57,14 +53,12 @@ struct CmuxMainWindowFullScreenCapabilityTests { @Test func canonicalBehaviorAddsFullScreenPrimaryToEmptyBehavior() { let result = CmuxMainWindow.canonicalCollectionBehavior([]) #expect(result.contains(.fullScreenPrimary)) - #expect(result.contains(.fullScreenDisallowsTiling)) #expect(!result.contains(.fullScreenNone)) } @Test func canonicalBehaviorDropsStaleFullScreenNone() { let result = CmuxMainWindow.canonicalCollectionBehavior([.fullScreenNone]) #expect(result.contains(.fullScreenPrimary)) - #expect(result.contains(.fullScreenDisallowsTiling)) #expect(!result.contains(.fullScreenNone)) } @@ -72,7 +66,6 @@ struct CmuxMainWindowFullScreenCapabilityTests { let base: NSWindow.CollectionBehavior = [.moveToActiveSpace] let result = CmuxMainWindow.canonicalCollectionBehavior(base) #expect(result.contains(.fullScreenPrimary)) - #expect(result.contains(.fullScreenDisallowsTiling)) #expect(result.contains(.moveToActiveSpace)) } From ca692d1f3a8d3b441f53594107e3a6a3add4e97f Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:52:45 -0700 Subject: [PATCH 3/9] fix: tie fullscreen tiling cleanup to window lifecycle --- Sources/App/MainWindowController.swift | 45 +++++++++++-- Sources/AppDelegate.swift | 41 ++---------- .../AppDelegateShortcutRoutingTests.swift | 66 +++++++++++++------ ...xMainWindowFullScreenCapabilityTests.swift | 4 ++ 4 files changed, 95 insertions(+), 61 deletions(-) diff --git a/Sources/App/MainWindowController.swift b/Sources/App/MainWindowController.swift index ed939af13e66..c1a93d9109de 100644 --- a/Sources/App/MainWindowController.swift +++ b/Sources/App/MainWindowController.swift @@ -12,6 +12,7 @@ final class MainWindowController: ReleasingWindowController { var shouldRetireZoomIntentForProgrammaticResize: ((CmuxMainWindow) -> Bool) = { _ in true } private var isFullScreenTransitionInProgress = false + private var clearsFullscreenTilingOptOutOnPresentation = false #if DEBUG private func logWindowEvent(_ event: String, notification: Notification) { @@ -24,9 +25,27 @@ final class MainWindowController: ReleasingWindowController { #endif override func managedWindowWillClose(_ window: NSWindow) { + clearFullscreenTilingOptOutIfNeeded(window) onClose?(window) } + /// Temporarily keeps a newly-created window out of the source fullscreen + /// Space until AppKit reports that the window has become active. + func disallowFullscreenTilingUntilPresentation() { + guard let window else { return } + window.collectionBehavior.insert(.fullScreenDisallowsTiling) + clearsFullscreenTilingOptOutOnPresentation = true + } + + /// Returns whether a new window needs a transient fullscreen tiling opt-out + /// while it is being presented from a native fullscreen source. + nonisolated static func shouldTemporarilyDisallowFullscreenTiling( + sourceWindow: NSWindow?, + restoringSessionWindow: Bool + ) -> Bool { + !restoringSessionWindow && sourceWindow?.styleMask.contains(.fullScreen) == true + } + func windowWillEnterFullScreen(_ notification: Notification) { setFullScreenTransitionInProgress(true, notification: notification) } @@ -86,27 +105,31 @@ final class MainWindowController: ReleasingWindowController { handleGeometryChange(notification) } -#if DEBUG - func windowDidMiniaturize(_ notification: Notification) { - logWindowEvent("didMiniaturize", notification: notification) - } - func windowDidBecomeKey(_ notification: Notification) { +#if DEBUG logWindowEvent("didBecomeKey", notification: notification) +#endif + clearFullscreenTilingOptOutIfNeeded(notification.object as? NSWindow) } func windowDidResignKey(_ notification: Notification) { +#if DEBUG logWindowEvent("didResignKey", notification: notification) +#endif } func windowDidBecomeMain(_ notification: Notification) { +#if DEBUG logWindowEvent("didBecomeMain", notification: notification) +#endif + clearFullscreenTilingOptOutIfNeeded(notification.object as? NSWindow) } func windowDidResignMain(_ notification: Notification) { +#if DEBUG logWindowEvent("didResignMain", notification: notification) - } #endif + } func windowShouldClose(_ sender: NSWindow) -> Bool { let shouldClose = shouldClose?(sender) ?? true @@ -184,4 +207,14 @@ final class MainWindowController: ReleasingWindowController { } placedWindow.recordUserPlacement() } + + private func clearFullscreenTilingOptOutIfNeeded(_ changedWindow: NSWindow?) { + guard clearsFullscreenTilingOptOutOnPresentation, + let window, + changedWindow === window else { + return + } + window.collectionBehavior.remove(.fullScreenDisallowsTiling) + clearsFullscreenTilingOptOutOnPresentation = false + } } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index e259c5495f5b..e72a219dcc77 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1214,7 +1214,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent /// instead of spawning the bundled `cmux diff` CLI, so shortcut-dispatch tests can /// assert routing without launching a subprocess. var debugOpenDiffViewerHandler: (() -> Void)? - var debugCreateMainWindowSourceIsNativeFullScreenOverride: Bool? // Keep debug-only windows alive when tests intentionally inject key mismatches. private var debugDetachedContextWindows: [NSWindow] = [] @@ -10490,16 +10489,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let sourceWindow = resolvedMainWindowSource(preferredSourceWindow) ?? sourceContext.flatMap { resolvedWindow(for: $0) } let existingFrame = sourceWindow?.frame - let sourceWindowIsNativeFullScreen: Bool = { -#if DEBUG - if let debugCreateMainWindowSourceIsNativeFullScreenOverride { - return debugCreateMainWindowSourceIsNativeFullScreenOverride - } -#endif - return sourceWindow?.styleMask.contains(.fullScreen) == true - }() let shouldTemporarilyDisallowFullScreenTiling = - sessionWindowSnapshot == nil && sourceWindowIsNativeFullScreen + MainWindowController.shouldTemporarilyDisallowFullscreenTiling( + sourceWindow: sourceWindow, + restoringSessionWindow: sessionWindowSnapshot != nil + ) let restoredFrame = resolvedWindowFrame(from: sessionWindowSnapshot) let persistedGeometryFrame = (restoredFrame == nil && sourceWindow == nil) ? resolvedPersistedWindowGeometryFrame() @@ -10525,13 +10519,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent window.minSize = minimumWindowSize window.contentMinSize = minimumWindowSize window.animationBehavior = .none - // A new window opened from a native fullscreen source must not be - // placed into that fullscreen Space while it is being presented. - // Remove the opt-out on the next run-loop turns so ordinary windows - // continue to support macOS Full Screen Tile with other apps. - if shouldTemporarilyDisallowFullScreenTiling { - window.collectionBehavior.insert(.fullScreenDisallowsTiling) - } window.title = "" window.titleVisibility = .hidden window.titlebarAppearsTransparent = true @@ -10588,6 +10575,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent && !self.isApplyingSessionRestore && !displayReconcilePending } + if shouldTemporarilyDisallowFullScreenTiling { + controller.disallowFullscreenTilingUntilPresentation() + } controller.onClose = { [weak self, weak controller] closingWindow in guard let self, let controller else { return } guard let exactOwner = self.mainWindowOwnerIdentity(forExactWindow: closingWindow), @@ -10662,23 +10652,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent respectActivationSuppression: false ) } - if shouldTemporarilyDisallowFullScreenTiling { - let clearFullScreenTilingOptOut: () -> Void = { [weak window] in - guard let window else { return } - window.collectionBehavior.remove(.fullScreenDisallowsTiling) - if window.collectionBehavior.contains(.fullScreenDisallowsTiling) { - var behavior = window.collectionBehavior - behavior.remove(.fullScreenDisallowsTiling) - window.collectionBehavior = behavior - } - } - RunLoop.main.perform { - clearFullScreenTilingOptOut() - } - DispatchQueue.main.async { - clearFullScreenTilingOptOut() - } - } if let explicitInitialFrame { window.setFrame(explicitInitialFrame, display: true) #if DEBUG diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 2683153c83ff..e4cfc3aaba3e 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -207,7 +207,6 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { } AppDelegate.shared?.shortcutLayoutCharacterProvider = KeyboardLayout.character(forKeyCode:modifierFlags:) AppDelegate.shared?.debugCloseMainWindowConfirmationHandler = nil - AppDelegate.shared?.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil if AppDelegate.shared?.dismissNotificationsPopoverIfShown() == true { RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) } @@ -1044,36 +1043,61 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { } func testCreateMainWindowTemporarilyDisallowsFullScreenTilingFromFullscreenSource() { - guard let appDelegate = AppDelegate.shared else { - XCTFail("Expected AppDelegate.shared") - return - } - - appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = true - - let newWindowId = appDelegate.createMainWindow() + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, + defer: false + ) + window.isReleasedWhenClosed = false + let controller = MainWindowController(window: window) defer { - closeWindow(withId: newWindowId) - } - - guard let newWindow = window(withId: newWindowId) else { - XCTFail("Expected new window") - return + window.close() } + controller.disallowFullscreenTilingUntilPresentation() XCTAssertTrue( - newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), + window.collectionBehavior.contains(.fullScreenDisallowsTiling), "A window spawned from native fullscreen should opt out while it is being presented" ) - appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil - waitUntil(timeout: 1.0) { - !newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling) + controller.windowDidBecomeKey(Notification(name: NSWindow.didBecomeKeyNotification, object: window)) + + XCTAssertFalse( + window.collectionBehavior.contains(.fullScreenDisallowsTiling), + "The fullscreen tiling opt-out should be cleared when presentation makes the window key" + ) + } + + func testFullscreenTilingOptOutOnlyAppliesToNativeFullscreenSources() { + let sourceWindow = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .resizable], + backing: .buffered, + defer: false + ) + sourceWindow.styleMask.insert(.fullScreen) + defer { + sourceWindow.close() } + XCTAssertTrue( + MainWindowController.shouldTemporarilyDisallowFullscreenTiling( + sourceWindow: sourceWindow, + restoringSessionWindow: false + ) + ) XCTAssertFalse( - newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), - "The fullscreen tiling opt-out should be cleared after initial presentation" + MainWindowController.shouldTemporarilyDisallowFullscreenTiling( + sourceWindow: sourceWindow, + restoringSessionWindow: true + ) + ) + XCTAssertFalse( + MainWindowController.shouldTemporarilyDisallowFullscreenTiling( + sourceWindow: nil, + restoringSessionWindow: false + ) ) } diff --git a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift index a9a9e9fd2c5d..3dd89386bf2e 100644 --- a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift +++ b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift @@ -45,6 +45,10 @@ struct CmuxMainWindowFullScreenCapabilityTests { !window.collectionBehavior.contains(.fullScreenNone), "Main window must never carry .fullScreenNone, which suppresses native fullscreen" ) + #expect( + !window.collectionBehavior.contains(.fullScreenDisallowsTiling), + "Main window must allow Full Screen Tile so other apps can join its fullscreen Space" + ) } // The capability decision is a pure, screen-agnostic transform so it runs From 65a485540d368adfd2caca45e4cde79c3b1297b8 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:25:26 -0700 Subject: [PATCH 4/9] fix: keep fullscreen source check on main actor --- Sources/App/MainWindowController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/App/MainWindowController.swift b/Sources/App/MainWindowController.swift index c1a93d9109de..83adde4ae312 100644 --- a/Sources/App/MainWindowController.swift +++ b/Sources/App/MainWindowController.swift @@ -39,7 +39,7 @@ final class MainWindowController: ReleasingWindowController { /// Returns whether a new window needs a transient fullscreen tiling opt-out /// while it is being presented from a native fullscreen source. - nonisolated static func shouldTemporarilyDisallowFullscreenTiling( + static func shouldTemporarilyDisallowFullscreenTiling( sourceWindow: NSWindow?, restoringSessionWindow: Bool ) -> Bool { From 9599fb62d1b41245e7b8b1a85dcceccecc4711ec Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:21:35 -0700 Subject: [PATCH 5/9] Fix fullscreen tiling test setup --- cmuxTests/AppDelegateShortcutRoutingTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index e4cfc3aaba3e..ed9fd8a11769 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -1072,11 +1072,10 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { func testFullscreenTilingOptOutOnlyAppliesToNativeFullscreenSources() { let sourceWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), - styleMask: [.titled, .resizable], + styleMask: [.titled, .resizable, .fullScreen], backing: .buffered, defer: false ) - sourceWindow.styleMask.insert(.fullScreen) defer { sourceWindow.close() } From efe9de59bc1e3f1c769621bb4393461c08b11451 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:39:53 -0700 Subject: [PATCH 6/9] Fix current-main app test compilation --- Sources/Surfaces/CmuxTuiRemoteRouting.swift | 14 ++++++++++++++ cmuxTests/CodexForkMonitorArgumentTests.swift | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/Surfaces/CmuxTuiRemoteRouting.swift b/Sources/Surfaces/CmuxTuiRemoteRouting.swift index 897870cc1361..2bbe72ad3fcd 100644 --- a/Sources/Surfaces/CmuxTuiRemoteRouting.swift +++ b/Sources/Surfaces/CmuxTuiRemoteRouting.swift @@ -2,6 +2,20 @@ import Foundation /// Pure remote catalog selector and placement resolution shared by the app and CLI. enum CmuxTuiRemoteRouting { + /// Builds the optional Codex fork ownership flags for a detached monitor. + static func codexForkMonitorArguments(environment: [String: String]) -> [String] { + guard let forkParent = environment["CMUX_AGENT_FORK_PARENT_SESSION_ID"], + !forkParent.isEmpty else { return [] } + var arguments = ["--fork-parent", forkParent] + if let launchID = environment["CMUX_AGENT_FORK_LAUNCH_ID"], !launchID.isEmpty { + arguments += ["--fork-launch-id", launchID] + } + if let ownerPID = environment["CMUX_CODEX_PID"], !ownerPID.isEmpty { + arguments += ["--fork-owner-pid", ownerPID] + } + return arguments + } + /// Every `cmux vm agent` option that takes a value, so the alias walk and /// the help scan skip the value instead of reading it as the first provider /// argument (or as `--help`). diff --git a/cmuxTests/CodexForkMonitorArgumentTests.swift b/cmuxTests/CodexForkMonitorArgumentTests.swift index 460ca14aef18..ae883d23a24c 100644 --- a/cmuxTests/CodexForkMonitorArgumentTests.swift +++ b/cmuxTests/CodexForkMonitorArgumentTests.swift @@ -10,7 +10,7 @@ import Testing struct CodexForkMonitorArgumentTests { @Test func forwardsForkParentClaimToDetachedMonitor() { - let arguments = CMUXCLI.codexForkMonitorArguments(environment: [ + let arguments = CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: [ "CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session", "CMUX_AGENT_FORK_LAUNCH_ID": "launch-id", "CMUX_CODEX_PID": "1234", @@ -25,6 +25,6 @@ struct CodexForkMonitorArgumentTests { @Test func omitsForkArgumentsForNormalCodexMonitor() { - #expect(CMUXCLI.codexForkMonitorArguments(environment: [:]).isEmpty) + #expect(CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: [:]).isEmpty) } } From fdec836305d67f9f6350700d170cb0b0ccaea273 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:50:03 -0700 Subject: [PATCH 7/9] Share Codex fork argument construction --- CLI/cmux.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 1920bc5bb039..16b413a0cacd 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -31792,16 +31792,7 @@ struct CMUXCLI { } static func codexForkMonitorArguments(environment: [String: String]) -> [String] { - guard let forkParent = environment[CodexForkSessionWatcher.parentSessionEnvironmentKey], - !forkParent.isEmpty else { return [] } - var arguments = ["--fork-parent", forkParent] - if let launchID = environment[CodexForkSessionWatcher.launchIDEnvironmentKey], !launchID.isEmpty { - arguments += ["--fork-launch-id", launchID] - } - if let ownerPID = environment["CMUX_CODEX_PID"], !ownerPID.isEmpty { - arguments += ["--fork-owner-pid", ownerPID] - } - return arguments + CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: environment) } /// Watches the Codex rollout until the turn settles. From a8f670d5858bc72c8438e689bdf7956617e6ce76 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:47:35 -0700 Subject: [PATCH 8/9] Preserve workspace owner when creating moved window --- Sources/AppDelegate.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 4bca02c87283..cc95f70fc15e 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5649,7 +5649,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent @discardableResult func moveWorkspaceToNewWindow(workspaceId: UUID, focus: Bool = true) -> UUID? { - let windowId = createMainWindow() + // Resolve the owner before creating the destination. The active/fallback + // window may differ from the workspace's source, especially when moving + // a workspace out of a native fullscreen window. + let sourceWindow = mainWindowContainingWorkspace(workspaceId) + let windowId = createMainWindow(sourceWindow: sourceWindow) guard let destinationManager = tabManagerFor(windowId: windowId) else { return nil } let bootstrapWorkspaceId = destinationManager.tabs.first?.id From 1ea96d7bcd969709b9767ba60d5a99c7df5186e0 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:31:40 -0700 Subject: [PATCH 9/9] Cover fullscreen window creation handoff --- .../AppDelegateShortcutRoutingTests.swift | 30 +++++++++++++++++++ cmuxTests/CodexForkMonitorArgumentTests.swift | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 1b424f674584..d4743c4d1069 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -1042,6 +1042,36 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { ) } + func testCreateMainWindowAppliesFullscreenSourceTilingOptOut() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + let sourceWindow = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .resizable, .fullScreen], + backing: .buffered, + defer: false + ) + sourceWindow.identifier = NSUserInterfaceItemIdentifier("cmux.main.test-source") + sourceWindow.isReleasedWhenClosed = false + defer { sourceWindow.close() } + + let windowId = appDelegate.createMainWindow(shouldActivate: false, sourceWindow: sourceWindow) + defer { closeWindow(withId: windowId) } + + guard let window = window(withId: windowId) else { + XCTFail("Expected test window") + return + } + + XCTAssertTrue( + window.collectionBehavior.contains(.fullScreenDisallowsTiling), + "A window created from native fullscreen should temporarily opt out of tiling" + ) + } + func testCreateMainWindowTemporarilyDisallowsFullScreenTilingFromFullscreenSource() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), diff --git a/cmuxTests/CodexForkMonitorArgumentTests.swift b/cmuxTests/CodexForkMonitorArgumentTests.swift index d0456be41fab..b8f4323c95a6 100644 --- a/cmuxTests/CodexForkMonitorArgumentTests.swift +++ b/cmuxTests/CodexForkMonitorArgumentTests.swift @@ -10,7 +10,7 @@ import Testing struct CodexForkMonitorArgumentTests { @Test func forwardsForkParentClaimToDetachedMonitor() { - let arguments = CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: [ + let arguments = CMUXCLI.codexForkMonitorArguments(environment: [ "CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session", "CMUX_AGENT_FORK_LAUNCH_ID": "launch-id", "CMUX_CODEX_PID": "1234",