diff --git a/CHANGELOG.md b/CHANGELOG.md index 0319dd1f..eebb65c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to cmux are documented here. +## [1.27.0] - 2026-02-11 + +### Fixed +- Muted traffic lights and toolbar items on macOS 14 (Sonoma) caused by `clipsToBounds` default change +- Toolbar buttons (sidebar, notifications, new tab) disappearing after toggling sidebar with Cmd+B +- Update check pill not appearing in titlebar on macOS 14 (Sonoma) + ## [1.26.0] - 2026-02-11 ### Fixed diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 0ab4e381..bbe50976 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -554,7 +554,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.26.0; + MARKETING_VERSION = 1.27.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -583,7 +583,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -599,7 +599,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.26.0; + MARKETING_VERSION = 1.27.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -652,10 +652,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.26.0; + MARKETING_VERSION = 1.27.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -669,10 +669,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.26.0; + MARKETING_VERSION = 1.27.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 9843269c..7de205b2 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -68,6 +68,14 @@ enum WindowGlassEffect { blurView.autoresizingMask = [.width, .height] blurView.layer?.zPosition = -1000 + // macOS 14 (Sonoma) changed clipsToBounds default from YES to NO. + // Explicitly restore clipping to prevent blur/tint from rendering + // beyond contentView bounds and interfering with titlebar compositing. + if #available(macOS 14, *) { + blurView.clipsToBounds = true + contentView.clipsToBounds = true + } + contentView.addSubview(blurView, positioned: .below, relativeTo: contentView.subviews.first) // Tint overlay on top of blur, still behind content @@ -77,6 +85,9 @@ enum WindowGlassEffect { tintOverlay.wantsLayer = true tintOverlay.layer?.backgroundColor = color.cgColor tintOverlay.layer?.zPosition = -999 + if #available(macOS 14, *) { + tintOverlay.clipsToBounds = true + } contentView.addSubview(tintOverlay, positioned: .above, relativeTo: blurView) objc_setAssociatedObject(window, &tintOverlayKey, tintOverlay, .OBJC_ASSOCIATION_RETAIN) } @@ -114,10 +125,12 @@ enum WindowGlassEffect { } final class SidebarState: ObservableObject { + static let didToggleNotification = Notification.Name("SidebarStateDidToggle") @Published var isVisible: Bool = true func toggle() { isVisible.toggle() + NotificationCenter.default.post(name: Self.didToggleNotification, object: nil) } } diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 81ba34b9..78d604b3 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -33,6 +33,11 @@ final class DevBuildAccessoryViewController: NSTitlebarAccessoryViewController { hostingView.autoresizingMask = [.width, .height] containerView.addSubview(hostingView) + if #available(macOS 14, *) { + containerView.clipsToBounds = true + hostingView.clipsToBounds = true + } + scheduleSizeUpdate() } @@ -42,11 +47,17 @@ final class DevBuildAccessoryViewController: NSTitlebarAccessoryViewController { override func viewDidAppear() { super.viewDidAppear() + view.isHidden = false + containerView.isHidden = false + hostingView.isHidden = false scheduleSizeUpdate() } override func viewDidLayout() { super.viewDidLayout() + view.isHidden = false + containerView.isHidden = false + hostingView.isHidden = false scheduleSizeUpdate() } @@ -63,6 +74,7 @@ final class DevBuildAccessoryViewController: NSTitlebarAccessoryViewController { hostingView.invalidateIntrinsicContentSize() hostingView.layoutSubtreeIfNeeded() let labelSize = hostingView.fittingSize + guard labelSize.width > 1 && labelSize.height > 1 else { return } let titlebarHeight = view.window.map { window in window.frame.height - window.contentLayoutRect.height } ?? labelSize.height @@ -368,6 +380,13 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont hostingView.autoresizingMask = [.width, .height] containerView.addSubview(hostingView) + // macOS 14 (Sonoma) changed clipsToBounds default to NO, which can cause + // titlebar accessory views to render incorrectly or disappear during layout. + if #available(macOS 14, *) { + containerView.clipsToBounds = true + hostingView.clipsToBounds = true + } + userDefaultsObserver = NotificationCenter.default.addObserver( forName: UserDefaults.didChangeNotification, object: nil, @@ -391,14 +410,24 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont override func viewDidAppear() { super.viewDidAppear() + ensureVisible() scheduleSizeUpdate() } override func viewDidLayout() { super.viewDidLayout() + ensureVisible() scheduleSizeUpdate() } + /// Sonoma can hide titlebar accessory views during layout transitions. + /// Force visibility on every layout pass. + private func ensureVisible() { + view.isHidden = false + containerView.isHidden = false + hostingView.isHidden = false + } + private func scheduleSizeUpdate() { guard !pendingSizeUpdate else { return } pendingSizeUpdate = true @@ -412,6 +441,8 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont hostingView.invalidateIntrinsicContentSize() hostingView.layoutSubtreeIfNeeded() let contentSize = hostingView.fittingSize + // Guard against zero-size frames during layout transitions (Sonoma) + guard contentSize.width > 1 && contentSize.height > 1 else { return } let titlebarHeight = view.window.map { window in window.frame.height - window.contentLayoutRect.height } ?? contentSize.height @@ -673,6 +704,11 @@ final class UpdateAccessoryViewController: NSTitlebarAccessoryViewController { hostingView.autoresizingMask = [.width, .height] containerView.addSubview(hostingView) + if #available(macOS 14, *) { + containerView.clipsToBounds = true + hostingView.clipsToBounds = true + } + stateCancellable = model.$state .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -688,14 +724,22 @@ final class UpdateAccessoryViewController: NSTitlebarAccessoryViewController { override func viewDidAppear() { super.viewDidAppear() + ensureVisible() scheduleSizeUpdate() } override func viewDidLayout() { super.viewDidLayout() + ensureVisible() scheduleSizeUpdate() } + private func ensureVisible() { + view.isHidden = false + containerView.isHidden = false + hostingView.isHidden = false + } + private func scheduleSizeUpdate() { guard !pendingSizeUpdate else { return } pendingSizeUpdate = true @@ -709,6 +753,7 @@ final class UpdateAccessoryViewController: NSTitlebarAccessoryViewController { hostingView.invalidateIntrinsicContentSize() hostingView.layoutSubtreeIfNeeded() let pillSize = hostingView.fittingSize + guard pillSize.width > 1 && pillSize.height > 1 else { return } let titlebarHeight = view.window.map { window in window.frame.height - window.contentLayoutRect.height } ?? pillSize.height @@ -750,6 +795,7 @@ final class UpdateTitlebarAccessoryController { attachToExistingWindows() installObservers() installStateObserver() + installSidebarToggleObserver() } func attach(to window: NSWindow) { @@ -830,6 +876,52 @@ final class UpdateTitlebarAccessoryController { window.identifier?.rawValue == "cmux.main" } + /// After sidebar toggle on Sonoma, titlebar accessories can disappear. Re-add if needed. + private func installSidebarToggleObserver() { + let center = NotificationCenter.default + observers.append(center.addObserver( + forName: SidebarState.didToggleNotification, + object: nil, + queue: .main + ) { [weak self] _ in + // Delay slightly to let SwiftUI layout settle before revalidating + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.revalidateAllAccessories() + } + }) + } + + private func revalidateAllAccessories() { + guard let updateViewModel else { return } + for window in attachedWindows.allObjects { + // Re-add controls if they were removed during layout + if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == controlsIdentifier }) { + let controls = TitlebarControlsAccessoryViewController( + notificationStore: TerminalNotificationStore.shared + ) + controls.layoutAttribute = .left + controls.view.identifier = controlsIdentifier + window.addTitlebarAccessoryViewController(controls) + controlsControllers.add(controls) + } + + // Re-add update accessory if it was removed and state is not idle + let isIdle = (updateViewModel.overrideState ?? updateViewModel.state).isIdle + if !isIdle && !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == updateIdentifier }) { + let accessory = UpdateAccessoryViewController(model: updateViewModel) + accessory.layoutAttribute = .right + accessory.view.identifier = updateIdentifier + window.addTitlebarAccessoryViewController(accessory) + } + + // Ensure all accessories are visible and properly sized + for controller in window.titlebarAccessoryViewControllers { + controller.view.isHidden = false + controller.view.needsLayout = true + } + } + } + private func installStateObserver() { guard let updateViewModel else { return } stateCancellable = Publishers.CombineLatest(updateViewModel.$state, updateViewModel.$overrideState) diff --git a/docs-site/content/docs/changelog.mdx b/docs-site/content/docs/changelog.mdx index 03aaadfb..700b9963 100644 --- a/docs-site/content/docs/changelog.mdx +++ b/docs-site/content/docs/changelog.mdx @@ -5,6 +5,13 @@ description: Release notes and version history for cmux All notable changes to cmux are documented here. +## [1.27.0] - 2026-02-11 + +### Fixed +- Muted traffic lights and toolbar items on macOS 14 (Sonoma) caused by `clipsToBounds` default change +- Toolbar buttons (sidebar, notifications, new tab) disappearing after toggling sidebar with Cmd+B +- Update check pill not appearing in titlebar on macOS 14 (Sonoma) + ## [1.26.0] - 2026-02-11 ### Fixed