From 12dc775c4d0930833e5b2ec6524fc734285fe57e Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 13 Jul 2026 16:53:15 -0700 Subject: [PATCH 1/4] test: cover Settings window chrome --- cmux.xcodeproj/project.pbxproj | 4 ++ cmuxTests/SettingsWindowChromeTests.swift | 50 +++++++++++++++++++ ...SettingsWindowNavigationRoutingTests.swift | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 cmuxTests/SettingsWindowChromeTests.swift diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index c146675df12e..d20b16e9b672 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1282,6 +1282,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources D10000000000000000A00016 /* SettingsSidebarBetaBehaviorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10000000000000000A00017 /* SettingsSidebarBetaBehaviorUITests.swift */; }; D10000000000000000A00014 /* SettingsTerminalBehaviorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10000000000000000A00015 /* SettingsTerminalBehaviorUITests.swift */; }; D10000000000000000A00010 /* SettingsUITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10000000000000000A00011 /* SettingsUITestSupport.swift */; }; + D80100010000000000000001 /* SettingsWindowChromeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80100010000000000000002 /* SettingsWindowChromeTests.swift */; }; D36090030000000000000001 /* SettingsWindowFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36090030000000000000002 /* SettingsWindowFactory.swift */; }; D37777030000000000000001 /* SettingsWindowGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37777030000000000000002 /* SettingsWindowGeometry.swift */; }; D37777070000000000000001 /* SettingsWindowNavigationDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37777070000000000000002 /* SettingsWindowNavigationDelivery.swift */; }; @@ -3058,6 +3059,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D10000000000000000A00017 /* SettingsSidebarBetaBehaviorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebarBetaBehaviorUITests.swift; sourceTree = ""; }; D10000000000000000A00015 /* SettingsTerminalBehaviorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTerminalBehaviorUITests.swift; sourceTree = ""; }; D10000000000000000A00011 /* SettingsUITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITestSupport.swift; sourceTree = ""; }; + D80100010000000000000002 /* SettingsWindowChromeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowChromeTests.swift; sourceTree = ""; }; D36090030000000000000002 /* SettingsWindowFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/SettingsWindowFactory.swift; sourceTree = ""; }; D37777030000000000000002 /* SettingsWindowGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/SettingsWindowGeometry.swift; sourceTree = ""; }; D37777070000000000000002 /* SettingsWindowNavigationDelivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/SettingsWindowNavigationDelivery.swift; sourceTree = ""; }; @@ -5120,6 +5122,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D37777050000000000000002 /* AppDelegatePresentPreferencesWindowTests.swift */, D37777020000000000000002 /* SettingsWindowNavigationRoutingTests.swift */, D37777060000000000000002 /* SettingsWindowPresentationContractTests.swift */, + D80100010000000000000002 /* SettingsWindowChromeTests.swift */, D36090010000000000000004 /* SettingsWindowPresenterTests.swift */, D36090010000000000000006 /* CmuxMainWindowConstrainFrameTests.swift */, D36090020000000000000006 /* CmuxMainWindowFullScreenCapabilityTests.swift */, @@ -7461,6 +7464,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */, 583A675AA1224E8D82A44883 /* SetAutoTitleSocketTests.swift in Sources */, A50019B2 /* SettingsSearchIndexTests.swift in Sources */, + D80100010000000000000001 /* SettingsWindowChromeTests.swift in Sources */, D37777020000000000000001 /* SettingsWindowNavigationRoutingTests.swift in Sources */, D37777010000000000000001 /* SettingsWindowOpenRegressionTests.swift in Sources */, D37777060000000000000001 /* SettingsWindowPresentationContractTests.swift in Sources */, diff --git a/cmuxTests/SettingsWindowChromeTests.swift b/cmuxTests/SettingsWindowChromeTests.swift new file mode 100644 index 000000000000..7cb6210915cc --- /dev/null +++ b/cmuxTests/SettingsWindowChromeTests.swift @@ -0,0 +1,50 @@ +import AppKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +#if DEBUG +extension SettingsWindowSharedStateSuites { + /// Window-construction coverage for the modern Settings chrome contract + /// (https://github.com/manaflow-ai/cmux/issues/8010). + @MainActor + @Suite(.serialized) + struct SettingsWindowChromeTests { + @Test func factoryBuildsModernUnifiedChromeWithWorkingSidebarToggle() throws { + let window = SettingsWindowFactory.makeSettingsWindow(onContentAppear: {}) + defer { + window.contentViewController = nil + window.contentView = nil + window.close() + } + + #expect(window.styleMask.contains(.fullSizeContentView)) + #expect(window.toolbarStyle == .unifiedCompact) + #expect(window.titlebarAppearsTransparent) + #expect(window.titleVisibility == .hidden) + #expect(window.titlebarSeparatorStyle == .none) + + let toolbar = try #require(window.toolbar) + #expect(!toolbar.allowsUserCustomization) + #expect(!toolbar.autosavesConfiguration) + #expect(toolbar.displayMode == .iconOnly) + + let sidebarToggle = try #require( + toolbar.items.first { $0.itemIdentifier == .toggleSidebar } + ) + #expect(sidebarToggle.target === window) + #expect(sidebarToggle.action == #selector(NSSplitViewController.toggleSidebar(_:))) + + let recorder = SettingsSidebarToggleRecorder() + defer { recorder.stopObserving() } + let action = try #require(sidebarToggle.action) + #expect(NSApp.sendAction(action, to: sidebarToggle.target, from: sidebarToggle)) + #expect(recorder.receivedCount == 1) + } + } +} +#endif diff --git a/cmuxTests/SettingsWindowNavigationRoutingTests.swift b/cmuxTests/SettingsWindowNavigationRoutingTests.swift index 33ff31303a81..250ff5363d9e 100644 --- a/cmuxTests/SettingsWindowNavigationRoutingTests.swift +++ b/cmuxTests/SettingsWindowNavigationRoutingTests.swift @@ -310,7 +310,7 @@ private final class SettingsNavigationTargetRecorder: NSObject { /// `SettingsWindowRoot.sidebarToggleRequestName`) so the test target does not /// depend on package-symbol visibility, which differs across toolchains. @MainActor -private final class SettingsSidebarToggleRecorder: NSObject { +final class SettingsSidebarToggleRecorder: NSObject { private(set) var receivedCount = 0 override init() { From 2a9733c2b320e805931c3e4eb3b20cebdbd2b2ce Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 13 Jul 2026 16:54:50 -0700 Subject: [PATCH 2/4] fix: restore modern Settings window chrome --- Sources/App/SettingsWindowFactory.swift | 48 ++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/Sources/App/SettingsWindowFactory.swift b/Sources/App/SettingsWindowFactory.swift index a4fac37c2510..524621c2c510 100644 --- a/Sources/App/SettingsWindowFactory.swift +++ b/Sources/App/SettingsWindowFactory.swift @@ -14,6 +14,7 @@ import os @MainActor enum SettingsWindowFactory { private nonisolated static let log = Logger(subsystem: "com.cmuxterm.app", category: "Settings") + static let toolbarIdentifier = NSToolbar.Identifier("cmux.settings.toolbar") /// `onContentAppear` is invoked from the hosted content's `onAppear`, so /// the presenter that owns this window learns when the content's @@ -29,15 +30,39 @@ enum SettingsWindowFactory { let hostingController = NSHostingController( rootView: SettingsWindowHostRoot(onContentAppear: onContentAppear) ) - // Bridge SwiftUI's navigation title, the sidebar toggle, and the - // search field into the AppKit window's titlebar/toolbar. - hostingController.sceneBridgingOptions = [.toolbars, .title] + // The AppKit owner configures the toolbar explicitly below; bridge only + // SwiftUI's navigation title so an implicit scene toolbar can never + // replace or remove the Settings chrome contract. + hostingController.sceneBridgingOptions = [.title] let window = SettingsHostWindow(contentViewController: hostingController) - window.styleMask = [.titled, .closable, .miniaturizable, .resizable] + window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] window.title = String(localized: "settings.title", defaultValue: "Settings") + configureChrome(on: window) window.setContentSize(NSSize(width: 980, height: 680)) return window } + + /// Establishes the complete modern Settings chrome invariant at + /// construction time. The AppKit-owned window must also own its toolbar; + /// `sceneBridgingOptions` only forwards toolbar content explicitly declared + /// by a hosted SwiftUI view and otherwise leaves a bare legacy titlebar. + private static func configureChrome(on window: SettingsHostWindow) { + window.titlebarAppearsTransparent = true + window.titleVisibility = .hidden + window.titlebarSeparatorStyle = .none + + let toolbar = NSToolbar(identifier: toolbarIdentifier) + toolbar.allowsUserCustomization = false + toolbar.autosavesConfiguration = false + toolbar.displayMode = .iconOnly + toolbar.insertItem(withItemIdentifier: .toggleSidebar, at: 0) + if let sidebarToggle = toolbar.items.first(where: { $0.itemIdentifier == .toggleSidebar }) { + sidebarToggle.target = window + sidebarToggle.action = #selector(SettingsHostWindow.toggleSidebar(_:)) + } + window.toolbar = toolbar + window.toolbarStyle = .unifiedCompact + } } extension SettingsWindowPresenter { @@ -49,9 +74,16 @@ extension SettingsWindowPresenter { /// main actor and warn under strict concurrency. static func handleSidebarToggleIfSettingsWindowIsKey(keyWindow: NSWindow?) -> Bool { guard keyWindow?.identifier?.rawValue == windowIdentifier else { return false } - NotificationCenter.default.post(name: SettingsWindowRoot.sidebarToggleRequestName, object: nil) + requestSidebarToggle() return true } + + /// Shared mutation path for the toolbar item and the app's Toggle Left + /// Sidebar command. The SwiftUI split view owns the visibility state and + /// consumes this request in ``SettingsWindowRoot``. + static func requestSidebarToggle() { + NotificationCenter.default.post(name: SettingsWindowRoot.sidebarToggleRequestName, object: nil) + } } /// Settings window class that records the moment close teardown begins, so @@ -62,6 +94,12 @@ extension SettingsWindowPresenter { class SettingsHostWindow: NSWindow { private(set) var isClosingSettingsWindow = false + /// Receives the standard AppKit toolbar item's `toggleSidebar:` action and + /// forwards it through the same route as the app menu command. + @objc func toggleSidebar(_ sender: Any?) { + SettingsWindowPresenter.requestSidebarToggle() + } + override func close() { isClosingSettingsWindow = true super.close() From d93011b4d74fe3385c6508e3d6b9310f32652133 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 13 Jul 2026 17:00:20 -0700 Subject: [PATCH 3/4] fix: define Settings toolbar item contract --- Sources/App/SettingsWindowFactory.swift | 21 ++++++++++++++++++++- cmuxTests/SettingsWindowChromeTests.swift | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/App/SettingsWindowFactory.swift b/Sources/App/SettingsWindowFactory.swift index 524621c2c510..cf8547dac70a 100644 --- a/Sources/App/SettingsWindowFactory.swift +++ b/Sources/App/SettingsWindowFactory.swift @@ -52,6 +52,7 @@ enum SettingsWindowFactory { window.titlebarSeparatorStyle = .none let toolbar = NSToolbar(identifier: toolbarIdentifier) + toolbar.delegate = window toolbar.allowsUserCustomization = false toolbar.autosavesConfiguration = false toolbar.displayMode = .iconOnly @@ -91,7 +92,7 @@ extension SettingsWindowPresenter { /// window even when a foreign `willClose` observer re-enters `show()` before /// the presenter's own observer runs (notification-observer order is not a /// lifecycle invariant). -class SettingsHostWindow: NSWindow { +class SettingsHostWindow: NSWindow, NSToolbarDelegate { private(set) var isClosingSettingsWindow = false /// Receives the standard AppKit toolbar item's `toggleSidebar:` action and @@ -100,6 +101,24 @@ class SettingsHostWindow: NSWindow { SettingsWindowPresenter.requestSidebarToggle() } + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + [.toggleSidebar] + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + [.toggleSidebar] + } + + func toolbar( + _ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool + ) -> NSToolbarItem? { + // AppKit creates its standard toggle-sidebar item itself and does not + // ask the delegate for it. No custom identifiers are allowed here. + nil + } + override func close() { isClosingSettingsWindow = true super.close() diff --git a/cmuxTests/SettingsWindowChromeTests.swift b/cmuxTests/SettingsWindowChromeTests.swift index 7cb6210915cc..5a1da0a29569 100644 --- a/cmuxTests/SettingsWindowChromeTests.swift +++ b/cmuxTests/SettingsWindowChromeTests.swift @@ -29,6 +29,7 @@ extension SettingsWindowSharedStateSuites { #expect(window.titlebarSeparatorStyle == .none) let toolbar = try #require(window.toolbar) + #expect(toolbar.delegate === window) #expect(!toolbar.allowsUserCustomization) #expect(!toolbar.autosavesConfiguration) #expect(toolbar.displayMode == .iconOnly) From 1d68eef65d1068d6b9563bf81671c7165ddddfb4 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 13 Jul 2026 17:07:36 -0700 Subject: [PATCH 4/4] fix: preserve live Settings toolbar routing --- Sources/App/SettingsWindowFactory.swift | 20 +++++++++++++++----- cmuxTests/SettingsWindowChromeTests.swift | 6 ++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/App/SettingsWindowFactory.swift b/Sources/App/SettingsWindowFactory.swift index cf8547dac70a..40a795d639f6 100644 --- a/Sources/App/SettingsWindowFactory.swift +++ b/Sources/App/SettingsWindowFactory.swift @@ -32,7 +32,9 @@ enum SettingsWindowFactory { ) // The AppKit owner configures the toolbar explicitly below; bridge only // SwiftUI's navigation title so an implicit scene toolbar can never - // replace or remove the Settings chrome contract. + // replace or remove the Settings chrome contract. The sidebar search + // remains inside the hosted NavigationSplitView: `.searchable` with + // `.sidebar` placement does not require toolbar bridging. hostingController.sceneBridgingOptions = [.title] let window = SettingsHostWindow(contentViewController: hostingController) window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] @@ -57,10 +59,6 @@ enum SettingsWindowFactory { toolbar.autosavesConfiguration = false toolbar.displayMode = .iconOnly toolbar.insertItem(withItemIdentifier: .toggleSidebar, at: 0) - if let sidebarToggle = toolbar.items.first(where: { $0.itemIdentifier == .toggleSidebar }) { - sidebarToggle.target = window - sidebarToggle.action = #selector(SettingsHostWindow.toggleSidebar(_:)) - } window.toolbar = toolbar window.toolbarStyle = .unifiedCompact } @@ -119,6 +117,18 @@ class SettingsHostWindow: NSWindow, NSToolbarDelegate { nil } + func toolbarWillAddItem(_ notification: Notification) { + guard + let sidebarToggle = notification.userInfo?[NSToolbarUserInfoKey.itemKey] as? NSToolbarItem, + sidebarToggle.itemIdentifier == .toggleSidebar + else { return } + // AppKit may recreate standard items when the toolbar attaches to a + // window. Configure every inserted instance so the live item always + // uses the shared Settings toggle path. + sidebarToggle.target = self + sidebarToggle.action = #selector(toggleSidebar(_:)) + } + override func close() { isClosingSettingsWindow = true super.close() diff --git a/cmuxTests/SettingsWindowChromeTests.swift b/cmuxTests/SettingsWindowChromeTests.swift index 5a1da0a29569..149e2cf2a803 100644 --- a/cmuxTests/SettingsWindowChromeTests.swift +++ b/cmuxTests/SettingsWindowChromeTests.swift @@ -17,6 +17,7 @@ extension SettingsWindowSharedStateSuites { @Test func factoryBuildsModernUnifiedChromeWithWorkingSidebarToggle() throws { let window = SettingsWindowFactory.makeSettingsWindow(onContentAppear: {}) defer { + window.orderOut(nil) window.contentViewController = nil window.contentView = nil window.close() @@ -34,6 +35,11 @@ extension SettingsWindowSharedStateSuites { #expect(!toolbar.autosavesConfiguration) #expect(toolbar.displayMode == .iconOnly) + // AppKit recreates standard items as a toolbar attaches to its + // live window. Assert the post-attachment item, not just the + // factory-time instance. + window.setFrameOrigin(NSPoint(x: -10_000, y: -10_000)) + window.orderBack(nil) let sidebarToggle = try #require( toolbar.items.first { $0.itemIdentifier == .toggleSidebar } )