Skip to content

Fix terminal black screen on macOS 26.3.1 by dispatching Ghostty callbacks to main thread - #1937

Merged
lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
rerun0510:fix/macos26-background-thread
Mar 23, 2026
Merged

lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
rerun0510:fix/macos26-background-thread

Conversation

@rerun0510

@rerun0510 rerun0510 commented Mar 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • What changed? Wrapped four Ghostty action_cb handlers (SCROLLBAR, CELL_SIZE, COLOR_CHANGE, CONFIG_CHANGE) in DispatchQueue.main.async so that all AppKit view property mutations and NotificationCenter posts occur on the main thread.
  • Why? On macOS 26.3.1, these mutations were happening on Ghostty's background I/O thread, triggering "Publishing changes from background threads is not allowed" errors and causing the terminal to render as a completely black screen. Earlier macOS versions tolerated these off-main-thread mutations silently; macOS 26.3.1 enforces stricter main-thread checks that break the SwiftUI view update cycle.

Testing

  • Launched on macOS 26.3.1 — terminal renders normally, no black screen
  • Verified no "Publishing changes from background threads" warnings in debug log

Demo Video

  • Before fix (black screen):
2026-03-22.17.52.43.mov
  • After fix (normal rendering):
2026-03-22.17.54.29.mov

Summary by cubic

Fixes the black screen on macOS 26.3.1 by moving Ghostty action callback UI updates to the main thread. Prevents background-thread AppKit mutations that break SwiftUI rendering.

  • Bug Fixes
    • Wrapped SCROLLBAR, CELL_SIZE, COLOR_CHANGE, and CONFIG_CHANGE handlers in DispatchQueue.main.async so view updates and NotificationCenter posts run on the main thread.
    • Applies background color changes/clears on the main thread and refreshes the window background when active.
    • Verified on macOS 26.3.1: terminal renders normally; no “Publishing changes from background threads” warnings.

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

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability and responsiveness of scrollbar adjustments, cell size changes, color and background modifications, and configuration updates.

Publishing changes from background threads is not allowed in macOS 26
(Tahoe). Ghostty action callbacks run on I/O threads but were modifying
AppKit view properties and posting notifications without dispatching to
the main thread.

Fixes:
- GHOSTTY_ACTION_SCROLLBAR: wrap in DispatchQueue.main.async
- GHOSTTY_ACTION_CELL_SIZE: wrap in DispatchQueue.main.async
- GHOSTTY_ACTION_COLOR_CHANGE: wrap background color updates in main async
- GHOSTTY_ACTION_CONFIG_CHANGE: wrap background color clear in main async
@vercel

vercel Bot commented Mar 22, 2026

Copy link
Copy Markdown

@rerun0510 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 22, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34de486a-e830-4c3f-911b-f0b239cc261c

📥 Commits

Reviewing files that changed from the base of the PR and between 76c1e63 and ad57e10.

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

📝 Walkthrough

Walkthrough

Threading refactoring in GhosttyTerminalView that dispatches UI updates for scrollbar, cell size, color changes, and config modifications explicitly onto the main queue instead of executing inline on callback threads.

Changes

Cohort / File(s) Summary
Main Queue Dispatch Refactoring
Sources/GhosttyTerminalView.swift
Action handlers for GHOSTTY_ACTION_SCROLLBAR, GHOSTTY_ACTION_CELL_SIZE, GHOSTTY_ACTION_COLOR_CHANGE, and GHOSTTY_ACTION_CONFIG_CHANGE now explicitly dispatch surface view updates and related notifications/method calls to the main queue via DispatchQueue.main.async with appropriate capture semantics.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 A queue so main, a thread so neat,
Updates now dispatched, the dance complete—
No more inline, just async bliss,
Where scrollbars dance and colors kiss! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change—dispatching Ghostty callbacks to the main thread to fix a black screen issue on macOS 26.3.1.
Description check ✅ Passed The description includes Summary (what changed and why), Testing (verification steps and results), and demo videos, but the Checklist section is incomplete with all items unchecked.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

@greptile-apps

greptile-apps Bot commented Mar 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a black-screen regression on macOS 26.3.1 by dispatching AppKit property mutations and NotificationCenter posts from Ghostty's background I/O thread to the main thread via DispatchQueue.main.async. The fix is targeted and follows the pattern already established by other action handlers in the same switch statement.

Key points:

  • GHOSTTY_ACTION_SCROLLBAR and GHOSTTY_ACTION_CELL_SIZE: straightforward and correct — property write and notification post are now atomically batched on main.
  • GHOSTTY_ACTION_COLOR_CHANGE: correctly moves backgroundColor assignment and applySurfaceBackground() into the main-thread block; the pre-dispatch log line still reads surfaceView properties from the background thread, which is pre-existing behaviour and not a new concern.
  • GHOSTTY_ACTION_CONFIG_CHANGE: introduces a subtle reordering — updateDefaultBackground() now runs synchronously on the background thread before the async block's applySurfaceBackground() executes. This is actually more semantically correct (the surface is painted with the freshly-updated config color) but is undocumented. Additionally, the post-dispatch debug log at line 2411 reads surfaceView.backgroundColor from the background thread while the async block may concurrently write it to nil on main — a new data race (debug-only path).

