Skip to content

Honor Ghostty background-opacity across all cmux chrome#667

Merged
lawrencecchen merged 2 commits intomainfrom
issue-263-bg-opacity
Mar 1, 2026
Merged

Honor Ghostty background-opacity across all cmux chrome#667
lawrencecchen merged 2 commits intomainfrom
issue-263-bg-opacity

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented Feb 28, 2026

Summary

  • Parse background-opacity from Ghostty config and propagate it through bonsplit tab bar (RRGGBBAA hex via bonsplit PR Release v1.17.0 #14), browser panel/omnibar, titlebar, empty panel, and window background
  • Add GhosttyBackgroundTheme helper for consistent color+opacity resolution across all UI surfaces
  • Decouple glass effect from sidebar blend mode — bgGlassEnabled defaults to false so opacity works independently
  • Bonsplit submodule updated to support RRGGBBAA hex chrome colors

Test plan

  • Set background-opacity = 0.8 in Ghostty config, launch cmux — terminal, tab bar, browser chrome, and titlebar should all be translucent
  • Verify glass effect can be enabled/disabled independently in Settings without affecting opacity
  • Verify opacity = 1.0 (default) looks identical to before this change
  • Verify split panes with different background colors each respect opacity

Closes #263
Supersedes #274

Summary by CodeRabbit

  • New Features

    • Configurable background opacity via a new background-opacity setting (default: 1.0)
    • Enhanced titlebar and window translucency/rendering for improved blended backgrounds
    • Added Bing and Kagi as browser search engine options
    • UI now uses the app background theme for empty panel chrome
  • Behavior Changes

    • Background "glass" is disabled by default
  • Tests

    • Added unit tests for background theming, opacity handling, and config parsing

Parse background-opacity from Ghostty config and propagate it through
the entire chrome pipeline: bonsplit tab bar (via RRGGBBAA hex),
browser panel/omnibar, titlebar, empty panel, and window background.

Decouple glass effect from sidebar blend mode — bgGlassEnabled now
defaults to false so opacity works independently. Add
GhosttyBackgroundTheme helper for consistent color+opacity resolution
across all UI surfaces.

Fixes #263
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 1, 2026 11:49am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abab8ef and 56b9f3a.

📒 Files selected for processing (6)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/WorkspaceContentView.swift
  • Sources/cmuxApp.swift
  • cmuxTests/GhosttyConfigTests.swift

📝 Walkthrough

Walkthrough

Adds background-opacity support and centralizes Ghostty background color handling. Introduces GhosttyBackgroundTheme, opacity-aware chrome color generation, new titlebar layer view, translucency decision flow (clear vs. glass fallback), view-hierarchy transparency helper, and corresponding tests and config parsing.

Changes

Cohort / File(s) Summary
Config
Sources/GhosttyConfig.swift
Added backgroundOpacity: Double with default 1.0 and parsing for background-opacity.
Window / Titlebar & Translucency
Sources/ContentView.swift
Added TitlebarLayerBackground (NSViewRepresentable), introduced translucency decision flow (shouldForceTransparentHosting, shouldApplyWindowGlassFallback), made view-hierarchy transparency helper, and applied GhosttyBackgroundTheme color. Also updated NSColor.hexString(includeAlpha:) API usage.
Terminal view & opacity helpers
Sources/GhosttyTerminalView.swift
Exposed opacity-aware helpers (cmuxShouldUseClearWindowBackground(for:), cmuxTransparentWindowBaseColor()), clamped opacity handling, panelBackgroundFillColor(for:), and updated background fill/isOpaque logic for terminal surface views.
Background theme & browser panel
Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserPanelView.swift
Introduced GhosttyBackgroundTheme utilities for opacity clamping and notification-driven color extraction; switched web view and UI chrome to theme-based color; added ghosttyBackgroundGeneration invalidation state; expanded search engines (bing, kagi).
Workspace / Chrome color computation
Sources/Workspace.swift, Sources/WorkspaceContentView.swift
Added bonsplitChromeHex(backgroundColor:backgroundOpacity:), made bonsplitAppearance and applyGhosttyChrome opacity-aware, propagated runtime defaultBackgroundOpacity() through appearance resolution, and updated EmptyPanelView to use theme color.
Defaults / App storage
Sources/cmuxApp.swift
Changed bgGlassEnabled default from true to false.
Tests
cmuxTests/CmuxWebViewKeyEquivalentTests.swift, cmuxTests/GhosttyConfigTests.swift
Added tests for background-opacity parsing, GhosttyBackgroundTheme color clamping and notification extraction, and Bonsplit chrome hex alpha inclusion/omission. (Note: some test additions are duplicated in the diff.)
Vendor
vendor/bonsplit
Updated submodule pointer to newer commit.
sequenceDiagram
    participant Config as GhosttyConfig
    participant App as Workspace/Resolve
    participant Theme as GhosttyBackgroundTheme
    participant Content as ContentView
    participant Window as NSWindow

    Config->>App: parse background-opacity
    App->>Theme: request currentColor(opacity)
    Theme->>App: color with clamped alpha
    App->>Content: apply chrome/background (hex with opacity)
    Content->>Content: evaluate shouldForceTransparentHosting / shouldApplyWindowGlassFallback
    alt Force transparent hosting
        Content->>Window: set isOpaque = false, background = near-clear
        Content->>Content: makeViewHierarchyTransparent(rootView)
    else Apply glass fallback / opaque
        Content->>Window: apply glass layer or themed opaque background
    end
    Window->>Window: render with opacity-aware layers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and mixed the light,
I gave the titlebar softer sight,
Opacity stitched through chrome and pane,
Now backgrounds whisper — not in vain,
A tiny rabbit cheers transparency bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Honor Ghostty background-opacity across all cmux chrome' accurately and clearly describes the main objective: implementing support for Ghostty's background-opacity setting across all UI surfaces in cmux.
Linked Issues check ✅ Passed The PR successfully addresses issue #263 by parsing background-opacity from Ghostty config [GhosttyConfig.swift], propagating it across all UI surfaces [ContentView, GhosttyTerminalView, BrowserPanel, WorkspaceContentView, Workspace], and introducing GhosttyBackgroundTheme for consistent resolution.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing background-opacity support and maintaining consistency; the only tangential but justified change is bgGlassEnabled default flip to false, which decouples glass from opacity as explicitly noted in PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-263-bg-opacity

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/cmuxApp.swift (1)

2376-2423: ⚠️ Potential issue | 🟡 Minor

Reset action now conflicts with the new bgGlassEnabled default.

Line 2376 defaults bgGlassEnabled to false, but the reset path still forces it to true (Line 2422). This makes “Reset” re-enable glass unexpectedly.

Proposed fix
                     Button("Reset") {
                         bgGlassTintHex = "#000000"
                         bgGlassTintOpacity = 0.03
                         bgGlassMaterial = "hudWindow"
-                        bgGlassEnabled = true
+                        bgGlassEnabled = false
                         updateWindowGlassTint()
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 2376 - 2423, The Reset button currently
forces bgGlassEnabled = true while the `@AppStorage` declaration sets
bgGlassEnabled default to false, causing Reset to unexpectedly re-enable glass;
update the Reset action in the Button closure (where bgGlassTintHex,
bgGlassTintOpacity, bgGlassMaterial, bgGlassEnabled, and updateWindowGlassTint()
are set) so bgGlassEnabled is reset to the same default (false) or, better, read
a single source-of-truth default constant and assign that value instead of
hardcoding true.
Sources/ContentView.swift (1)

2434-2456: ⚠️ Potential issue | 🟠 Major

Add a true “disable” path for translucency/glass state.

Line 2441 only applies the “on” state. When opacity returns to 1.0 or glass is disabled, window state is not restored, and the view hierarchy from Line 2520 remains force-cleared. This can leave stale translucency and break the expected “opacity=1.0 matches prior behavior” flow.

🔧 Suggested fix
-            if shouldForceTransparentHosting {
+            if shouldForceTransparentHosting {
                 window.isOpaque = false
                 // Keep the window clear whenever translucency is active. Relying only on
                 // terminal focus-driven updates can leave stale opaque window fills.
                 window.backgroundColor = NSColor.white.withAlphaComponent(0.001)
                 // Configure contentView hierarchy for transparency.
                 if let contentView = window.contentView {
                     makeViewHierarchyTransparent(contentView)
                 }
+            } else {
+                window.isOpaque = true
+                window.backgroundColor = .windowBackgroundColor
+                // Also restore content hierarchy backgrounds/opacity here.
             }

             if shouldApplyWindowGlassFallback {
                 // Apply liquid glass effect to the window with tint from settings
                 let tintColor = (NSColor(hex: bgGlassTintHex) ?? .black).withAlphaComponent(bgGlassTintOpacity)
                 WindowGlassEffect.apply(to: window, tintColor: tintColor)
+            } else {
+                WindowGlassEffect.remove(from: window)
             }

Also, WindowGlassEffect.remove(from:) should actually detach fallback views, not only clear associated-object references.

Also applies to: 2520-2528

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2434 - 2456, The code only sets up
translucency when shouldForceTransparentHosting is true but never restores the
prior state when opacity returns to 1.0 or glass is disabled; update the branch
logic so when shouldForceTransparentHosting is false you revert changes: set
window.isOpaque = true, restore a non-transparent backgroundColor (e.g., remove
the near-clear alpha or reset to the prior window background), and call a new or
existing inverse of makeViewHierarchyTransparent (e.g., makeViewHierarchyOpaque
or remove transparency on window.contentView) to restore the view hierarchy;
also ensure WindowGlassEffect.remove(from:) truly detaches any
fallback/translucency views from the window (not just clearing associated-object
refs) and call WindowGlassEffect.remove(from: window) when
shouldApplyWindowGlassFallback becomes false so fallback views are removed.
🧹 Nitpick comments (6)
Sources/Panels/BrowserPanel.swift (1)

7-52: Consider removing now-duplicated background resolver logic from BrowserPanel.

With GhosttyBackgroundTheme introduced on Line 7, the older BrowserPanel private helpers (clampedGhosttyBackgroundOpacity, resolvedGhosttyBackgroundColor, resolvedBrowserChromeBackgroundColor) are overlapping paths and can drift. Prefer a single resolver path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 7 - 52, The BrowserPanel
still contains duplicate background-resolver helpers
(clampedGhosttyBackgroundOpacity, resolvedGhosttyBackgroundColor,
resolvedBrowserChromeBackgroundColor) that overlap with the new
GhosttyBackgroundTheme utilities; remove those private helper methods from
BrowserPanel and update any usages to call GhosttyBackgroundTheme.color(from:),
GhosttyBackgroundTheme.color(backgroundColor:opacity:), or
GhosttyBackgroundTheme.currentColor() as appropriate so there is a single
canonical resolver path (search for the helper names in BrowserPanel and replace
their callers with the matching GhosttyBackgroundTheme API).
Sources/GhosttyTerminalView.swift (1)

2377-2393: Cache the clear-window decision once per call for readability.

The same predicate is evaluated multiple times in one method; storing it once improves clarity and keeps logging/state branches tightly coupled.

♻️ Proposed cleanup
         applySurfaceBackground()
         let color = effectiveBackgroundColor()
-        if cmuxShouldUseClearWindowBackground(for: color.alphaComponent) {
+        let useClearWindowBackground = cmuxShouldUseClearWindowBackground(for: color.alphaComponent)
+        if useClearWindowBackground {
             window.backgroundColor = cmuxTransparentWindowBaseColor()
             window.isOpaque = false
         } else {
             window.backgroundColor = color
             window.isOpaque = color.alphaComponent >= 1.0
         }
         if GhosttyApp.shared.backgroundLogEnabled {
-            let signature = "\(cmuxShouldUseClearWindowBackground(for: color.alphaComponent) ? "transparent" : color.hexString()):\(String(format: "%.3f", color.alphaComponent))"
+            let signature = "\(useClearWindowBackground ? "transparent" : color.hexString()):\(String(format: "%.3f", color.alphaComponent))"
             if signature != lastLoggedWindowBackgroundSignature {
                 lastLoggedWindowBackgroundSignature = signature
@@
                 GhosttyApp.shared.logBackground(
-                    "window background applied tab=\(tabId?.uuidString ?? "unknown") surface=\(terminalSurface?.id.uuidString ?? "unknown") source=\(source) override=\(overrideHex) default=\(defaultHex) transparent=\(cmuxShouldUseClearWindowBackground(for: color.alphaComponent)) color=\(color.hexString()) opacity=\(String(format: "%.3f", color.alphaComponent))"
+                    "window background applied tab=\(tabId?.uuidString ?? "unknown") surface=\(terminalSurface?.id.uuidString ?? "unknown") source=\(source) override=\(overrideHex) default=\(defaultHex) transparent=\(useClearWindowBackground) color=\(color.hexString()) opacity=\(String(format: "%.3f", color.alphaComponent))"
                 )
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2377 - 2393, Cache the result
of cmuxShouldUseClearWindowBackground(for: color.alphaComponent) in a local Bool
(e.g., let useClearBackground = cmuxShouldUseClearWindowBackground(for:
color.alphaComponent)) at the start of the block and use that variable
everywhere instead of calling the predicate repeatedly: use it to choose
window.backgroundColor and window.isOpaque, to build the signature
checked/assigned to lastLoggedWindowBackgroundSignature, and to populate the
"transparent=" field in the GhosttyApp.shared.logBackground call; keep all other
logic (backgroundColor override, hex strings, opacity formatting, and
lastLoggedWindowBackgroundSignature comparison) unchanged.
cmuxTests/CmuxWebViewKeyEquivalentTests.swift (1)

1306-1329: Consolidate overlapping BrowserPanel opacity tests.

Line 1306 adds valuable NSNumber payload coverage, but the assertion body is almost identical to the existing test around Line 1249. Consider parameterizing the payload type (Double vs NSNumber) through a shared helper to reduce duplicate maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift` around lines 1306 - 1329, The
new test testBrowserPanelRefreshesUnderPageBackgroundColorWithGhosttyOpacity
duplicates assertion logic from the existing similar test; refactor by
extracting a shared helper that accepts the notification payload (allowing both
Double and NSNumber for GhosttyNotificationKey.backgroundOpacity) and performs
the NotificationCenter.post + underPageBackgroundColor assertions for
BrowserPanel; reuse this helper from both
testBrowserPanelRefreshesUnderPageBackgroundColorWithGhosttyOpacity and the
earlier test to remove duplicated assertion code while keeping coverage for
.ghosttyDefaultBackgroundDidChange and GhosttyNotificationKey keys.
Sources/WorkspaceContentView.swift (1)

407-407: EmptyPanelView may not update when background opacity changes.

Unlike BrowserPanelView which has its own .onReceive for .ghosttyDefaultBackgroundDidChange, EmptyPanelView computes the background inline without any reactive update mechanism. Since it's rendered inside BonsplitView's content closure, parent state changes in WorkspaceContentView may not reliably trigger re-evaluation of these closures.

If a user changes background-opacity in Ghostty config while an empty panel is visible, it might not update until the view is recreated.

Consider adding a similar pattern to EmptyPanelView:

♻️ Add reactive background update to EmptyPanelView
 struct EmptyPanelView: View {
     `@ObservedObject` var workspace: Workspace
     let paneId: PaneID
     `@AppStorage`(KeyboardShortcutSettings.Action.newSurface.defaultsKey) private var newSurfaceShortcutData = Data()
     `@AppStorage`(KeyboardShortcutSettings.Action.openBrowser.defaultsKey) private var openBrowserShortcutData = Data()
+    `@State` private var ghosttyBackgroundGeneration: Int = 0
+
+    private var backgroundColor: Color {
+        _ = ghosttyBackgroundGeneration
+        return Color(nsColor: GhosttyBackgroundTheme.currentColor())
+    }
     
     // ... existing code ...
     
     var body: some View {
         VStack(spacing: 16) {
             // ... existing content ...
         }
         .frame(maxWidth: .infinity, maxHeight: .infinity)
-        .background(Color(nsColor: GhosttyBackgroundTheme.currentColor()))
+        .background(backgroundColor)
+        .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in
+            ghosttyBackgroundGeneration &+= 1
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/WorkspaceContentView.swift` at line 407, EmptyPanelView's inline
background using GhosttyBackgroundTheme.currentColor() won't react to
background-opacity changes; update EmptyPanelView in WorkspaceContentView to
subscribe to Ghostty's background-change notifications (the same publisher used
by BrowserPanelView, e.g. NotificationCenter.default.publisher(for:
.ghosttyDefaultBackgroundDidChange)) and call a view update (e.g. store a
`@State/`@Published color or opacity and set it in the onReceive) so the
.background(Color(nsColor: GhosttyBackgroundTheme.currentColor())) modifier is
re-evaluated when the notification fires; locate EmptyPanelView and add an
onReceive that updates a local state backing the background color/opacity.
Sources/Panels/BrowserPanelView.swift (1)

740-742: Consider: WebView placeholder background does not apply opacity.

Line 741 uses browserChromeBackgroundColor (without opacity) while the address bar now uses browserChromeBackground (with opacity). When shouldRenderWebView is false, the placeholder area will be opaque while the address bar above is translucent, creating a visual inconsistency.

If this is intentional (e.g., placeholder should remain opaque), a brief comment would clarify. Otherwise, consider using browserChromeBackground here as well.

💡 Optional: Use opacity-aware color for consistency
 } else {
-    Color(nsColor: browserChromeBackgroundColor)
+    browserChromeBackground
         .contentShape(Rectangle())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanelView.swift` around lines 740 - 742, When
shouldRenderWebView is false the placeholder uses browserChromeBackgroundColor
(opaque) while the address bar uses the opacity-aware browserChromeBackground,
creating a visual mismatch; update the placeholder in BrowserPanelView (the
branch that currently creates Color(nsColor: browserChromeBackgroundColor)) to
use browserChromeBackground instead so the same opacity is applied, or if
opacity difference is intentional add a brief comment explaining why the opaque
browserChromeBackgroundColor is required.
Sources/Workspace.swift (1)

1072-1078: Consider de-emphasizing the alpha-derived overload to avoid future opacity regressions.

This overload is convenient, but deriving opacity from backgroundColor.alphaComponent can silently ignore external opacity sources if reused casually. Consider marking it as transitional/deprecated or adding a doc warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 1072 - 1078, The shorthand overload
applyGhosttyChrome(backgroundColor: NSColor, reason: String = "unspecified")
should be de-emphasized to avoid accidental opacity regressions: either annotate
it as deprecated (e.g. with `@available`(*, deprecated, message: "...")) pointing
callers to use applyGhosttyChrome(backgroundColor:backgroundOpacity:reason:) or
add a clear doc comment above the overload warning that alphaComponent-derived
opacity may be incorrect and callers should prefer the explicit
backgroundOpacity parameter; update any internal call sites to use the explicit
overload and run tests to catch regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyConfig.swift`:
- Around line 152-155: The "background-opacity" parsing currently assigns any
Double(value) to backgroundOpacity; change this to validate the parsed Double
for finiteness and clamp it into the [0.0, 1.0] range before assigning. In the
switch case handling "background-opacity" (where you currently call
Double(value) and set backgroundOpacity), parse to a Double, ensure it is finite
(not NaN or infinite), then clamp with min/max (or equivalent) to 0.0/1.0; if
the value is non-finite or cannot be parsed, leave backgroundOpacity unchanged
(or handle via existing error path) so invalid values do not propagate.

In `@Sources/Workspace.swift`:
- Around line 1012-1013: The code currently treats alpha values >= 0.999 as
opaque; change the logic so any alpha less than 1.0 is preserved — update the
includeAlpha computation (themedColor.alphaComponent < 0.999) to use a strict
comparison against 1.0 (e.g., includeAlpha = themedColor.alphaComponent < 1.0 or
an equivalent epsilon-aware check) so themedColor.alphaComponent is passed
correctly to hexString(includeAlpha:) and near‑opaque configured opacities are
not dropped.

In `@vendor/bonsplit`:
- Line 1: The parent repo's submodule pointer for bonsplit points to commit
335facd9fd1d81a3c71fea69345af30f7e3601f9 which is not present on the bonsplit
remote main; push that commit (or fast-forward main to include it) to the
bonsplit remote main branch, verify the commit exists on remote, then update and
commit the submodule pointer in the parent repository (the bonsplit submodule
entry) so it references the now-pushed commit.

---

Outside diff comments:
In `@Sources/cmuxApp.swift`:
- Around line 2376-2423: The Reset button currently forces bgGlassEnabled = true
while the `@AppStorage` declaration sets bgGlassEnabled default to false, causing
Reset to unexpectedly re-enable glass; update the Reset action in the Button
closure (where bgGlassTintHex, bgGlassTintOpacity, bgGlassMaterial,
bgGlassEnabled, and updateWindowGlassTint() are set) so bgGlassEnabled is reset
to the same default (false) or, better, read a single source-of-truth default
constant and assign that value instead of hardcoding true.

In `@Sources/ContentView.swift`:
- Around line 2434-2456: The code only sets up translucency when
shouldForceTransparentHosting is true but never restores the prior state when
opacity returns to 1.0 or glass is disabled; update the branch logic so when
shouldForceTransparentHosting is false you revert changes: set window.isOpaque =
true, restore a non-transparent backgroundColor (e.g., remove the near-clear
alpha or reset to the prior window background), and call a new or existing
inverse of makeViewHierarchyTransparent (e.g., makeViewHierarchyOpaque or remove
transparency on window.contentView) to restore the view hierarchy; also ensure
WindowGlassEffect.remove(from:) truly detaches any fallback/translucency views
from the window (not just clearing associated-object refs) and call
WindowGlassEffect.remove(from: window) when shouldApplyWindowGlassFallback
becomes false so fallback views are removed.

---

Nitpick comments:
In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift`:
- Around line 1306-1329: The new test
testBrowserPanelRefreshesUnderPageBackgroundColorWithGhosttyOpacity duplicates
assertion logic from the existing similar test; refactor by extracting a shared
helper that accepts the notification payload (allowing both Double and NSNumber
for GhosttyNotificationKey.backgroundOpacity) and performs the
NotificationCenter.post + underPageBackgroundColor assertions for BrowserPanel;
reuse this helper from both
testBrowserPanelRefreshesUnderPageBackgroundColorWithGhosttyOpacity and the
earlier test to remove duplicated assertion code while keeping coverage for
.ghosttyDefaultBackgroundDidChange and GhosttyNotificationKey keys.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 2377-2393: Cache the result of
cmuxShouldUseClearWindowBackground(for: color.alphaComponent) in a local Bool
(e.g., let useClearBackground = cmuxShouldUseClearWindowBackground(for:
color.alphaComponent)) at the start of the block and use that variable
everywhere instead of calling the predicate repeatedly: use it to choose
window.backgroundColor and window.isOpaque, to build the signature
checked/assigned to lastLoggedWindowBackgroundSignature, and to populate the
"transparent=" field in the GhosttyApp.shared.logBackground call; keep all other
logic (backgroundColor override, hex strings, opacity formatting, and
lastLoggedWindowBackgroundSignature comparison) unchanged.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 7-52: The BrowserPanel still contains duplicate
background-resolver helpers (clampedGhosttyBackgroundOpacity,
resolvedGhosttyBackgroundColor, resolvedBrowserChromeBackgroundColor) that
overlap with the new GhosttyBackgroundTheme utilities; remove those private
helper methods from BrowserPanel and update any usages to call
GhosttyBackgroundTheme.color(from:),
GhosttyBackgroundTheme.color(backgroundColor:opacity:), or
GhosttyBackgroundTheme.currentColor() as appropriate so there is a single
canonical resolver path (search for the helper names in BrowserPanel and replace
their callers with the matching GhosttyBackgroundTheme API).

In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 740-742: When shouldRenderWebView is false the placeholder uses
browserChromeBackgroundColor (opaque) while the address bar uses the
opacity-aware browserChromeBackground, creating a visual mismatch; update the
placeholder in BrowserPanelView (the branch that currently creates
Color(nsColor: browserChromeBackgroundColor)) to use browserChromeBackground
instead so the same opacity is applied, or if opacity difference is intentional
add a brief comment explaining why the opaque browserChromeBackgroundColor is
required.

In `@Sources/Workspace.swift`:
- Around line 1072-1078: The shorthand overload
applyGhosttyChrome(backgroundColor: NSColor, reason: String = "unspecified")
should be de-emphasized to avoid accidental opacity regressions: either annotate
it as deprecated (e.g. with `@available`(*, deprecated, message: "...")) pointing
callers to use applyGhosttyChrome(backgroundColor:backgroundOpacity:reason:) or
add a clear doc comment above the overload warning that alphaComponent-derived
opacity may be incorrect and callers should prefer the explicit
backgroundOpacity parameter; update any internal call sites to use the explicit
overload and run tests to catch regressions.

In `@Sources/WorkspaceContentView.swift`:
- Line 407: EmptyPanelView's inline background using
GhosttyBackgroundTheme.currentColor() won't react to background-opacity changes;
update EmptyPanelView in WorkspaceContentView to subscribe to Ghostty's
background-change notifications (the same publisher used by BrowserPanelView,
e.g. NotificationCenter.default.publisher(for:
.ghosttyDefaultBackgroundDidChange)) and call a view update (e.g. store a
`@State/`@Published color or opacity and set it in the onReceive) so the
.background(Color(nsColor: GhosttyBackgroundTheme.currentColor())) modifier is
re-evaluated when the notification fires; locate EmptyPanelView and add an
onReceive that updates a local state backing the background color/opacity.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1815745 and abab8ef.

📒 Files selected for processing (11)
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • Sources/cmuxApp.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift
  • cmuxTests/GhosttyConfigTests.swift
  • vendor/bonsplit

Comment on lines +152 to +155
case "background-opacity":
if let opacity = Double(value) {
backgroundOpacity = opacity
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate and clamp parsed background-opacity range.

Line 153 currently accepts any Double. Clamp to [0.0, 1.0] (and reject non-finite values) to prevent invalid opacity state from propagating.

🔧 Proposed fix
                 case "background-opacity":
-                    if let opacity = Double(value) {
-                        backgroundOpacity = opacity
+                    if let opacity = Double(value), opacity.isFinite {
+                        backgroundOpacity = min(1.0, max(0.0, opacity))
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` around lines 152 - 155, The "background-opacity"
parsing currently assigns any Double(value) to backgroundOpacity; change this to
validate the parsed Double for finiteness and clamp it into the [0.0, 1.0] range
before assigning. In the switch case handling "background-opacity" (where you
currently call Double(value) and set backgroundOpacity), parse to a Double,
ensure it is finite (not NaN or infinite), then clamp with min/max (or
equivalent) to 0.0/1.0; if the value is non-finite or cannot be parsed, leave
backgroundOpacity unchanged (or handle via existing error path) so invalid
values do not propagate.

Comment on lines +1012 to +1013
let includeAlpha = themedColor.alphaComponent < 0.999
return themedColor.hexString(includeAlpha: includeAlpha)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t drop near-opaque configured opacity values.

At Line 1012, themedColor.alphaComponent < 0.999 treats values in [0.999, 1.0) as fully opaque (no alpha in hex), which can violate exact background-opacity intent.

🔧 Proposed fix
-        let includeAlpha = themedColor.alphaComponent < 0.999
+        let includeAlpha = themedColor.alphaComponent < 1.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let includeAlpha = themedColor.alphaComponent < 0.999
return themedColor.hexString(includeAlpha: includeAlpha)
let includeAlpha = themedColor.alphaComponent < 1.0
return themedColor.hexString(includeAlpha: includeAlpha)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 1012 - 1013, The code currently treats
alpha values >= 0.999 as opaque; change the logic so any alpha less than 1.0 is
preserved — update the includeAlpha computation (themedColor.alphaComponent <
0.999) to use a strict comparison against 1.0 (e.g., includeAlpha =
themedColor.alphaComponent < 1.0 or an equivalent epsilon-aware check) so
themedColor.alphaComponent is passed correctly to hexString(includeAlpha:) and
near‑opaque configured opacities are not dropped.

@@ -1 +1 @@
Subproject commit c4b8f5cc3def0a44c1c3634d4f358a66fd956606
Subproject commit 335facd9fd1d81a3c71fea69345af30f7e3601f9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the bonsplit submodule commit is on the remote main branch

cd vendor/bonsplit 2>/dev/null || {
  echo "Error: vendor/bonsplit directory not found"
  exit 1
}

# Get current remote URL
echo "Remote URL:"
git config --get remote.origin.url

# Fetch latest from remote
echo -e "\nFetching origin/main..."
git fetch origin main 2>&1 | head -20

# Check if commit exists locally
echo -e "\nChecking if commit 335facd9fd1d81a3c71fea69345af30f7e3601f9 is known locally..."
if git cat-file -t 335facd9fd1d81a3c71fea69345af30f7e3601f9 2>/dev/null | grep -q commit; then
  echo "✓ Commit exists locally"
else
  echo "✗ Commit does NOT exist locally"
fi

# Check if commit is ancestor of origin/main
echo -e "\nChecking if commit is on origin/main..."
if git merge-base --is-ancestor 335facd9fd1d81a3c71fea69345af30f7e3601f9 origin/main 2>/dev/null; then
  echo "✓ Commit 335facd9fd1d81a3c71fea69345af30f7e3601f9 IS on origin/main"
  exit 0
else
  echo "✗ Commit 335facd9fd1d81a3c71fea69345af30f7e3601f9 is NOT an ancestor of origin/main"
  # Try to show what's on origin/main
  echo -e "\nLatest commit on origin/main:"
  git log origin/main -1 --oneline 2>/dev/null || echo "Could not retrieve"
  exit 1
fi

Repository: manaflow-ai/cmux

Length of output: 566


Submodule commit must be pushed to remote main before updating the parent repository pointer.

The new bonsplit commit 335facd9fd1d81a3c71fea69345af30f7e3601f9 does not exist on the remote main branch. Per the established workflow, submodule commits must be pushed to the remote main branch before updating the pointer in the parent repository. Without this, the submodule reference will be broken for other developers who clone the repository.

Action required: Push the commit to the bonsplit remote main branch, then update the parent repository pointer to that commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/bonsplit` at line 1, The parent repo's submodule pointer for bonsplit
points to commit 335facd9fd1d81a3c71fea69345af30f7e3601f9 which is not present
on the bonsplit remote main; push that commit (or fast-forward main to include
it) to the bonsplit remote main branch, verify the commit exists on remote, then
update and commit the submodule pointer in the parent repository (the bonsplit
submodule entry) so it references the now-pushed commit.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR successfully implements configurable background opacity across all cmux chrome surfaces by parsing background-opacity from Ghostty config and propagating it through the UI stack. The implementation introduces a GhosttyBackgroundTheme helper for consistent color+opacity resolution, extends the hex color format to support RRGGBBAA for translucent surfaces, and decouples the glass effect from opacity handling.

Key changes:

  • Parses and clamps background-opacity from Ghostty config with proper validation
  • Propagates opacity through tab bar (via bonsplit RRGGBBAA hex), browser panel, titlebar, and empty panels
  • Prevents double-translucency stacking by using clear panel backgrounds when terminal is translucent
  • Changes bgGlassEnabled default from true to false to allow opacity to work independently
  • Adds comprehensive test coverage for opacity parsing, hex formatting, and notification handling
  • Updates bonsplit submodule to support RRGGBBAA chrome colors

The implementation is architecturally sound with proper separation of concerns, consistent opacity clamping, and thoughtful prevention of visual artifacts from layered translucency.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are well-architected with thorough test coverage, proper opacity clamping throughout, and careful handling of window transparency to prevent visual artifacts. The code follows established patterns, includes detailed comments explaining non-obvious logic, and the feature can be disabled via config if issues arise.
  • No files require special attention

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Introduced GhosttyBackgroundTheme helper for consistent color+opacity resolution across UI surfaces
Sources/GhosttyTerminalView.swift Updated window background transparency logic, added helpers to prevent double-translucency stacking, clamped opacity values
Sources/ContentView.swift Decoupled glass effect from opacity, changed default bgGlassEnabled to false, refactored transparency handling with new helper method
Sources/Workspace.swift Updated bonsplit chrome color propagation to include RRGGBBAA hex format when translucent
cmuxTests/GhosttyConfigTests.swift Added tests for background opacity parsing and RRGGBBAA hex format
cmuxTests/CmuxWebViewKeyEquivalentTests.swift Added comprehensive tests for GhosttyBackgroundTheme helper and browser panel opacity handling

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Ghostty Config<br/>background-opacity] --> B[GhosttyConfig.parse]
    B --> C[GhosttyApp.refreshDefaultBackground<br/>clamps opacity 0-1]
    C --> D[GhosttyApp.shared.defaultBackgroundOpacity]
    
    D --> E[GhosttyBackgroundTheme Helper]
    E --> F[Titlebar Background]
    E --> G[Browser Panel Chrome]
    E --> H[Empty Panel Background]
    
    D --> I[Workspace.bonsplitChromeHex]
    I --> J{opacity < 0.999?}
    J -->|Yes| K[RRGGBBAA hex format]
    J -->|No| L[RRGGBB hex format]
    K --> M[Bonsplit Tab Bar]
    L --> M
    
    D --> N{opacity < 0.999?}
    N -->|Yes| O[Window: transparent<br/>Panel: clear background]
    N -->|No| P[Window: opaque<br/>Panel: colored background]
    
    style E fill:#e1f5ff
    style I fill:#e1f5ff
    style O fill:#fff4e1
    style P fill:#fff4e1
Loading

Last reviewed commit: abab8ef

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 28, 2026

Additional Comments (1)

Sources/cmuxApp.swift
Reset button sets bgGlassEnabled = true, but the new app default is false (line 2376). Consider whether this should reset to false for consistency.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abab8ef9de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

next.backgroundColor = resolvedBackground
// Use the runtime opacity from the Ghostty engine, which may differ from the
// file-level value parsed by GhosttyConfig.load().
next.backgroundOpacity = defaultBackgroundOpacity()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger titlebar refresh when only opacity changes

This now stores Ghostty's runtime backgroundOpacity, but the subsequent refresh gating still only compares background hex values, so an opacity-only update (for example changing background-opacity and reloading config without changing color) skips onThemeRefreshRequest. In that scenario the root ContentView never bumps titlebarThemeGeneration, leaving titlebar/window chrome at stale opacity until some unrelated UI state change causes a rerender.

Useful? React with 👍 / 👎.

Use CALayer-level opacity for the titlebar background instead of SwiftUI
Color alpha, matching the terminal's Metal compositing path. Account for
the double alpha stacking in the terminal area (Bonsplit container bg +
Ghostty renderer) so the titlebar visually matches.

Also fix opacity-only config changes not triggering titlebar refresh on
Cmd+Shift+, reload.
@lawrencecchen lawrencecchen merged commit bc1b6fd into main Mar 1, 2026
5 of 8 checks passed
@lawrencecchen lawrencecchen deleted the issue-263-bg-opacity branch March 1, 2026 11:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56b9f3a173

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2478 to +2481
// Browser-focused workspaces may not have an active terminal panel to refresh
// the NSWindow background. Keep opaque theme changes applied here as well.
window.backgroundColor = currentThemeBackground
window.isOpaque = currentThemeBackground.alphaComponent >= 0.999
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reapply browser-window background when Ghostty theme changes

This fallback path does not actually run on subsequent theme/opacity updates, so browser-focused workspaces can keep a stale NSWindow background after background-opacity changes. refreshGhosttyAppearanceConfig already skips terminalPanel.applyWindowBackgroundIfActive() when no terminal is focused, and this code relies on WindowAccessor to handle that case, but WindowAccessor defaults to dedupeByWindow = true and short-circuits callbacks unless the window instance changes (Sources/WindowAccessor.swift, guard on lastWindow !== window), so the newly added browser fallback is effectively one-shot.

Useful? React with 👍 / 👎.

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
)

* Honor Ghostty background-opacity across all cmux chrome

Parse background-opacity from Ghostty config and propagate it through
the entire chrome pipeline: bonsplit tab bar (via RRGGBBAA hex),
browser panel/omnibar, titlebar, empty panel, and window background.

Decouple glass effect from sidebar blend mode — bgGlassEnabled now
defaults to false so opacity works independently. Add
GhosttyBackgroundTheme helper for consistent color+opacity resolution
across all UI surfaces.

Fixes manaflow-ai#263

* Titlebar and chrome opacity matches terminal background-opacity

Use CALayer-level opacity for the titlebar background instead of SwiftUI
Color alpha, matching the terminal's Metal compositing path. Account for
the double alpha stacking in the terminal area (Bonsplit container bg +
Ghostty renderer) so the titlebar visually matches.

Also fix opacity-only config changes not triggering titlebar refresh on
Cmd+Shift+, reload.
ohikouta added a commit to ohikouta/cmux that referenced this pull request Apr 3, 2026
…arency

Ghostty's `background-opacity` setting becomes ineffective when using
native macOS fullscreen because the desktop background is removed.
This adds support for `macos-non-native-fullscreen` config option
(matching Ghostty's own implementation) which expands the window to
fill the screen without entering a native fullscreen Space, keeping
the desktop visible behind the transparent terminal.

Also clamps `background-opacity` config values to 0.0-1.0 range and
re-applies window transparency state on native fullscreen exit.

Addresses coderabbitai review feedback from PR manaflow-ai#667.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ghostty transparency not supported

1 participant