Skip to content

Reclaim offscreen terminal renderer GPU memory (IOSurface) non-destructively - #5857

Merged
lawrencecchen merged 22 commits into
mainfrom
feat-surface-leak-repro
Jun 12, 2026
Merged

lawrencecchen merged 22 commits into
mainfrom
feat-surface-leak-repro

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Each terminal surface that becomes visible spins up a Ghostty Metal renderer holding an IOSurface (~40MB) plus a renderer thread. When the surface goes offscreen, cmux only calls ghostty_surface_set_occlusion(false), which pauses drawing but never releases the IOSurface. Renderer resources accumulate with every surface ever visited and only free on close, so memory grows unbounded over a long session.

A live capture of a 28h stable session reached 13.7GB footprint, 826 threads, 603 IOSurfaces, with the main thread pinned in SwiftUI layout over the bloated tree. Reproduced in a tagged build via the debug socket: visiting 15 surfaces once added +59 IOSurfaces / +614MB that never released until close.

Addresses #4607 and #5731.

Fix

Drive Ghostty's existing displayUnrealized() / displayRealized() (which free and recreate only the Metal swap-chain IOSurfaces) for offscreen, idle surfaces, while keeping the PTY/io thread and terminal state alive. No process is killed (unlike agent hibernation), so it is safe to default ON.

A new RendererRealizationController releases the renderer of surfaces that are offscreen and idle past a threshold, keeping the maxWarmRenderers most-recently-visible surfaces warm so tab switching stays instant. setVisibleInUI(true) re-realizes the renderer before the next draw.

Changes

  • Ghostty fork (submodule bump): new Darwin-only C API ghostty_surface_set_renderer_realized(surface, bool) via a display_realized renderer-thread mailbox message. Runs on the renderer thread, the surface is occluded when released (macOS drawFrame early-returns on !visible), and both calls take draw_mutex.
  • TerminalSurface.releaseRenderer() / realizeRenderer() with a strict-alternation dedup flag (mirrors Ghostty's swap-chain defunct state so displayRealized's assert(defunct) always holds); re-realize hook in setVisibleInUI; reset on createSurface.
  • RendererRealizationController + pure RendererRealizationPlanner (LRU by last-visible + idle threshold + warm cap).
  • RendererRealizationSettings (default on, idle 30s, warm 12) wired into cmux.json, settings navigation, a command-palette toggle, and en/ja localization.
  • RendererRealizationPlannerTests.

Verification

Driven via the debug socket on a tagged build:

stage threads IOSurface footprint registry
baseline 16 13 346 MB 3
after visiting 15 (the leak) 77 64 985 MB 18
home +16s (reclamation) 76 36 518 MB 18
re-visit a released surface 77 38 559 MB 18

Reclaimed ~467MB while the registry stayed at 18 (PTYs alive). Re-visiting a released surface re-realized and showed the live shell prompt, not a blank or restarted terminal.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes default-on terminal rendering lifecycle and GPU resource ownership on the main thread; mitigations include visibility gates and enqueue-success state sync, but regressions could cause blank frames or delayed reclaim behavior.

Overview
Adds default-on, non-destructive GPU memory reclamation for off-screen terminals: cmux can release each surface’s Metal/IOSurface renderer while the PTY and shell keep running, then rebuild the renderer when the tab is shown again.

A new RendererRealizationController (started at launch) runs periodic passes over registered surfaces, using a pure RendererRealizationPlanner (LRU warm cap + off-screen idle threshold) to choose which realized renderers to drop. TerminalSurface gains releaseRenderer() / realizeRenderer() wired to Ghostty’s new ghostty_surface_set_renderer_realized API, with visibility tracked from setVisibleInUI so visible terminals are never released and re-show realizes before draw; mirror state updates only when the renderer mailbox enqueue succeeds, with an immediate controller pass as a backstop if realize drops.

terminal.rendererRealization.* settings (enabled, idle seconds, max warm renderers) are added across catalog, Settings UI, command palette, cmux.json import/schema, navigation/search, localization, and templates. The Ghostty fork pin and checksums are updated for the new C API; RendererRealizationPlannerTests cover the policy.

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


Summary by cubic

Reclaims GPU memory from off‑screen terminals by releasing only Metal swap‑chain IOSurfaces and rebuilding them on show, gated by portal visibility so on‑screen terminals are never released. Upgrades ghostty to a non‑blocking .instant renderer‑realize API and, if a re‑show enqueue drops, triggers an immediate pass to re‑realize on the next runloop to avoid blank frames. Fixes #4607 and #5731.

  • New Features

    • Runtime: RendererRealizationController runs periodic LRU passes to release off‑screen, idle renderers, keeps a warm cap, re‑realizes on setVisibleInUI(true) before draw via non‑blocking enqueue, and self‑heals any visible‑but‑unrealized surface; pure planner + tests.
    • Settings & UX: Default‑on terminal.rendererRealization.* (enabled, idleSeconds, maxWarmRenderers) with Settings UI + search, Command Palette toggle, cmux.json schema + managed‑config import, and en/ja strings; always‑on timer picks up changes without restart.
    • Dependencies: Bump ghostty fork to the .instant enqueue C API for display_realized; docs updated.
  • Bug Fixes

    • Safety: Never release a visible terminal; validate a live Ghostty surface before native calls; flip realize/unrealize state only on successful enqueue to prevent desync and blank frames; if a re‑show realize enqueue drops, schedule an immediate controller pass to re‑realize on the next runloop.
    • Correctness: Stamp “last visible” on show and exactly once on hide so off‑screen idle is measured from the hide moment; controller re‑realizes any visible‑but‑unrealized surface as a backstop.

Written for commit 3b3854c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Configurable GPU memory reclamation for offscreen terminal surfaces (enable/disable, idle timeout, warm-renderer limit) with an automatic background controller started at launch.
    • Command-palette toggle to enable/disable the reclamation feature.
  • Tests

    • Unit tests covering the reclamation selection policy and edge cases.
  • Documentation

    • English and Japanese localization for the settings, updated settings template, search/navigation, and runtime docs.
  • Chores

    • Updated Ghostty fork pin and associated checksum.

