Skip to content

Keep iOS terminal render recovery live - #7301

Closed
azooz2003-bit wants to merge 28 commits into
mainfrom
feat-ios-terminal-render-freeze
Closed

azooz2003-bit wants to merge 28 commits into
mainfrom
feat-ios-terminal-render-freeze

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Remove the iOS terminal-wide render recovery pause that blocked output, geometry sync, snapshots, rendering, and deadline pumping when an old Ghostty surface free was still pending.
  • Keep active surface recovery live while retired surface frees drain on their old queue, with backlog logging instead of blocking new recovery.
  • Add a regression test for a stuck prior surface free so the old frozen/black terminal path fails before the fix and passes after it.

Verification

  • RED before fix: xcodebuild test -workspace ios/cmux.xcworkspace -scheme cmux-ios -destination 'platform=iOS Simulator,id=2B86B925-1A19-4C96-B04B-34DFD37DB168' -only-testing:cmuxFeatureTests/TerminalViewportSpacingTests -derivedDataPath /tmp/cmux-tfzreg-suite -resultBundlePath /tmp/cmux-tfzreg-suite.xcresult failed with render.recover.paused and reset count 0.
  • GREEN after fix: xcodebuild test -workspace ios/cmux.xcworkspace -scheme cmux-ios -destination 'platform=iOS Simulator,id=2B86B925-1A19-4C96-B04B-34DFD37DB168' -only-testing:cmuxFeatureTests/TerminalViewportSpacingTests -derivedDataPath /tmp/cmux-tfzfix-suite -resultBundlePath /tmp/cmux-tfzfix-suite.xcresult passed.
  • git diff --check origin/main...HEAD passed.
  • macOS tagged reload: ./scripts/reload.sh --tag tfrz passed after cloud reload was blocked by missing hq helper scripts/lib/maclease-heartbeat.sh.
  • iOS simulator tagged reload: ./ios/scripts/reload.sh --tag tfrz --simulator cmux-tfzreg-codex --no-setup passed on simulator cmux-tfzreg-codex, UDID 2B86B925-1A19-4C96-B04B-34DFD37DB168.
  • Evidence captured under /tmp/tfrz-evidence, including simulator screenshots and /tmp/tfrz-evidence/ios-relaunch-frames/contact-sheet.png.

Verification gaps

  • The full 10-minute multi-workspace iOS terminal UI soak did not complete because the tagged simulator app remained on Still loading or Reconnecting after auto sign-in and pairing attempts. Route warming for /handler/sign-in and /handler/after-sign-in returned 500 because this machine has dogfood email/password credentials but not the Stack server/public keys needed for the local Next.js auth routes.
  • xctrace Time Profiler attach and all-processes runs hung past their time limits and produced non-exportable partial traces, so profiler evidence is not available from this environment.

Summary by cubic

Keeps iOS terminal recovery live and preserves the last visible frame across reconnects until replay. Foreground resume reasserts the push subscription, proves delivery with mobile.events.ping/mobile.events.pong, refreshes reconnect routes from host status, and restarts only when proof or reassertion fails (or the host lacks terminal.event_ping.v1); runs only when the first scene becomes active.

  • Bug Fixes

    • Foreground liveness: scene-scoped via setSceneForegroundActive(_:, sceneID:); resume only on first active scene; cancels pong waiters and probe timeouts on background; subscribes to and consumes mobile.events.pong; sends mobile.events.ping only when supported; restarts on a non‑answering reassert or if the pong isn’t consumed; legacy hosts (no already_subscribed or no terminal.event_ping.v1) use the restart path. Adds tests for pong‑drop restarts, legacy acks (replay without restart), healthy‑stream foreground resumes, and all‑scenes‑inactive gating.
    • Recovery stays live: warns at 1 pending free and pauses at 2 to cap backlog; keeps the renderer mounted; rejects output/geometry while paused instead of queuing onto a wedged surface. Preserves the last frame by capturing a visual/text snapshot off‑main, caching it by surface ID with TTL/size bounds, and reapplying it on remount until fresh output lands. Adds regression tests for stuck frees, backlog caps, and snapshot fallback.
    • Chat/terminal UX: keeps the terminal renderer mounted under chat; chat overlays the terminal, disables terminal hit‑testing/accessibility, restores keyboard focus on exit, suppresses terminal autofocus on remount, closes the browser when entering chat or creating a terminal, and clears chat state when opening the browser.
    • Fixed weak request capture in ComposerDictationController for CI stability.
  • New Features

    • Push‑stream proof: subscribes to mobile.events.pong, implements mobile.events.ping on the host (MobileHostService), and verifies delivery on foreground without tearing down a healthy stream.
    • Reconnect route refresh: decodes routes in mobile.host.status and updates persisted attach routes to handle dev ephemeral ports. Adds decoding tests.

