Skip to content

Fix terminal TUI background seam - #3903

Merged
austinywang merged 4 commits into
mainfrom
issue-3655-terminal-bg-tui-seam
May 12, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-3655-terminal-bg-tui-seam

Conversation

@austinywang

@austinywang austinywang commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Keep solid opaque, unblurred terminal backgrounds renderer-owned instead of forcing macos-background-from-layer for every terminal.
  • Preserve host-layer ownership for translucent or blurred terminal backgrounds where the macOS compositor is required.
  • Add a regression assertion that opaque terminal backdrops do not expose a host-layer background color.

Repro

  1. Built and launched tagged DEV with ./scripts/reload.sh --tag issue-3655-terminal-bg-tui-seam --launch.
  2. Opened a cmux workspace pane and ran claude.
  3. Compared against standalone Ghostty with empty/fresh local Ghostty config, running the same Claude Code binary: /Applications/cmux.app/Contents/Resources/bin/claude.
  4. Captured local evidence at /tmp/cmux-claude-window.png and /tmp/ghostty-claude-typed.png.

Observed: Claude Code's input/statusline chrome in cmux rendered over a visibly different backdrop than surrounding terminal scrollback. The same TUI in standalone Ghostty blended with the surrounding terminal background.

Expected: solid opaque terminal backgrounds should be composited through the same renderer path as explicit ANSI cell backgrounds so Claude Code chrome blends into the scrollback.

Commit Structure

  • e60b06251 adds the failing regression test only.
  • caa37c5dc applies the renderer-owned background fix.

Fixes #3655

Verification

  • git diff --check

Not run locally: XCTest/build execution per repository policy; CI will run on the PR.


Note

Medium Risk
Changes terminal backdrop ownership logic and config injection based on opacity/blur, which can affect rendering/compositing across macOS window surfaces. Regression risk is mostly visual (seams, transparency/blur behavior) rather than security or data correctness.

Overview
Fixes terminal background seams by no longer forcing macos-background-from-layer = true for all terminals; instead, GhosttyTerminalView derives usesHostLayerBackground from background-opacity and background-blur (including fallback config) and injects the matching macos-background-from-layer value.

WindowAppearanceSnapshot adds a shared usesHostLayerBackground(backgroundOpacity:backgroundBlur:) helper (with an opacity threshold to avoid float round-trip issues) and new tests assert that opaque, unblurred terminals are renderer-owned while translucent or blurred terminals remain host-layer owned.

Reviewed by Cursor Bugbot for commit 75b2ece. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes a visible seam in TUIs by keeping solid, opaque terminal backgrounds renderer-owned. We now only use the macOS host layer for blurred or translucent terminals, clarified the opacity threshold to avoid float rounding issues, and synced with main.

  • Bug Fixes
    • Derive usesHostLayerBackground from background-opacity and background-blur (including fallback config); stop forcing macos-background-from-layer = true.
    • Treat opacity ≥ 0.999 as opaque to prevent rounding from flipping ownership; centralize logic in WindowAppearanceSnapshot (usesHostLayerBackground, terminalRenderingMode).
    • Expand tests: assert opaque, unblurred terminals use a renderer-owned background (no host-layer color), and that translucent or blurred terminals remain host-layer owned.

Written for commit 75b2ece. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved background rendering reliability by preventing floating-point precision issues from affecting background layer decisions.
    • Enhanced handling of background opacity and blur settings for more consistent visual behavior.

Review Change Stack

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.
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.
@vercel

vercel Bot commented May 12, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled May 12, 2026 3:36am
cmux-staging Building Building Preview, Comment May 12, 2026 3:36am

@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR centralizes the decision logic for whether the terminal uses host-layer background by introducing opacity threshold handling and helper methods in WindowAppearanceSnapshot, then refactoring GhosttyTerminalView to use these helpers instead of hardcoded settings, and adding corresponding tests.

Changes

Terminal Background Ownership Logic

Layer / File(s) Summary
WindowAppearanceSnapshot host-layer background helpers
Sources/Windowing/WindowAppearanceSnapshot.swift
Defines opaqueBackgroundOwnershipThreshold to treat opacity values extremely close to 1.0 as opaque (avoiding floating-point round-trip issues), and adds usesHostLayerBackground(backgroundOpacity:backgroundBlur:) and terminalRenderingMode(backgroundOpacity:backgroundBlur:) helpers that derive host ownership from blur state and clamped opacity.
GhosttyTerminalView background decision integration
Sources/GhosttyTerminalView.swift
Introduces defaultBackgroundOpacityValue(from:) and usesHostLayerBackground(for:) helpers; replaces hardcoded macos-background-from-layer = true in fallback initialization and config loading with values computed via the new helpers; refactors opacity assignment in updateDefaultBackground to use the new opacity helper.
WindowAppearanceSnapshot and dynamic rendering mode tests
cmuxTests/WindowAppearanceSnapshotTests.swift
Adds three snapshot tests validating backdrop ownership behavior for unblurred translucent, unblurred opaque, and blurred backgrounds; updates makeSnapshot to compute terminalRenderingMode dynamically from opacity and blur instead of hardcoding .windowHostBackdrop.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#3382: Modifies WindowAppearanceSnapshot and terminal backdrop ownership logic used by GhosttyTerminalView.
  • manaflow-ai/cmux#2378: Introduced earlier hardcoded macos-background-from-layer and opacity logic that this PR centralizes via WindowAppearanceSnapshot helpers.
  • manaflow-ai/cmux#3166: Also modifies WindowAppearanceSnapshot helpers and GhosttyTerminalView host-layer background decisions.

Poem

🐰 Opacity threshold clear as morning dew,
Host-layer blur decisions now shine true,
Background seams no longer peek through—
TUI cells and scrollback finally match their hue! ✨

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix terminal TUI background seam' directly describes the main change: eliminating a visible seam in terminal TUI rendering by adjusting background ownership logic.
Description check ✅ Passed The PR description includes a detailed summary of what changed and why, reproduction steps, and verification notes, though a testing section and demo video are absent.
Linked Issues check ✅ Passed The PR fully addresses issue #3655 by deriving terminal background ownership from opacity/blur, centralizing logic in WindowAppearanceSnapshot, and adding regression tests to prevent future seams.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the background seam issue: GhosttyTerminalView derives background ownership, WindowAppearanceSnapshot adds helpers, and tests validate the new behavior.
Cmux Swift Actor Isolation ✅ Passed PR introduces no Swift 6 actor isolation violations. New static methods are pure value-logic with no mutable state. No MainActor issues, Sendable violations, or background context access introduced.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization primitives (semaphores, sleeps, locks) introduced. New code adds deterministic helper functions for background ownership computation.
Cmux No Hacky Sleeps ✅ Passed Rule applies only to TypeScript, JavaScript, shell, and build/runtime scripts. This PR modifies only Swift files, which are explicitly excluded and covered by swift-blocking-runtime.md instead.
Cmux Swift Concurrency ✅ Passed No async patterns introduced. Only synchronous functions and constants for background rendering logic. No DispatchQueue, Combine, Tasks, or completion-handlers added.
Cmux Swift @Concurrent ✅ Passed PR adds only synchronous pure-computation helpers. No async functions, no @concurrent annotations, no concurrency violations detected.
Cmux Swift File And Package Boundaries ✅ Passed Focused bug fix. GhosttyTerminalView: +14 lines (13,747 total); WindowAppearanceSnapshot: +27 lines (343 total). No oversized files, no excessive additions to large files, no mixed responsibilities.
Cmux Swift Logging ✅ Passed PR adds no prohibited logging (print/NSLog/debugPrint/dump). New functions are clean utility code for background rendering decisions with no logging violations.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state decorators, GeometryReader, lazy containers, or render-time mutations. Changes are to model-layer snapshot struct and tests only.
Cmux Architecture Rethink ✅ Passed Centralizes background ownership in WindowAppearanceSnapshot without timing repairs, dispatch delays, sleeps, locks, observers, or split lifecycle. Clear ownership boundaries with regression tests.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Changes modify existing GhosttyApp/GhosttyNSView and add rendering-logic to WindowAppearanceSnapshot. No new NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup. Not applicable.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-3655-terminal-bg-tui-seam

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

The PR branch was behind origin/main after opening the PR. Merging main keeps CI evaluating the terminal background fix against the current hook and IME changes already accepted upstream.

Constraint: iterate-pr requires syncing the PR branch with the base branch before CI iteration.

Confidence: high

Scope-risk: moderate

Directive: Preserve the renderer-owned terminal background commits as the functional change; this merge only brings the branch current with main.

