Skip to content

Recover iOS terminal render pipeline stalls - #7098

Merged
austinywang merged 59 commits into
mainfrom
issue-7093-ios-terminal-render-freeze
Jun 30, 2026
Merged

austinywang merged 59 commits into
mainfrom
issue-7093-ios-terminal-render-freeze

Conversation

@austinywang

@austinywang austinywang commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add bounded wait/recovery around iOS terminal output and geometry application so a wedged Ghostty output/render queue cannot leave shell delivery permanently in-flight
  • rebuild the local Ghostty surface on render/output stalls, switch to a fresh per-surface work queue, and request an authoritative replay from the Mac
  • reset the shell terminal-output delivery queue on abandoned chunks so stale pending bytes do not grow unbounded or replay into the rebuilt surface

Fixes #7093

Root Cause

The iOS input path and render path are separate. Keystrokes travel as input RPCs, but terminal output is delivered through a render/output stream that waits for GhosttySurfaceView.processOutputAndWait to finish before MobileShellComposite acknowledges the chunk.

If libghostty's local iOS render/output path wedges, renderInFlight can stay true or processOutputAndWait can stop returning. The host session continues to receive input, but the shell delivery queue remains inFlight and pending output accumulates behind it. That matches the reported behavior: echo gets progressively laggier, the phone stops repainting, and only an app restart clears the local renderer state.

Changes

  • Replaced the single static terminal output queue with a per-surface-generation GhosttySurfaceWorkQueue so recovery can abandon a stuck queue and continue on a fresh queue.
  • Added deadlines for output and geometry application. On timeout, the view rebuilds its local Ghostty surface, invalidates stale callbacks by generation, and asks the shell to replay authoritative terminal state.
  • Added render-in-flight stall detection from the display-link pump so a stuck render_now self-heals instead of leaving frame production disabled.
  • Added shell reset/replay API: abandoned chunks reset TerminalOutputDeliveryQueue, invalidate stale stream tokens, and request a replay.
  • Added bounded pending-depth diagnostics (terminal.output.pending) next to the existing oq.render.LAG signal.
  • Kept copy/debug terminal snapshots on the matched surface generation's queue so recovery does not race the wrong queue.

Tests

  • arch -arm64 swift test --filter terminalOutputResetDropsStalledBacklogAndInvalidatesOldAcks in Packages/iOS/CmuxMobileShell
  • arch -arm64 swift test in Packages/iOS/CmuxMobileShell (295 tests)
  • scripts/lint-ios-package-conventions.sh
  • git diff --check

I also tried SwiftPM builds for Packages/iOS/CmuxMobileTerminal and Packages/iOS/CmuxMobileShellUI, but this clone does not currently have a usable root GhosttyKit.xcframework, so SwiftPM stops before compiling those targets with local binary target 'GhosttyKit' ... does not contain a binary artifact. I did not run the macOS DEV build or any reload command per the issue instructions; CI is the iOS compile/build gate for this PR.

Localization

No new user-facing UI strings were added. New strings are debug diagnostics / log messages only; no localization catalog changes required.


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


Summary by cubic

Stops iOS terminal freezes by detecting stalled Ghostty render/output and self-healing via render‑pipeline rebuilds plus authoritative replay (fixes #7093). Adds bounded, rate‑limited recoveries, caps replay follow‑ups to prevent loops, defers replay until recovery can run, and localizes fallback labels and the debug‑logs menu.

  • Bug Fixes
    • Detect render stalls. Rebuild the pipeline and switch to a fresh per‑surface GhosttySurfaceWorkQueue (bounded, rate‑limited retries). Cap pending frees to 1. Notify host via ghosttySurfaceViewDidResetRenderPipeline. Defer replay until recovery can run.
    • Harden replay barrier and retries. Drop live deltas while active, allow bypassReplayBarrier, and track drops per surface/generation. Cancel superseded tasks and ignore stale responses. Retry failures up to 2 with stable scheduling; auto‑request follow‑ups on pre‑ack drops and cap follow‑ups to 1 per surface. Preserve the barrier across retry exhaustion and replay‑ack resets; clear on abort, empty replay, remount, or stale client. Rate‑limit drop logging. Ensure replay fallback preserves alternate‑screen suppression.
    • API/UI safety. Delivery/apply return Bool; on false call terminalOutputDidReset or terminalOutputNeedsReplay. Make visible snapshot async and deadline‑bounded on the output queue; keep the snapshot provider immutable. Cancel stale copyable‑text reads with a Mutex and check before viewport fallback. Bound/log terminal.output.pending. Fail blocked surface waiters. Localize runtime‑failure and copy‑logs labels; fix UI localization import.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added per-surface terminal replay control APIs to explicitly reset abandoned output and request authoritative replays.
    • Added recovery signaling when the Ghostty render pipeline resets, triggering replay as needed.
  • Bug Fixes
    • Prevented stale/out-of-order terminal replay results from overwriting newer output via a replay barrier.
    • Committed terminal screen/byte state only after successful delivery acceptance.
    • Improved handling of replay acknowledgements, stalled outputs, dropped-output retries, and retry exhaustion.
  • Tests
    • Expanded terminal replay barrier, liveness replay, and reset/ack sequencing coverage.
  • Documentation / UI
    • Updated debug “Copy Debug Logs” snapshot timing and added localized runtime failure messaging.

@vercel

vercel Bot commented Jun 30, 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 30, 2026 4:23pm
cmux-staging Building Building Preview, Comment Jun 30, 2026 4:23pm

@coderabbitai

coderabbitai Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds generation-aware render recovery and replay-barrier handling for terminal output, updates UI callbacks to report resets and request replay, switches visible snapshot reads to async, and expands tests and liveness support for replay recovery scenarios.

Changes

Render recovery and replay barrier

Layer / File(s) Summary
Work queue, protocol, and copyable text
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceWorkQueue.swift, Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTerminalOutputSinking.swift, Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift
Adds the serial per-surface work queue type, new output-reset/replay protocol methods, and routes copyable terminal text reads onto the matched view’s queue.
Surface generation and recovery
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Adds the reset delegate callback, instance queue ownership, generation tracking, render stall timing, pending-operation bookkeeping, deadline checks, recovery teardown/rebuild, and generation-checked async output, geometry, snapshot, and copyable-text operations.
Replay barrier state and delivery flow
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
Adds replay-barrier state, barrier lifecycle helpers, replay request deduping and staleness checks, barrier-aware replay application, live-byte gating, and replay-control entry points.
UI reset and replay callbacks
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, ios/cmux/Resources/Localizable.xcstrings
Treats some size/output operations as fallible, reports resets on failure, forwards render-pipeline reset callbacks to replay requests, awaits visible terminal snapshots before copying logs or composing feedback, and adds localized strings for the updated UI text.
Replay barrier tests and replay response support
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellReplayFallbackScreenTests.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalReplayBarrierAckResetTests.swift
Adds scripted replay responses and async coverage for stalled backlog drops, reset-driven barrier clearing, follow-up replay, retry exhaustion, and replay-ack handling.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through the queue with a jitter and spring,
Then barred stale bytes from the render-ring.
Reset, replay, and fresh frames aligned,
Gen-safe paws kept the old freeze confined.
Now the terminal twinkles, no longer astray,
And bunny ears salute a smoother display.


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error MobileShellComposite.swift (7422 lines) and GhosttySurfaceView.swift (4266 lines) each gained hundreds of lines in already oversized files, with no shrink or extraction. Move the replay-barrier/recovery state machine into a smaller package type/protocol and keep these oversized files as thin glue; reduce the big-file deltas below the boundary.
Out of Scope Changes check ⚠️ Warning The PR also adds localized debug/menu strings and a runtime-fallback UI text change that are not required by the stall-recovery objective. Split the localization and fallback-text updates into a separate PR unless they are needed for the stall fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: recovering iOS terminal render pipeline stalls.
Linked Issues check ✅ Passed The changes address the iOS client-side render stall, add bounded recovery/replay handling, and prevent unbounded terminal output buildup as required.
Cmux Swift Actor Isolation ✅ Passed New APIs stay on @MainActor UI/store paths; the only new Sendable reference type is a queue wrapper with confined mutable state and an explicit safety note.
Cmux Swift Blocking Runtime ✅ Passed Production changes are async/callback-driven; sleeps/polling appear only in tests, and the small locks have documented low-level carve-outs.
Cmux Browser Automation Off-Main ✅ Passed PR diff vs origin/main only touches iOS shell/terminal files; no browser.* routing or ControlCommandExecutionPolicy changes are present.
Cmux Expensive Synchronous Load ✅ Passed Touched production Swift files add async replay/snapshot plumbing only; no expensive agent-history/file loads on MainActor or interactive paths were introduced.
Cmux Cache Substitution Correctness ✅ Passed The PR keeps snapshot/copy paths on live Ghostty reads with generation checks and bounded fallbacks; no fresh authoritative read was replaced by a stale cache.
Cmux No Hacky Sleeps ✅ Passed No covered TS/JS/shell/runtime-script changes add fixed sleeps; the new waits are test-only or Swift, which is out of scope.
Cmux Algorithmic Complexity ✅ Passed Changed hot replay/delivery paths use O(1) keyed lookups; the only scan/sort is a rare copy/debug path, not a scalable hot path.
Cmux Swift Concurrency ✅ Passed PASS: the new DispatchQueue is a serialized libghostty/C boundary, and the new Tasks are stored or limited to UI/OS callback hops; no Combine or completion-handler expansion.
Cmux Swift @Concurrent ✅ Passed Changed async paths stay UI-bound and hop off-main via outputQueue/Task.detached; no nonisolated async work needing @concurrent or invalid annotation found.
Cmux Swiftpm Lockfiles ✅ Passed Only two Swift source files changed; no .gitignore, Package.resolved, or Xcode project/workspace lockfiles were modified, so the SwiftPM lockfile policy isn't violated.
Cmux Swift Logging ✅ Passed The only new NSLog is under #if DEBUG, and the new runtime diagnostics use the existing DEBUG-only MobileDebugLog.anchormux path; no forbidden production logging was added.
Cmux User-Facing Error Privacy ✅ Passed New user-facing copy is generic (“Terminal renderer failed to start.”) and no added alerts/errors expose vendor, internal, token, or payload details.
Cmux Full Internationalization ✅ Passed New user-facing text uses localized APIs, and both added catalog keys include matching en/ja translations.
Cmux Swiftui State Layout ✅ Passed The edited SwiftUI files only add action-handler async snapshot/replay hooks; no new ObservableObject/@published, no GeometryReader/layout mutation, and the bridge view is exempt.
Cmux Architecture Rethink ✅ Passed PASS: the new lock is confined to the Ghostty callback bridge, timing/polling is test-only, and replay/render ownership stays explicit with documented invariants.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes terminal/replay plumbing and a debug menu item; no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed All changed paths are hand-written source/tests/config/localization; no logs, screenshots, temp dirs, build outputs, or other artifact paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed New production APIs are runtime recovery hooks with production callers; no added DEBUG/test-only seam or test-hook name appears in the PR diff.
Cmux No Ambient Global State ✅ Passed No new file-scope API or singleton-like runtime state was introduced; the changes stay on existing owning types and constructable helpers.
Description check ✅ Passed The PR description covers summary, root cause, changes, tests, and localization, and is mostly aligned with the repository template.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7093-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.

@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: 4

🤖 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/GhosttySurfaceRepresentable.swift`:
- Around line 426-430: The render-pipeline reset callback in
ghosttySurfaceViewDidResetRenderPipeline is forwarding replay based only on
surfaceID, which can let a late callback from an old GhosttySurfaceView trigger
recovery for the current owner. Add a guard at the start of the callback to
verify self.surfaceView is still the same instance as the passed surfaceView
before creating the Task and calling terminalOutputNeedsReplay(surfaceID:). Keep
the replay request tied to the active authoritative surface view in
GhosttySurfaceRepresentable.

In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 2277-2282: Replace the `Task.sleep`-based recovery in
`GhosttySurfaceView` with a real, cancellation-aware deadline owned by the
current surface generation. Update the timeout logic around
`outputApplyTimeoutNanoseconds` and the related `Task`/deadline handling so
slow-but-valid applies do not tear down the surface prematurely; use the
existing generation/state coordination points in `GhosttySurfaceView` and the
apply/rebuild path instead of a sleeping task on the main actor. Also remove the
matching sleep-based stall recovery near the other referenced timeout block so
both paths share the same scheduler/signal-driven mechanism.
- Around line 2678-2679: The reset flow is requesting terminal replay twice
because `initializeSurface()` is followed by
`ghosttySurfaceViewDidResetRenderPipeline(_:)`, while
`GhosttySurfaceRepresentable.attach(...)` already handles `false` from the
`*AndWait` recovery path via `terminalOutputDidReset(...)` and
`requestTerminalReplay(...)`. Update `GhosttySurfaceView` so only one recovery
path is authoritative: either keep the delegate callback or the `false`-return
handling, but not both, and gate `ghosttySurfaceViewDidResetRenderPipeline(_:)`
so it does not fire for recoveries that already bubble back through
`attach(...)` and `terminalOutputDidReset(...)`.
- Around line 3783-3798: The visible snapshot loop in GhosttySurfaceView uses a
single shared DispatchTime deadline, so later items are measured against an
already-expired timeout. Move the timeout calculation inside the pending/item
loop in the visible snapshot path so each queue wait gets its own fresh 600 ms
budget, and keep the rest of the done/holder handling unchanged.
🪄 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: f1bc0d35-1893-4ee8-90ad-82f49e6a1a31

📥 Commits

Reviewing files that changed from the base of the PR and between 0f7e510 and c382199.

📒 Files selected for processing (9)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTerminalOutputSinking.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceWorkQueue.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/SurfaceOperationWaiter.swift

@greptile-apps

greptile-apps Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a bounded, rate-limited self-healing layer around the iOS terminal render pipeline. When libghostty's output or geometry application stalls (detected via display-link deadline), the view tears down the wedged surface, creates a fresh per-surface GhosttySurfaceWorkQueue, and requests an authoritative replay from the Mac — stopping the progressive lag and repaint freeze described in #7093.

  • Surface recovery: GhosttySurfaceView now tracks surfaceGeneration and outputQueueGeneration; stale queue callbacks are discarded by generation check; recoverRenderPipeline is bounded by maxPendingSurfaceFrees = 1 and rate-limited via renderPipelineRecoveryPaused while an old surface free drains.
  • Replay barrier: MobileShellComposite introduces a per-surface barrier token that drops live output deltas while a replay is in flight, with bounded follow-ups (cap 1) and failure retries (cap 2), preventing both replay loops and permanently-stuck barriers.
  • Queue reset API: terminalOutputDidReset / terminalOutputNeedsReplay give the UI layer explicit hooks to abandon stale chunks and request fresh state, replacing the implicit "drain and hope" behaviour.

Confidence Score: 5/5

The recovery path is carefully bounded: one pending surface free at a time, two RPC retries per barrier, one follow-up replay per surface — no unbounded loops or silent discards.

The core invariants (generation checks guard stale queue callbacks, beginTerminalReplayBarrier resets the queue and stream token before any bypass delivery so immediate != nil is guaranteed for the replay chunk, completePendingSurfaceOperations and pauseRenderPipelineRecovery are mutually exclusive code paths) all hold under inspection. The replay barrier correctly drops live deltas without leaking state, and the recovery-paused window fails every incoming operation with false rather than silently advancing the stream. No correctness bugs were found; remaining observations are about file size and parallel-dictionary organisation.

GhosttySurfaceView.swift and MobileShellComposite.swift are the two largest files and carry the most new state; reviewers should pay particular attention to recoverRenderPipeline and the ten new barrier dictionaries when returning to maintain this code.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift Core of the recovery mechanism: adds per-surface GhosttySurfaceWorkQueue, surfaceGeneration staleness checks, renderInFlightSince stall detection, deadline pump via display link, recoverRenderPipeline, and pauseRenderPipelineRecovery; processOutputAndWait/applyViewSizeAndWait/useNaturalViewSizeAndWait now return Bool; copyableTextForCurrentSurface moved to async with bounded deadline; grew from 3747 to 4264 lines.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Introduces the replay-barrier machinery: 10 new dictionaries/sets tracking barrier tokens, drop counts, ack tokens, retry counts, and follow-up counts per surface; requestTerminalReplay now tracks in-flight request IDs, cancels superseded tasks, retries on failure (cap 2), and handles stale-client and stale-sequence cases; grew from 7371 to 7783 lines.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift Delivery helpers updated: deliverTerminalBytes/deliverTerminalRenderGrid return Bool; barrier drop counting and rate-limited logging added; terminalOutputDidReset and terminalOutputNeedsReplay implement the reset/replay API; completeTerminalOutput clears the barrier and schedules follow-ups when drops occurred during barrier window.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift For-await loop now checks Bool returns from processOutputAndWait, useNaturalViewSizeAndWait, and applyViewSizeAndWait; calls terminalOutputDidReset on false; implements ghosttySurfaceViewDidResetRenderPipeline delegate to call terminalOutputNeedsReplay; localizes the runtime-failure fallback label.
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceWorkQueue.swift New 23-line type wrapping a per-generation DispatchQueue; replaces the static Self.outputQueue; lastAccessibilityTextTime (debug-only) moved from a nonisolated(unsafe) static var to a per-instance field, eliminating cross-surface sharing.
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift Removes the private CopyableTextSurfaceHandle carrier struct; copyableTextForCopySheet now delegates to the new copyableTextForCurrentSurface method (async, deadline-bounded); safety argument preserved via generation check on main-actor hop.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift visibleTerminalSnapshot() calls moved into Task { @mainactor } blocks to match the now-async API; debug Copy Debug Logs label localized via L10n.string.
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTerminalOutputSinking.swift Adds terminalOutputDidReset and terminalOutputNeedsReplay to the MobileTerminalOutputSinking protocol.
ios/cmux/Resources/Localizable.xcstrings Adds mobile.terminal.rendererFailed (production fallback UI) and mobile.debug.copyLogs (debug-only menu label) with both en and ja translations, matching the catalog's two supported locales.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift Substantially expanded test suite (617 new lines): covers barrier drop/ack sequencing, reset/replay on stalled backlog, follow-up replay after pre-ack drops, ack-reset retry paths, and barrier preservation on retry exhaustion.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift Test support updated to drive the new barrier/reset protocol in liveness test scenarios; adds LivenessHostRouter and TransportBox helpers for replay-flow testing.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellReplayFallbackScreenTests.swift New test file covering replay fallback screen preservation, including alternate-screen suppression across barrier/replay cycles.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalReplayBarrierAckResetTests.swift New test file covering terminalOutputDidReset when a barrier ack is active, verifying retry sequencing and token invalidation.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalReplayBarrierFollowUpTests.swift New test file covering the follow-up replay cap (maxTerminalReplayBarrierFollowUps = 1) to prevent replay loops after pre-ack drops.
.github/swift-file-length-budget.tsv Budget entries updated: GhosttySurfaceView.swift 3747 to 4264, MobileShellComposite.swift 7371 to 7783; new test-file entries added; all changes reflect actual new line counts.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant DL as DisplayLink (handleDisplayLinkFire)
    participant GSV as GhosttySurfaceView (@MainActor)
    participant OQ as GhosttySurfaceWorkQueue (serial BG)
    participant REP as GhosttySurfaceRepresentable
    participant MSC as MobileShellComposite (@MainActor)
    participant MAC as Mac RPC

    Note over GSV: renderInFlight = true (render_now dispatched)
    DL->>GSV: handleDisplayLinkFire()
    GSV->>GSV: checkSurfaceOperationDeadlines(now)
    GSV->>GSV: "renderInFlightSince stall detected (>=2s)"
    GSV->>GSV: recoverRenderPipeline(replay:.delegateWhenNoCaller)
    GSV->>OQ: enqueueSurfaceFree(oldSurface) [old queue]
    GSV->>GSV: "surfaceGeneration += 1, new outputQueue, initializeSurface()"
    GSV->>REP: ghosttySurfaceViewDidResetRenderPipeline()
    REP->>MSC: terminalOutputNeedsReplay(surfaceID)
    MSC->>MSC: beginTerminalReplayBarrier → drops live output
    MSC->>MAC: mobile.terminal.replay RPC
    MAC-->>MSC: replay response (renderGrid or bytes)
    MSC->>MSC: deliverTerminalRenderGrid(bypassReplayBarrier:true)
    MSC-->>REP: AsyncStream yields replay chunk
    REP->>GSV: processOutputAndWait(chunk.data) → true
    REP->>MSC: terminalOutputDidProcess → barrier cleared
    Note over MSC: Live output resumes
    Note over GSV,OQ: Old queue render_now completes
    OQ-->>GSV: "ghostty_surface_free(oldSurface) → pendingSurfaceFreeCount -= 1"
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 DL as DisplayLink (handleDisplayLinkFire)
    participant GSV as GhosttySurfaceView (@MainActor)
    participant OQ as GhosttySurfaceWorkQueue (serial BG)
    participant REP as GhosttySurfaceRepresentable
    participant MSC as MobileShellComposite (@MainActor)
    participant MAC as Mac RPC

    Note over GSV: renderInFlight = true (render_now dispatched)
    DL->>GSV: handleDisplayLinkFire()
    GSV->>GSV: checkSurfaceOperationDeadlines(now)
    GSV->>GSV: "renderInFlightSince stall detected (>=2s)"
    GSV->>GSV: recoverRenderPipeline(replay:.delegateWhenNoCaller)
    GSV->>OQ: enqueueSurfaceFree(oldSurface) [old queue]
    GSV->>GSV: "surfaceGeneration += 1, new outputQueue, initializeSurface()"
    GSV->>REP: ghosttySurfaceViewDidResetRenderPipeline()
    REP->>MSC: terminalOutputNeedsReplay(surfaceID)
    MSC->>MSC: beginTerminalReplayBarrier → drops live output
    MSC->>MAC: mobile.terminal.replay RPC
    MAC-->>MSC: replay response (renderGrid or bytes)
    MSC->>MSC: deliverTerminalRenderGrid(bypassReplayBarrier:true)
    MSC-->>REP: AsyncStream yields replay chunk
    REP->>GSV: processOutputAndWait(chunk.data) → true
    REP->>MSC: terminalOutputDidProcess → barrier cleared
    Note over MSC: Live output resumes
    Note over GSV,OQ: Old queue render_now completes
    OQ-->>GSV: "ghostty_surface_free(oldSurface) → pendingSurfaceFreeCount -= 1"
Loading

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

@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: 3

🤖 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/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift`:
- Around line 138-152: The new replay assertions are relying on pollUntil, but
that helper still uses Task.sleep and makes the test depend on real time. Update
the test support in MobileShellRenderGridLivenessTestSupport and the
TerminalOutputDeliveryQueueTests flow to wait on a real signal instead, such as
a router event/continuation for the replay request or advancing the existing
fake clock, so the assertions in terminal output replay/reset paths no longer
use wall-clock polling.

In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 3878-3893: `VisibleSnapshotRequest` and `visibleSnapshotSection`
are using a stale raw surface pointer without validating it against the current
surface generation. Capture `surfaceGeneration` when building each request in
`visibleTerminalSnapshot`, then in `visibleSnapshotSection` verify `view.surface
=== surface` and that the generation still matches before enqueuing the read on
`outputQueue`; if either check fails, return nil/fail closed so an outdated
surface is never read after an await.
- Around line 3992-4029: Mark the helper payload types in
GhosttySurfaceView.swift as nonisolated so they are not implicitly tied to
`@MainActor` isolation. Update the declarations of RenderPipelineRecoveryReplay,
PendingSurfaceOperation, PendingVisibleSnapshot, VisibleSnapshotRequest, and
VisibleSnapshotRead to be explicitly nonisolated while keeping their stored
properties unchanged. This should ensure these coordination values can safely
cross main-actor and work-queue closures without pulling UI isolation into the
payload types.
🪄 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: c0046640-d73a-4425-870f-aa032426b58d

📥 Commits

Reviewing files that changed from the base of the PR and between c382199 and de50f84.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (7)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.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: 3

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Line 6727: The replay failure log in MobileShellComposite.swift is exposing
dynamic error details as public text. Update the CMUX_REPLAY logging around the
mobileShellLog.error call to avoid String(describing: error) being marked
.public; instead mark the error value private or replace it with a stable
non-sensitive error category while keeping surfaceID public. Keep the change
localized to the replay failure path so production logs redact sensitive
upstream/auth context.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalOutputDelivery.swift:
- Around line 140-149: terminalOutputDidReset currently bails out when
terminalReplayBarrierTokensBySurfaceID already has a token, which leaves the
in-flight queue item and ack token stuck. Update
MobileShellComposite+TerminalOutputDelivery’s terminalOutputDidReset flow to
always restart the reset path: clear or replace the existing replay barrier
state, then call beginTerminalReplayBarrier and requestTerminalReplay even when
a barrier is already active. Keep the existing surface/token guards, and ensure
the reset logic drains the queued item so terminalOutputDidProcess can resume
normally.

In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 2777-2784: The recovery path in recoverRenderPipeline is dropping
the rebuild when pendingSurfaceFreeCount hits Self.maxPendingSurfaceFrees, so a
wedged surface can stay unrecovered; keep a pending recovery request instead of
returning false immediately, and have the free-drain completion trigger the
rebuild for the current surface. Update the logic around renderInFlight,
needsAnotherRender, and the drain callback so the caller is not told replay is
needed until a rebuild is actually scheduled, and make the same change in the
related code path near the reuse/recover handling.
🪄 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: 6e2c1175-c716-4f32-9aea-dd30e9a4a625

📥 Commits

Reviewing files that changed from the base of the PR and between de50f84 and 5b2d48d.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift

Comment thread Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Outdated
@austinywang
austinywang merged commit 2aaee8d into main Jun 30, 2026
36 checks passed
austinywang added a commit that referenced this pull request Jul 2, 2026
…values-for

Resolved conflicts:
- .github/swift-file-length-budget.tsv: regenerated via swift_file_length_budget.py --write-budget
- ChatScrollEdgeCoordinator.swift, ChatTranscriptTableView.swift: took origin/main
  (main superseded this branch's inline #if compiler(>=6.2) glass-API guards with the
  applyScrollEdgeEffects helper + scroll-momentum work in #7072/#7109; branch predated it)
- GhosttySurfaceView.swift: took origin/main and removed the now-orphaned
  GhosttySurfaceHandle.swift; main's GhosttySurfaceWorkQueue redesign (#7098) supersedes
  this branch's GhosttySurfaceHandle Sendable-wrapper approach for the same surface-pointer
  safety concern. Codex transcript payload (resolver realpath fix, service shutdown()/race
  guard, MobileShellComposite isolated deinit) preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jul 27, 2026
4 tasks done

This branch was successfully deployed

1 active deployment
Preview – cmux — 36383767 Deployed Jun 30, 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.

iOS: terminal stops rendering after typing for a while (input still reaches host) — client-side render pipeline wedges, only app restart recovers

1 participant