Written for commit 2877a79. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved render-pipeline recovery using warning/recovery backlog thresholds, stopping recovery at the limit and keeping surface operations flowing.
    • Snapshot fallback is captured/applied so the last visible terminal text remains available until replay completes.
    • Foreground resume/liveness monitoring now follows scenePhase, preventing unnecessary watchdog/silence failures while backgrounded.
  • New Features
    • Added setAppForegroundActive(_:) and a focusTerminalInput(surfaceID:) API for restoring keyboard focus.
    • Chat mode overlays on the terminal, temporarily disabling terminal hit-testing while active.
  • Tests
    • Added recovery and foreground-resume regression coverage to validate resets, snapshot visibility, and replay behavior.

Note

Medium Risk
Changes span iOS terminal rendering, mobile RPC event plumbing, and foreground sync behavior—high user impact but scoped to mobile attach paths with substantial new regression tests.

Overview
Improves iOS remote terminal reliability across render recovery, Mac push events, and foreground resume—without treating every app switch like a full reconnect.

Render recovery no longer pauses the active GhosttySurfaceView when one old surface free is still draining. Recovery stays live with backlog warnings at one pending free and hard stop at two; while paused, output/geometry is rejected instead of queued onto a wedged surface. Recovery captures and shows the last visible terminal text (per-surface cache + off-main reads) until Mac replay lands, including on remount via applyCachedSnapshotFallback.

Foreground / push stream: Multi-scene setSceneForegroundActive gates liveness so background silence is not misread as a dead stream. Resume reasserts subscriptions (restartEventStream: false) and, when the host advertises terminal.event_ping.v1, proves delivery with mobile.events.ping / mobile.events.pong before trusting the stream; failed proof or stuck reassert triggers listener restart + replay. Older Macs without ping use a legacy restart path when subscribe ack cannot be proven.

Mac host: Adds mobile.events.ping, terminal.event_ping.v1, and routes on mobile.host.status so phones can refresh persisted reconnect routes after handshake. Chat/browser/terminal chrome closes the browser when entering chat or opening browser and suppresses terminal autofocus on chrome-driven remounts.

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

@vercel

vercel Bot commented Jul 4, 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 Jul 9, 2026 4:16am
cmux-staging Building Building Preview, Comment Jul 9, 2026 4:16am

@coderabbitai

coderabbitai Bot commented Jul 4, 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

GhosttySurfaceView now keeps render recovery active while it captures and replays snapshot fallback state. MobileShellComposite and the root view now track foreground activity to gate render-grid liveness, with tests covering recovery, foreground-resume, chat/terminal presentation behavior, and dictation tap capture.

Changes

Render pipeline recovery and snapshot fallback

Layer / File(s) Summary
Recovery state and threshold definition
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift, Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceWorkQueue.swift
Replaces paused-state bookkeeping and maxPendingSurfaceFrees with surfaceFreeBacklogWarningThreshold and surfaceFreeBacklogRecoveryLimit, adds lastRecoverySnapshotText, initializes surfaceHasReceivedOutput to false, adds lastRecoverySnapshotTime, and adds the DEBUG recovery simulation helpers.
Recovery flow and snapshot fallback
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Rewrites recoverRenderPipeline to remove paused-state branching, enforce the new backlog limits, log backlog warnings, capture recovery snapshot text on the output queue, and unhide/flush snapshot fallback presentation when recovery captures fallback data.
Removal of pause-gated early returns
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Removes paused-recovery guards from deadline pumping, output completion, render dispatch, geometry sync, copyable text, and visible snapshot reads.
Recovery test seam and integration tests
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift, ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalViewportSpacingTests.swift
Adds render-pipeline reset counting in viewport spacing tests and verifies stuck-prior-free recovery, backlog-limit rejection, and snapshot visibility through replay.