Tested: Clean git merge from origin/main.

Not-tested: Local build/test execution per repository policy.
@greptile-apps

greptile-apps Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a visible background seam in TUI applications (e.g. Claude Code) running inside cmux terminals by routing solid opaque terminal backgrounds through Ghostty's renderer rather than the macOS compositor's host layer. Previously, macos-background-from-layer = true was forced for every terminal; now that value is derived from background-opacity and background-blur so only translucent or blurred terminals retain host-layer ownership.

  • WindowAppearanceSnapshot gains two new static helpers (usesHostLayerBackground(backgroundOpacity:backgroundBlur:) and terminalRenderingMode(backgroundOpacity:backgroundBlur:)) that centralise the opacity/blur → rendering-mode decision, along with a named threshold constant (opaqueBackgroundOwnershipThreshold = 0.999) with an explanatory comment.
  • GhosttyTerminalView now reads background-opacity and background-blur from the Ghostty config (both in the primary load path and the fallback path) before setting macos-background-from-layer, and extracts that read into defaultBackgroundOpacityValue(from:) / usesHostLayerBackground(for:) helpers.
  • Tests add the three complementary regression cases requested in the prior review thread: opaque+unblurred (renderer-owned), translucent (host-layer), and blurred-with-opaque-color (host-layer).

Confidence Score: 5/5

Safe to merge; changes are scoped to terminal background rendering mode selection and have no impact on security, data, or correctness of other subsystems.

The change correctly replaces an unconditional macos-background-from-layer = true with a value derived from two config keys (background-opacity, background-blur). The decision logic is centralised, clearly documented, and covered by three new regression tests that verify all three branches (renderer-owned, translucent host-layer, blurred host-layer). Both feedback items from the previous review thread — the missing threshold comment and the missing inverse test cases — are addressed in this revision. No actor-isolation, blocking-primitive, logging, or architectural-rethink concerns were found in the changed paths.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Windowing/WindowAppearanceSnapshot.swift Adds usesHostLayerBackground / terminalRenderingMode static helpers and a documented opaqueBackgroundOwnershipThreshold constant to centralise the opacity/blur → rendering-mode decision; no logic issues found.
Sources/GhosttyTerminalView.swift Replaces unconditional macos-background-from-layer = true with a config-derived value in both the primary and fallback config paths; extracts two small private helpers; pre-finalization ghostty_config_get usage is consistent with existing patterns in the file.
cmuxTests/WindowAppearanceSnapshotTests.swift Adds the three regression tests requested in the prior review thread (opaque renderer-owned, translucent host-layer, blurred host-layer) and updates makeSnapshot to derive terminalRenderingMode from opacity/blur instead of hardcoding .windowHostBackdrop.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Load config files] --> B[defaultBackgroundOpacityValue]
    A --> C[defaultBackgroundBlurValue]
    B --> D{usesHostLayerBackground?}
    C --> D
    D -- "blur != .disabled" --> E[true: host-layer owned]
    D -- "opacity < 0.999" --> E
    D -- "opacity >= 0.999 && blur == .disabled" --> F[false: renderer-owned]
    E --> G["macos-background-from-layer = true"]
    F --> H["macos-background-from-layer = false"]
    G --> I[setUsesHostLayerBackground true]
    H --> J[setUsesHostLayerBackground false]
    I --> K[terminalRenderingMode: .windowHostBackdrop]
    J --> L[terminalRenderingMode: .ghosttyRendererOwnedBackgroundImage]
Loading

Reviews (2): Last reviewed commit: "Document terminal background ownership t..." | Re-trigger Greptile

if backgroundBlur != .disabled {
return true
}
return clampedOpacity(backgroundOpacity) < 0.999

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The 0.999 threshold used to classify a terminal as "opaque" is a magic constant with no explanation. Without a comment, a future reader won't know whether this is meant to absorb floating-point round-trip imprecision (e.g. a user-typed 1.0 that comes back as 0.9999… after a Double → CGFloat → Double cycle), or whether values like 0.999 are intentionally treated as fully opaque. Adding a named constant or an inline comment clarifies the intent.

