From 9d67c5c62465433e0a5edc2d9ebb8a6611d4a49f Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 15 Aug 2026 19:00:25 -0700 Subject: [PATCH 1/3] test: cover browser chrome authority across focus --- cmuxTests/BrowserChromeMetricsTests.swift | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/cmuxTests/BrowserChromeMetricsTests.swift b/cmuxTests/BrowserChromeMetricsTests.swift index 5b3843155ab6..a4b7723535a3 100644 --- a/cmuxTests/BrowserChromeMetricsTests.swift +++ b/cmuxTests/BrowserChromeMetricsTests.swift @@ -1,4 +1,7 @@ import CoreGraphics +import AppKit +import CmuxFoundation +import SwiftUI import Testing #if canImport(cmux_DEV) @@ -81,4 +84,52 @@ import Testing #expect(metrics.scale == 1) #expect(metrics.buttonIconSize == Self.legacy.buttonIconSize) } + + @Test + func browserToolbarAndDockTextUseSurfaceAuthorityAcrossAppearanceAndFocus() { + let cases: [(surface: ColorScheme, app: ColorScheme, focused: Bool)] = [ + (.light, .light, false), + (.light, .light, true), + (.light, .dark, false), + (.light, .dark, true), + (.dark, .light, false), + (.dark, .light, true), + (.dark, .dark, false), + (.dark, .dark, true), + ] + + for testCase in cases { + let themeBackground = testCase.surface == .light + ? NSColor.white.withAlphaComponent(0.2) + : NSColor.black.withAlphaComponent(0.2) + let appBackground = testCase.app == .dark ? NSColor.black : NSColor.white + let browserScheme = resolvedBrowserChromeColorScheme( + for: testCase.surface, + themeBackgroundColor: themeBackground, + windowBackgroundColor: appBackground + ) + + #expect( + browserScheme == testCase.surface, + "Browser toolbar authority changed for surface=\(testCase.surface) app=\(testCase.app) focused=\(testCase.focused)" + ) + + let dockText = SidebarAppearanceColorResolver().activeForegroundColor( + opacity: 1, + for: testCase.surface + ) + let expectedTextIsWhite = testCase.surface == .dark + let resolvedDockText = dockText.usingColorSpace(.sRGB) ?? dockText + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + resolvedDockText.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + #expect((red > 0.99) == expectedTextIsWhite) + #expect((green > 0.99) == expectedTextIsWhite) + #expect((blue > 0.99) == expectedTextIsWhite) + #expect(alpha == 1) + } + } } From f3a2eb940c1a1c089e3466d7fa2712e2dd889a82 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 15 Aug 2026 19:15:41 -0700 Subject: [PATCH 2/3] fix: keep browser chrome on resolved surface appearance --- Sources/Panels/BrowserPanelView.swift | 66 +++++++++++++---------- cmuxTests/BrowserChromeMetricsTests.swift | 43 ++++++++------- cmuxTests/BrowserPanelTests.swift | 9 ++-- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index f7fabb7a3981..1f733cfb6925 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -187,14 +187,14 @@ func resolvedBrowserChromeBackgroundColor( } func resolvedBrowserChromeColorScheme( - for colorScheme: ColorScheme, - themeBackgroundColor: NSColor, - windowBackgroundColor: NSColor + for surfaceColorScheme: ColorScheme, + ambientColorScheme: ColorScheme? = nil ) -> ColorScheme { - let perceivedBackgroundColor = themeBackgroundColor.alphaComponent < 0.999 - ? cmuxCompositedNSColor(themeBackgroundColor, over: windowBackgroundColor) - : themeBackgroundColor - return cmuxReadableColorScheme(for: perceivedBackgroundColor) + // Browser chrome is composed inside the already-resolved cmux surface. + // The inherited scheme can change when SwiftUI/AppKit hosting or focus + // changes, but it must not replace the surface authority. + _ = ambientColorScheme + return surfaceColorScheme } func resolvedBrowserOmnibarPillBackgroundColor( @@ -217,7 +217,6 @@ func resolvedBrowserOmnibarPillBackgroundColor( private struct BrowserChromeStyle { let backgroundColor: NSColor - let colorScheme: ColorScheme let omnibarPillBackgroundColor: NSColor static func resolve( @@ -230,18 +229,13 @@ private struct BrowserChromeStyle { themeBackgroundColor: themeBackgroundColor, drawsBackground: drawsBackground ) - // The browser is hosted inside the window/Dock chrome. Its semantic - // colors must follow the resolved cmux scheme injected by the parent, - // even when a translucent theme would otherwise be composited against - // AppKit's ambient window appearance. - let chromeColorScheme = colorScheme + let chromeColorScheme = resolvedBrowserChromeColorScheme(for: colorScheme) let omnibarPillBackgroundColor = resolvedBrowserOmnibarPillBackgroundColor( for: chromeColorScheme, themeBackgroundColor: themeBackgroundColor ) return BrowserChromeStyle( backgroundColor: backgroundColor, - colorScheme: chromeColorScheme, omnibarPillBackgroundColor: omnibarPillBackgroundColor ) } @@ -268,8 +262,11 @@ struct BrowserPanelView: View { /// panels in `DockSplitStore`). When set, it overrides the workspace lookup /// in `isCurrentPaneOwner`; `nil` preserves the main-area behavior. let paneOwnershipOverride: Bool? + private let resolvedColorScheme: ColorScheme private let resolvedThemeBackgroundColor: NSColor - @Environment(\.colorScheme) private var colorScheme + /// Inherited SwiftUI appearance is observed only to refresh WebKit's + /// system theme. It is never used to resolve browser toolbar colors. + @Environment(\.colorScheme) private var inheritedColorScheme @Environment(\.cmuxCanvasInlineBrowserHosting) private var canvasInlineBrowserHosting @Environment(\.paneDropZone) private var paneDropZone /// Held detector instance used to summarize installed browsers rather than @@ -374,6 +371,7 @@ struct BrowserPanelView: View { self.isVisibleInUI = isVisibleInUI self.portalPriority = portalPriority self.paneOwnershipOverride = paneOwnershipOverride + self.resolvedColorScheme = resolvedColorScheme self.resolvedThemeBackgroundColor = resolvedThemeBackgroundColor self.onRequestPanelFocus = onRequestPanelFocus self._browserChromeStyle = State(initialValue: BrowserChromeStyle.resolve( @@ -459,10 +457,6 @@ struct BrowserPanelView: View { browserChromeStyle.backgroundColor } - private var browserChromeColorScheme: ColorScheme { - browserChromeStyle.colorScheme - } - private var browserContentAccessibilityIdentifier: String { "BrowserPanelContent.\(panel.id.uuidString)" } @@ -508,7 +502,7 @@ struct BrowserPanelView: View { return BrowserPortalOmnibarSuggestionsConfiguration( panelId: panel.id, popupFrame: frame, - colorScheme: browserChromeColorScheme, + colorScheme: resolvedColorScheme, engineName: searchConfiguration.displayName, items: omnibarState.suggestions, selectedIndex: omnibarState.selectedSuggestionIndex, @@ -831,7 +825,11 @@ struct BrowserPanelView: View { panel.setBrowserThemeMode(normalizedMode) } - private func handleSystemColorSchemeChange() { + private func handleInheritedColorSchemeChange() { + panel.refreshAppearanceDrivenColors() + } + + private func handleResolvedColorSchemeChange() { refreshBrowserChromeStyle() panel.refreshAppearanceDrivenColors() } @@ -1003,7 +1001,7 @@ struct BrowserPanelView: View { .frame(width: omnibarPillFrame.width) .offset(x: omnibarPillFrame.minX, y: omnibarPillFrame.maxY + 3) .zIndex(1000) - .environment(\.colorScheme, browserChromeColorScheme) + .environment(\.colorScheme, resolvedColorScheme) } } @@ -1084,8 +1082,11 @@ struct BrowserPanelView: View { .onChange(of: browserThemeModeRaw) { _ in handleBrowserThemeModeRawChange() } - .onChange(of: colorScheme) { _ in - handleSystemColorSchemeChange() + .onChange(of: inheritedColorScheme) { _ in + handleInheritedColorSchemeChange() + } + .onChange(of: resolvedColorScheme) { _ in + handleResolvedColorSchemeChange() } .onChange(of: resolvedThemeBackgroundIdentity) { _, _ in refreshBrowserChromeStyle() @@ -1130,6 +1131,10 @@ struct BrowserPanelView: View { .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in refreshBrowserChromeStyle() } + // Keep every SwiftUI browser control on the resolved cmux surface + // scheme, even when this view is rehosted under an ambient AppKit + // appearance during a focus transition. + .environment(\.colorScheme, resolvedColorScheme) } private var addressBar: some View { @@ -1196,7 +1201,7 @@ struct BrowserPanelView: View { } // Keep the omnibar stack above WKWebView so the suggestions popup is visible. .zIndex(1) - .environment(\.colorScheme, browserChromeColorScheme) + .environment(\.colorScheme, resolvedColorScheme) } private var addressBarButtonBar: some View { @@ -1412,7 +1417,7 @@ struct BrowserPanelView: View { .buttonStyle(OmnibarAddressButtonStyle()) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) .popover(isPresented: $isBrowserProfileMenuPresented, arrowEdge: .bottom) { - browserProfilePopover.browserChromePopoverAppearance(browserChromeColorScheme) + browserProfilePopover.browserChromePopoverAppearance(resolvedColorScheme) } .safeHelp( String( @@ -1486,7 +1491,7 @@ struct BrowserPanelView: View { .buttonStyle(OmnibarAddressButtonStyle()) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) .popover(isPresented: $isBrowserThemeMenuPresented, arrowEdge: .bottom) { - browserThemeModePopover.browserChromePopoverAppearance(browserChromeColorScheme) + browserThemeModePopover.browserChromePopoverAppearance(resolvedColorScheme) } .safeHelp( String( @@ -1516,7 +1521,7 @@ struct BrowserPanelView: View { } .buttonStyle(OmnibarAddressButtonStyle()) .popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) { - browserImportHintPopover.browserChromePopoverAppearance(browserChromeColorScheme) + browserImportHintPopover.browserChromePopoverAppearance(resolvedColorScheme) } .safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data")) .accessibilityIdentifier("BrowserImportHintToolbarChip") @@ -1855,7 +1860,10 @@ struct BrowserPanelView: View { private func refreshBrowserChromeStyle() { browserChromeStyle = BrowserChromeStyle.resolve( - for: colorScheme, + for: resolvedBrowserChromeColorScheme( + for: resolvedColorScheme, + ambientColorScheme: inheritedColorScheme + ), themeBackgroundColor: resolvedThemeBackgroundColor, drawsBackground: panel.drawsConfiguredWebViewBackgroundForCurrentPage() ) diff --git a/cmuxTests/BrowserChromeMetricsTests.swift b/cmuxTests/BrowserChromeMetricsTests.swift index a4b7723535a3..e526084d2b0b 100644 --- a/cmuxTests/BrowserChromeMetricsTests.swift +++ b/cmuxTests/BrowserChromeMetricsTests.swift @@ -1,6 +1,5 @@ import CoreGraphics import AppKit -import CmuxFoundation import SwiftUI import Testing @@ -87,31 +86,37 @@ import Testing @Test func browserToolbarAndDockTextUseSurfaceAuthorityAcrossAppearanceAndFocus() { - let cases: [(surface: ColorScheme, app: ColorScheme, focused: Bool)] = [ - (.light, .light, false), - (.light, .light, true), - (.light, .dark, false), - (.light, .dark, true), - (.dark, .light, false), - (.dark, .light, true), - (.dark, .dark, false), - (.dark, .dark, true), + let cases: [(surface: ColorScheme, app: ColorScheme)] = [ + (.light, .light), + (.light, .dark), + (.dark, .light), + (.dark, .dark), ] for testCase in cases { - let themeBackground = testCase.surface == .light - ? NSColor.white.withAlphaComponent(0.2) - : NSColor.black.withAlphaComponent(0.2) - let appBackground = testCase.app == .dark ? NSColor.black : NSColor.white - let browserScheme = resolvedBrowserChromeColorScheme( + let unfocusedScheme = resolvedBrowserChromeColorScheme( for: testCase.surface, - themeBackgroundColor: themeBackground, - windowBackgroundColor: appBackground + ambientColorScheme: testCase.app + ) + // A focus/hosting transition can expose the opposite inherited + // scheme even though the browser surface did not change. + let focusedAmbient = testCase.app == .dark ? ColorScheme.light : .dark + let focusedScheme = resolvedBrowserChromeColorScheme( + for: testCase.surface, + ambientColorScheme: focusedAmbient ) #expect( - browserScheme == testCase.surface, - "Browser toolbar authority changed for surface=\(testCase.surface) app=\(testCase.app) focused=\(testCase.focused)" + unfocusedScheme == testCase.surface, + "Browser toolbar authority changed while unfocused for surface=\(testCase.surface) app=\(testCase.app)" + ) + #expect( + focusedScheme == testCase.surface, + "Browser toolbar authority changed while focused for surface=\(testCase.surface) app=\(testCase.app)" + ) + #expect( + focusedScheme == unfocusedScheme, + "Browser toolbar scheme flipped across focus for surface=\(testCase.surface) app=\(testCase.app)" ) let dockText = SidebarAppearanceColorResolver().activeForegroundColor( diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index f12eda9138b1..5fa6d43c6597 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -455,16 +455,13 @@ final class BrowserPanelChromeBackgroundColorTests: XCTestCase { XCTAssertEqual(themeBackground.alphaComponent, 1.0, accuracy: 0.001) } - func testBrowserChromeColorSchemeAccountsForTranslucentBackground() { - let darkTranslucentBackground = NSColor(srgbRed: 0.02, green: 0.03, blue: 0.04, alpha: 0.05) - + func testBrowserChromeColorSchemeUsesExplicitSurfaceAuthority() { XCTAssertEqual( resolvedBrowserChromeColorScheme( for: .dark, - themeBackgroundColor: darkTranslucentBackground, - windowBackgroundColor: .white + ambientColorScheme: .light ), - .light + .dark ) } From f0da2a52628e476f1df4475972f7bb6d91a6c020 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 15 Aug 2026 19:21:17 -0700 Subject: [PATCH 3/3] fix: preserve inherited appearance for WebKit refresh --- Sources/Panels/BrowserPanelView.swift | 9 ++++++--- Sources/Panels/PanelContentView.swift | 5 +++++ cmuxTests/BrowserChromeMetricsTests.swift | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 1f733cfb6925..ae03538e8c36 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -264,9 +264,10 @@ struct BrowserPanelView: View { let paneOwnershipOverride: Bool? private let resolvedColorScheme: ColorScheme private let resolvedThemeBackgroundColor: NSColor - /// Inherited SwiftUI appearance is observed only to refresh WebKit's - /// system theme. It is never used to resolve browser toolbar colors. - @Environment(\.colorScheme) private var inheritedColorScheme + /// Appearance captured from the host before the parent injects the + /// surface-resolved scheme. It is observed only to refresh WebKit's system + /// theme and is never used to resolve browser toolbar colors. + private let inheritedColorScheme: ColorScheme @Environment(\.cmuxCanvasInlineBrowserHosting) private var canvasInlineBrowserHosting @Environment(\.paneDropZone) private var paneDropZone /// Held detector instance used to summarize installed browsers rather than @@ -361,6 +362,7 @@ struct BrowserPanelView: View { portalPriority: Int, paneOwnershipOverride: Bool? = nil, resolvedColorScheme: ColorScheme, + inheritedColorScheme: ColorScheme, resolvedThemeBackgroundColor: NSColor, onRequestPanelFocus: @escaping () -> Void ) { @@ -372,6 +374,7 @@ struct BrowserPanelView: View { self.portalPriority = portalPriority self.paneOwnershipOverride = paneOwnershipOverride self.resolvedColorScheme = resolvedColorScheme + self.inheritedColorScheme = inheritedColorScheme self.resolvedThemeBackgroundColor = resolvedThemeBackgroundColor self.onRequestPanelFocus = onRequestPanelFocus self._browserChromeStyle = State(initialValue: BrowserChromeStyle.resolve( diff --git a/Sources/Panels/PanelContentView.swift b/Sources/Panels/PanelContentView.swift index 0c2ee0959f53..c2589f35a86e 100644 --- a/Sources/Panels/PanelContentView.swift +++ b/Sources/Panels/PanelContentView.swift @@ -25,6 +25,10 @@ struct PanelContentView: View { let customSidebarUnread: SidebarUnreadModel = TerminalNotificationStore.shared.sidebarUnread let hasUnreadNotification: Bool let terminalAgentContext: String + /// Appearance inherited from the host before this view injects the + /// surface-resolved scheme for its rendered panel subtree. Browser WebKit + /// system theming may observe this value; browser chrome must not. + @Environment(\.colorScheme) private var inheritedColorScheme /// Explicit browser pane-ownership signal for hosts whose panels live outside /// the main `Workspace` tree (the Dock). `nil` keeps the main-area behavior. var paneOwnershipOverride: Bool? = nil @@ -77,6 +81,7 @@ struct PanelContentView: View { portalPriority: portalPriority, paneOwnershipOverride: paneOwnershipOverride, resolvedColorScheme: windowAppearance.resolvedColorScheme, + inheritedColorScheme: inheritedColorScheme, resolvedThemeBackgroundColor: windowAppearance.resolvedChromeBackgroundColor, onRequestPanelFocus: onRequestPanelFocus ) diff --git a/cmuxTests/BrowserChromeMetricsTests.swift b/cmuxTests/BrowserChromeMetricsTests.swift index e526084d2b0b..7d9e3d68f5aa 100644 --- a/cmuxTests/BrowserChromeMetricsTests.swift +++ b/cmuxTests/BrowserChromeMetricsTests.swift @@ -137,4 +137,23 @@ import Testing #expect(alpha == 1) } } + + @Test + func inheritedAppearanceOnlyChangeKeepsBrowserChromeStableForWebKitRefresh() { + let surfaceScheme: ColorScheme = .light + let beforeRefresh = resolvedBrowserChromeColorScheme( + for: surfaceScheme, + ambientColorScheme: .dark + ) + let afterRefresh = resolvedBrowserChromeColorScheme( + for: surfaceScheme, + ambientColorScheme: .light + ) + + // `BrowserPanelView` refreshes WebKit when this inherited value + // changes, while the toolbar authority must remain unchanged. + #expect(beforeRefresh == surfaceScheme) + #expect(afterRefresh == surfaceScheme) + #expect(afterRefresh == beforeRefresh) + } }