Skip to content

Fix stale terminal foreground after theme switch - #3852

Merged
austinywang merged 2 commits into
mainfrom
issue-3851-theme-switch-stale-fg
May 11, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-3851-theme-switch-stale-fg

Conversation

@austinywang

@austinywang austinywang commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Testing

  • Added a regression test for the app-config-reload surface refresh sequence.
  • Not run locally per task instruction; CI will run the regression.

Note

Medium Risk
Touches terminal surface refresh behavior during Ghostty config reload; incorrect ordering or nil-surface handling could cause missed updates or visual regressions across all open terminals.

Overview
Fixes stale terminal colors after theme/config reload by pushing the reloaded Ghostty config into each live terminal surface before repainting.

Introduces GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload to enforce the sequence soft per-surface config reload → hosted background refresh → forced surface redraw, and updates AppDelegate.refreshTerminalSurfacesAfterGhosttyConfigReload to use a live-surface guard.

Adds unit tests verifying the call order and the behavior when the surface is unavailable.

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


Summary by cubic

Fixes #3851 by soft-reloading each live terminal surface with the app’s reloaded Ghostty config before repaint to prevent stale foreground colors after theme switches. The surface update, host background refresh, and redraw now run in one ordered pass.

  • Bug Fixes
    • Added a helper to run: soft per-surface config reload → host background refresh → force redraw; used in AppDelegate via liveSurfaceForGhosttyAccess.
    • Added regression tests for call ordering and the unavailable-surface path.

Written for commit ecaf2e1. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved terminal surface refresh after Ghostty config reload: uses the live-surface access path to ensure background, styling, and visual updates are consistently applied across open terminal panels, even when a live surface is unavailable.
  • Tests

    • Added unit tests verifying refresh behavior both when surfaces are present and when they are nil.

Review Change Stack

The appearance-change path currently reaches each terminal surface after the app Ghostty config reload, but the per-surface sequence still only refreshes the host background and redraws. This regression pins the required ordering before the production fix: soft surface config update, host background refresh, then visual refresh.

Constraint: Issue 3851 requires a two-commit regression-test-then-fix structure

Confidence: high

Scope-risk: narrow

Directive: Keep app-level Ghostty config reload and per-surface redraw as one ordered sequence

Tested: git diff --check

Not-tested: Local XCTest/build per user instruction; test is intentionally red until the follow-up fix commit
@vercel

vercel Bot commented May 11, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 11, 2026 10:04am
cmux-staging Building Building Preview, Comment May 11, 2026 10:04am

@coderabbitai

coderabbitai Bot commented May 11, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27627122-483d-45e4-babc-6a7c870bbb13

📥 Commits

Reviewing files that changed from the base of the PR and between 11d694f and ecaf2e1.

📒 Files selected for processing (1)
  • Sources/App/GhosttySurfaceConfigurationRefresh.swift

📝 Walkthrough

Walkthrough

This PR introduces a GhosttySurfaceConfigurationRefresh helper to orchestrate per-surface Ghostty configuration reload, background refresh, and visual refresh after app-level config changes. It integrates this into AppDelegate.refreshTerminalSurfacesAfterGhosttyConfigReload so terminal surfaces receive updated palette colors during appearance switches.

Changes

Surface Configuration Refresh Orchestration

Layer / File(s) Summary
Surface Refresh Helper
Sources/App/GhosttySurfaceConfigurationRefresh.swift
New @MainActor enum with nonisolated forceRefreshReason and applyAfterAppConfigReload(...) that conditionally calls reloadSurfaceConfiguration(surface, true, source) when a surface exists, then always calls refreshHostBackground() and forceRefresh(forceRefreshReason).
AppDelegate Integration
Sources/AppDelegate.swift
refreshTerminalSurfacesAfterGhosttyConfigReload(source:) now uses terminalPanel.surface.liveSurfaceForGhosttyAccess(...) and invokes GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload, wiring closures for GhosttyApp.shared.reloadSurfaceConfiguration(...), terminalPanel.hostedView.refreshHostBackgroundAfterGhosttyConfigReload(), and terminalPanel.surface.forceRefresh(reason:).
Project Build Configuration
GhosttyTabs.xcodeproj/project.pbxproj
Registers GhosttySurfaceConfigurationRefresh.swift in PBXBuildFile, PBXFileReference, Sources group, and GhosttyTabs PBXSourcesBuildPhase so it is compiled into the app.
Unit Tests
cmuxTests/AppearanceSettingsTests.swift
Adds two @MainActor tests: one verifies reload+host refresh+force-refresh order when a surface exists; the other verifies reload is skipped when surface is nil while host refresh and force-refresh still occur.