Foreground-active render-grid liveness

Layer / File(s) Summary
Foreground activity state and resume flow
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Adds foreground-active state, updates resumeForegroundRefresh() to mark the app active and resync output without restarting the event stream, and introduces setAppForegroundActive(_:) plus the watchdog restart helper.
Liveness gating and scene-phase wiring
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
Starts the render-grid watchdog only while foreground-active, suppresses background liveness failure checks, and updates the root view to clear foreground state when the scene becomes inactive.
Foreground-resume liveness test
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
Adds a regression test that background-silences the terminal, verifies no extra subscription is issued while inactive, then confirms resumeForegroundRefresh() reasserts the subscription and replays mounted surfaces without restarting the listener.

Chat presentation and input handoff

Layer / File(s) Summary
Chat overlay and input resigning
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
Keeps the terminal mounted under the chat overlay on iOS, changes the active browser branch order, and calls GhosttySurfaceView.resignActiveInput() when chat mode is enabled.

Dictation tap request capture

Layer / File(s) Summary
Weak request binding
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/ComposerDictationController.swift
Switches the weak captured SFSpeechAudioBufferRecognitionRequest reference in makeTapBlock(...) from weak let to weak var.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#7098: Both PRs update the client-side render-pipeline recovery flow in GhosttySurfaceView and its integration with iOS terminal output recovery/replay.
  • manaflow-ai/cmux#7196: Both PRs modify GhosttySurfaceView’s recoverRenderPipeline flow and rely on render-recovery reset behavior.
  • manaflow-ai/cmux#7108: Both PRs adjust the iOS chat-mode WorkspaceDetailView transition for chatContent(session).

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Cmux No Test Or Debug Seam In Production Source ❌ Error GhosttySurfaceView.swift under Sources/ adds two *ForTesting methods in a DEBUG block; they’re only called from tests, so this is a new production test seam. Move the observation/control into the test target via @testable import (or a dedicated debug-only file/folder), and keep production Sources/ free of test-only helpers.
✅ Passed checks (24 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: New store/UI methods stay on @MainActor/SwiftUI; added queue state is confined to the documented serial work queue, so no new isolation regression appears.
Cmux Swift Blocking Runtime ✅ Passed PASS: Diff removes a pause and adds foreground gating; no new semaphores, sync waits, sleeps, or locks were introduced. Existing timers/polling predated the PR.
Cmux Browser Automation Off-Main ✅ Passed Diff only touches iOS terminal/shell rendering and tests; no changes to TerminalController, ControlCommandExecutionPolicy, or browser socket automation routing.
Cmux Expensive Synchronous Load ✅ Passed PASS: The diff only adjusts terminal/chat recovery flow; no touched Swift file adds or moves RestorableAgentSessionIndex.load(), SharedLiveAgentIndex, or sync file/JSON parsing.
Cmux Cache Substitution Correctness ✅ Passed lastRecoverySnapshotText is only a transient snapshot-fallback UI cache, refreshed from outputQueue and cleared/hidden on cold-cache cases; no persistence/history/undo consumer trusts it.
Cmux No Hacky Sleeps ✅ Passed PASS: The diff only touches Swift plus a TSV config; no changed TypeScript/JS/shell/runtime files contain sleeps, timers, polling, or delayed waits.
Cmux Algorithmic Complexity ✅ Passed The PR adds one new registry lookup and lifecycle toggles, but no nested scans, per-target rescans, or batch sorting over scalable collections; the new sort/filter is a single UI lookup.
Cmux Swift Concurrency ✅ Passed The diff adds no new DispatchQueue/Combine/completion-handler/fire-and-forget Task patterns; it only adjusts existing queue- and Task-bound code.
Cmux Swift @Concurrent ✅ Passed No changed Swift code adds invalid @concurrent or missing actor hops; the new async reads stay UI-bound on MainActor or hop to outputQueue before heavy work.
Cmux Swift File And Package Boundaries ✅ Passed PASS: The PR only adds small, cohesive iOS/UI and Ghostty glue in existing files; no new oversized files, no large growth, and no app-root/package boundary violation.
Cmux Swiftpm Lockfiles ✅ Passed Diff only changes source/tests; no Package.swift, Package.resolved, .gitignore, or Xcode package-reference files changed, so the SwiftPM lockfile rule isn’t violated.
Cmux Swift Logging ✅ Passed The only added log path uses the existing DEBUG-only MobileDebugLog.anchormux; no new print/NSLog/file logging or sensitive-data logging was introduced.
Cmux User-Facing Error Privacy ✅ Passed PASS: The PR only changes internal debug logs/comments/tests and UI behavior; no new user-facing errors, alerts, or recovery copy expose sensitive implementation details.
Cmux Full Internationalization ✅ Passed No user-facing text was added or changed; the PR only adds comments, debug/test strings, and logic, with no .xcstrings, InfoPlist, or web message locale files touched.
Cmux Swiftui State Layout ✅ Passed The changed SwiftUI views use @Observable/@State, not new ObservableObject/@published, and I found no GeometryReader or lazy-row store-reference pattern violations.
Cmux Architecture Rethink ✅ Passed The PR keeps a clear owner/invariant: scenePhase only bridges lifecycle into the store, while the existing liveness watchdog is merely gated; terminal recovery changes are local correctness fixes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The diff only touches terminal/shell views and tests; no new NSWindow/NSPanel/WindowGroup or cmux.* identifier assignments, so scripts/lint_auxiliary_window_close_shortcuts.py doesn’t apply.
Cmux Source Artifacts ✅ Passed All changed paths are source/config/test files; no logs, screenshots, recordings, temp dirs, caches, or build output were added.
Cmux No Ambient Global State ✅ Passed No new file-scope funcs, globals, or singletons; added APIs are instance methods or a static method on existing GhosttySurfaceView owner type.
Title check ✅ Passed The title is concise and accurately summarizes the main change: keeping iOS terminal render recovery live.
Description check ✅ Passed The description is structured and detailed, covering summary and verification, though the demo video, review trigger, and checklist sections are omitted.
✨ 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-ios-terminal-render-freeze

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.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a frozen-terminal regression on iOS where a single stalled Ghostty surface_free would pause the entire render recovery pipeline, blocking output, geometry sync, and deadline pumping for the active surface. Recovery now stays live — it logs at one pending free and only pauses at two — while stale surface frees drain on their old queues. A snapshot-fallback mechanism captures the last visible terminal frame before recovery begins and holds it until the Mac replay lands, preventing a blank-terminal flash.

  • Recovery pipeline: recoverRenderPipeline now follows a warn-at-1 / pause-at-2 strategy for pending surface frees; surfaceFreeBacklogWarningThreshold and surfaceFreeBacklogRecoveryLimit replace the old single maxPendingSurfaceFrees = 1 that paused on the very first stale free.
  • Snapshot fallback: captureSnapshotFallbackForRecovery() saves the last visible text before dismantling the old surface; requestSnapshotFallbackTextForRecovery issues an off-main read from the retiring surface's own queue; applyCachedSnapshotFallback(surfaceID:) reapplies the cached frame on remount.
  • Scene-aware foreground liveness: setSceneForegroundActive(_:sceneID:) gates resumeForegroundRefresh() to the first inactive-to-active transition; foreground resume now reasserts mobile.events.subscribe rather than forcing a full listener restart, then proves the push path with a new mobile.events.ping/mobile.events.pong RPC pair.

Confidence Score: 5/5

The recovery pipeline change is well-isolated with comprehensive regression tests covering the stuck-free, backlog-cap, and snapshot-fallback paths; the ping/pong path correctly cancels waiters on backgrounding before any restart decision.

All core correctness invariants are preserved: surfaceHasReceivedOutput still gates snapshot hiding; finishTerminalEventPong uses removeValue to prevent double-resumption; cancelTerminalEventPongWaiters drains correctly on backgrounding; off-main snapshot reads are ordered ahead of surface frees on the serial outputQueue. Visibility widenings follow the canonical pattern consumed via @testable import with no ForTesting wrapper in production callers.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift Core fix: recovery-limit constants changed from a 1-free pause to warn-at-1/pause-at-2; snapshot fallback capture/restore added before/after surface teardown; surfaceHasReceivedOutput delayed to first real bytes; several private members widened to internal for @testable access.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalEventPing.swift New extension implementing ping/pong delivery verification; verifyTerminalEventStreamDelivery uses a DispatchSource one-shot timer (established codebase pattern) for the probe timeout; cancelTerminalEventPongWaiters correctly drains continuations on background.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Foreground lifecycle: isAppForegroundActive, foregroundActiveSceneIDs, terminalSubscriptionRefreshID, and pong state added; resumeForegroundRefresh now reasserts rather than restarts; liveness watchdog gated on foreground state.
Sources/Mobile/MobileHostService.swift Added mobile.events.ping handler in handleSubscriptionRPC (now async for sendEvent); correctly validates stream ID, topic, and nonce; returns delivered: false when the topic is not in the subscription.
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift New @mainactor snapshot-fallback cache keyed by hostSurfaceID; bounded to 32 entries / 65536 chars / 30-minute TTL; newer capturedAt wins on collision.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift Scene lifecycle wired through setSceneForegroundActive; resumeForegroundRefresh only called on the first active-scene transition.
ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalViewportSpacingTests.swift Three new regression tests covering stuck-prior-free, backlog cap, and snapshot fallback. Test harness accesses internal members via @testable import (canonical pattern).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant RootView as CMUXMobileRootView
    participant Shell as MobileShellComposite
    participant Mac as MobileHostService
    participant SurfaceView as GhosttySurfaceView

    RootView->>Shell: setSceneForegroundActive(true, sceneID)
    RootView->>Shell: resumeForegroundRefresh()
    Shell->>Mac: mobile.events.subscribe (reassert)
    Mac-->>Shell: already_subscribed true
    Shell->>Mac: mobile.events.ping
    Mac->>Shell: event mobile.events.pong
    Mac-->>Shell: delivered true
    Note over Shell: stream healthy, no restart

    Note over SurfaceView: Render recovery
    SurfaceView->>SurfaceView: captureSnapshotFallbackForRecovery()
    SurfaceView->>SurfaceView: "initializeSurface() surfaceHasReceivedOutput=false"
    SurfaceView->>SurfaceView: flushSnapshotFallbackPresentation()
    Mac->>SurfaceView: process_output replay
    SurfaceView->>SurfaceView: "surfaceHasReceivedOutput=true hide snapshot"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant RootView as CMUXMobileRootView
    participant Shell as MobileShellComposite
    participant Mac as MobileHostService
    participant SurfaceView as GhosttySurfaceView

    RootView->>Shell: setSceneForegroundActive(true, sceneID)
    RootView->>Shell: resumeForegroundRefresh()
    Shell->>Mac: mobile.events.subscribe (reassert)
    Mac-->>Shell: already_subscribed true
    Shell->>Mac: mobile.events.ping
    Mac->>Shell: event mobile.events.pong
    Mac-->>Shell: delivered true
    Note over Shell: stream healthy, no restart

    Note over SurfaceView: Render recovery
    SurfaceView->>SurfaceView: captureSnapshotFallbackForRecovery()
    SurfaceView->>SurfaceView: "initializeSurface() surfaceHasReceivedOutput=false"
    SurfaceView->>SurfaceView: flushSnapshotFallbackPresentation()
    Mac->>SurfaceView: process_output replay
    SurfaceView->>SurfaceView: "surfaceHasReceivedOutput=true hide snapshot"
Loading

Reviews (19): Last reviewed commit: "Stabilize iOS reconnect recovery" | Re-trigger Greptile

Comment on lines +782 to +790
@discardableResult
func simulateRenderRecoveryWithStuckPriorFreeForTesting() -> Bool {
pendingSurfaceFreeCount = max(pendingSurfaceFreeCount, Self.surfaceFreeBacklogWarningThreshold)
return recoverRenderPipeline(
reason: "test_stuck_prior_free",
stalledMs: Int(Self.renderPipelineStallDeadline * 1000),
replay: .delegateWhenNoCaller
)
}

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 Test seam in production Sources

simulateRenderRecoveryWithStuckPriorFreeForTesting() is a new #if DEBUG-gated method named …ForTesting that manipulates the private field pendingSurfaceFreeCount and calls the private method recoverRenderPipeline with no production caller — this is the pattern cmux-no-test-debug-seam-in-production-source explicitly rejects. The #if DEBUG guard does not make the accessor acceptable in shipping Sources/.

The canonical fix already demonstrated by isRenderDispatchSuppressed in this same file: widen pendingSurfaceFreeCount and recoverRenderPipeline from private to internal, remove this wrapper from production source, and have the test set the count and call recoverRenderPipeline directly via its existing @testable import CmuxMobileTerminal.

Rule Used: Flag Swift files under a production Sources path (... (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: 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
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 782-791: Remove the test-only seam from GhosttySurfaceView by
eliminating simulateRenderRecoveryWithStuckPriorFreeForTesting() from Sources
and exposing the underlying recovery state/path through existing production
symbols instead. Keep recoverRenderPipeline and pendingSurfaceFreeCount testable
via `@testable` import from the test target, and update the tests to exercise the
same behavior without a public method named for testing.
🪄 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: 96032ab0-6fea-4b18-b974-3d840118bce7

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7c147 and f9acebc.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalViewportSpacingTests.swift

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Verification update after initial PR body:

  • Direct attach on simulator 2B86B925-1A19-4C96-B04B-34DFD37DB168 progressed the tagged iOS app from Still loading to the workspace list.
  • Rendered all four seeded terminal workspaces in the iOS UI: Freeze Soak 2, Freeze Soak 1, Freeze Soak 4, and Freeze Soak 3.
  • Captured a 737.346667-second simulator video at /tmp/tfrz-evidence/ios-terminal-soak.mov; frame split and contact sheet are at /tmp/tfrz-evidence/ios-terminal-soak-frames/contact-sheet.png.
  • The contact sheet shows visible terminal content throughout the 12+ minute capture, with no black terminal frame or frozen blank render.
  • Resource samples stayed low/stable: RSS moved from about 281 MB to 271 MB to 238 MB; CPU stayed roughly 0.5 to 0.9 percent.
  • iOS log evidence is at /tmp/tfrz-evidence/ios-terminal-soak-logs.txt; final terminal renderer health logs show tick.alive win=true suspended=false renderInFlight=false needsDraw=false contents=true surf=402x632.

Remaining disclosure: xctrace Time Profiler still hangs and emits non-exportable partial traces in this environment. A post-soak brand-new workspace open was not separately proven because Simulator focus became unreliable with many concurrent simulator windows, so I stopped before risking clicks into another agent's simulator.

@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
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 2396-2400: The recovery snapshot logic in GhosttySurfaceView
should not use a time-based throttle for correctness-critical text capture.
Update the recovery path around recoverySnapshotText and
workQueue.lastRecoverySnapshotTime so it always uses the authoritative viewport
snapshot from the serialized surface transition/output flow, or fails closed
when no current snapshot is available. Remove the fixed 0.5s staleness window
and keep the “last visible terminal text” state sourced directly from the
current surface snapshot path.
- Around line 3829-3837: The early return in
updateSnapshotFallback(text:html:clearWhenEmpty:) treats an empty text snapshot
as empty overall, which skips a valid HTML fallback. Update the logic so text
and html are evaluated independently: only clear/hide snapshotFallbackView when
both inputs are empty (or when clearWhenEmpty explicitly applies), and allow
lastSnapshotFallbackHTML/html to render even if snapshot is empty. Keep the fix
localized to updateSnapshotFallback(text:html:clearWhenEmpty:) and its use of
snapshotFallbackView and lastSnapshotFallbackHTML.
🪄 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: ad9fe435-0c05-4467-9e43-b8ab72369770

📥 Commits

Reviewing files that changed from the base of the PR and between 4010b05 and 6e70bd9.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (6)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceWorkQueue.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalViewportSpacingTests.swift

@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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift`:
- Around line 183-211: The chat overlay logic in
WorkspaceDetailView.detailSurfaceContent only wraps detailContent() when
activeBrowser is nil, so chatContent never appears in browser mode. Update the
activeBrowser branch to use the same terminalContentWithChatOverlay-style ZStack
behavior around browserContent(browser), or otherwise prevent isChatMode from
being shown while a browser is active. Keep the fix localized to
detailSurfaceContent and terminalContentWithChatOverlay so the overlay mounts
consistently regardless of whether browserContent or detailContent is rendered.
🪄 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: 4c4f345c-1d90-409f-82a0-8782366b312d

📥 Commits

Reviewing files that changed from the base of the PR and between 6e70bd9 and c410538.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift

@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
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift`:
- Around line 108-121: The visibility lookup in focusTerminalInput duplicates
the same filter/sort/compactMap/first-where predicate used by
copyableTerminalText, so the visibility contract is defined in two places.
Extract a shared helper such as resolveVisibleSurfaceView(hostSurfaceID:) in
GhosttySurfaceRegistry and have both focusTerminalInput and copyableTerminalText
use it. Keep the existing hidden/window/alpha checks and ordering behavior
identical in the shared helper.
🪄 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: 54939a54-9da2-4568-9e9a-fd6a8de2a07c

📥 Commits

Reviewing files that changed from the base of the PR and between c410538 and 4bc7c43.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalViewportSpacingTests.swift

Comment on lines +108 to +121
public static func focusTerminalInput(surfaceID: String) -> Bool {
registeredSurfaceViews = registeredSurfaceViews.filter { $0.value.value != nil }
guard let matchingView = registeredSurfaceViews
.sorted(by: { $0.key < $1.key })
.compactMap(\.value.value)
.first(where: { candidate in
candidate.hostSurfaceID == surfaceID && candidate.surface != nil
&& candidate.window != nil && !candidate.isHidden
&& candidate.alpha > 0.01
})
else {
return false
}
matchingView.focusInput()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate lookup logic vs. copyableTerminalText.

The filter/sort/compactMap/first-where visibility predicate here is nearly identical to the one in copyableTerminalText above. Consider extracting a shared resolveVisibleSurfaceView(hostSurfaceID:) helper to keep the visibility contract (hidden/window/alpha thresholds) defined once.

♻️ Proposed refactor
+    private static func resolveVisibleSurfaceView(hostSurfaceID surfaceID: String) -> GhosttySurfaceView? {
+        registeredSurfaceViews = registeredSurfaceViews.filter { $0.value.value != nil }
+        return registeredSurfaceViews
+            .sorted { $0.key < $1.key }
+            .compactMap(\.value.value)
+            .first { candidate in
+                candidate.hostSurfaceID == surfaceID && candidate.surface != nil
+                    && candidate.window != nil && !candidate.isHidden
+                    && candidate.alpha > 0.01
+            }
+    }
+
     `@MainActor`
     public static func focusTerminalInput(surfaceID: String) -> Bool {
-        registeredSurfaceViews = registeredSurfaceViews.filter { $0.value.value != nil }
-        guard let matchingView = registeredSurfaceViews
-            .sorted(by: { $0.key < $1.key })
-            .compactMap(\.value.value)
-            .first(where: { candidate in
-                candidate.hostSurfaceID == surfaceID && candidate.surface != nil
-                    && candidate.window != nil && !candidate.isHidden
-                    && candidate.alpha > 0.01
-            })
-        else {
+        guard let matchingView = resolveVisibleSurfaceView(hostSurfaceID: surfaceID) else {
             return false
         }
         matchingView.focusInput()
         return true
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift`
around lines 108 - 121, The visibility lookup in focusTerminalInput duplicates
the same filter/sort/compactMap/first-where predicate used by
copyableTerminalText, so the visibility contract is defined in two places.
Extract a shared helper such as resolveVisibleSurfaceView(hostSurfaceID:) in
GhosttySurfaceRegistry and have both focusTerminalInput and copyableTerminalText
use it. Keep the existing hidden/window/alpha checks and ordering behavior
identical in the shared helper.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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 using default effort 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 2877a79. Configure here.

refreshTerminalEventSubscription(
reason: reason,
restartOnFailure: restartOnSubscriptionFailure
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Foreground resume replays surfaces redundantly on verified streams

Low Severity

resyncTerminalOutput with restartOnSubscriptionFailure: true always replays all mounted surfaces immediately (line 6843-6844), and then refreshTerminalEventSubscription replays them again (line 6424-6425) when alreadySubscribed == nil (legacy hosts omitting the field). This double replay fires on every foreground resume for affected host versions, sending two full terminal buffer RPCs per mounted surface even when the ping/pong proves the stream is healthy.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2877a79. Configure here.

@azooz2003-bit
azooz2003-bit enabled auto-merge (squash) July 9, 2026 18:10
auto-merge was automatically disabled September 23, 2026 08:35

Pull request was closed

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 2877a79c Deployed Jul 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants