From e60b062519f5c4f3a1bddc1be87c0ff5b6cb382c Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 11 May 2026 19:30:36 -0700 Subject: [PATCH 1/3] Expose renderer-owned terminal background invariant Claude Code paints explicit ANSI background cells for its input box and statusline. The regression captures the expected backdrop ownership boundary for solid opaque terminals before changing the production path. Constraint: Repository policy forbids local test execution; this commit is intentionally expected to fail before the fix. Confidence: high Scope-risk: narrow Directive: Keep opaque terminal backgrounds renderer-owned unless blur or translucency requires the host compositor. Tested: Not run locally per repository policy; regression is expected to be red before the fix. Not-tested: Local XCTest execution. --- cmuxTests/WindowAppearanceSnapshotTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cmuxTests/WindowAppearanceSnapshotTests.swift b/cmuxTests/WindowAppearanceSnapshotTests.swift index 0396108065ec..c404c56ee1cb 100644 --- a/cmuxTests/WindowAppearanceSnapshotTests.swift +++ b/cmuxTests/WindowAppearanceSnapshotTests.swift @@ -110,6 +110,22 @@ final class WindowAppearanceSnapshotTests: XCTestCase { XCTAssertEqual(plan.windowBackgroundColor.hexString(includeAlpha: true), "#272822FF") } + func testOpaqueUnblurredTerminalBackgroundIsRendererOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 1.0, + backgroundBlur: .disabled + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .ghosttyRendererOwnedBackgroundImage) + } + func testDebugBackgroundGlassUsesWindowGlassPhase() { let snapshot = makeSnapshot( unifySurfaceBackdrops: false, From caa37c5dcdb41d82f30352773fdeb23d77f7ad11 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 11 May 2026 19:34:32 -0700 Subject: [PATCH 2/3] Keep opaque terminal backgrounds in the renderer cmux was forcing Ghostty to leave every default terminal background transparent and then filling that area from the host window. That split default cells and explicit ANSI background cells across different compositor paths, which made Claude Code chrome show a visible seam. The policy now keeps solid opaque, unblurred terminal backgrounds inside Ghostty's renderer and reserves host-layer ownership for translucent or blurred terminal backgrounds where the macOS compositor is required. Constraint: background-opacity and background-blur still need host-layer ownership for compositor effects. Rejected: Tune the fallback theme palette | this would not remove the split renderer/host compositing path. Confidence: high Scope-risk: moderate Directive: Do not force macos-background-from-layer for opaque unblurred terminals; explicit ANSI cell backgrounds must share the renderer path with default cells. Tested: git diff --check Not-tested: Local XCTest/build execution per repository policy; CI pending. --- Sources/GhosttyTerminalView.swift | 36 +++++++++++++------ .../Windowing/WindowAppearanceSnapshot.swift | 22 ++++++++++++ cmuxTests/WindowAppearanceSnapshotTests.swift | 5 ++- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 599dee0c4961..e7e3669f4806 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2098,8 +2098,9 @@ class GhosttyApp { return } + let fallbackShouldUseHostLayerBackground = usesHostLayerBackground(for: fallbackConfig) loadInlineGhosttyConfig( - "macos-background-from-layer = true", + "macos-background-from-layer = \(fallbackShouldUseHostLayerBackground)", into: fallbackConfig, prefix: "cmux-renderer-bg", logLabel: "renderer background (fallback)" @@ -2112,7 +2113,7 @@ class GhosttyApp { ) loadCmuxOwnedGhosttyKeybindOverrides(fallbackConfig) let fallbackRenderingModeChanged = setUsesHostLayerBackground( - true, + fallbackShouldUseHostLayerBackground, source: "initialize.fallbackConfig" ) ghostty_config_finalize(fallbackConfig) @@ -2248,15 +2249,17 @@ class GhosttyApp { } #endif loadCJKFontFallbackIfNeeded(config) + let shouldUseHostLayerBackground = usesHostLayerBackground(for: config) let renderingModeChanged = setUsesHostLayerBackground( - true, + shouldUseHostLayerBackground, source: "loadDefaultConfigFilesWithLegacyFallback" ) - // Let cmux own the window-level backdrop once, while Ghostty keeps - // rendering text, cell backgrounds, and background images. This avoids - // separate translucent fills for terminal and chrome surfaces. + // Let Ghostty paint solid opaque terminal backgrounds so default cells + // and explicit ANSI background cells share one renderer/compositor path. + // Host-layer ownership remains required for translucent and blurred + // terminal backgrounds. loadInlineGhosttyConfig( - "macos-background-from-layer = true", + "macos-background-from-layer = \(shouldUseHostLayerBackground)", into: config, prefix: "cmux-renderer-bg", logLabel: "renderer background" @@ -3214,10 +3217,7 @@ class GhosttyApp { let resolvedCursorText = ghosttyColorValue(from: config, key: "cursor-text", fallback: baseline.cursorTextColor) let resolvedSelectionBackground = ghosttyColorValue(from: config, key: "selection-background", fallback: baseline.selectionBackground) let resolvedSelectionForeground = ghosttyColorValue(from: config, key: "selection-foreground", fallback: baseline.selectionForeground) - var opacity = baseline.backgroundOpacity - let opacityKey = "background-opacity" - _ = ghostty_config_get(config, &opacity, opacityKey, UInt(opacityKey.lengthOfBytes(using: .utf8))) - opacity = min(1.0, max(0.0, opacity)) + let opacity = defaultBackgroundOpacityValue(from: config) let backgroundBlur = defaultBackgroundBlurValue(from: config) applyDefaultBackground( color: resolvedColor, @@ -3234,6 +3234,20 @@ class GhosttyApp { ) } + private func defaultBackgroundOpacityValue(from config: ghostty_config_t) -> Double { + var opacity = Self.fallbackAppearanceConfig.backgroundOpacity + let key = "background-opacity" + _ = ghostty_config_get(config, &opacity, key, UInt(key.lengthOfBytes(using: .utf8))) + return Double(WindowAppearanceSnapshot.clampedOpacity(opacity)) + } + + private func usesHostLayerBackground(for config: ghostty_config_t) -> Bool { + WindowAppearanceSnapshot.usesHostLayerBackground( + backgroundOpacity: defaultBackgroundOpacityValue(from: config), + backgroundBlur: defaultBackgroundBlurValue(from: config) + ) + } + private func defaultBackgroundBlurValue(from config: ghostty_config_t) -> GhosttyBackgroundBlur { var value: Int16 = 0 let key = "background-blur" diff --git a/Sources/Windowing/WindowAppearanceSnapshot.swift b/Sources/Windowing/WindowAppearanceSnapshot.swift index 2b809560fc92..5468f1ef8459 100644 --- a/Sources/Windowing/WindowAppearanceSnapshot.swift +++ b/Sources/Windowing/WindowAppearanceSnapshot.swift @@ -285,6 +285,28 @@ struct WindowAppearanceSnapshot { usesHostLayerBackground ? .windowHostBackdrop : .ghosttyRendererOwnedBackgroundImage } + static func usesHostLayerBackground( + backgroundOpacity: Double, + backgroundBlur: GhosttyBackgroundBlur + ) -> Bool { + if backgroundBlur != .disabled { + return true + } + return clampedOpacity(backgroundOpacity) < 0.999 + } + + static func terminalRenderingMode( + backgroundOpacity: Double, + backgroundBlur: GhosttyBackgroundBlur + ) -> GhosttyTerminalBackdropRenderingMode { + terminalRenderingMode( + usesHostLayerBackground: usesHostLayerBackground( + backgroundOpacity: backgroundOpacity, + backgroundBlur: backgroundBlur + ) + ) + } + var compositedTerminalBackgroundColor: NSColor { terminalBackgroundColor.withAlphaComponent(terminalBackgroundOpacity) } diff --git a/cmuxTests/WindowAppearanceSnapshotTests.swift b/cmuxTests/WindowAppearanceSnapshotTests.swift index c404c56ee1cb..7652ef6231a4 100644 --- a/cmuxTests/WindowAppearanceSnapshotTests.swift +++ b/cmuxTests/WindowAppearanceSnapshotTests.swift @@ -153,7 +153,10 @@ final class WindowAppearanceSnapshotTests: XCTestCase { terminalBackgroundColor: NSColor(hex: "#272822") ?? .black, terminalBackgroundOpacity: backgroundOpacity, terminalBackgroundBlur: backgroundBlur, - terminalRenderingMode: .windowHostBackdrop, + terminalRenderingMode: WindowAppearanceSnapshot.terminalRenderingMode( + backgroundOpacity: Double(backgroundOpacity), + backgroundBlur: backgroundBlur + ), unifySurfaceBackdrops: unifySurfaceBackdrops, sidebarSettings: SidebarBackdropSettingsSnapshot( materialRawValue: SidebarMaterialOption.sidebar.rawValue, From 75b2ece3a4fd0c8765b501e5e1e1dfea653969bb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 11 May 2026 20:15:21 -0700 Subject: [PATCH 3/3] Document terminal background ownership threshold Greptile correctly flagged that the opacity threshold needed intent and that the host-layer inverse paths needed coverage. Naming the threshold and adding translucent plus blurred assertions keeps the renderer-owned path narrow and understandable. Constraint: Review feedback was low-priority but directly improved the regression boundary. Confidence: high Scope-risk: narrow Directive: Keep host-layer ownership covered for translucent and blurred backgrounds when adjusting terminal rendering policy. Tested: git diff --check Not-tested: Local XCTest execution per repository policy; CI pending. --- .../Windowing/WindowAppearanceSnapshot.swift | 7 +++- cmuxTests/WindowAppearanceSnapshotTests.swift | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Sources/Windowing/WindowAppearanceSnapshot.swift b/Sources/Windowing/WindowAppearanceSnapshot.swift index 5468f1ef8459..c3eac3a914af 100644 --- a/Sources/Windowing/WindowAppearanceSnapshot.swift +++ b/Sources/Windowing/WindowAppearanceSnapshot.swift @@ -213,6 +213,11 @@ struct WindowGlassSettingsSnapshot { } struct WindowAppearanceSnapshot { + /// Treat opacity values within floating-point round-trip tolerance of 1.0 + /// as opaque so user-configured `1.0` does not accidentally trigger host + /// ownership after Double/CGFloat conversions. + private static let opaqueBackgroundOwnershipThreshold: CGFloat = 0.999 + let terminalBackgroundColor: NSColor let terminalBackgroundOpacity: CGFloat let terminalBackgroundBlur: GhosttyBackgroundBlur @@ -292,7 +297,7 @@ struct WindowAppearanceSnapshot { if backgroundBlur != .disabled { return true } - return clampedOpacity(backgroundOpacity) < 0.999 + return clampedOpacity(backgroundOpacity) < opaqueBackgroundOwnershipThreshold } static func terminalRenderingMode( diff --git a/cmuxTests/WindowAppearanceSnapshotTests.swift b/cmuxTests/WindowAppearanceSnapshotTests.swift index 7652ef6231a4..53d55a0406a0 100644 --- a/cmuxTests/WindowAppearanceSnapshotTests.swift +++ b/cmuxTests/WindowAppearanceSnapshotTests.swift @@ -126,6 +126,38 @@ final class WindowAppearanceSnapshotTests: XCTestCase { XCTAssertEqual(renderingMode, .ghosttyRendererOwnedBackgroundImage) } + func testTranslucentTerminalBackgroundStaysHostLayerOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 0.9, + backgroundBlur: .disabled + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNotNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .windowHostBackdrop) + } + + func testBlurredTerminalBackgroundStaysHostLayerOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 1.0, + backgroundBlur: .radius(20) + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNotNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .windowHostBackdrop) + } + func testDebugBackgroundGlassUsesWindowGlassPhase() { let snapshot = makeSnapshot( unifySurfaceBackdrops: false,