Sequence Diagram(s)

sequenceDiagram
  participant AppDelegate
  participant GhosttySurfaceConfigurationRefresh
  participant GhosttyApp
  participant HostedView
  participant TerminalSurface

  AppDelegate->>GhosttySurfaceConfigurationRefresh: applyAfterAppConfigReload(liveSurface?, source, reloadCB, refreshHostBG, forceRefreshCB)
  alt surface exists
    GhosttySurfaceConfigurationRefresh->>GhosttyApp: reloadSurfaceConfiguration(surface, true, source)
  end
  GhosttySurfaceConfigurationRefresh->>HostedView: refreshHostBackground()
  GhosttySurfaceConfigurationRefresh->>TerminalSurface: forceRefresh(forceRefreshReason)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#818: Modifies AppDelegate.refreshTerminalSurfacesAfterGhosttyConfigReload and is related to the same refresh flow changes.
  • manaflow-ai/cmux#3480: Introduced per-surface reload paths (reloadSurfaceConfiguration) that this PR calls from the appearance-refresh flow.
  • manaflow-ai/cmux#2101: Added TerminalSurface.liveSurfaceForGhosttyAccess(...) which AppDelegate now uses to obtain live surfaces.

"I'm a rabbit with a clever plan,
I nudged the surfaces, one by one—hop, hop, span.
Reload the config, brighten the view,
Refresh the host, and force redraw too.
Colors restored — a joyful rabbit chew! 🐇✨"

🚥 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 Title 'Fix stale terminal foreground after theme switch' clearly and concisely summarizes the main change—resolving the specific visual issue of stale text colors after appearance switches.
Description check ✅ Passed Description covers the summary (what changed and why), testing approach (regression test added; CI to run), and includes linked issue reference and risk assessment; all required template sections are addressed.
Linked Issues check ✅ Passed Changes fully satisfy issue #3851 requirements: adds per-surface soft config reload via GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload, integrates into AppDelegate.refreshTerminalSurfacesAfterGhosttyConfigReload with correct ordering (reload → background refresh → redraw), and includes unit tests for call sequence validation.
Out of Scope Changes check ✅ Passed All changes are in scope: new GhosttySurfaceConfigurationRefresh module encapsulates the refresh logic, AppDelegate is updated to use the new helper, and tests validate the new behavior without unrelated modifications.
Cmux Swift Actor Isolation ✅ Passed GhosttySurfaceConfigurationRefresh explicitly marked @MainActor with nonisolated string constant; no shared mutable Sendable types or implicit MainActor protocols introduced.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing primitives detected. Code is purely synchronous, callback-based, and @MainActor-safe with no semaphores, locks, sleeps, or blocking dispatch calls.
Cmux No Hacky Sleeps ✅ Passed PR is Swift-only. Check applies to TypeScript, JavaScript, shell, and build scripts. Swift timing is covered by separate rule swift-blocking-runtime.md.
Cmux Swift Concurrency ✅ Passed No legacy async patterns. Uses @MainActor with synchronous closures for surface refresh orchestration. No DispatchQueue, Combine, fire-and-forget Tasks, or problematic completion handlers.
Cmux Swift @Concurrent ✅ Passed All functions are synchronous with correct annotations. @MainActor enum and nonisolated static constant follow Swift concurrency rules correctly. No @concurrent/async violations.
Cmux Swift File And Package Boundaries ✅ Passed 18-line orchestration enum and ~25 lines added to AppDelegate, both well below thresholds. No mixed responsibilities or package boundary violations. Focused bug fix with pragmatic glue code placement.
Cmux Swift Logging ✅ Passed Code complies with swift-logging.md. No print/debugPrint/dump/NSLog in production code. cmuxDebugLog is guarded with #if DEBUG. Tests are exempt.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI View code or state violations. Changes are AppKit-focused and utility code only. No problematic state patterns detected.
Cmux Architecture Rethink ✅ Passed Introduces small correctness fix via clean coordinator pattern. No timing repairs, mutable state, observers, or split lifecycle. Clear invariant and ownership. Fixes issue #3851 root cause.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone windows added. PR modifies terminal surface refresh logic (allowed) and adds a helper enum. No NSWindow, NSPanel, NSWindowController, Window, or WindowGroup code.

