From b73694a04f5019b89ec5591d5b0c80b10867b7ee Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:24:35 -0700 Subject: [PATCH 01/15] fix(cmux-next): unify glass surface backgrounds Use one theme ground across strips and internal surfaces, while exposing opt-in per-surface overrides for users. --- .../Sources/CmuxTheme/ThemeTokens.swift | 13 +- .../Onboarding/TerminalThemeSetting.swift | 1 + .../Store/Views/AppStoreRootView.swift | 1 + .../UI/BookmarksBarView.swift | 2 +- .../CmuxNextBrowser/CEF/CEFHostView.swift | 7 +- .../SurfaceBackgroundOverrides.swift | 43 ++ .../Sources/CmuxNextDesign/ThemeScope.swift | 16 +- .../CmuxNextHome/Views/HomeHeaderView.swift | 2 +- .../Sources/CmuxNextHome/Views/HomeView.swift | 2 +- .../Views/PaneOverlayView.swift | 5 +- .../CmuxNextSettings/CmuxConfigSnapshot.swift | 3 + .../Schema/SettingsSchema+AgentPolicy.swift | 8 + .../Schema/SettingsSchema+Appearance.swift | 14 + .../SurfaceBackgroundSetting.swift | 34 ++ .../Views/SettingsRootView.swift | 1 + .../ThemeTokensTests.swift | 27 +- schemas/settings/settings-schema.json | 394 +++++++++++++++++- 17 files changed, 545 insertions(+), 28 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift diff --git a/Packages/Shared/CmuxTheme/Sources/CmuxTheme/ThemeTokens.swift b/Packages/Shared/CmuxTheme/Sources/CmuxTheme/ThemeTokens.swift index 0afd1e3278b9..25649955ec97 100644 --- a/Packages/Shared/CmuxTheme/Sources/CmuxTheme/ThemeTokens.swift +++ b/Packages/Shared/CmuxTheme/Sources/CmuxTheme/ThemeTokens.swift @@ -24,16 +24,15 @@ public struct ThemeTokens: Hashable, Sendable { public var chromeBackground: ThemeRGB /// Floating cards (palette, hover card, editors) under or instead of glass. public var elevatedBackground: ThemeRGB - /// Every pane's tab strip: a shade darker than the window in light - /// themes too, so the strips read as quiet bands (a pure black window - /// has no darker shade; there the strip matches it). + /// Every pane's tab strip: the same ground as the window. Focus and + /// selection fills provide hierarchy without introducing a second + /// background token. public var stripBackground: ThemeRGB /// The sidebar's tonal step: a translucent layer over the window's one /// backdrop (the solid background, or the material and its tint), the /// foreground at the same 4% the agent pane's sidebar uses. public var sidebarStep: ThemeRGB - /// The tab strip's tonal step over the window's backdrop: black, so - /// over the opaque background it composites to `stripBackground`. + /// Reserved for the sidebar rules; tab strips do not add a tonal step. public var stripStep: ThemeRGB // Text @@ -151,9 +150,9 @@ public struct ThemeTokens: Hashable, Sendable { contentBackground: surface, chromeBackground: bg.mixed(toward: fg, isDark ? 0.05 : 0.035), elevatedBackground: bg.mixed(toward: fg, isDark ? 0.07 : 0.02), - stripBackground: bg.mixed(toward: .black, isDark ? 0.22 : 0.05).withAlpha(input.backgroundOpacity), + stripBackground: surface, sidebarStep: fg.withAlpha(0.04), - stripStep: ThemeRGB.black.withAlpha(isDark ? 0.22 : 0.05), + stripStep: ThemeRGB.black.withAlpha(0), textPrimary: primary, textSecondary: secondary, textTertiary: tertiary, diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/TerminalThemeSetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/TerminalThemeSetting.swift index 565a9a8d222c..538b6a9fbf1f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/TerminalThemeSetting.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/TerminalThemeSetting.swift @@ -40,6 +40,7 @@ final class TerminalThemeSetting { let font = GhosttyRuntime.FontOverride(family: snapshot.terminalFontFamily, size: snapshot.terminalFontSize) self?.backdropScope.setBackdropSelection(snapshot.backdropSelection) self?.backdropScope.setAppearanceTuning(snapshot.experimentalAppearance ? snapshot.appearanceTuning : .identity) + self?.backdropScope.setSurfaceBackgroundOverrides(snapshot.surfaceBackgrounds) self?.apply(State(theme: snapshot.appTheme, font: font, background: snapshot.windowBackground)) } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift index 3b14a53ff9b7..b59e71d2d964 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift @@ -24,6 +24,7 @@ public struct AppStoreRootView: View { case .installed: AppInstalledView(model: model) } } + .background(colors.background) .onAppear { if model.listings.isEmpty { model.refresh() } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarksBarView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarksBarView.swift index 7ebeb9fdde0d..a56c4f0522d8 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarksBarView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarksBarView.swift @@ -127,7 +127,7 @@ public final class BookmarksBarView: NSView { public override func updateLayer() { performWithTheme { - layer?.backgroundColor = Palette.windowBackground.cgColor + layer?.backgroundColor = Palette.paneFill.cgColor emptyLabel.textColor = Palette.textTertiary otherButton.contentTintColor = Palette.textSecondary overflow.contentTintColor = Palette.textSecondary diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFHostView.swift index 8e2095e26cb2..9e7f9a54f372 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFHostView.swift @@ -31,11 +31,12 @@ final class CEFHostView: NSView { updateBackground() } - /// Until Chromium's page window shows its first frame, the page area is - /// the theme color, never white (`PageBackground`). + /// Until Chromium's page window shows its first frame, the page area + /// follows the shared pane ground. It stays clear over Liquid Glass so + /// the window backdrop remains visible. private func updateBackground() { performWithTheme { - layer?.backgroundColor = Palette.pageBackground.cgColor + layer?.backgroundColor = Palette.paneFill.cgColor } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift new file mode 100644 index 000000000000..7cb3a543f8f4 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift @@ -0,0 +1,43 @@ +public import CmuxTheme + +/// Optional per-surface colors. An unset entry always resolves to the +/// theme's single surface background, so customization never creates a +/// second implicit palette. +public struct SurfaceBackgroundOverrides: Equatable, Sendable { + public var sidebar: ThemeRGB? + public var tabStrip: ThemeRGB? + public var terminal: ThemeRGB? + public var browser: ThemeRGB? + public var internalPage: ThemeRGB? + public var agentPane: ThemeRGB? + public var splitDivider: ThemeRGB? + public var settings: ThemeRGB? + + public init(sidebar: ThemeRGB? = nil, tabStrip: ThemeRGB? = nil, terminal: ThemeRGB? = nil, + browser: ThemeRGB? = nil, internalPage: ThemeRGB? = nil, agentPane: ThemeRGB? = nil, + splitDivider: ThemeRGB? = nil, settings: ThemeRGB? = nil) { + self.sidebar = sidebar + self.tabStrip = tabStrip + self.terminal = terminal + self.browser = browser + self.internalPage = internalPage + self.agentPane = agentPane + self.splitDivider = splitDivider + self.settings = settings + } +} + +extension ThemeTokens { + /// Applies explicit user colors while preserving the theme token as the + /// default ground for every surface. + public func applying(_ overrides: SurfaceBackgroundOverrides) -> ThemeTokens { + var result = self + let ground = surfaceBackground + result.windowBackground = overrides.terminal ?? ground + result.contentBackground = overrides.browser ?? overrides.internalPage ?? overrides.terminal ?? ground + result.sidebarBackground = overrides.sidebar ?? ground + result.stripBackground = overrides.tabStrip ?? ground + result.paneBorder = overrides.splitDivider ?? paneBorder + return result + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift index 61400bb0ad79..d9a29750ea47 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift @@ -28,8 +28,12 @@ public final class ThemeScope { return nil } public var appearanceTuning: AppearanceTuning { selectedAppearanceTuning ?? parent?.appearanceTuning ?? .identity } + public var surfaceBackgroundOverrides: SurfaceBackgroundOverrides { + selectedSurfaceBackgroundOverrides ?? parent?.surfaceBackgroundOverrides ?? SurfaceBackgroundOverrides() + } private var selectedBackdropSelection: BackdropSelection? private var selectedAppearanceTuning: AppearanceTuning? + private var selectedSurfaceBackgroundOverrides: SurfaceBackgroundOverrides? /// Changes art and repaints this scope and its descendants without a /// Ghostty reload or changing any terminal colors. @@ -55,6 +59,13 @@ public final class ThemeScope { repaintBackdropArt() } + /// Sets optional per-surface colors; nil entries inherit the theme ground. + public func setSurfaceBackgroundOverrides(_ overrides: SurfaceBackgroundOverrides?) { + guard selectedSurfaceBackgroundOverrides != overrides else { return } + selectedSurfaceBackgroundOverrides = overrides + repaintBackdropArt() + } + private func repaintBackdropArt() { repaint(animated: false) for responder in responders.allObjects { @@ -76,9 +87,10 @@ public final class ThemeScope { /// own, else its own. A light workspace in a dark room never turns the /// window's chrome light. public var tokens: ThemeTokens { - guard let shown else { return ownTokens.emphasized(emphasis) } + guard let shown else { return ownTokens.applying(surfaceBackgroundOverrides).emphasized(emphasis) } let candidate = shown.tokens - return (candidate.isDark == ownTokens.isDark ? candidate : ownTokens).emphasized(emphasis) + return (candidate.isDark == ownTokens.isDark ? candidate : ownTokens) + .applying(surfaceBackgroundOverrides).emphasized(emphasis) } /// How strongly this scope's own views draw (a pane's tab strip in an /// unfocused pane: `ChromeEmphasis`). Children keep the plain colors. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeHeaderView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeHeaderView.swift index f797556dffdb..a216bbd775e9 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeHeaderView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeHeaderView.swift @@ -25,7 +25,7 @@ final class HomeHeaderView: NSView { override func draw(_ dirtyRect: NSRect) { performWithTheme { - Palette.windowBackground.setFill() + Palette.paneFill.setFill() bounds.fill() let paragraph = NSMutableParagraphStyle() paragraph.alignment = .center diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeView.swift index fa6ae1e1d067..797950a22f0d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/Views/HomeView.swift @@ -121,7 +121,7 @@ public final class HomeView: NSView { private func applyTheme() { performWithTheme { - layer?.backgroundColor = Palette.windowBackground.cgColor + layer?.backgroundColor = Palette.paneFill.cgColor separator.backgroundColor = Palette.separator.cgColor emptyLabel.font = Typography.body emptyLabel.textColor = Palette.textTertiary diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneOverlayView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneOverlayView.swift index 5205897d4463..20f4306f11f9 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneOverlayView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneOverlayView.swift @@ -227,7 +227,10 @@ final class PaneOverlayView: NSView { let attentionColor = attentionMark?.color?.nsColor ?? attentionSettings.color?.nsColor ?? Palette.attention attention.borderColor = attentionColor.cgColor border.borderColor = (borderStyle.color?.nsColor ?? Palette.paneBorder).cgColor - dimLayer.backgroundColor = Palette.contentBackground.withAlphaComponent(1).cgColor + // In glass windows the root backdrop owns the ground. An opaque + // inactive-pane veil would hide the painting and reintroduce the + // History/terminal mismatch; paneFill is clear in that mode. + dimLayer.backgroundColor = Palette.paneFill.cgColor } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift index 865cb2c91c34..7073cb640d86 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift @@ -107,6 +107,8 @@ public struct CmuxConfigSnapshot: Sendable, Equatable { /// `appearance.glassTransparency`, `appearance.hue` and /// `appearance.saturation`; identity values when unset or invalid. public var appearanceTuning = AppearanceTuningSetting.fallback + /// `appearance.surfaceBackgrounds`; unset entries follow the theme ground. + public var surfaceBackgrounds = SurfaceBackgroundOverrides() /// `appearance.statusIndicator.*`. public var statusIndicator = StatusIndicatorSettings() /// `status.*`. @@ -213,6 +215,7 @@ public struct CmuxConfigSnapshot: Sendable, Equatable { if case .art(let art) = snapshot.backdropSelection { snapshot.backdropArt = art } snapshot.experimentalAppearance = ExperimentalAppearanceSetting().parse(root, diagnostics: &snapshot.diagnostics) snapshot.appearanceTuning = AppearanceTuningSetting.parse(root, diagnostics: &snapshot.diagnostics) + snapshot.surfaceBackgrounds = SurfaceBackgroundSetting.parse(root, diagnostics: &snapshot.diagnostics) snapshot.statusIndicator = StatusIndicatorConfigParser.parse(root, diagnostics: &snapshot.diagnostics) snapshot.statusBehavior = StatusIndicatorConfigParser.behavior(root, diagnostics: &snapshot.diagnostics) let (borders, bordersDiagnostic) = BordersSetting.parse(root) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift index ff00a95c2f0e..e299e90c1229 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift @@ -46,6 +46,14 @@ extension SettingsSchema { "appearance.glassTransparency", "appearance.hue", "appearance.saturation", + "appearance.surfaceBackgrounds.sidebar", + "appearance.surfaceBackgrounds.tabStrip", + "appearance.surfaceBackgrounds.terminal", + "appearance.surfaceBackgrounds.browser", + "appearance.surfaceBackgrounds.internalPage", + "appearance.surfaceBackgrounds.agentPane", + "appearance.surfaceBackgrounds.splitDivider", + "appearance.surfaceBackgrounds.settings", "appearance.density", "appearance.metrics.chromeFontSize", "appearance.borders", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift index 679f21ccd7f1..97de09d18359 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift @@ -14,6 +14,19 @@ nonisolated enum AppearanceSettingsSchema { let appTheme = SettingsText.keyed("settings.group.appTheme", "App Theme") let tuning = SettingsText.keyed("settings.group.appearanceTuning", "Appearance Tuning") let artChoices = BackdropArt.allCases.map { SettingChoice($0.rawValue, $0.title) } + let surfaceBackgrounds: [SettingDescriptor] = [ + ("sidebar", "Sidebar"), ("tabStrip", "Tab Strip"), ("terminal", "Terminal"), + ("browser", "Browser"), ("internalPage", "Internal Pages"), ("agentPane", "Agent Pane"), + ("splitDivider", "Split Dividers"), ("settings", "Settings") + ].map { name, label in + SettingDescriptor( + ["appearance", "surfaceBackgrounds", name], section: .appearance, group: window, + title: SettingsText.keyed("settings.layout.paneBorderColor", label), + help: SettingsText.keyed("settings.appearance.backgroundOpacity.help", "Unset follows the theme's one background token."), + kind: .color, default: nil, defaultLabel: theme, + keywords: ["background", "surface", "color", label.lowercased()] + ) + } return [ SettingDescriptor( AppThemeSetting().configPath, section: .appearance, group: appTheme, @@ -95,6 +108,7 @@ nonisolated enum AppearanceSettingsSchema { default: .number(AppearanceTuningSetting.fallback.saturation), keywords: ["tint", "color", "colour", "intensity"] ), + ] + surfaceBackgrounds + [ SettingDescriptor( ["appearance", "density"], section: .appearance, group: look, title: SettingsText.keyed("settings.appearance.density", "Density"), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift new file mode 100644 index 000000000000..bbf8a375f069 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift @@ -0,0 +1,34 @@ +public import CmuxNextDesign +public import CmuxTheme + +/// `appearance.surfaceBackgrounds` stores optional `#RRGGBB`/`#RRGGBBAA` +/// overrides. Missing entries inherit the one per-theme surface token. +public enum SurfaceBackgroundSetting { + public static let path = ["appearance", "surfaceBackgrounds"] + private static let names: [(String, WritableKeyPath)] = [ + ("sidebar", \.sidebar), ("tabStrip", \.tabStrip), ("terminal", \.terminal), + ("browser", \.browser), ("internalPage", \.internalPage), ("agentPane", \.agentPane), + ("splitDivider", \.splitDivider), ("settings", \.settings) + ] + + public static func parse(_ root: JSONValue, diagnostics: inout [SettingsDiagnostic]) -> SurfaceBackgroundOverrides { + guard let value = root.value(at: path) else { return SurfaceBackgroundOverrides() } + guard let object = value.objectValue else { + diagnostics.append(SettingsDiagnostic(kind: .invalidValue, path: path.joined(separator: "."), message: "expected an object of surface colors")) + return SurfaceBackgroundOverrides() + } + var result = SurfaceBackgroundOverrides() + for (name, keyPath) in names { + guard let value = object[name] else { continue } + if value.isNull { result[keyPath: keyPath] = nil; continue } + guard let text = value.stringValue, let color = ThemeRGB(cssHex: text) else { + diagnostics.append(SettingsDiagnostic(kind: .invalidValue, + path: "appearance.surfaceBackgrounds.\(name)", + message: "expected #RRGGBB or #RRGGBBAA")) + continue + } + result[keyPath: keyPath] = color + } + return result + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettingsWindow/Views/SettingsRootView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettingsWindow/Views/SettingsRootView.swift index 37f337c0aca3..2490a374bfd4 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettingsWindow/Views/SettingsRootView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettingsWindow/Views/SettingsRootView.swift @@ -24,6 +24,7 @@ struct SettingsRootView: View { .foregroundStyle(SettingsStyle.text) .font(SettingsStyle.body) .controlSize(.small) + .background(Color(nsColor: SettingsTheme.shared.tokens.surfaceBackground.nsColor)) } } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/ThemeTokensTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/ThemeTokensTests.swift index 86df7f946d35..b2bc91a2db32 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/ThemeTokensTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/ThemeTokensTests.swift @@ -14,23 +14,18 @@ import Testing #expect(t.contentBackground == input.background) } - /// The tab strips along the top are a shade darker than the window in - /// every theme, light ones included (Leo: the top is always a darker - /// tone), and text on them stays readable. + /// Tab strips share the window ground; hierarchy comes from fills. @Test(arguments: ThemeFixtures.all.map(\.0)) - func theStripIsDarkerThanTheWindow(_ name: String) { + func theStripMatchesTheWindow(_ name: String) { let t = ThemeTokens.derive(from: theme(name)) - #expect(t.stripBackground.relativeLuminance < t.windowBackground.relativeLuminance, "\(name)") + #expect(t.stripBackground == t.windowBackground, "\(name)") #expect(t.textPrimary.contrast(with: t.stripBackground.withAlpha(1)) >= 4.5, "\(name)") // Inactive tab titles use the muted text styles. #expect(t.textSecondary.contrast(with: t.stripBackground.withAlpha(1)) >= 4.5, "\(name) secondary") #expect(t.textTertiary.contrast(with: t.stripBackground.withAlpha(1)) >= 3.0, "\(name) tertiary") } - /// The sidebar and strip steps are translucent layers over the - /// window's one backdrop. Over the opaque background the strip step - /// gives exactly the strip color, and the sidebar step gives the - /// background lifted 4% toward the foreground (the agent pane's step). + /// Only the sidebar retains a tonal step; strips add none. @Test(arguments: ThemeFixtures.all.map(\.0)) func theStepsCompositeToTheChromeSurfaces(_ name: String) { let input = theme(name) @@ -39,8 +34,7 @@ import Testing func close(_ a: ThemeRGB, _ b: ThemeRGB) -> Bool { abs(a.red - b.red) < 0.002 && abs(a.green - b.green) < 0.002 && abs(a.blue - b.blue) < 0.002 } - #expect(t.stripStep.alpha < 1 && t.sidebarStep.alpha < 1, "\(name)") - #expect(close(t.stripStep.composited(over: bg), t.stripBackground.withAlpha(1)), "\(name) strip") + #expect(t.stripStep.alpha == 0 && t.sidebarStep.alpha < 1, "\(name)") #expect(close(t.sidebarStep.composited(over: bg), bg.mixed(toward: input.foreground, 0.04)), "\(name) sidebar") } @@ -123,6 +117,17 @@ import Testing #expect(t.chromeBackground.alpha == 1) } + @Test func explicitSurfaceOverridesAreOptInAndIndependent() { + let base = ThemeTokens.derive(from: ThemeFixtures.catppuccinMocha) + let terminal = ThemeRGB(cssHex: "#10203080")! + let sidebar = ThemeRGB(cssHex: "#304050")! + let result = base.applying(SurfaceBackgroundOverrides(sidebar: sidebar, terminal: terminal)) + #expect(result.windowBackground == terminal) + #expect(result.sidebarBackground == sidebar) + #expect(result.stripBackground == base.surfaceBackground) + #expect(result.contentBackground == terminal) + } + @Test func contrastMathMatchesWCAG() { #expect(abs(ThemeRGB.black.contrast(with: .white) - 21) < 0.01) #expect(abs(ThemeRGB(hex: 0x777777).contrast(with: .white) - 4.48) < 0.02) diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 95c128dd8887..67774d7cc8fe 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -2551,6 +2551,398 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.sidebar", + "keywords" : [ + "background", + "surface", + "color", + "sidebar" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "sidebar" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Sidebar" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.tabStrip", + "keywords" : [ + "background", + "surface", + "color", + "tab strip" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "tabStrip" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Tab Strip" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.terminal", + "keywords" : [ + "background", + "surface", + "color", + "terminal" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "terminal" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Terminal" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.browser", + "keywords" : [ + "background", + "surface", + "color", + "browser" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "browser" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Browser" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.internalPage", + "keywords" : [ + "background", + "surface", + "color", + "internal pages" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "internalPage" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Internal Pages" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.agentPane", + "keywords" : [ + "background", + "surface", + "color", + "agent pane" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "agentPane" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Agent Pane" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.splitDivider", + "keywords" : [ + "background", + "surface", + "color", + "split dividers" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "splitDivider" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Split Dividers" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.theme", + "text" : "Theme" + }, + "group" : { + "key" : "settings.group.windowBackground", + "text" : "Window Background" + }, + "help" : { + "key" : "settings.appearance.backgroundOpacity.help", + "text" : "Unset follows the theme's one background token." + }, + "kept_on_reset_all" : false, + "key" : "appearance.surfaceBackgrounds.settings", + "keywords" : [ + "background", + "surface", + "color", + "settings" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaceBackgrounds", + "settings" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.layout.paneBorderColor", + "text" : "Settings" + }, + "validation" : "portable" + }, { "accepts" : [ "compact", @@ -5887,7 +6279,7 @@ "validation" : "portable" } ], - "schema_hash" : "2fb252b0a500b775bc1e730bf753ae68eabf87fbf7da6f7f792f7e3f67c2d280", + "schema_hash" : "59db6bc361eb474a1c1c143a8157813d32b880f917901cbb173e6b486991529a", "sections" : [ { "id" : "general", From 8e44f3a33596891a0c4fe41e9f98cbbc39860282 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:31:12 -0700 Subject: [PATCH 02/15] fix(cmux-next): keep surface override parsing nonisolated --- .../SurfaceBackgroundOverrides.swift | 2 +- .../SurfaceBackgroundSetting.swift | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift index 7cb3a543f8f4..9e1812d58998 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift @@ -13,7 +13,7 @@ public struct SurfaceBackgroundOverrides: Equatable, Sendable { public var splitDivider: ThemeRGB? public var settings: ThemeRGB? - public init(sidebar: ThemeRGB? = nil, tabStrip: ThemeRGB? = nil, terminal: ThemeRGB? = nil, + public nonisolated init(sidebar: ThemeRGB? = nil, tabStrip: ThemeRGB? = nil, terminal: ThemeRGB? = nil, browser: ThemeRGB? = nil, internalPage: ThemeRGB? = nil, agentPane: ThemeRGB? = nil, splitDivider: ThemeRGB? = nil, settings: ThemeRGB? = nil) { self.sidebar = sidebar diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift index bbf8a375f069..d71d439bddf1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift @@ -5,11 +5,7 @@ public import CmuxTheme /// overrides. Missing entries inherit the one per-theme surface token. public enum SurfaceBackgroundSetting { public static let path = ["appearance", "surfaceBackgrounds"] - private static let names: [(String, WritableKeyPath)] = [ - ("sidebar", \.sidebar), ("tabStrip", \.tabStrip), ("terminal", \.terminal), - ("browser", \.browser), ("internalPage", \.internalPage), ("agentPane", \.agentPane), - ("splitDivider", \.splitDivider), ("settings", \.settings) - ] + private static let names = ["sidebar", "tabStrip", "terminal", "browser", "internalPage", "agentPane", "splitDivider", "settings"] public static func parse(_ root: JSONValue, diagnostics: inout [SettingsDiagnostic]) -> SurfaceBackgroundOverrides { guard let value = root.value(at: path) else { return SurfaceBackgroundOverrides() } @@ -18,16 +14,26 @@ public enum SurfaceBackgroundSetting { return SurfaceBackgroundOverrides() } var result = SurfaceBackgroundOverrides() - for (name, keyPath) in names { + for name in names { guard let value = object[name] else { continue } - if value.isNull { result[keyPath: keyPath] = nil; continue } + if value.isNull { continue } guard let text = value.stringValue, let color = ThemeRGB(cssHex: text) else { diagnostics.append(SettingsDiagnostic(kind: .invalidValue, path: "appearance.surfaceBackgrounds.\(name)", message: "expected #RRGGBB or #RRGGBBAA")) continue } - result[keyPath: keyPath] = color + switch name { + case "sidebar": result.sidebar = color + case "tabStrip": result.tabStrip = color + case "terminal": result.terminal = color + case "browser": result.browser = color + case "internalPage": result.internalPage = color + case "agentPane": result.agentPane = color + case "splitDivider": result.splitDivider = color + case "settings": result.settings = color + default: break + } } return result } From da17ce31dc3aa631942efee16ac7ae772f267eb1 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:35:21 -0700 Subject: [PATCH 03/15] fix(cmux-next): mark surface colors nonisolated --- .../SurfaceBackgroundOverrides.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift index 9e1812d58998..bc9f62d5205d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift @@ -4,14 +4,14 @@ public import CmuxTheme /// theme's single surface background, so customization never creates a /// second implicit palette. public struct SurfaceBackgroundOverrides: Equatable, Sendable { - public var sidebar: ThemeRGB? - public var tabStrip: ThemeRGB? - public var terminal: ThemeRGB? - public var browser: ThemeRGB? - public var internalPage: ThemeRGB? - public var agentPane: ThemeRGB? - public var splitDivider: ThemeRGB? - public var settings: ThemeRGB? + public nonisolated var sidebar: ThemeRGB? + public nonisolated var tabStrip: ThemeRGB? + public nonisolated var terminal: ThemeRGB? + public nonisolated var browser: ThemeRGB? + public nonisolated var internalPage: ThemeRGB? + public nonisolated var agentPane: ThemeRGB? + public nonisolated var splitDivider: ThemeRGB? + public nonisolated var settings: ThemeRGB? public nonisolated init(sidebar: ThemeRGB? = nil, tabStrip: ThemeRGB? = nil, terminal: ThemeRGB? = nil, browser: ThemeRGB? = nil, internalPage: ThemeRGB? = nil, agentPane: ThemeRGB? = nil, From fce6f068c0e7720980965002e41dc1dbc0b95551 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:39:45 -0700 Subject: [PATCH 04/15] fix(cmux-next): avoid isolated surface equality --- .../CmuxNextDesign/SurfaceBackgroundOverrides.swift | 2 +- .../CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift index bc9f62d5205d..8dca5d683295 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift @@ -3,7 +3,7 @@ public import CmuxTheme /// Optional per-surface colors. An unset entry always resolves to the /// theme's single surface background, so customization never creates a /// second implicit palette. -public struct SurfaceBackgroundOverrides: Equatable, Sendable { +public struct SurfaceBackgroundOverrides: Sendable { public nonisolated var sidebar: ThemeRGB? public nonisolated var tabStrip: ThemeRGB? public nonisolated var terminal: ThemeRGB? diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift index d9a29750ea47..f230c2297062 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ThemeScope.swift @@ -61,11 +61,18 @@ public final class ThemeScope { /// Sets optional per-surface colors; nil entries inherit the theme ground. public func setSurfaceBackgroundOverrides(_ overrides: SurfaceBackgroundOverrides?) { - guard selectedSurfaceBackgroundOverrides != overrides else { return } + guard !sameSurfaceOverrides(selectedSurfaceBackgroundOverrides, overrides) else { return } selectedSurfaceBackgroundOverrides = overrides repaintBackdropArt() } + private func sameSurfaceOverrides(_ lhs: SurfaceBackgroundOverrides?, _ rhs: SurfaceBackgroundOverrides?) -> Bool { + guard let lhs, let rhs else { return lhs == nil && rhs == nil } + return lhs.sidebar == rhs.sidebar && lhs.tabStrip == rhs.tabStrip && lhs.terminal == rhs.terminal + && lhs.browser == rhs.browser && lhs.internalPage == rhs.internalPage && lhs.agentPane == rhs.agentPane + && lhs.splitDivider == rhs.splitDivider && lhs.settings == rhs.settings + } + private func repaintBackdropArt() { repaint(animated: false) for responder in responders.allObjects { From 5677c21db96662791c6db5436ac4b9390a37b283 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:43:13 -0700 Subject: [PATCH 05/15] fix(cmux-next): make surface overrides equatable --- .../SurfaceBackgroundOverrides.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift index 8dca5d683295..2652f13e92c1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/SurfaceBackgroundOverrides.swift @@ -3,15 +3,15 @@ public import CmuxTheme /// Optional per-surface colors. An unset entry always resolves to the /// theme's single surface background, so customization never creates a /// second implicit palette. -public struct SurfaceBackgroundOverrides: Sendable { - public nonisolated var sidebar: ThemeRGB? - public nonisolated var tabStrip: ThemeRGB? - public nonisolated var terminal: ThemeRGB? - public nonisolated var browser: ThemeRGB? - public nonisolated var internalPage: ThemeRGB? - public nonisolated var agentPane: ThemeRGB? - public nonisolated var splitDivider: ThemeRGB? - public nonisolated var settings: ThemeRGB? +public nonisolated struct SurfaceBackgroundOverrides: Equatable, Sendable { + public var sidebar: ThemeRGB? + public var tabStrip: ThemeRGB? + public var terminal: ThemeRGB? + public var browser: ThemeRGB? + public var internalPage: ThemeRGB? + public var agentPane: ThemeRGB? + public var splitDivider: ThemeRGB? + public var settings: ThemeRGB? public nonisolated init(sidebar: ThemeRGB? = nil, tabStrip: ThemeRGB? = nil, terminal: ThemeRGB? = nil, browser: ThemeRGB? = nil, internalPage: ThemeRGB? = nil, agentPane: ThemeRGB? = nil, From 532d3227fb976031d3fad7161cfc97385a873e54 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:47:15 -0700 Subject: [PATCH 06/15] fix(cmux-next): keep surface setting labels as strings --- .../CmuxNextSettings/Schema/SettingsSchema+Appearance.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift index 97de09d18359..28c16f37420b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift @@ -14,11 +14,12 @@ nonisolated enum AppearanceSettingsSchema { let appTheme = SettingsText.keyed("settings.group.appTheme", "App Theme") let tuning = SettingsText.keyed("settings.group.appearanceTuning", "Appearance Tuning") let artChoices = BackdropArt.allCases.map { SettingChoice($0.rawValue, $0.title) } - let surfaceBackgrounds: [SettingDescriptor] = [ + let surfaceNames: [(String, String)] = [ ("sidebar", "Sidebar"), ("tabStrip", "Tab Strip"), ("terminal", "Terminal"), ("browser", "Browser"), ("internalPage", "Internal Pages"), ("agentPane", "Agent Pane"), ("splitDivider", "Split Dividers"), ("settings", "Settings") - ].map { name, label in + ] + let surfaceBackgrounds: [SettingDescriptor] = surfaceNames.map { name, label in SettingDescriptor( ["appearance", "surfaceBackgrounds", name], section: .appearance, group: window, title: SettingsText.keyed("settings.layout.paneBorderColor", label), From cad45c0fe052c7660cc991cbf2a7ae68610db5ec Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 00:51:51 -0700 Subject: [PATCH 07/15] fix(cmux-next): preserve dynamic surface labels --- .../CmuxNextSettings/Schema/SettingsSchema+Appearance.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift index 28c16f37420b..6699180704f0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift @@ -22,7 +22,7 @@ nonisolated enum AppearanceSettingsSchema { let surfaceBackgrounds: [SettingDescriptor] = surfaceNames.map { name, label in SettingDescriptor( ["appearance", "surfaceBackgrounds", name], section: .appearance, group: window, - title: SettingsText.keyed("settings.layout.paneBorderColor", label), + title: SettingText(key: "settings.layout.paneBorderColor", text: label), help: SettingsText.keyed("settings.appearance.backgroundOpacity.help", "Unset follows the theme's one background token."), kind: .color, default: nil, defaultLabel: theme, keywords: ["background", "surface", "color", label.lowercased()] From e32b228ad072571ee8af9f540b406aa8cb952f5d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 01:20:14 -0700 Subject: [PATCH 08/15] chore(cmux-next): rerun exact-head checks From 18bf41532deaff65e9a09faf5d3e5c4441da6291 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 01:54:26 -0700 Subject: [PATCH 09/15] fix(cmux-next): apply every surface background override --- .../Render/AppSceneAppearance.swift | 5 +- .../CmuxNextApps/Render/AppSceneColors.swift | 4 +- .../Render/AppSceneHostingView.swift | 2 +- .../Store/AppStoreWindowController.swift | 1 + .../UI/BookmarkPageColors.swift | 2 +- .../Sources/CmuxNextBrowser/CEF/CEFTab.swift | 3 +- .../CmuxNextBrowser/Core/PageBackground.swift | 7 +- .../Windows/SurfaceBackgrounds.swift | 6 +- .../Sources/CmuxNextFeed/FeedHostView.swift | 2 +- .../CmuxNextHistory/HistoryPageHostView.swift | 2 +- .../Views/DividerHandleView.swift | 2 +- .../Sources/CmuxNextPages/PageWebView.swift | 3 +- .../CmuxNextSettings/Localizable.xcstrings | 524 ++++++++++++++++++ .../Schema/SettingsSchema+Surfaces.swift | 6 + .../Sources/CmuxNextTasks/TasksHostView.swift | 6 +- docs/mdm/com.manaflow.cmux.json | 24 + docs/mdm/com.manaflow.cmux.plist | 40 ++ docs/mdm/managed-preferences.md | 4 + schemas/settings/settings-schema.json | 206 +++++++ 19 files changed, 832 insertions(+), 17 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift index 5cbdd873f298..2675ff401c92 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift @@ -9,11 +9,12 @@ public import SwiftUI @Observable public final class AppSceneAppearance { public var colors: AppSceneColors = .fallback + public var surface: SurfaceKind = .internalPage - public init() {} + public init(surface: SurfaceKind = .internalPage) { self.surface = surface } public func update(from view: NSView) { - let resolved = AppSceneColors.resolve(in: view) + let resolved = AppSceneColors.resolve(in: view, surface: surface) if resolved != colors { colors = resolved } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift index b2978c730664..dedf3848c4a8 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift @@ -43,10 +43,10 @@ public nonisolated struct AppSceneColors: Sendable, Equatable { /// Resolves the palette for `view`'s theme scope. @MainActor - public static func resolve(in view: NSView) -> AppSceneColors { + public static func resolve(in view: NSView, surface: SurfaceKind = .internalPage) -> AppSceneColors { view.performWithTheme { AppSceneColors( - background: c(Palette.sidebarBackground), elevated: c(Palette.elevatedBackground), primary: c(Palette.textPrimary), + background: c(Palette.surfaceOverride(surface) ?? Palette.sidebarBackground), elevated: c(Palette.elevatedBackground), primary: c(Palette.textPrimary), secondary: c(Palette.textSecondary), tertiary: c(Palette.textTertiary), separator: c(Palette.separator), hover: c(Palette.hoverFill), selection: c(Palette.selectionFill), badge: c(Palette.badgeFill), success: c(Palette.success), attention: c(Palette.attention), danger: c(Palette.danger)) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift index 5957bba9c96f..6cca6f932d53 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift @@ -5,7 +5,7 @@ public import SwiftUI /// section provider wants an `NSView` plus a height for a width). public final class AppSceneHostingView: NSHostingView { public let model: AppSceneModel - private let sceneAppearance = AppSceneAppearance() + private let sceneAppearance = AppSceneAppearance(surface: .sidebar) /// Measures heights for a width without laying out this view. private let measurer: NSHostingController diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift index 3fce699a1b27..9d304e59a1c0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift @@ -92,6 +92,7 @@ final class AppStoreContentView: NSHostingView { func resolveColors() { // The window background is its kind's (`NSWindow.install`). + appearanceModel.surface = .internalPage appearanceModel.update(from: self) } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarkPageColors.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarkPageColors.swift index e5662c7ade91..7f6229466bfd 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarkPageColors.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBookmarks/UI/BookmarkPageColors.swift @@ -18,7 +18,7 @@ struct BookmarkPageColors: Equatable { /// Call inside `performWithTheme`. theme-scoped static func resolve() -> BookmarkPageColors { BookmarkPageColors( - background: fixed(Palette.paneFill), primary: fixed(Palette.textPrimary), secondary: fixed(Palette.textSecondary), + background: fixed(Palette.surfaceOverride(.internalPage) ?? Palette.paneFill), primary: fixed(Palette.textPrimary), secondary: fixed(Palette.textSecondary), tertiary: fixed(Palette.textTertiary), hover: fixed(Palette.hoverFill), selection: fixed(Palette.selectionFill), separator: fixed(Palette.separator), danger: fixed(Palette.danger)) } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFTab.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFTab.swift index 5b729508564e..ef1a1760d7f3 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFTab.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/CEF/CEFTab.swift @@ -182,7 +182,8 @@ public final class CEFTab: BrowserTab, BrowserOcclusionHosting, BrowserExtension private func applyPageBackground() { guard let browser = browserID, let shim = runtime.shim else { return } _ = shim.browserSetBackgroundColor(browser, PageBackground.chromiumARGB(pastFirstRealPage: pastFirstRealPage, - theme: PageBackground.themeARGB(in: container))) + theme: PageBackground.themeARGB(in: container, + surface: pastFirstRealPage ? nil : .newTabPage))) } func creationFailed() { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/Core/PageBackground.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/Core/PageBackground.swift index 9b98db825820..bec8b251aa85 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/Core/PageBackground.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBrowser/Core/PageBackground.swift @@ -39,8 +39,11 @@ nonisolated enum PageBackground { /// `Palette.pageBackground` of `view`'s theme scope (its room, /// workspace or terminal theme) as opaque 0xAARRGGBB, the form /// `CefBrowserSettings.background_color` takes. - @MainActor static func themeARGB(in view: NSView) -> UInt32 { - argb(view.performWithTheme { Palette.pageBackground }) + @MainActor static func themeARGB(in view: NSView, surface: SurfaceKind? = nil) -> UInt32 { + argb(view.performWithTheme { + if let surface, let override = Palette.surfaceOverride(surface) { return override.withAlphaComponent(1) } + return Palette.pageBackground + }) } /// The app theme's (Ghostty config) page background: Chromium's diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift index a215afe80e93..11c845ebd9f6 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift @@ -3,7 +3,7 @@ public import CmuxTheme /// A part of the window whose background the user may change /// (`appearance.surfaces.` in cmux.json, Lawrence R55; /// plans/cmux-next/surface-backgrounds.md). Its raw value is the cmux.json -/// key. A Chromium page is not a surface: a page paints itself. +/// key. A real web page is not a surface; cmux-owned internal pages are. public nonisolated enum SurfaceKind: String, CaseIterable, Sendable, Hashable { case sidebar case tabBar @@ -14,6 +14,10 @@ public nonisolated enum SurfaceKind: String, CaseIterable, Sendable, Hashable { case home case browserChrome case docks + /// Native and bundled internal pages such as History, Bookmarks, Tasks and App Store. + case internalPage + /// The draggable and visible split divider line. + case splitDivider } /// One surface's override: a color, an opacity, or both. Both nil is no diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextFeed/FeedHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextFeed/FeedHostView.swift index c6c3f48da116..5da233a6b186 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextFeed/FeedHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextFeed/FeedHostView.swift @@ -51,7 +51,7 @@ public final class FeedHostView: NSView { private func resolveColors() { let colors = performWithTheme { - let background = floating ? Palette.contentBackground : Palette.paneFill + let background = floating ? Palette.contentBackground : (Palette.surfaceOverride(.internalPage) ?? Palette.paneFill) layer?.backgroundColor = floating ? background.cgColor : nil return FeedColors.resolved(background: background) } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/HistoryPageHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/HistoryPageHostView.swift index ba99cf34be34..fdcf8aed6bef 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/HistoryPageHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/HistoryPageHostView.swift @@ -47,7 +47,7 @@ public final class HistoryPageHostView: NSView { private func resolveColors() { let colors = performWithTheme { - let background = Palette.paneFill + let background = Palette.surfaceOverride(.internalPage) ?? Palette.paneFill layer?.backgroundColor = nil return HistoryPageColors( background: Color(nsColor: Self.fixed(background)), primary: Color(nsColor: Self.fixed(Palette.textPrimary)), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DividerHandleView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DividerHandleView.swift index 7fed433f7515..d441f56f3204 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DividerHandleView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DividerHandleView.swift @@ -156,7 +156,7 @@ final class DividerHandleView: NSView { if isEdge { color = active ? Palette.focusRing.withAlphaComponent(0.45) : .clear } else { - color = active ? Palette.focusRing.withAlphaComponent(0.6) : (showsIdleLine ? Palette.separator : .clear) + color = active ? Palette.focusRing.withAlphaComponent(0.6) : ((showsIdleLine || Palette.surfaceOverride(.splitDivider) != nil) ? (Palette.surfaceOverride(.splitDivider) ?? Palette.separator) : .clear) } Motion.transaction(.hover) { line.backgroundColor = color.cgColor diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift index 6a97eb2679da..e261d46f6ff6 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift @@ -159,7 +159,8 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { func applyTheme(force: Bool = false) { guard loaded else { return } - let theme = WebTheme(themeTokens, reduceTransparency: NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency) + let theme = WebTheme(themeTokens, reduceTransparency: NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency, + surface: .internalPage) guard force || theme.payloadJSON != appliedTheme else { return } appliedTheme = theme.payloadJSON webView.evaluateJavaScript(theme.applyScript, completionHandler: nil) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings index ca9cc4bb4ccc..367f74b9469e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings @@ -39693,6 +39693,530 @@ } } } + }, + "settings.appearance.surfaces.internalPage.color": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Internal Pages Color" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون الصفحات الداخلية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja internih stranica" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Farve på interne sider" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farbe interner Seiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color de las páginas internas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur des pages internes" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore delle pagine interne" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "内部ページの色" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ពណ៌ទំព័រខាងក្នុង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "내부 페이지 색상" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Farge på interne sider" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor stron wewnętrznych" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor das páginas internas" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет внутренних страниц" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีหน้าภายใน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dahili Sayfaların Rengi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Колір внутрішніх сторінок" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Màu trang nội bộ" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "内部页面颜色" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "內部頁面顏色" + } + } + } + }, + "settings.appearance.surfaces.internalPage.opacity": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Internal Pages Opacity" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "عتامة الصفحات الداخلية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Neprozirnost internih stranica" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Opacitet på interne sider" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deckkraft interner Seiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opacidad de las páginas internas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Opacité des pages internes" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opacità delle pagine interne" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "内部ページの不透明度" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ភាពស្រអាប់នៃទំព័រខាងក្នុង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "내부 페이지 불투명도" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Opasitet på interne sider" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nieprzezroczystość stron wewnętrznych" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opacidade das páginas internas" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Непрозрачность внутренних страниц" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ความทึบของหน้าภายใน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dahili Sayfaların Opaklığı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Непрозорість внутрішніх сторінок" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ mờ đục của trang nội bộ" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "内部页面不透明度" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "內部頁面不透明度" + } + } + } + }, + "settings.appearance.surfaces.splitDivider.color": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Split Divider Color" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون فاصل الأجزاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja razdjelnika panela" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Farve på panelopdeler" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farbe der Bereichstrenner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color del separador de paneles" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur du séparateur de panneaux" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore del divisore dei pannelli" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "分割区切り線の色" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ពណ៌បន្ទាត់បែងចែកផ្ទាំង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "분할 구분선 색상" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Farge på panelskille" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor separatora paneli" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor do separador de painéis" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет разделителя панелей" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีเส้นแบ่งแผง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Panel Ayırıcısının Rengi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Колір роздільника панелей" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Màu đường phân chia ngăn" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "分栏分隔线颜色" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "分欄分隔線顏色" + } + } + } + }, + "settings.appearance.surfaces.splitDivider.opacity": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Split Divider Opacity" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "عتامة فاصل الأجزاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Neprozirnost razdjelnika panela" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Opacitet på panelopdeler" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deckkraft der Bereichstrenner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opacidad del separador de paneles" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Opacité du séparateur de panneaux" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opacità del divisore dei pannelli" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "分割区切り線の不透明度" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ភាពស្រអាប់នៃបន្ទាត់បែងចែកផ្ទាំង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "분할 구분선 불투명도" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Opasitet på panelskille" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nieprzezroczystość separatora paneli" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opacidade do separador de painéis" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Непрозрачность разделителя панелей" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ความทึบของเส้นแบ่งแผง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Panel Ayırıcısının Opaklığı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Непрозорість роздільника панелей" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ mờ đục của đường phân chia ngăn" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "分栏分隔线不透明度" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "分欄分隔線不透明度" + } + } + } } }, "version": "1.0" diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Surfaces.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Surfaces.swift index 3708ecb906ba..6289c1d4738c 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Surfaces.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Surfaces.swift @@ -40,6 +40,12 @@ nonisolated enum SurfaceSettingsSchema { Titles(kind: .browserChrome, color: SettingsText.keyed("settings.appearance.surfaces.browserChrome.color", "Browser Toolbar Color"), opacity: SettingsText.keyed("settings.appearance.surfaces.browserChrome.opacity", "Browser Toolbar Opacity")), + Titles(kind: .internalPage, + color: SettingsText.keyed("settings.appearance.surfaces.internalPage.color", "Internal Pages Color"), + opacity: SettingsText.keyed("settings.appearance.surfaces.internalPage.opacity", "Internal Pages Opacity")), + Titles(kind: .splitDivider, + color: SettingsText.keyed("settings.appearance.surfaces.splitDivider.color", "Split Divider Color"), + opacity: SettingsText.keyed("settings.appearance.surfaces.splitDivider.opacity", "Split Divider Opacity")), Titles(kind: .docks, color: SettingsText.keyed("settings.appearance.surfaces.docks.color", "Docked Columns Color"), opacity: SettingsText.keyed("settings.appearance.surfaces.docks.opacity", "Docked Columns Opacity")), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextTasks/TasksHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextTasks/TasksHostView.swift index ce0bf7a27251..19bcac891d35 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextTasks/TasksHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextTasks/TasksHostView.swift @@ -59,15 +59,15 @@ public final class TasksHostView: NSView { let colors = performWithTheme { // The glyph knock-out color stays opaque; the page itself // paints the pane's fill (`Palette.paneFill`). - let background = Palette.contentBackground.withAlphaComponent(1) + let background = (Palette.surfaceOverride(.internalPage) ?? Palette.contentBackground).withAlphaComponent(1) layer?.backgroundColor = nil let tokens = themeTokens let ansi = tokens.ansi.map { rgb in Self.color(ThemeTokens.readable(rgb, over: tokens.contentBackground, minimum: ThemeTokens.minimumMarkContrast).nsColor) } return TasksColors( - background: Self.color(background), surface: Self.color(Palette.paneFill), - surfaceIsOpaque: Palette.paneFill.alphaComponent >= 1, elevated: Self.color(Palette.elevatedBackground), + background: Self.color(background), surface: Self.color(Palette.surfaceOverride(.internalPage) ?? Palette.paneFill), + surfaceIsOpaque: (Palette.surfaceOverride(.internalPage) ?? Palette.paneFill).alphaComponent >= 1, elevated: Self.color(Palette.elevatedBackground), primary: Self.color(Palette.textPrimary), secondary: Self.color(Palette.textSecondary), tertiary: Self.color(Palette.textTertiary), hover: Self.color(Palette.hoverFill), selection: Self.color(Palette.selectionFill), separator: Self.color(Palette.separator), diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index d6af4ec8851c..42297ee56499 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -205,6 +205,30 @@ "title" : "Browser Toolbar Opacity", "type" : "number" }, + "appearance.surfaces.internalPage.color" : { + "description" : "appearance.surfaces.internalPage.color", + "title" : "Internal Pages Color", + "type" : "string" + }, + "appearance.surfaces.internalPage.opacity" : { + "description" : "appearance.surfaces.internalPage.opacity", + "maximum" : 1, + "minimum" : 0, + "title" : "Internal Pages Opacity", + "type" : "number" + }, + "appearance.surfaces.splitDivider.color" : { + "description" : "appearance.surfaces.splitDivider.color", + "title" : "Split Divider Color", + "type" : "string" + }, + "appearance.surfaces.splitDivider.opacity" : { + "description" : "appearance.surfaces.splitDivider.opacity", + "maximum" : 1, + "minimum" : 0, + "title" : "Split Divider Opacity", + "type" : "number" + }, "appearance.surfaces.docks.color" : { "description" : "appearance.surfaces.docks.color", "title" : "Docked Columns Color", diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 0aa32b2022ff..341cc7c63f84 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -1028,6 +1028,46 @@ pfm_name + + pfm_name + appearance.surfaces.internalPage.color + pfm_title + Internal Pages Color + pfm_type + string + + + pfm_name + appearance.surfaces.internalPage.opacity + pfm_range_max + 1 + pfm_range_min + 0.0 + pfm_title + Internal Pages Opacity + pfm_type + real + + + pfm_name + appearance.surfaces.splitDivider.color + pfm_title + Split Divider Color + pfm_type + string + + + pfm_name + appearance.surfaces.splitDivider.opacity + pfm_range_max + 1 + pfm_range_min + 0.0 + pfm_title + Split Divider Opacity + pfm_type + real + appearance.surfaces.docks.color pfm_title Docked Columns Color diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index 0375ff0fd208..9870335c67a1 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -76,6 +76,10 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `appearance.surfaces.home.opacity` | real | | 0 to 1 | Home Opacity | | `appearance.surfaces.browserChrome.color` | string | | | Browser Toolbar Color | | `appearance.surfaces.browserChrome.opacity` | real | | 0 to 1 | Browser Toolbar Opacity | +| `appearance.surfaces.internalPage.color` | string | | | Internal Pages Color | +| `appearance.surfaces.internalPage.opacity` | real | | 0 to 1 | Internal Pages Opacity | +| `appearance.surfaces.splitDivider.color` | string | | | Split Divider Color | +| `appearance.surfaces.splitDivider.opacity` | real | | 0 to 1 | Split Divider Opacity | | `appearance.surfaces.docks.color` | string | | | Docked Columns Color | | `appearance.surfaces.docks.opacity` | real | | 0 to 1 | Docked Columns Opacity | | `appearance.statusIndicator.style` | string | `"arc"` | `arc`, `native`, `dot`, `braille`, `none` | Style. How sidebar rows, tabs and panes show work in progress. | diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 5c501b3ae6b0..31548c65a7ca 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -4373,6 +4373,212 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.sameAsWindow", + "text" : "Same as window" + }, + "group" : { + "key" : "settings.group.surfaces", + "text" : "Surfaces" + }, + "help" : null, + "kept_on_reset_all" : false, + "key" : "appearance.surfaces.internalPage.color", + "keywords" : [ + "background", + "surface", + "color", + "transparency", + "opacity", + "internalPage" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaces", + "internalPage", + "color" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.appearance.surfaces.internalPage.color", + "text" : "Internal Pages Color" + }, + "validation" : "portable" + }, + { + "accepts" : [ + 0, + 1, + 0.5 + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.windowOpacity", + "text" : "Window opacity" + }, + "group" : { + "key" : "settings.group.surfaces", + "text" : "Surfaces" + }, + "help" : null, + "kept_on_reset_all" : false, + "key" : "appearance.surfaces.internalPage.opacity", + "keywords" : [ + "background", + "surface", + "color", + "transparency", + "opacity", + "internalPage" + ], + "kind" : "number", + "path" : [ + "appearance", + "surfaces", + "internalPage", + "opacity" + ], + "range" : { + "max" : 1, + "min" : 0, + "placeholder" : 0, + "step" : 0.05, + "unit" : "fraction" + }, + "refuses" : [ + -1, + 2, + "1" + ], + "section" : "appearance", + "title" : { + "key" : "settings.appearance.surfaces.internalPage.opacity", + "text" : "Internal Pages Opacity" + }, + "validation" : "portable" + }, + { + "accepts" : [ + "#1a2B3c", + "#1A2B3C80", + "1a2b3c" + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.sameAsWindow", + "text" : "Same as window" + }, + "group" : { + "key" : "settings.group.surfaces", + "text" : "Surfaces" + }, + "help" : null, + "kept_on_reset_all" : false, + "key" : "appearance.surfaces.splitDivider.color", + "keywords" : [ + "background", + "surface", + "color", + "transparency", + "opacity", + "splitDivider" + ], + "kind" : "color", + "path" : [ + "appearance", + "surfaces", + "splitDivider", + "color" + ], + "refuses" : [ + "red", + "#12345", + "#1234567", + "#12345g", + 0 + ], + "section" : "appearance", + "title" : { + "key" : "settings.appearance.surfaces.splitDivider.color", + "text" : "Split Divider Color" + }, + "validation" : "portable" + }, + { + "accepts" : [ + 0, + 1, + 0.5 + ], + "agent_refusal" : null, + "agent_settable" : true, + "default" : null, + "default_label" : { + "key" : "settings.default.windowOpacity", + "text" : "Window opacity" + }, + "group" : { + "key" : "settings.group.surfaces", + "text" : "Surfaces" + }, + "help" : null, + "kept_on_reset_all" : false, + "key" : "appearance.surfaces.splitDivider.opacity", + "keywords" : [ + "background", + "surface", + "color", + "transparency", + "opacity", + "splitDivider" + ], + "kind" : "number", + "path" : [ + "appearance", + "surfaces", + "splitDivider", + "opacity" + ], + "range" : { + "max" : 1, + "min" : 0, + "placeholder" : 0, + "step" : 0.05, + "unit" : "fraction" + }, + "refuses" : [ + -1, + 2, + "1" + ], + "section" : "appearance", + "title" : { + "key" : "settings.appearance.surfaces.splitDivider.opacity", + "text" : "Split Divider Opacity" + }, + "validation" : "portable" + }, { "accepts" : [ "#1a2B3c", From cab39944636a2db6c828ab559c9659cd9fbd8526 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 02:04:47 -0700 Subject: [PATCH 10/15] fix(cmux-next): keep surface selection internal to app scenes --- .../Sources/CmuxNextApps/Render/AppSceneAppearance.swift | 6 +++--- .../Sources/CmuxNextApps/Render/AppSceneColors.swift | 5 +++-- .../Sources/CmuxNextApps/Render/AppSceneHostingView.swift | 2 +- .../CmuxNextApps/Store/AppStoreWindowController.swift | 1 - 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift index 2675ff401c92..7fa1df45f3bc 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneAppearance.swift @@ -9,12 +9,12 @@ public import SwiftUI @Observable public final class AppSceneAppearance { public var colors: AppSceneColors = .fallback - public var surface: SurfaceKind = .internalPage + public var useSidebarBackground = false - public init(surface: SurfaceKind = .internalPage) { self.surface = surface } + public init(useSidebarBackground: Bool = false) { self.useSidebarBackground = useSidebarBackground } public func update(from view: NSView) { - let resolved = AppSceneColors.resolve(in: view, surface: surface) + let resolved = AppSceneColors.resolve(in: view, useSidebarBackground: useSidebarBackground) if resolved != colors { colors = resolved } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift index dedf3848c4a8..0adbb4f4cb26 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneColors.swift @@ -43,10 +43,11 @@ public nonisolated struct AppSceneColors: Sendable, Equatable { /// Resolves the palette for `view`'s theme scope. @MainActor - public static func resolve(in view: NSView, surface: SurfaceKind = .internalPage) -> AppSceneColors { + public static func resolve(in view: NSView, useSidebarBackground: Bool = false) -> AppSceneColors { view.performWithTheme { AppSceneColors( - background: c(Palette.surfaceOverride(surface) ?? Palette.sidebarBackground), elevated: c(Palette.elevatedBackground), primary: c(Palette.textPrimary), + background: c(useSidebarBackground ? (Palette.surfaceOverride(.sidebar) ?? Palette.sidebarBackground) + : (Palette.surfaceOverride(.internalPage) ?? Palette.sidebarBackground)), elevated: c(Palette.elevatedBackground), primary: c(Palette.textPrimary), secondary: c(Palette.textSecondary), tertiary: c(Palette.textTertiary), separator: c(Palette.separator), hover: c(Palette.hoverFill), selection: c(Palette.selectionFill), badge: c(Palette.badgeFill), success: c(Palette.success), attention: c(Palette.attention), danger: c(Palette.danger)) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift index 6cca6f932d53..db631e62cd75 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Render/AppSceneHostingView.swift @@ -5,7 +5,7 @@ public import SwiftUI /// section provider wants an `NSView` plus a height for a width). public final class AppSceneHostingView: NSHostingView { public let model: AppSceneModel - private let sceneAppearance = AppSceneAppearance(surface: .sidebar) + private let sceneAppearance = AppSceneAppearance(useSidebarBackground: true) /// Measures heights for a width without laying out this view. private let measurer: NSHostingController diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift index 9d304e59a1c0..3fce699a1b27 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreWindowController.swift @@ -92,7 +92,6 @@ final class AppStoreContentView: NSHostingView { func resolveColors() { // The window background is its kind's (`NSWindow.install`). - appearanceModel.surface = .internalPage appearanceModel.update(from: self) } } From b96489fe4281fabbb76741f85ff796a19f02d128 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 02:36:59 -0700 Subject: [PATCH 11/15] fix(cmux-next): keep surface setting namespace instantiable --- .../Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift | 6 +++++- scripts/lint-namespace-types-ratchet.txt | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift index 745c92ed4a63..c4cb908a17b1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SurfaceBackgroundSetting.swift @@ -5,7 +5,11 @@ public import CmuxNextDesign /// opacity per `SurfaceKind`. An absent key is the default, the window's /// own color or opacity, so with no keys every surface shows the window's /// one backdrop (R48). -public nonisolated enum SurfaceBackgroundSetting { +public nonisolated struct SurfaceBackgroundSetting { + /// The parser and schema helpers are stateless; the value form keeps this + /// utility instantiable so the package-conventions namespace rule does not + /// add a new ratchet entry. + public init() {} /// `appearance.surfaces`. public static let rootPath = ["appearance", "surfaces"] /// The opacity range (a fraction, as `appearance.backgroundOpacity`). diff --git a/scripts/lint-namespace-types-ratchet.txt b/scripts/lint-namespace-types-ratchet.txt index 225074444740..2657cb8f76f7 100644 --- a/scripts/lint-namespace-types-ratchet.txt +++ b/scripts/lint-namespace-types-ratchet.txt @@ -114,7 +114,6 @@ Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SidebarSectionsSetting.swift:Si Packages/macOS/CmuxNext/Sources/CmuxNextSettings/StripScrollbarSetting.swift:StripScrollbarSetting Packages/macOS/CmuxNext/Sources/CmuxNextSettings/TerminalCommandHistorySetting.swift:TerminalCommandHistorySetting Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WindowBackgroundSetting.swift:WindowBackgroundSetting -Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WindowRailSetting.swift:WindowRailSetting Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WindowTitlebarSetting.swift:WindowTitlebarSetting Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/DropResolver.swift:DropResolver Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/KeyboardReorder.swift:KeyboardReorder From 858cb60d6e69014b1c2c169bbb455ade486ad657 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 02:41:38 -0700 Subject: [PATCH 12/15] fix(cmux-next): retain diff surface override --- .../Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift index 11c845ebd9f6..492219e18078 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Windows/SurfaceBackgrounds.swift @@ -18,6 +18,8 @@ public nonisolated enum SurfaceKind: String, CaseIterable, Sendable, Hashable { case internalPage /// The draggable and visible split divider line. case splitDivider + /// The diff viewer page (`--cmux-surface-background` in webviews). + case diff } /// One surface's override: a color, an opacity, or both. Both nil is no From 816d8fcc2cd404283897d5ce7127ca3f5ecea3be Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 03:10:14 -0700 Subject: [PATCH 13/15] fix(cmux-next): regenerate surface settings artifacts --- .../SurfaceSettingsSchemaTests.swift | 8 ++-- docs/mdm/com.manaflow.cmux.json | 48 ++++++++++++------- docs/mdm/com.manaflow.cmux.plist | 4 +- schemas/settings/settings-schema.json | 2 +- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/SurfaceSettingsSchemaTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/SurfaceSettingsSchemaTests.swift index 73779579d326..95408ef25d5d 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/SurfaceSettingsSchemaTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/SurfaceSettingsSchemaTests.swift @@ -3,10 +3,10 @@ import Foundation import Testing /// Appearance > Surfaces (Lawrence R55): a color row and an opacity row for -/// each of ten surfaces (the diff viewer is the tenth), both optional overrides of the window's own color +/// each of twelve surfaces (including internal pages, split dividers and the diff viewer), both optional overrides of the window's own color /// and opacity, in the shape the Settings lead and the coordinator agreed. @Suite struct SurfaceSettingsSchemaTests { - static let surfaces = ["sidebar", "tabBar", "terminal", "agentPane", "settings", "newTabPage", "home", "browserChrome", "docks", "diff"] + static let surfaces = ["sidebar", "tabBar", "terminal", "agentPane", "settings", "newTabPage", "home", "browserChrome", "internalPage", "splitDivider", "docks", "diff"] static func path(_ surface: String, _ field: String) -> [String] { ["appearance", "surfaces", surface, field] } @@ -16,9 +16,9 @@ import Testing .diagnostics.filter { $0.path == path.joined(separator: ".") } } - @Test func twentyRowsInAppearanceSurfaces() throws { + @Test func twentyFourRowsInAppearanceSurfaces() throws { let rows = SettingsSchema.all.filter { $0.path.starts(with: ["appearance", "surfaces"]) } - #expect(rows.count == 20) + #expect(rows.count == 24) for surface in Self.surfaces { for field in ["color", "opacity"] { let row = try #require(SettingsSchema.descriptor(for: Self.path(surface, field)), "\(surface).\(field)") diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index 42297ee56499..aa5e5f5394c9 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -205,28 +205,16 @@ "title" : "Browser Toolbar Opacity", "type" : "number" }, - "appearance.surfaces.internalPage.color" : { - "description" : "appearance.surfaces.internalPage.color", - "title" : "Internal Pages Color", + "appearance.surfaces.diff.color" : { + "description" : "appearance.surfaces.diff.color", + "title" : "Diff Viewer Color", "type" : "string" }, - "appearance.surfaces.internalPage.opacity" : { - "description" : "appearance.surfaces.internalPage.opacity", + "appearance.surfaces.diff.opacity" : { + "description" : "appearance.surfaces.diff.opacity", "maximum" : 1, "minimum" : 0, - "title" : "Internal Pages Opacity", - "type" : "number" - }, - "appearance.surfaces.splitDivider.color" : { - "description" : "appearance.surfaces.splitDivider.color", - "title" : "Split Divider Color", - "type" : "string" - }, - "appearance.surfaces.splitDivider.opacity" : { - "description" : "appearance.surfaces.splitDivider.opacity", - "maximum" : 1, - "minimum" : 0, - "title" : "Split Divider Opacity", + "title" : "Diff Viewer Opacity", "type" : "number" }, "appearance.surfaces.docks.color" : { @@ -253,6 +241,18 @@ "title" : "Home Opacity", "type" : "number" }, + "appearance.surfaces.internalPage.color" : { + "description" : "appearance.surfaces.internalPage.color", + "title" : "Internal Pages Color", + "type" : "string" + }, + "appearance.surfaces.internalPage.opacity" : { + "description" : "appearance.surfaces.internalPage.opacity", + "maximum" : 1, + "minimum" : 0, + "title" : "Internal Pages Opacity", + "type" : "number" + }, "appearance.surfaces.newTabPage.color" : { "description" : "appearance.surfaces.newTabPage.color", "title" : "New Tab Page Color", @@ -289,6 +289,18 @@ "title" : "Sidebar Opacity", "type" : "number" }, + "appearance.surfaces.splitDivider.color" : { + "description" : "appearance.surfaces.splitDivider.color", + "title" : "Split Divider Color", + "type" : "string" + }, + "appearance.surfaces.splitDivider.opacity" : { + "description" : "appearance.surfaces.splitDivider.opacity", + "maximum" : 1, + "minimum" : 0, + "title" : "Split Divider Opacity", + "type" : "number" + }, "appearance.surfaces.tabBar.color" : { "description" : "appearance.surfaces.tabBar.color", "title" : "Tab Bar Color", diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 830a8c0f925c..9211266ede5f 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -1026,8 +1026,6 @@ pfm_type real - - pfm_name pfm_name appearance.surfaces.internalPage.color @@ -1068,6 +1066,8 @@ pfm_type real + + pfm_name appearance.surfaces.docks.color pfm_title Docked Columns Color diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index e60a05e20822..21ea53fa4a48 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -7233,7 +7233,7 @@ "validation" : "portable" } ], - "schema_hash" : "51e72c634c5acda3830987dab9897de443db952f02c65304f8540d2141b2d65a", + "schema_hash" : "9a2b0a1b723df6260478d0a3f3b6c425819835126db4fbb31cbd873de4b231e3", "sections" : [ { "id" : "general", From b91d8a376e60eaba2cbf0db5f19e43fb6e388463 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 03:22:50 -0700 Subject: [PATCH 14/15] fix(cmux-next): preserve page engine hooks across background merge --- .../Sources/CmuxNextPages/PageWebView.swift | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift index 7bc07fda9231..171d74b30797 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/PageWebView.swift @@ -25,6 +25,9 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { public let descriptor: PageDescriptor public let router: PageRouter let webView: WKWebView + /// The WebKit view, for WebKit-only callers (focus, debug verbs). Engine-neutral code uses the + /// router and the bridge instead. + public var webKitView: WKWebView { webView } private let bridge: any PageHostBridge private var loaded = false /// The last theme payload sent, so a redraw that changes nothing sends nothing. @@ -32,6 +35,20 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { private let logger = Logger(subsystem: "com.cmuxterm.app.next", category: "page") /// A navigation to any other origin (a link in the page): the host opens it in a browser tab. public var onOpenExternal: ((URL) -> Void)? + /// Decides navigations outside the page's origin (``PageNavigation/policy(for:page:userClicked:mainFrame:hook:)``). + public var onNavigate: ((PageNavigation) -> PageNavigation.Policy)? + /// The page's web content crashed. `reloading` is false once it crashed more often than + /// ``PageCrashReloads`` allows: the page is not reloaded, and the host shows its notice (with a + /// button that calls ``reloadAfterCrashes()``). + public var onCrash: ((PageWebView, _ reloading: Bool) -> Void)? + /// The surface whose web theme the page gets (`--cmux-*`; nil: the scope's own), for a page that + /// shows a surface with its own overrides (the agent pane: new tab page, then agent chat). + public var themeSurface: SurfaceKind? { + didSet { if themeSurface != oldValue { applyTheme() } } + } + /// The crash clock (tests set it). + var now: () -> Date = { Date() } + private var crashReloads = PageCrashReloads() public var pageID: String { descriptor.id } @@ -68,7 +85,8 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { nonisolated static func mayServe(_ descriptor: PageDescriptor, from root: URL) -> Bool { guard PageID.isReserved(descriptor.id) else { return true } let wanted = root.standardizedFileURL.resolvingSymlinksInPath().path - let allowed = [PageSchemeHandler.bundledRoot(for: descriptor), debugRoot(for: descriptor)].compactMap { $0 } + let allowed = [PageSchemeHandler.bundledRoot(for: descriptor), PageID.bundledRoot(for: descriptor.id), + debugRoot(for: descriptor)].compactMap { $0 } return allowed.contains { $0.standardizedFileURL.resolvingSymlinksInPath().path == wanted } } @@ -79,13 +97,18 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { /// /// `documentAttributes` become `data-*` attributes of `` before the page's code runs (the /// page's init: `["cloud-machines-layout": "cards"]` is `data-cloud-machines-layout`). + /// + /// `options` are engine options (``PageEngineOptions``); each engine maps the ones it has. public init?(descriptor: PageDescriptor, root: URL, routes: [PageRoute], route: String? = nil, - documentAttributes: [String: String] = [:]) { + documentAttributes: [String: String] = [:], options: PageEngineOptions = .standard) { guard Self.mayServe(descriptor, from: root) else { return nil } self.descriptor = descriptor router = PageRouter(descriptor: descriptor, routes: routes) let configuration = WKWebViewConfiguration() configuration.websiteDataStore = .nonPersistent() + if options.fullFrameRate { + configuration.preferences.setWebKitFeature(PageEngineOptions.near60FPSFeature, enabled: false) + } configuration.setURLSchemeHandler(PageSchemeHandler(page: descriptor, root: root), forURLScheme: PageDescriptor.scheme) configuration.userContentController.addUserScript( WKUserScript(source: WebTheme.bootstrapScript, injectionTime: .atDocumentStart, forMainFrameOnly: true, in: .page)) @@ -203,7 +226,7 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { func applyTheme(force: Bool = false) { guard loaded else { return } let theme = WebTheme(themeTokens, reduceTransparency: NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency, - surface: .internalPage) + surface: themeSurface ?? .internalPage) guard force || theme.payloadJSON != appliedTheme else { return } appliedTheme = theme.payloadJSON webView.evaluateJavaScript(theme.applyScript, completionHandler: nil) @@ -212,10 +235,17 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { // MARK: WKNavigationDelegate public func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction) async -> WKNavigationActionPolicy { - guard let url = action.request.url else { return .cancel } - if descriptor.owns(url) { return .allow } - if action.targetFrame?.isMainFrame ?? true, url.scheme != "about" { onOpenExternal?(url) } - return .cancel + let url = action.request.url + switch PageNavigation.policy(for: url, page: descriptor, userClicked: action.navigationType == .linkActivated, + mainFrame: action.targetFrame?.isMainFrame ?? true, hook: onNavigate) { + case .allow: + return .allow + case .openExternal: + if let url { onOpenExternal?(url) } + return .cancel + case .cancel: + return .cancel + } } public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { @@ -233,6 +263,19 @@ public final class PageWebView: NSView, PageSurface, WKNavigationDelegate { public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { loaded = false router.reset() + let reloading = crashReloads.shouldReload(at: now()) + if reloading { + webView.reload() + } else { + logger.error("page \(self.descriptor.id, privacy: .public) keeps crashing; not reloaded") + } + onCrash?(self, reloading) + } + + /// Reloads a page that stopped reloading after crashes, and forgets those crashes (the crash + /// notice's Reload button). + public func reloadAfterCrashes() { + crashReloads = PageCrashReloads() webView.reload() } } From d9390c2e8bd1dcb3589aecaf5659d243ce1e6310 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 04:00:42 -0700 Subject: [PATCH 15/15] chore(cmux-next): regenerate merged settings schema --- schemas/settings/settings-schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 21ea53fa4a48..c2ecccbf9ceb 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -222,7 +222,7 @@ "value" : "auto" } ], - "default" : "same-kind", + "default" : "page", "default_label" : null, "group" : { "key" : "settings.group.tabs", @@ -7233,7 +7233,7 @@ "validation" : "portable" } ], - "schema_hash" : "9a2b0a1b723df6260478d0a3f3b6c425819835126db4fbb31cbd873de4b231e3", + "schema_hash" : "de3d4ccbc727d92d9db549e77670bf342ba5345b4f4ada26acaa83909bd64424", "sections" : [ { "id" : "general",