Skip to content

Fix stale Metal drawable after terminal layer realization - #6057

Merged
austinywang merged 16 commits into
mainfrom
issue-6041-stale-metal-drawable
Jun 14, 2026
Merged

austinywang merged 16 commits into
mainfrom
issue-6041-stale-metal-drawable

Conversation

@austinywang

@austinywang austinywang commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #6041

Summary

  • Adds a deterministic AppKit regression test that stamps an initial 800x600-era Metal drawable, forces the terminal view to full portal bounds while GhosttyNSView.layer is a plain CALayer, then installs a stale GhosttyMetalLayer and waits for drawableSize to reconcile.
  • Fixes GhosttyNSView.updateSurfaceSize(size:) so a positive-size, in-window resize against a non-Metal backing layer schedules the existing deferred resize retry. The retry is bounded, coalesced, and resets once a Metal layer is observed.
  • Counts only retries that actually enqueue work, so coalesced calls do not consume the bounded non-Metal retry budget.
  • Tracks when a non-Metal resize still needs Metal-layer reconciliation; if the bounded retry window exhausts before the layer realizes, GhosttyMetalLayer.setSurfaceView(_:) triggers the same guarded reconcile once the layer attaches.

Reproduction

  • Read issue Terminal surface renders into a stale too-small Metal drawable after portal reveal/reparent (grid grows, drawableSize doesn't) #6041 in full and reproduced the core precondition deterministically in /Users/austinwang/manaflow/term/cmux2-repro-6041 on unfixed code.
  • Focused command used the cmux-unit scheme with isolated DerivedData and CMUX_SKIP_ZIG_BUILD=1; it did not launch the app or touch the default socket.
  • Unfixed result: GhosttyDrawableSizeRetryTests/reconcilesDrawableAfterFullSizeUpdateRunsBeforeMetalLayerRealizes() failed because no deferred retry was queued and the realized Metal layer remained stale:
    • debugDeferredSurfaceSizeRetryQueuedForTesting() was false
    • realizedLayer.drawableSize stayed (1600.0, 1200.0) instead of reconciling to the full backing size (2592.0, 1786.0)
  • The live Ghostty grid-growth assertion was not cleanly unit-testable locally: the app-hosted test harness did not reliably initialize the runtime surface pixel state. The committed test therefore targets the AppKit/Metal drawable precondition directly rather than faking a grid assertion.

Testing

  • git diff --check
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • ./scripts/lint-pbxproj-test-wiring.sh
  • ./scripts/check-pbxproj.sh
  • RED verified locally on unfixed code as above.
  • Local fixed-branch reruns were blocked by workstation disk/code-signing/Xcode result failures after the test and framework changes compiled far enough to reach the runner. CI is the green gate for the pushed head.

Demo Video

  • Not used; this uses the deterministic local precondition harness rather than the intermittent visual repro.

Localization

  • No user-facing strings were added or changed. No localization catalog changes needed.

Checklist

  • Reproduced before relying on the fix
  • Pushed to manaflow-ai/cmux origin
  • CI green
  • Review comments fully addressed on latest head

@vercel

vercel Bot commented Jun 13, 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 Jun 14, 2026 1:55am
cmux-staging Building Building Preview, Comment Jun 14, 2026 1:55am

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds bounded retry state and scheduling in GhosttyNSView for deferred surface sizing when the backing layer is not a CAMetalLayer, plus a unit test (and project wiring) that verifies drawable-size reconciliation after a non‑Metal refresh followed by Metal layer realization.

Changes

Drawable-size retry validation

Layer / File(s) Summary
Non-metal deferred drawable-size retry
Sources/GhosttyTerminalView.swift
Adds GhosttyNSView state (per-size retry counter, lastDrawableSize, max retry cap), updates resolvedSurfaceSize(preferred:), adds metal vs non‑metal branches in updateSurfaceSize that reset state for CAMetalLayer or schedule bounded retries for non‑Metal layers, schedules forceRefresh from GhosttyMetalLayer.setSurfaceView, and exposes DEBUG-only test accessors.
Drawable-size retry test and helpers
cmuxTests/GhosttyDrawableSizeRetryTests.swift
Adds a @MainActor test suite GhosttyDrawableSizeRetryTests with a test that boots an NSWindow hosting TerminalSurface, forces a non‑Metal CALayer refresh, installs a GhosttyMetalLayer seeded with a stale drawableSize, and spins the run loop until the metal layer’s drawableSize matches the expected backing pixel size. Includes helpers to find GhosttyNSView and to drain deferred surface-size retry work.
Project file wiring for test
cmux.xcodeproj/project.pbxproj
Registers GhosttyDrawableSizeRetryTests.swift in the cmuxTests target via PBXBuildFile, PBXFileReference, group children inclusion, and PBXSourcesBuildPhase entry.

Sequence Diagram(s)

sequenceDiagram
  participant Test as GhosttyDrawableSizeRetryTests
  participant NSWindow
  participant TerminalSurface
  participant GhosttyNSView
  participant GhosttyMetalLayer
  participant RunLoop

  Test->>NSWindow: create and show
  Test->>TerminalSurface: attach view to window
  TerminalSurface->>GhosttyNSView: reconcile geometry, compute expected backing drawable size
  Test->>GhosttyNSView: swap to non-Metal CALayer and force refresh
  GhosttyNSView->>RunLoop: schedule deferred retry
  Test->>GhosttyNSView: install GhosttyMetalLayer with stale drawableSize
  RunLoop->>GhosttyMetalLayer: deferred tick -> layer realizes
  GhosttyMetalLayer->>GhosttyNSView: drawableSize updated -> test asserts convergence
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4562: Also modifies Xcode project wiring to add test Swift files to the cmuxTests target’s PBXSourcesBuildPhase.

Suggested reviewers

  • jesstelford

Poem

🐇
I hopped through layers, soft and bright,
From non‑Metal dusk to Metal light,
I nudged a retry until sizes align,
Pixels stretched, the terminal's fine—
A rabbit claps: the test passed right.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error In Sources/GhosttyTerminalView.swift, updateSurfaceSize schedules a deferred surface-size retry via DispatchQueue.main.async when layer isn’t CAMetalLayer, adding timing-based synchronization per s... Avoid delayed main-queue scheduling (DispatchQueue.main.async). Trigger drawable-size reconciliation from an explicit layer-realization signal/callback, or at least use a cancellation-aware Task on MainActor with documented completion cr...
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 (19 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary fix: addressing stale Metal drawable size after a terminal layer is realized, which is the core issue resolved in this PR.
Linked Issues check ✅ Passed The PR directly addresses issue #6041 by implementing the proposed self-healing retry logic in updateSurfaceSize() and adding comprehensive test coverage that validates drawable reconciliation.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue: test file addition, view refresh on layer attachment, and deferred retry logic for non-Metal layers; no unrelated modifications present.
Cmux Swift Actor Isolation ✅ Passed Inspected Sources/GhosttyTerminalView.swift changes: UI state updates are dispatched to main actor (Task { @MainActor ... }, DispatchQueue.main.async). No new implicit MainActor models/services or...
Cmux Expensive Synchronous Load ✅ Passed Checked GhosttyTerminalView.swift, GhosttyDrawableSizeRetryTests.swift, and cmux.xcodeproj/project.pbxproj for RestorableAgentSessionIndex.load/SharedLiveAgentIndex.shared and no such expensive syn...
Cmux Cache Substitution Correctness ✅ Passed PR #6057 only adds cmuxTests/GhosttyDrawableSizeRetryTests.swift and wires it in project.pbxproj; no production Swift/TS/JS changes in persistence/history/undo/snapshot cache-substitution paths.
Cmux No Hacky Sleeps ✅ Passed PR #6057 changes only pbxproj + 2 Swift files per GitHub files list; no TypeScript/JS/shell/build/runtime scripts were modified, so runtime-no-hacky-sleeps scope isn’t triggered.
Cmux Algorithmic Complexity ✅ Passed Production updates (updateSurfaceSize/resolvedSurfaceSize/scheduling) have no sort/filter/map/scan loops; non-metal deferred retries are capped (maxDeferredSurfaceSizeNonMetalRetryCount = 8). Test-...
Cmux Swift Concurrency ✅ Passed Only added Swift concurrency is a main-actor fire-and-forget Task in GhosttyMetalLayer.setSurfaceView plus bounded DispatchQueue.main.async retry; no new background DispatchQueue/global Combine, co...
Cmux Swift @Concurrent ✅ Passed Checked Sources/GhosttyTerminalView.swift and cmuxTests/GhosttyDrawableSizeRetryTests.swift: no nonisolated async or @concurrent introduced; UI-bound Task uses @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed GhosttyTerminalView.swift change is AppKit/Metal view glue (drawable retry + DEBUG helpers) with small localized additions; new GhosttyDrawableSizeRetryTests.swift is 114 LOC and isolated to cmuxTe...
Cmux Swift Logging ✅ Passed swift-logging.md rules checked. In Sources/GhosttyTerminalView.swift there are no print/debugPrint/dump; the only NSLog is inside #if DEBUG, and there’s no new file-scoped Logger constant pattern f...
Cmux User-Facing Error Privacy ✅ Passed Checked cmuxTests/GhosttyDrawableSizeRetryTests.swift and Sources/GhosttyTerminalView.swift additions: no NSAlert/alert/recovery/user-facing error copy; debug env var use is inside #if DEBUG only.
Cmux Full Internationalization ✅ Passed Inspection of the PR-mentioned Swift/CXcode files shows no user-facing strings or any i18n/catalog/web routing changes; git status shows no localization-file modifications.
Cmux Swiftui State Layout ✅ Passed PR changes are Xcode project wiring + AppKit NSView drawable-size retry helpers/tests; no SwiftUI state-layout patterns (ObservableObject/@Published/GeometryReader/lazy list store refs/render-time...
Cmux Architecture Rethink ✅ Passed PR adds self-healing surface-size deferred retry for non-Metal layers using existing DispatchQueue.main.async gated by a queue flag and retry cap; only the new test uses bounded RunLoop polling.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR #6057 only adds a cmuxTests regression test and updates cmux.xcodeproj; no new standalone cmux auxiliary windows or cmux.* identifier assignments found.
Cmux Source Artifacts ✅ Passed PR only changes source/config files (cmux.xcodeproj/project.pbxproj plus 2 Swift sources); no generated logs, build output, DerivedData, caches, or temp artifacts added per source-control rules.
Description check ✅ Passed The PR description covers summary, reproduction, testing, demo video, localization, and checklist, with only the review-trigger block missing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6041-stale-metal-drawable

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.

@austinywang
austinywang force-pushed the issue-6041-stale-metal-drawable branch from 8d46b04 to d06295c Compare June 13, 2026 18:09

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 6606-6608: Replace the one-shot RunLoop.current.run(until:
Date().addingTimeInterval(0.05)) with a bounded poll that repeatedly advances
the run loop in short increments (e.g., 0.01s) until realizedLayer.drawableSize
== expectedDrawableSize or a hard timeout elapses (e.g., 1.0s); keep the final
assertion (`#expect`(realizedLayer.drawableSize == expectedDrawableSize)) after
the loop and, if you prefer, fail the test explicitly on timeout (XCTFail or
equivalent). Use the existing realizedLayer.drawableSize and
expectedDrawableSize checks inside the loop and RunLoop.current.run(until:) for
the short sleeps so the test is deterministic on slower CI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c7257e9-c9a9-4feb-b114-1f16b3d4ebb7

📥 Commits

Reviewing files that changed from the base of the PR and between 0b82dfe and 8d46b04.

📒 Files selected for processing (1)
  • cmuxTests/GhosttyConfigTests.swift

Comment thread cmuxTests/GhosttyConfigTests.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a race where a stale GhosttyMetalLayer drawable size could persist after AppKit replaces the terminal view's backing layer during split/portal realization, and adds a deterministic regression test that reproduces the precondition.

  • Production fix (GhosttyTerminalView.swift): updateSurfaceSize() now detects a non-Metal backing layer, schedules a bounded coalesced retry (up to 8 hops via Task { @MainActor }), and sets needsSurfaceSizeRetryAfterMetalLayerRealizes; makeBackingLayer() dispatches a rescue Task that calls reconcileSurfaceSizeAfterMetalLayerAttachIfNeeded() once the layer is installed, resetting the counter and re-running the reconcile.
  • Regression test (GhosttyDrawableSizeRetryTests.swift): Stamps an initial 800×600 drawable, installs a plain CALayer to simulate pre-Metal realization, drives a full-size update, then installs a stale GhosttyMetalLayer and uses bounded run-loop polling to verify that drawableSize reconciles.
  • Project wiring (cmux.xcodeproj/project.pbxproj): New test file is correctly added to both the file-reference section and the cmux-unit target build phase.

Confidence Score: 3/5

The fix correctly bounds and coalesces retries, but the rescue path in makeBackingLayer() carries an undocumented AppKit timing assumption and splits "pending reconcile" ownership across two new mutable flags with no named invariant; both issues need addressing before merge.

The production path now has two entry points to updateSurfaceSize() after a Metal layer swap: the bounded retry Task chain and the makeBackingLayer() rescue Task. The rescue Task relies on AppKit installing self.layer synchronously before the main run loop yields — an assumption not documented in code. The two new flags (needsSurfaceSizeRetryAfterMetalLayerRealizes and deferredSurfaceSizeNonMetalRetryCount) can desync from deferredSurfaceSizeRetryQueued, and the exhausted-retry case where the rescue alone holds the drawable-reconcile contract has no documented invariant. These are real gaps in a latency-sensitive rendering path.

Sources/GhosttyTerminalView.swift — specifically makeBackingLayer() (line 7123) and the updateSurfaceSize() non-Metal else-if branch (lines 7568–7572)

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds bounded non-Metal retry logic and a makeBackingLayer() Task to reconcile stale drawable sizes; introduces two new mutable flags without a documented invariant and an undocumented AppKit timing dependency
cmuxTests/GhosttyDrawableSizeRetryTests.swift New deterministic regression test that exercises the stale-drawable precondition; uses bounded run-loop polling (acceptable test scaffolding) and correctly observes the production repair path end-to-end
cmux.xcodeproj/project.pbxproj Wires the new test file into both the file-reference section and the cmux-unit target build phase; no structural concerns

Sequence Diagram

sequenceDiagram
    participant AppKit
    participant GhosttyNSView
    participant updateSurfaceSize
    participant ScheduledTask

    Note over GhosttyNSView: Resize arrives while layer is non-Metal
    GhosttyNSView->>updateSurfaceSize: updateSurfaceSize(size:)
    updateSurfaceSize->>updateSurfaceSize: "layer is not CAMetalLayer, count < 8"
    updateSurfaceSize->>ScheduledTask: scheduleDeferredSurfaceSizeRetryIfNeeded() → true
    updateSurfaceSize->>GhosttyNSView: "needsSurfaceSizeRetryAfterMetalLayerRealizes = true, count += 1"

    Note over AppKit: AppKit calls makeBackingLayer()
    AppKit->>GhosttyNSView: makeBackingLayer()
    GhosttyNSView->>ScheduledTask: Task MainActor → reconcileSurfaceSizeAfterMetalLayerAttachIfNeeded()
    AppKit->>GhosttyNSView: "self.layer = newMetalLayer (synchronous, before run loop yields)"

    Note over ScheduledTask: Next main-actor turn
    ScheduledTask->>GhosttyNSView: reconcileSurfaceSizeAfterMetalLayerAttachIfNeeded()
    GhosttyNSView->>GhosttyNSView: guard needsSurfaceSizeRetryAfterMetalLayerRealizes passes
    GhosttyNSView->>GhosttyNSView: "deferredSurfaceSizeNonMetalRetryCount = 0"
    GhosttyNSView->>updateSurfaceSize: updateSurfaceSize()
    updateSurfaceSize->>updateSurfaceSize: "layer is CAMetalLayer → update drawableSize, flag = false"
Loading

Reviews (10): Last reviewed commit: "merge: resolve conflicts with main" | Re-trigger Greptile

Comment on lines +72 to +74
RunLoop.current.run(until: Date().addingTimeInterval(0.05))

#expect(realizedLayer.drawableSize == expectedDrawableSize)

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 Wall-clock timeout couples test correctness to CI scheduler latency

RunLoop.current.run(until: Date().addingTimeInterval(0.05)) ties the pass/fail boundary to 50 ms of real time. Any production fix that lands a DispatchQueue.main.async callback will reliably beat that budget under normal conditions, but a second hop (e.g. an async dispatch that itself enqueues another dispatch, a deferred retry with a timer, or a KVO cascade that re-schedules) can miss the window on a loaded runner and produce a spurious failure. Since the test is @MainActor and already running on the main thread, a more robust approach is to directly observe realizedLayer.drawableSize via KVO or a notification until it matches, with a generous but bounded timeout — eliminating any false-negative risk from CI load while still protecting against infinite spin.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

Already addressed in the current branch: GhosttyDrawableSizeRetryTests uses condition-based bounded polling for drawable reconciliation, so the fixed 50 ms delay called out here is no longer present. This thread is outdated.

— Claude Code

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.

Already addressed in the current test: the fixed 50ms drain was replaced with a condition-based loop that advances the run loop until the drawable matches or a bounded deadline is reached.

— Claude Code

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.

Current HEAD no longer uses the fixed 50ms drain. The regression test waits on realizedLayer.drawableSize until it matches the expected backing size with a bounded run-loop pump, and CI is green on that path.

— Claude Code

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@cmuxTests/GhosttyDrawableSizeRetryTests.swift`:
- Around line 65-74: Add an explicit assertion to verify the intermediate stale
drawable size before the retry runs: after installing the Metal layer (after
surfaceView.layer = realizedLayer) assert that realizedLayer.drawableSize ==
staleDrawableSize so the test verifies the before state; keep the existing final
assertion that realizedLayer.drawableSize == expectedDrawableSize to confirm the
retry corrected it. Use the existing GhosttyMetalLayer instance (realizedLayer)
and the staleDrawableSize/expectedDrawableSize variables.
- Line 72: Replace the hardcoded RunLoop.current.run(until:
Date().addingTimeInterval(0.05)) drain in GhosttyDrawableSizeRetryTests with a
condition-based wait: poll realizedLayer.drawableSize against
expectedDrawableSize until a deadline (e.g., Date().addingTimeInterval(0.5)),
calling short RunLoop runs (or sleeping briefly) inside the loop to let the
main-queue reconciliation run, and then assert equality after the loop (fail if
deadline reached). Refer to realizedLayer.drawableSize and expectedDrawableSize
when implementing the loop condition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9cb32ebf-3a1c-4d12-8cf5-3292241206fd

📥 Commits

Reviewing files that changed from the base of the PR and between 8d46b04 and d06295c.

📒 Files selected for processing (2)
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/GhosttyDrawableSizeRetryTests.swift

Comment thread cmuxTests/GhosttyDrawableSizeRetryTests.swift Outdated
Comment thread cmuxTests/GhosttyDrawableSizeRetryTests.swift Outdated

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 8422-8435: The non‑Metal branch increments
deferredSurfaceSizeNonMetalRetryCount before calling
scheduleDeferredSurfaceSizeRetryIfNeeded(), which can waste retries when the
scheduler coalesces via deferredSurfaceSizeRetryQueued; change the logic so the
counter is only incremented when a retry is actually enqueued: either move the
increment into the protected section inside
scheduleDeferredSurfaceSizeRetryIfNeeded() immediately after
deferredSurfaceSizeRetryQueued is set, or modify
scheduleDeferredSurfaceSizeRetryIfNeeded() to return Bool (true if it actually
enqueued) and only increment deferredSurfaceSizeNonMetalRetryCount when that
Bool is true; update usages accordingly to reference
deferredSurfaceSizeNonMetalRetryCount, deferredSurfaceSizeRetryQueued and
scheduleDeferredSurfaceSizeRetryIfNeeded().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c49bb4c-1498-402d-ba81-955dc7a543da

📥 Commits

Reviewing files that changed from the base of the PR and between de4d4cf and fac6eec.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/GhosttyDrawableSizeRetryTests.swift

Comment thread Sources/GhosttyTerminalView.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed Greptile's dual-path drawable reconciliation concern in 574040b by removing the extra GhosttyMetalLayer.setSurfaceView Task. The fix now has one owner for this path: the bounded updateSurfaceSize retry, with the retry counter only incrementing when a retry is actually enqueued.

@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed Greptile's retry-exhaustion concern in 4e3e61e. updateSurfaceSize now records a pending Metal-layer reconciliation when it encounters a non-Metal backing layer, and setSurfaceView only invokes the guarded reconcile when that pending flag exists, so late Metal realization has an event-driven recovery without unbounded polling.

@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed Greptile's fallback edge case in 2e51813. The layer-attach fallback now resets the non-Metal retry counter before calling the guarded reconcile, so if AppKit still reports a non-Metal layer at that moment, the retry path can re-arm instead of dead-ending after an exhausted budget.

This branch was successfully deployed

1 active deployment
Preview – cmux — 0b1070fe Deployed Jun 14, 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.

Terminal surface renders into a stale too-small Metal drawable after portal reveal/reparent (grid grows, drawableSize doesn't)

1 participant