Confidence Score: 4/5

  • This PR is safe to merge; the core threading fix is correct and well-scoped, with only a minor debug-mode data race remaining.
  • The fix directly addresses the reported root cause (off-main AppKit mutations) using the established DispatchQueue.main.async pattern already present throughout the file. All four handlers behave consistently after the change. The one concrete issue — a data race on surfaceView.backgroundColor in the CONFIG_CHANGE debug log — only affects backgroundLogEnabled debug builds and cannot cause a crash or silent data corruption for users. The ordering change in CONFIG_CHANGE is a net improvement. No regression in the two earlier-macOS code paths is expected.
  • Sources/GhosttyTerminalView.swift lines 2409–2413 (GHOSTTY_ACTION_CONFIG_CHANGE debug log race)

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Wraps four Ghostty action callbacks in DispatchQueue.main.async to fix main-thread violations on macOS 26.3.1. Core fix is sound; introduces a minor data race on surfaceView.backgroundColor in the debug log path of GHOSTTY_ACTION_CONFIG_CHANGE, and a silent ordering change where updateDefaultBackground now runs before applySurfaceBackground.

Sequence Diagram

sequenceDiagram
    participant GT as Ghostty I/O Thread
    participant MT as Main Thread (DispatchQueue.main)

    Note over GT,MT: GHOSTTY_ACTION_SCROLLBAR / CELL_SIZE
    GT->>GT: Build scrollbar/cellSize value
    GT->>MT: DispatchQueue.main.async { set property + post notification }
    GT-->>GT: return true (immediate)

    Note over GT,MT: GHOSTTY_ACTION_COLOR_CHANGE
    GT->>GT: Build newColor (NSColor)
    GT->>GT: logBackground (pre-dispatch, background thread)
    GT->>MT: DispatchQueue.main.async [self] { set backgroundColor + applySurfaceBackground + applyWindowBackgroundIfActive }
    GT-->>GT: return true

    Note over GT,MT: GHOSTTY_ACTION_CONFIG_CHANGE (new ordering)
    GT->>MT: DispatchQueue.main.async [self] { clear staleOverride + applySurfaceBackground + applyWindowBackgroundIfActive }
    GT->>GT: updateDefaultBackground(...) ← runs BEFORE async block executes
    GT->>GT: logBackground (reads surfaceView.backgroundColor — potential race)
    GT-->>GT: return true
    MT->>MT: applySurfaceBackground() reads NEW defaultBackgroundColor
Loading

Comments Outside Diff (2)

  1. Sources/GhosttyTerminalView.swift, line 2409-2413 (link)

    P1 Data race on surfaceView.backgroundColor in debug log

    After the refactor, surfaceView.backgroundColor is written to nil inside the async main-thread block (line 2394), while the debug log below (line 2411) reads surfaceView.backgroundColor synchronously on the Ghostty background I/O thread — potentially at the same moment the main thread is writing it. This is a new data race introduced by moving the write to a different thread.

    Before this PR the write also happened on the background thread, so both accesses were on the same thread; now they are on two concurrent threads with no synchronization.

    Because this is gated behind backgroundLogEnabled, the practical impact is low (debug builds only), but the read could return a torn or stale value. Consider moving the log inside the async block where surfaceView.backgroundColor has already been cleared, or capture the value before dispatching:

    let overrideSnapshot = surfaceView.backgroundColor
    DispatchQueue.main.async { [self] in
        if let staleOverride = overrideSnapshot {
            surfaceView.backgroundColor = nil
            // … rest of block
        }
    }
    if backgroundLogEnabled {
        logBackground(
            "surface config change deferred terminal bg apply … override=\(overrideSnapshot?.hexString() ?? "nil") default=\(defaultBackgroundColor.hexString())"
        )
    }
  2. Sources/GhosttyTerminalView.swift, line 2392-2408 (link)

    P2 Ordering change: updateDefaultBackground now runs before applySurfaceBackground

    In the original code the execution order was synchronous on the background thread:

    1. Clear stale backgroundColor override
    2. applySurfaceBackground() (reads the OLD defaultBackgroundColor)
    3. applyWindowBackgroundIfActive() (dispatched async to main)
    4. updateDefaultBackground(...) (updates defaultBackgroundColor to new config value)

    After this change the order is:

    1. Dispatch async block (deferred)
    2. updateDefaultBackground(...) runs immediately on background thread — sets defaultBackgroundColor to the new value
    3. Async block runs on main thread — applySurfaceBackground() now reads the NEW defaultBackgroundColor

    This is actually more semantically correct (the surface background is applied with the freshly updated config color), but it is a subtle behavioral difference worth documenting with a comment so future readers understand the intentional ordering. Without a comment, the two DispatchQueue.main.async and inline calls look like they could be reordered freely.

Reviews (1): Last reviewed commit: "fix: dispatch UI updates to main thread ..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file


Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

@lawrencecchen
lawrencecchen merged commit c9616c5 into manaflow-ai:main Mar 23, 2026
13 of 14 checks passed
@lawrencecchen

Copy link
Copy Markdown
Contributor

Thank you for the contribution!

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.

2 participants