…ctively

Long sessions accumulated terminal surfaces that each kept a live Ghostty Metal
renderer (IOSurface ~40MB + renderer thread) forever after being visited, even
once offscreen. occlusion only paused drawing, never releasing the IOSurface, so
memory grew unbounded (a captured 28h session reached 13.7GB / 826 threads / 603
IOSurfaces with the main thread pinned in SwiftUI layout over the bloated tree).

Reproduced in a tagged build via the debug socket: visiting 15 surfaces once
added +59 IOSurfaces / +614MB that never released until close.

Fix: drive Ghostty's existing displayUnrealized()/displayRealized() (which free
and recreate only the Metal swap-chain IOSurfaces) for offscreen, idle surfaces,
keeping the PTY/io thread and terminal state alive. A new
RendererRealizationController releases the renderer of surfaces that are
offscreen and idle past a threshold, keeping the most-recently-visible
maxWarmRenderers warm so tab switching stays instant; setVisibleInUI(true)
re-realizes before the next draw. Non-destructive (no process kill), default ON.

Verified: visiting 15 surfaces then returning home reclaimed ~467MB while the
registry stayed at 18 (PTYs alive); re-visiting a released surface re-realized
and showed the live shell, not a blank/restarted terminal.

Addresses #4607 and
#5731.

- New libghostty C API ghostty_surface_set_renderer_realized (submodule bump).
- TerminalSurface.releaseRenderer()/realizeRenderer() with strict-alternation
  dedup; re-realize hook in setVisibleInUI; reset on createSurface.
- RendererRealizationController + pure RendererRealizationPlanner (LRU + idle).
- RendererRealizationSettings (default on, idle 30s, warm 12) wired into
  cmux.json, settings navigation, command palette toggle, en/ja localization.
- RendererRealizationPlannerTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 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 Jun 12, 2026 8:56am
cmux-staging Building Building Preview, Comment Jun 12, 2026 8:56am

@coderabbitai

coderabbitai Bot commented Jun 11, 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

This PR implements non-destructive offscreen renderer reclamation: a planner selects idle, realized, offscreen terminal surfaces to release GPU renderers; surfaces track realization state and last-visible timestamps; a background controller runs periodic evaluations; settings, parsing, UI toggles, tests, localization, and build wiring are included.

Changes

Renderer Reclamation Feature

Layer / File(s) Summary
Reclamation policy & settings definitions
Sources/App/RendererRealizationController.swift, Sources/App/WorkspaceRuntimeSettings.swift, cmuxTests/RendererRealizationPlannerTests.swift
Pure RendererRealizationPlanner.selectedSurfaceIds() filters to realized surfaces, ranks by LRU last-visible timestamp with deterministic UUID tie-break, preserves up to maxWarmRenderers, skips visible surfaces, and requires idleSeconds before selection. RendererRealizationSettings stores persisted values (enabled, idleSeconds, maxWarmRenderers) with defaults, clamping, and change notifications. Tests cover disabled state, visibility, idle thresholds, warm cap behavior, realization filtering, and tie-breaking.
Terminal surface renderer state & lifecycle
Sources/GhosttyTerminalView.swift
TerminalSurface gains rendererRealized, rendererLastVisibleAt, rendererPortalVisible, isRendererRealized, noteBecameVisibleForRendererReclamation(), releaseRenderer(), and realizeRenderer(). New surfaces initialize realized state; visibility updates realize renderer and stamp last-visible time.
Reclamation controller & runtime evaluation
Sources/App/RendererRealizationController.swift, Sources/AppDelegate.swift
RendererRealizationController singleton observes settings, runs a repeating background timer that triggers main-actor evaluate(now:), stamps visible surfaces, collects records via AppDelegate.rendererRealizationRecords(), computes selections via planner, and calls releaseRenderer() on chosen surfaces. Controller is started during app launch.
Settings persistence & file parsing
Sources/KeyboardShortcutSettingsFileStore.swift, Sources/KeyboardShortcutSettingsFileStore+Template.swift, Sources/CmuxSettingsJSONPathSupport.swift
Settings parsing recognizes terminal.rendererRealization with validation for enabled, idleSeconds, and maxWarmRenderers; managed-default application tracks these keys and invokes RendererRealizationSettings.notifyDidChange(). Default settings template includes the new configuration block.
Command palette toggle & settings navigation
Sources/CommandPalette/CommandPaletteSettingsToggle.swift, Sources/SettingsNavigation.swift
Adds a command-palette toggle (palette.toggleSetting.rendererRealization) wired to RendererRealizationSettings and a renderer-realization settings search entry plus anchor mappings for each key.
Settings UI catalog, section, and tests
Packages/CmuxSettings/..., Packages/CmuxSettingsUI/..., Packages/CmuxSettingsUI/Tests/...
Adds three catalog DefaultsKey entries, inserts curated setting entries, exposes UI controls (toggle and steppers) in the Terminal settings section, and updates settings-row anchor-resolution tests to include the new keys.
Build system, localization, & submodule
cmux.xcodeproj/project.pbxproj, Resources/Localizable.xcstrings, ghostty, scripts/ghosttykit-checksums.txt, docs/ghostty-fork.md
Project file updated to include new source and test files; localization adds English and Japanese strings for the new settings; ghostty submodule pointer advanced; GhosttyKit checksum entry and fork documentation updated.

Sequence Diagram(s)