Suggested change
return clampedOpacity(backgroundOpacity) < 0.999
// Treat opacity values within floating-point round-trip tolerance of 1.0 as
// fully opaque, so a user-configured value of 1.0 that loses a ULP through
// Double → CGFloat → Double conversion never accidentally triggers host-layer
// ownership.
let opaqueThreshold: CGFloat = 0.999
return clampedOpacity(backgroundOpacity) < opaqueThreshold

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed by naming and documenting the opaque background ownership threshold so the 0.999 tolerance is explicit.

— Claude Code

Comment on lines +113 to +127
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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing complementary regression assertions

The new test verifies the opaque+unblurred → renderer-owned direction, but the inverse paths — translucent opacity → host-layer, and backgroundBlur != .disabled → host-layer — are not asserted. Without those, a future refactor of usesHostLayerBackground that accidentally widened the renderer-owned path (e.g., removing the blur check) would not be caught by CI. Consider adding at least one translucent case and one blur-enabled case that assert renderingMode == .windowHostBackdrop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed by adding inverse regression assertions for translucent and blurred terminal backgrounds staying host-layer-owned.

— Claude Code

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

2101-2117: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Surface-scoped opacity/blur changes can still use the wrong compositor path.

setUsesHostLayerBackground(...) is now only driven during app/fallback config loading. Later surface-scoped config updates still flow through updateDefaultBackground(...), but they do not recompute this flag, so a surface that becomes translucent or blurred after startup can keep the startup ownership decision and render through the wrong path. That breaks the invariant this PR is trying to preserve for translucent/blurred terminals. Based on learnings: keep “surface-scoped” Ghostty config reloads strictly scoped to the target surface.

Also applies to: 2252-2262

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 2101 - 2117, The
surface-scoped Ghostty config reloads are not recomputing the compositor path
flag, so surfaces that change translucency/blur after startup keep the startup
ownership decision; to fix, ensure any surface-scoped update (e.g., within
updateDefaultBackground(...)) recomputes the flag by calling
usesHostLayerBackground(for: targetConfig) and then invoking
setUsesHostLayerBackground(theComputedFlag, source:
"updateDefaultBackground.surface") for that surface only; keep
loadInlineGhosttyConfig and loadCmuxOwnedGhosttyKeybindOverrides scoped to the
target surface and avoid relying solely on the app/fallback initialization path
(the initialization calls around usesHostLayerBackground(for: fallbackConfig)
remain but do not substitute for per-surface recomputation).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2101-2117: The surface-scoped Ghostty config reloads are not
recomputing the compositor path flag, so surfaces that change translucency/blur
after startup keep the startup ownership decision; to fix, ensure any
surface-scoped update (e.g., within updateDefaultBackground(...)) recomputes the
flag by calling usesHostLayerBackground(for: targetConfig) and then invoking
setUsesHostLayerBackground(theComputedFlag, source:
"updateDefaultBackground.surface") for that surface only; keep
loadInlineGhosttyConfig and loadCmuxOwnedGhosttyKeybindOverrides scoped to the
target surface and avoid relying solely on the app/fallback initialization path
(the initialization calls around usesHostLayerBackground(for: fallbackConfig)
remain but do not substitute for per-surface recomputation).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 779085b5-e913-4302-af89-11611ef43e70

📥 Commits

Reviewing files that changed from the base of the PR and between 31a99c2 and 75b2ece.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/Windowing/WindowAppearanceSnapshot.swift
  • cmuxTests/WindowAppearanceSnapshotTests.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 75b2ece. Configure here.

backgroundBlur: defaultBackgroundBlurValue(from: config)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Runtime config changes leave rendering mode stale

Medium Severity

The GHOSTTY_ACTION_CONFIG_CHANGE handler calls updateDefaultBackground (which updates stored opacity/blur) but never recalculates usesHostLayerBackground. Previously this was safe because usesHostLayerBackground was always true. Now that the initialization paths dynamically set it to false for opaque unblurred backgrounds, a runtime config change that transitions opacity from opaque to translucent leaves the flag stale at false, causing WindowAppearanceSnapshot.current(...) to produce a snapshot with terminalRenderingMode = .ghosttyRendererOwnedBackgroundImage when it needs .windowHostBackdrop.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 75b2ece. Configure here.

@austinywang
austinywang merged commit f35b130 into main May 12, 2026
32 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 75b2ece3 Deployed May 12, 2026 by vercel[bot]
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.

Default terminal background doesn't match TUI cell colors — Claude Code input box and statusline render with visible seam

1 participant