✏️ 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-3851-theme-switch-stale-fg

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.

@greptile-apps

greptile-apps Bot commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes stale terminal foreground colors after theme switches by soft-reloading each live terminal surface's configuration before the existing hosted-background refresh and force-redraw on app-level Ghostty config reload.

  • New helper GhosttySurfaceConfigurationRefresh encapsulates the three-step refresh sequence (reload surface config → refresh host background → force redraw) with injection-friendly closures, making the call order testable.
  • AppDelegate.refreshTerminalSurfacesAfterGhosttyConfigReload now gates the surface-config reload behind liveSurfaceForGhosttyAccess, protecting against stale/freed Ghostty surface pointers that could corrupt state during teardown.
  • Regression tests cover both the ordered-call path (live surface) and the nil-surface skip path.

Confidence Score: 5/5

Safe to merge; the change is a focused, well-tested fix with no regressions introduced.

The fix is narrow and correct: the new liveSurfaceForGhosttyAccess guard prevents calling into a freed Ghostty surface pointer, the three-step sequence is enforced in the right order, and both the happy path and the nil-surface path are covered by regression tests. forceRefresh already guards internally against a nil surface. No new timing dependencies, flags, or singletons are introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/GhosttySurfaceConfigurationRefresh.swift New helper enum encapsulating the three-step app-config-reload refresh sequence with non-escaping closures; @mainactor isolation is appropriate given the UI closures it wraps.
Sources/AppDelegate.swift Replaces the bare two-step refresh with a liveSurfaceForGhosttyAccess guard + GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload; stale-pointer safety and correct call order are now enforced at the call site.
cmuxTests/AppearanceSettingsTests.swift Adds two focused unit tests validating call ordering and nil-surface skip behavior.
GhosttyTabs.xcodeproj/project.pbxproj Routine Xcode project registration of the new Swift file; entries are consistent across all three sections.

Reviews (2): Last reviewed commit: "Keep terminal palettes fresh after appea..." | Re-trigger Greptile

@@ -0,0 +1,18 @@
@MainActor
enum GhosttySurfaceConfigurationRefresh {
static let forceRefreshReason = "appDelegate.refreshAfterGhosttyConfigReload"

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 forceRefreshReason is a pure immutable string constant, but because the enclosing enum is @MainActor, this static let is implicitly main-actor-isolated. Any call site outside a @MainActor context (e.g., a nonisolated logging helper or a background diagnostic) would need to await MainActor.run { ... } just to read a plain string. The actor isolation rule flags value-only utilities that should be nonisolated to avoid unnecessary main-actor coupling.

Suggested change
static let forceRefreshReason = "appDelegate.refreshAfterGhosttyConfigReload"
nonisolated static let forceRefreshReason = "appDelegate.refreshAfterGhosttyConfigReload"

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

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 marking forceRefreshReason nonisolated so reading the plain string does not require MainActor isolation.

— Claude Code

The app-level Ghostty config reload now pushes the already-loaded app config into each live terminal surface with a soft surface reload before repainting. That matches the direct GHOSTTY_ACTION_RELOAD_CONFIG ordering and prevents dark-theme foreground palettes from being reused after a dark-to-light appearance switch.

Constraint: Use soft reload so surfaces reuse the app config already loaded by the appearance path

Rejected: Force redraw only | redraw uses stale per-surface palette state

Confidence: high

Scope-risk: narrow

Directive: Do not separate per-surface config update from the subsequent redraw on app-level config reloads

Tested: git diff --check

Not-tested: Local XCTest/build per user instruction; CI will run the regression

This branch was successfully deployed

1 active deployment
Preview – cmux — ecaf2e18 Deployed May 11, 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.

Dark → light appearance switch leaves stale foreground colors in already-running terminal sessions (white-on-white text)

1 participant