sequenceDiagram
  participant AppDelegate
  participant Controller as RendererRealizationController
  participant Planner as RendererRealizationPlanner
  participant Surface as TerminalSurface
  AppDelegate->>Controller: start()
  Controller->>AppDelegate: rendererRealizationRecords()
  AppDelegate-->>Controller: [(surfaceId,isVisible,isRealized,lastVisibleAt)]
  Controller->>Planner: selectedSurfaceIds(inputs, settings, now)
  Planner-->>Controller: Set<surfaceId> to release
  loop For each selected surface
    Controller->>Surface: releaseRenderer(surfaceId)
    Surface->>Surface: ghostty_surface_set_renderer_realized(false)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#5012: Adds terminal.rendererRealization keys to settings search/index-anchor resolution tests and aligns with the settings additions here.

Suggested reviewers

  • jesstelford

Poem

🐰 I hop where renderers sleep,
Timestamps tucked in burrowed keep,
Idle shells we gently free,
Warm again when you call to me,
A nibble saved for memory.


Important

Pre-merge checks failed

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

❌ Failed checks (6 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error New production renderer controller schedules a repeating DispatchSourceTimer (DispatchSource.makeTimerSource + timer.schedule(..., repeating:)) to drive reclamation passes. Replace the repeating timer with an actor/explicit signal-driven mechanism (e.g., async task with cancellation-aware scheduler) so no production timers/polling are needed.
Cmux Algorithmic Complexity ❌ Error Every 20s controller.evaluate calls TerminalSurfaceRegistry.allSurfaces() (sort) then Planner.selectedSurfaceIds() (filter+sorted realized) on up to ~1000 surfaces; no benchmark/bounds. Avoid full sorts each pass (use linear top-K selection/heap or incremental LRU), cache registry snapshot, and add a benchmark/profiling note for N≈1000.
Cmux Swift Concurrency ❌ Error RendererRealizationController.swift adds a .utility DispatchQueue + DispatchSourceTimer for periodic work and spawns fire-and-forget Task { @MainActor ... } from callbacks. Replace the DispatchSourceTimer/DispatchQueue scheduler with a Swift-concurrency Task loop (async/await) and route to the main actor without creating uncancelled fire-and-forget Tasks.
Cmux Swift @Concurrent ❌ Error GhosttyTerminalView.swift has private nonisolated static func free(...) async inside an actor, but it lacks @concurrent despite rules requiring it for nonisolated async work leaving the actor. Add @concurrent to TerminalSurfaceRuntimeTeardownCoordinator.free(...) (or change isolation/call site so the async work is explicitly off the caller actor).
Cmux Swift File And Package Boundaries ❌ Error RendererRealizationPlanner is pure, unit-tested, and implemented in app target Sources/App/RendererRealizationController.swift (file imports AppKit), so independently testable domain logic isn’... Extract RendererRealizationPlannerInput + RendererRealizationPlanner.selectedSurfaceIds (and any minimal settings value types they need) into a new SwiftPM package; keep AppKit/Ghostty controller glue in the app target.
Cmux Full Internationalization ❌ Error PR adds new Resources/Localizable.xcstrings key settings.terminal.rendererRealization with only en+ja localizations; catalog already has other locales, so other locales will fall back to English. Add translated entries for settings.terminal.rendererRealization (and related subtitle/idle/maxWarmRenderers keys, if separate) for every locale already present in Resources/Localizable.xcstrings before shipping.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.89% 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 clearly and concisely describes the main change: non-destructive reclamation of offscreen terminal renderer GPU memory (IOSurface).
Linked Issues check ✅ Passed The PR fully addresses issue #4607's objectives: non-destructive renderer reclamation for idle offscreen surfaces, PTY preservation, configurable heuristics (idle timeout, warm cap), and verified memory reclamation without losing session state.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to renderer realization: controller, settings, UI integration, localization, tests, and documentation updates directly support the non-destructive GPU memory reclamation objective.
Cmux Swift Actor Isolation ✅ Passed Reviewed new Swift concurrency code: RendererRealizationController is @MainActor and timer/event handlers dispatch back via Task { @MainActor in evaluate }; new settings/planner value types are Sen...
Cmux Expensive Synchronous Load ✅ Passed Checked PR-named Swift files and repo-wide for RestorableAgentSessionIndex.load: no such call in new renderer/settings code; only existing AppDelegate buildSessionSnapshot uses it, and PR adds only...
Cmux Cache Substitution Correctness ✅ Passed No cache-substitution issues found in PR logic: renderer settings read directly from UserDefaults with defaults, managedUserDefaults is written from config (not trusted cache), and only cachedSnaps...
Cmux No Hacky Sleeps ✅ Passed PR #5857 only changes Swift, xcstrings, xcodeproj, docs, and checksums—no TS/JS/shell/build-runtime scripts—so it can’t introduce hacky sleeps per runtime-no-hacky-sleeps.md.
Cmux Swift Logging ✅ Passed Searched PR #5857 “Files changed” diff HTML for forbidden Swift logging tokens (print/debugPrint/dump/NSLog and Logger); no matches found.
Cmux User-Facing Error Privacy ✅ Passed Checked PR-related localization strings and the new renderer/settings Swift files for user-facing alerts/errors; found no NSAlert/UI error copy exposing vendor/upstream details—localizedDescription...
Cmux Swiftui State Layout ✅ Passed SwiftUI diffs add renderer settings via new @State in TerminalSection; GhosttyTerminalView expands existing ObservableObject with new stored vars but no added @Published/ObservableObject/GeometryRe...
Cmux Architecture Rethink ✅ Passed RendererRealizationController adds a timer + settings notification, but releases are strictly gated by rendererPortalVisible with idempotent release/realize and tested LRU planner; no sleep/lock/sp...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: PR adds renderer realization controller/planner/settings; checked PR Swift files for NSWindow/NSPanel/WindowGroup and cmux.* identifier assignments—none introduced, so aux close-shortcut poli...
Cmux Source Artifacts ✅ Passed Compared PR’s 61 changed paths against source-control-artifacts fail patterns (DerivedData/tmp/artifacts/hidden scratch dirs); no matches. Added files are Swift source/tests; ghostty change is only...
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the problem, fix, changes, and verification with detailed examples.
✨ 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 feat-surface-leak-repro

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 Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a default-on, non-destructive GPU memory reclamation system for offscreen terminal surfaces: cmux releases each idle surface's Metal swap chain/IOSurface (~40MB) while keeping the PTY and shell running, rebuilding the renderer on re-show. A new RendererRealizationController drives periodic LRU passes using a pure RendererRealizationPlanner, and TerminalSurface gains releaseRenderer()/realizeRenderer() backed by a new Ghostty C API.

  • RendererRealizationController + RendererRealizationPlanner: timer-driven LRU policy with a warm cap and idle threshold; pure planner with unit tests; mirrors AgentHibernationController's architecture exactly.
  • TerminalSurface state tracking: three new fields (rendererRealized, rendererPortalVisible, rendererLastVisibleAt) mirror Ghostty's swap-chain defunct state; only advance on successful enqueue; self-heal path in evaluate() re-realizes visible-but-unrealized surfaces.
  • Settings wiring: terminal.rendererRealization.* keys added across cmux.json schema, settings UI, command palette, navigation/search, and en/ja localization.

Confidence Score: 4/5

The core reclamation logic is well-guarded and the pure planner is independently tested, but the three-field mirror state added to TerminalSurface carries open edge cases from prior review threads that have not yet been addressed.

The realize/unrealize state machine is carefully designed — enqueue-success gating, idempotent guards, and the controller self-heal path cover the common failure modes. The open items from prior threads (rendererLastVisibleAt not reset on createSurface after hibernation restore, and unbounded Task accumulation from scheduleImmediatePass under a full mailbox) are real edge cases in the changed code paths. The planner itself and all the settings wiring are solid.

Sources/GhosttyTerminalView.swift and Sources/App/RendererRealizationController.swift carry the open edge cases noted in prior review threads.

Important Files Changed

Filename Overview
Sources/App/RendererRealizationController.swift New controller driving periodic GPU reclamation. Clean MainActor isolation, follows AgentHibernationController pattern. scheduleImmediatePass() has no dedup guard (already flagged in prior threads), and the DispatchSourceTimer is a legacy concurrency primitive for new code.
Sources/App/RendererRealizationPlanner.swift Pure, testable LRU policy. Deterministic sort, warm-cap guard, and visible-surface safety gate are all correct. O(n log n) over the surface list, acceptable for expected session sizes.
Sources/GhosttyTerminalView.swift Adds rendererRealized/rendererPortalVisible/rendererLastVisibleAt state and releaseRenderer/realizeRenderer methods. createSurface resets rendererRealized but not rendererLastVisibleAt (already flagged); setRendererPortalVisible correctly stamps on show and exactly once on hide.
Sources/App/WorkspaceRuntimeSettings.swift New RendererRealizationSettings enum with sanitized bounds, defaults, and didChangeNotification follows the AgentHibernationSettings pattern exactly.
cmuxTests/RendererRealizationPlannerTests.swift Well-structured pure-policy tests covering disabled state, visible-surface safety, idle threshold, warm cap, unrealized surfaces, and deterministic tie-breaking.
Resources/Localizable.xcstrings Seven new string keys added with only en and ja translations; the catalog supports 17 locales. Missing translations for 15 locales already flagged in a prior review thread.
Sources/KeyboardShortcutSettingsFileStore.swift cmux.json import parsing for rendererRealization block follows the agentHibernation pattern exactly with correct sanitized clamping and deferred didChange notification.
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift Three new SettingsCardRows for enable/idleSeconds/maxWarmRenderers following existing hibernation section layout. All text routes through String(localized:defaultValue:).
web/data/cmux.schema.json New rendererRealization object schema with correct types and minimum/maximum bounds matching sanitizers. Consistent with agentHibernation schema shape.
Sources/AppDelegate.swift Single line addition starting RendererRealizationController at the same lifecycle point as AgentHibernationController. No issues.

Sequence Diagram

sequenceDiagram
    participant Timer as DispatchSourceTimer
    participant Controller as RendererRealizationController
    participant Planner as RendererRealizationPlanner
    participant Surface as TerminalSurface
    participant Ghostty as ghostty_surface_t

    Note over Timer: fires every 20s on timerQueue
    Timer->>Controller: "Task @MainActor evaluate(now:)"
    activate Controller

    loop each visible surface
        Controller->>Surface: noteBecameVisibleForRendererReclamation()
        alt visible but !realized
            Controller->>Surface: realizeRenderer()
            Surface->>Ghostty: ghostty_surface_set_renderer_realized(true)
            alt enqueue success
                Surface-->>Surface: "rendererRealized = true"
            else enqueue dropped
                Surface->>Controller: scheduleImmediatePass()
            end
        end
    end

    Controller->>Planner: selectedSurfaceIds(inputs, settings, now)
    Planner-->>Controller: Set UUID offscreen + idle + outside warmCap

    loop each selected surface
        Controller->>Surface: releaseRenderer()
        Surface->>Ghostty: ghostty_surface_set_renderer_realized(false)
        alt enqueue success
            Surface-->>Surface: "rendererRealized = false"
        end
    end
    deactivate Controller

    Note over Surface: On tab show setVisibleInUI true
    Surface->>Surface: setRendererPortalVisible(true)
    Surface->>Surface: realizeRenderer()
    Surface->>Ghostty: ghostty_surface_set_renderer_realized(true)
Loading

Reviews (15): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines 150006 to 150028
}
}
},
"settings.terminal.rendererRealization": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reclaim Offscreen Terminal Memory"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "オフスクリーン端末のメモリを回収"
}
}
}
},
"settings.terminal.agentHibernation.subtitleOff": {
"extractionState": "manual",
"localizations": {

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.

P1 Missing translations for 15 supported locales

The new settings.terminal.rendererRealization key only ships en and ja translations, but Localizable.xcstrings already has entries for 17 locale codes: ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, ru, th, tr, uk. Users of any of the other 15 locales will see the raw English fallback string in the Settings UI. The sibling settings.terminal.agentHibernation key has the same gap, but this PR introduces a new key so the same pattern should not be carried forward. Translations for all 15 missing locale codes need to be added before merge.

Rule Used: Flag production user-facing text that is not fully... (source)

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!

Comment on lines +60 to +63
private let timerQueue = DispatchQueue(label: "com.cmux.renderer-realization", qos: .utility)
private var timer: DispatchSourceTimer?
private var settingsObserver: NSObjectProtocol?

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 New DispatchSourceTimer in production code

DispatchSource.makeTimerSource introduces a new timer-driven polling loop in shipped app code. The blocking-runtime rule flags new timer/polling primitives by default; the allowed exception is existing architectural debt. The sibling AgentHibernationController uses the exact same pattern, so this is consistent with the codebase, but both classes now carry this debt. The evaluate path is already wired to work without the timer (its signature accepts now: explicitly and the test suite drives it directly), so a future migration path would be to trigger evaluation on visibility-change events alone and drop the timer — but that is likely a separate cleanup.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

@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 `@ghostty`:
- Line 1: The CI failure is due to the new ghostty submodule SHA
(858e257f030a09529895111e696885e4395afdc9) not being pinned in
scripts/ghosttykit-checksums.txt so ensure-ghosttykit.sh cannot map
SHA→checksum; fix by adding an entry for that exact SHA and its matching
checksum to scripts/ghosttykit-checksums.txt and upload/place the corresponding
prebuilt GhosttyKit artifact used by ensure-ghosttykit.sh, or alternatively
update the ghostty submodule to point to an already-pinned SHA; touch the
ghostty submodule reference and the checksum list so they match.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 7209-7211: The computed accessor isRendererRealized should reflect
the actual runtime presence of the GPU-backed surface; change its implementation
to return true only when both rendererRealized is true and the TerminalSurface
actually has a non-nil surface (e.g. return rendererRealized && surface != nil).
Update the isRendererRealized getter in TerminalSurface so
RendererRealizationController will not treat nil/teared-down surfaces as
realized; keep the rendererRealized flag semantics but add the surface nil-check
to the accessor.
🪄 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: b246d152-9f5b-40d6-982c-5d43024ea6a8

📥 Commits

Reviewing files that changed from the base of the PR and between 8fef9f6 and 157fa87.

📒 Files selected for processing (13)
  • Resources/Localizable.xcstrings
  • Sources/App/RendererRealizationController.swift
  • Sources/App/WorkspaceRuntimeSettings.swift
  • Sources/AppDelegate.swift
  • Sources/CmuxSettingsJSONPathSupport.swift
  • Sources/CommandPalette/CommandPaletteSettingsToggle.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettingsFileStore+Template.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/SettingsNavigation.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/RendererRealizationPlannerTests.swift
  • ghostty

Comment thread ghostty Outdated
Comment thread Sources/GhosttyTerminalView.swift Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread Sources/App/RendererRealizationController.swift Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread Sources/GhosttyTerminalView.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0bcc7378f

ℹ️ 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".

setting(.terminal, "tab-bar-font-size", String(localized: "settings.terminal.tabBarFontSize", defaultValue: "Tab Bar Font Size"), "font size text scale terminal browser pane tab title surface-tab-bar-font-size"),
setting(.terminal, "agent-auto-resume", String(localized: "settings.terminal.agentAutoResume", defaultValue: "Resume Agent Sessions on Reopen"), "terminal.autoResumeAgentSessions auto resume restore reopen relaunch quit sessions agents claude code codex opencode rovo dev rovodev toggle"),
setting(.terminal, "agent-hibernation", String(localized: "settings.terminal.agentHibernation", defaultValue: "Agent Hibernation"), "terminal.agentHibernation idle hibernate suspend background agents claude code codex opencode live terminals"),
setting(.terminal, "renderer-realization", String(localized: "settings.terminal.rendererRealization", defaultValue: "Reclaim Offscreen Terminal Memory"), "terminal.rendererRealization renderer reclaim offscreen memory iosurface gpu idle warm release background terminals"),

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 Add the renderer setting to the active Settings UI

This only adds the renderer-realization entry to the legacy Sources/SettingsNavigation index; the settings window in this repo is built from Packages/CmuxSettingsUI (SettingsWindowScene constructs its package SettingsSearchIndex, and TerminalSection/TerminalCatalogSection still only define the existing terminal rows/keys). As a result, users who open Settings or search for this default-on memory reclamation setting land in Terminal with no row to toggle or tune terminal.rendererRealization.*, even though the feature is exposed through config and the command palette.

Useful? React with 👍 / 👎.

e610c8e166ce3ac2dc13f36e542b287bb78f9cd3 4a9dad1fe4d85a018d4259c5efcd0686033538be609610b8a5de4fee4f92f5c1
e5c962a72795088b9f6a478236a421fe00b0950e 8e556c99fd8b1a1b3969722fc4c3ef62bac0d459c9427530ce70b1cf9d0a4093
34cbf180d8917b802d61d9929cfb493594f2ab52 b42522b715e3e7f96d38fbb960b17c710853e935083092a7ac56fb3b907b053f
858e257f030a09529895111e696885e4395afdc9 a551961e840d90094eb902a93dd9266a6fb806b7c38b09620d6c1a37b3e8c10f

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 Update the Ghostty fork notes for this bump

This checksum accompanies a new Ghostty fork commit and submodule pointer, but docs/ghostty-fork.md was not updated and still says the current pinned fork head is 34cbf180d with the previous archive. The repo instructions require updating that fork document with each Ghostty change; leaving it stale means the new ghostty_surface_set_renderer_realized API and its conflict/rebuild notes are missing for the next fork refresh or GhosttyKit rebuild.

Useful? React with 👍 / 👎.

…ease

- Never release a visible surface: TerminalSurface now tracks rendererPortalVisible
  (driven by setVisibleInUI, the same signal as occlusion); releaseRenderer()
  hard-refuses when visible; the controller iterates TerminalSurfaceRegistry and
  reads each surface's own visibility instead of re-deriving it from
  agentHibernationVisiblePanelIdsForCurrentLayout(), which could mark a visible
  terminal offscreen when the auto-resume presentation flag cleared (Cursor HIGH).
- isRendererRealized now requires a live runtime surface (CodeRabbit).
- docs/ghostty-fork.md: record new pinned fork head 858e257 + renderer API (Codex).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews. Addressed in c6ceb19:

Cursor (HIGH) — releasing a visible surface. Real bug. I was deriving visibility from agentHibernationVisiblePanelIdsForCurrentLayout(), which returns no panels when agentHibernationAutoResumePresentationVisible is false, so a visible terminal could be classified offscreen and released. Fixed: TerminalSurface now tracks its own rendererPortalVisible, set by setVisibleInUI (the same signal that drives occlusion). releaseRenderer() hard-refuses when the surface is visible, and RendererRealizationController now iterates TerminalSurfaceRegistry.shared.allSurfaces() and reads each surface's own visibility, removing the fragile layout-derivation entirely.

CodeRabbit — isRendererRealized on a nil surface. Hardened to surface != nil && rendererRealized.

Codex — docs/ghostty-fork.md. Updated with the new pinned head 858e257f0 and the renderer API.

CodeRabbit — pin GhosttyKit checksum. Already done in a0bcc73 (the comment was on an earlier commit).

Cursor (MEDIUM) — realize flag set before Ghostty completes. No change needed. The realize message is pushed to the renderer-thread mailbox, which is fully drained before any drawFrame, so a re-show's realize is always processed before the refresh-triggered draw. While released, the surface is occluded and macOS drawFrame early-returns on !visible; a stray draw into a defunct swap chain is caught by nextFrame's error.Defunct (logged, no crash). The cmux-side flag is just a dedup mirror; the GPU state is governed by the renderer thread + draw_mutex.

Greptile — DispatchSourceTimer / en+ja only. Both mirror the existing AgentHibernationController and settings.terminal.agentHibernation (which also ships en+ja only). This is the established pattern for this settings family.

Codex (P2) — add to the Settings window (CmuxSettingsUI). The control is already reachable via the command palette ("Reclaim Offscreen Terminal Memory"), cmux.json, and the legacy settings index. The full Settings-window panel spans two more packages (CmuxSettings catalog keys + CmuxSettingsUI controls) and its own localization catalog; I'll add it as a fast-follow rather than expand this PR.

Comment thread Sources/GhosttyTerminalView.swift
# Conflicts:
#	.github/swift-file-length-budget.tsv
#	ghostty

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b64af833d5

ℹ️ 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 thread Sources/GhosttyTerminalView.swift Outdated
Comment on lines +7251 to +7253
guard let surface, rendererRealized, !rendererPortalVisible else { return }
rendererRealized = false
ghostty_surface_set_renderer_realized(surface, false)

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 Validate renderer pointers before calling Ghostty

When a TerminalSurface is in the stale-wrapper state that liveSurfaceForGhosttyAccess(reason:) is meant to quarantine (for example, the native surface was freed out of band but surface is still non-nil and the lifecycle is still live), the default-on reclamation timer still selects it because hasLiveSurface only checks those local fields. This direct call then passes the freed pointer to Ghostty instead of clearing the stale surface, so a background reclamation pass can crash; use the same live-surface validation path before invoking the new renderer-realization API.

Useful? React with 👍 / 👎.

- web/data/cmux.schema.json: add terminal.rendererRealization object so the new
  cmux.json keys validate (terminal had additionalProperties:false) (Codex P2).
- CmuxSettingsUI Settings window: catalog keys (CmuxSettings), TerminalSection
  toggle + idle/maxWarm steppers, curated search entries, and en/ja subtitle
  strings, so the default-on feature is reachable and tunable in Settings, not
  just the command palette / cmux.json (Codex P2).
- SettingsRowAnchorResolutionTests: cover the 3 new row paths (package test green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1851c926fc

ℹ️ 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".

? String(localized: "settings.terminal.rendererRealization.subtitleOn", defaultValue: "Off-screen terminals release their GPU renderer memory while idle and rebuild it instantly when you switch back. The process keeps running.")
: String(localized: "settings.terminal.rendererRealization.subtitleOff", defaultValue: "Every visited terminal keeps its full GPU renderer allocated until you close it, even when off-screen.")
) {
Toggle("", isOn: Binding(get: { rendererReclaim.current }, set: { rendererReclaim.set($0) }))

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 Notify the controller when enabling from Settings

When this toggle is changed in the Settings UI, DefaultsValueModel.set only writes UserDefaults; it does not post RendererRealizationSettings.didChangeNotification, which is the only notification RendererRealizationController.start() observes before creating its timer. In the common case where a user previously disabled terminal.rendererRealization.enabled and then turns it back on here, no timer is started until some other notification path or an app restart, so the setting appears enabled but offscreen renderer reclamation never runs.

Useful? React with 👍 / 👎.

lawrence703 and others added 2 commits June 11, 2026 15:24
…urface

The Settings-window toggle writes terminal.rendererRealization.enabled directly
via DefaultsValueModel, which does not post RendererRealizationSettings
.didChangeNotification, so a session launched with the feature disabled would not
start reclaiming when re-enabled until relaunch. Make the controller's timer
always-on (evaluate() already reads `enabled` fresh each pass and no-ops when
off), so re-enabling from any surface takes effect on the next pass; the change
notification still triggers an immediate pass for the paths that post it (Codex P2).

Verified: launched disabled, visited 12 surfaces (1338MB, no reclamation), flipped
enabled via a raw defaults write with no notification, reclaimed to 729MB on the
next pass without restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
releaseRenderer/realizeRenderer passed the stored ghostty_surface_t to the C API
guarding only on surface != nil. Because RendererRealizationController is
default-on and periodically scans every registered TerminalSurface wrapper, a
stale wrapper whose runtime surface was freed out-of-band could hand a dangling
pointer to Ghostty and crash. Route both calls through the existing
liveSurfaceForGhosttyAccess(reason:) contract (registry-ownership +
cmuxSurfacePointerAppearsLive validation, which also self-heals the stale
wrapper). The methods become @mainactor to call that guard (Codex P1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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 a071eb0. Configure here.

Comment thread Sources/GhosttyTerminalView.swift
lawrence703 and others added 2 commits June 11, 2026 15:49
cmux policy requires new non-UI tests to use Swift Testing, not XCTest.
Convert RendererRealizationPlannerTests to import Testing / @test / #expect,
matching the sibling SettingsRowAnchorResolutionTests (Codex P3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… file

cmux policy (Aziz):
- Add Swift-DocC documentation to the three new public rendererRealization
  DefaultsKeys in TerminalCatalogSection.
- Move RendererRealizationPlannerInput + RendererRealizationPlanner into their
  own RendererRealizationPlanner.swift so the new controller file holds one
  major type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Round 2 of autoreview triage (Codex structured review came back clean; remaining were cmux-policy/Aziz + one out-of-scope ghostty note):

Fixed (e327286):

  • Added Swift-DocC docs to the three new public rendererRealization DefaultsKeys.
  • Split RendererRealizationPlannerInput + RendererRealizationPlanner into their own RendererRealizationPlanner.swift so the new controller file holds one major type.
  • (Earlier 604e1ce) Converted the planner unit test to Swift Testing per policy.
  • (Earlier a071eb0) Routed releaseRenderer/realizeRenderer through liveSurfaceForGhosttyAccess so the default-on scanner validates the native pointer before the C call.

Kept as-is, with rationale:

  • RendererRealizationSettings lives in WorkspaceRuntimeSettings.swift alongside AgentHibernationSettings, RightSidebarBetaFeatureSettings, and the other runtime-settings enums. That file is the canonical home for this settings family, so adding it there is consistent; pulling one enum out would fragment the convention.
  • Codex's out-of-scope ghostty note (a .forever mailbox push could return 0 if the queue is torn down mid-wait) is closed by the liveness fix above: releaseRenderer/realizeRenderer only run for a surface that passes liveSurfaceForGhosttyAccess, i.e. its renderer thread is alive and draining the mailbox, so .forever makes space and never drops. A drop requires a dead renderer thread, which fails the liveness gate and is torn down rather than re-realized.

# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d4975da74

ℹ️ 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 thread web/data/cmux.schema.json
"rendererRealization": {
"type": "object",
"additionalProperties": false,
"description": "Reclaim off-screen terminal GPU renderer memory. cmux releases the Metal renderer (IOSurface) of a terminal that has stayed off-screen and idle while keeping its process and terminal state alive, then rebuilds the renderer instantly when the tab is visited again. Non-destructive and on by default.",

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 Localize the new schema descriptions

The repo instructions in /workspace/cmux/AGENTS.md require schema/config text to be localized, but these new rendererRealization schema descriptions are inline English only. The localized configuration page renders descriptionKey ? t(...) : property.description, so /ja/docs/configuration and other locales will show these new explanations in English unless this adds descriptionKey values and matching entries in the web message catalogs.

Useful? React with 👍 / 👎.

The C API now pushes the display_realized message .instant (non-blocking) instead
of .forever, and realizeRenderer no longer retries in a loop. Both ran on the
main actor and could stall the UI waiting for the renderer thread to drain. On
re-show the mailbox is empty so a single .instant enqueues immediately (no blank);
a rare full-mailbox drop is handled by flip-on-success + the controller's
re-realize backstop, never by blocking. Bumps the ghostty fork pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5af7b67684

ℹ️ 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".

e5c962a72795088b9f6a478236a421fe00b0950e 8e556c99fd8b1a1b3969722fc4c3ef62bac0d459c9427530ce70b1cf9d0a4093
34cbf180d8917b802d61d9929cfb493594f2ab52 b42522b715e3e7f96d38fbb960b17c710853e935083092a7ac56fb3b907b053f
858e257f030a09529895111e696885e4395afdc9 a551961e840d90094eb902a93dd9266a6fb806b7c38b09620d6c1a37b3e8c10f
d39ba5d849cb77b4a4290c44d68db03e366c5596 2a690c144c423d80e013c995dc7d47b8e0983789ea58c3eb70bb96a964bcaf26

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 Pin the checksum for the actual Ghostty submodule

Fresh evidence in this commit shows git ls-tree cf0da00c ghostty records the submodule as 5697db813b1b0fe14873093e9028f36513ddc187, but the new manifest entries pin 858e257f... and d39ba5d... instead. tests/test_ci_ghosttykit_checksum_present.sh reads the checked-in submodule SHA with git ls-tree HEAD ghostty and fails when that SHA is absent, so this bump will fail the GhosttyKit checksum guard until the 5697db... archive checksum is added.

Useful? React with 👍 / 👎.

Comment on lines +71 to +73
.init(section: .terminal, id: "renderer-realization", title: "Reclaim Offscreen Terminal Memory", synonyms: "terminal.rendererRealization.enabled renderer reclaim offscreen memory iosurface gpu idle warm release background terminals"),
.init(section: .terminal, id: "renderer-realization-idle", title: "Reclaim After Idle Seconds", synonyms: "terminal.rendererRealization.idleSeconds renderer reclaim idle seconds timeout delay offscreen memory"),
.init(section: .terminal, id: "renderer-realization-max", title: "Max Warm Renderers", synonyms: "terminal.rendererRealization.maxWarmRenderers max warm renderers limit count reclaim offscreen gpu"),

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 Localize the new settings search titles

These new curated entries feed SettingsSearchIndex.Entry.title, which SettingsSidebarEntryRow renders as the user-facing Settings search result title; adding English literals here means the Japanese Settings search sidebar shows these renderer settings in English even though the actual rows were localized in Localizable.xcstrings. Please use localized strings for the new titles (and add the corresponding catalog entries) so search results match the localized settings UI.

Useful? React with 👍 / 👎.

… doc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines 6775 to +6782
}
guard let createdSurface = surface else { return }
TerminalSurfaceRegistry.shared.registerRuntimeSurface(createdSurface, ownerId: id)
// A freshly created runtime surface always owns a live (non-defunct)
// swap chain, so it is realized. Reset the flag in case this object's
// previous runtime surface had been released before being freed (e.g.
// agent-hibernation suspend/restore), which would otherwise let a later
// realizeRenderer() double-realize and trip Ghostty's defunct assert.

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.

P1 rendererLastVisibleAt not reset in createSurface

createSurface correctly resets rendererRealized = true for the fresh Ghostty surface, but it does not reset rendererLastVisibleAt. After an agent-hibernation suspend/restore, rendererLastVisibleAt still holds the hide-time stamp from before hibernation. If that timestamp is older than idleSeconds (30 s by default), the RendererRealizationController will release the freshly-created renderer on its very next pass (every 20 s), immediately undoing createSurface's work. The surface is then re-realized when the user switches to it via setVisibleInUI, so there is no blank screen, but the restore → release → realize churn adds unnecessary latency and GPU allocation overhead on every hibernation restore.

Adding rendererLastVisibleAt = Date().timeIntervalSince1970 alongside the existing rendererRealized = true reset would ensure the fresh surface's idle clock starts from the moment of creation, consistent with what happens for a brand-new TerminalSurface.

If realizeRenderer's non-blocking enqueue drops (full mailbox), kick an immediate
RendererRealizationController pass so the now-visible-but-unrealized surface is
re-realized on the next main-actor turn rather than waiting for the periodic tick.
Bounds how long a re-shown terminal could draw against a defunct swap chain in the
pathological full-mailbox case, without blocking the main actor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +78 to +82
func scheduleImmediatePass() {
Task { @MainActor in
RendererRealizationController.shared.evaluate(now: Date())
}
}

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.

P1 Unbounded task accumulation from repeated scheduleImmediatePass() calls

scheduleImmediatePass() has no dedup guard. When N visible surfaces all fail their realizeRenderer() enqueue in one evaluate() pass (e.g., a momentarily full renderer mailbox during a workspace switch), each failure independently calls scheduleImmediatePass(), queuing N tasks. Each of those N tasks runs evaluate(), which may again fail for all N surfaces, queuing N more tasks — yielding N² tasks in the next wave, then N³, etc. A single common boolean flag (immediatePassPending) reset at the top of evaluate() would collapse the N concurrent schedules into at most one pending pass at a time, matching the existing timer's semantics.

# Conflicts:
#	.github/swift-file-length-budget.tsv

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b3854cc56

ℹ️ 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".

/// deterministically without the timer.
func evaluate(now: Date) {
let settings = RendererRealizationSettings.values()
guard settings.enabled else { return }

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 Run visible re-realization even when disabled

If a user disables terminal.rendererRealization.enabled after some off-screen terminals have already released their renderer, those terminals still rely on realizeRenderer() when shown again. When that enqueue drops, scheduleImmediatePass() calls this method as the documented backstop, but the early enabled guard returns before the visible-surface repair loop, leaving the now-visible terminal with rendererRealized == false until another visibility transition happens. The release-selection part should respect enabled, but the visible re-realization self-heal needs to run regardless so disabling the feature cannot strand previously released surfaces.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit aeb8847 into main Jun 12, 2026
23 checks passed
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 12, 2026
…browser CLI, pairing QR, iOS

PRs included:
- manaflow-ai#5816 ControlCommandCoordinator extraction (package coordinator skeleton; fork keeps legacy v2* dispatchers)
- manaflow-ai#5859 sidebar perf
- manaflow-ai#5857 RendererRealization (added as SurfaceHibernation adapter)
- manaflow-ai#5867 in-process custom sidebars
- manaflow-ai#5778 browser CLI / system-proxy bypass
- manaflow-ai#5872 minimal pairing QR
- iOS pairing/manual-entry stack
- 30+ hot fixes

Fork-side adjustments:
- Skip 21 TerminalController+Control* extension files (PR manaflow-ai#5816 architecture refactor not adopted)
- Add Sources/App/RendererRealizationSettingsAdapter.swift to bridge new RendererRealizationSettings to fork's existing SurfaceHibernationSettings
- Restore v2SurfaceDragToSplit shim removed by upstream
- Add SettingsNavigationTarget.customSidebars case
- Stub ghostty_surface_set_renderer_realized callsites pending GhosttyKit rebuild (zig 0.15.2 required, host has 0.16.0)
- Update ghostty submodule to 44b2baa81 (cherry-pick the 3 renderer commits onto fork's manaflow-ai#5128 link-fix pointer)
- Keep fork's CMUXSessionDaemon module pbxproj refs and SurfaceHibernation settings
lawrencecchen added a commit that referenced this pull request Jun 12, 2026
…utionTests vs RendererRealizationController)

The merge of origin/main surfaced a latent pbxproj UUID collision: this PR
assigned D36A00040000000000000001/0002 to AgentHibernationLifecycleResolutionTests.swift,
and main's offscreen-renderer work (#5857) independently used the same IDs for
Sources/App/RendererRealizationController.swift. After the merge both objects
shared one UUID, so the cmuxTests group resolved D36A0004...0002 to
App/RendererRealizationController.swift -> cmuxTests/App/RendererRealizationController.swift
(a nonexistent path), failing the build.

Re-ID the test file to the unused D36A00070000000000000001/0002 so each UUID
defines exactly one object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jul 28, 2026
4 tasks done

This branch was successfully deployed

1 active deployment
Preview – cmux — 3b3854cc Deployed Jun 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.

perf: surface hibernation — 187 surfaces hold 16GB physical footprint on long-lived sessions

2 participants