Skip to content

Smooth iOS terminal scrollback with local prefetch#6067

Merged
azooz2003-bit merged 2 commits into
mainfrom
task-ios-scrollback-window-cache
Jun 14, 2026
Merged

Smooth iOS terminal scrollback with local prefetch#6067
azooz2003-bit merged 2 commits into
mainfrom
task-ios-scrollback-window-cache

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • let the iOS Ghostty mirror consume coalesced scroll deltas immediately so drag/scroll movement is local and smooth
  • add bounded render-grid scrollback prefetch on first/long scrolls, with Mac render-grid responses remaining authoritative
  • share stale-frame guarding for authoritative terminal render-grid delivery
  • refresh the Swift file-length budget for existing over-budget files so the required guard reflects the current tree

Testing

  • swift test --package-path Packages/CmuxMobileShell passed, 130 tests
  • git diff --check origin/main...HEAD passed
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv passed
  • ./scripts/reload-cloud.sh --tag swbk fell back local and built macOS tag swbk
  • ios/scripts/reload.sh --tag swbk --simulator "iPhone 17" passed and launched simulator bundle dev.cmux.ios.swbk
  • ios/scripts/reload-cloud.sh --tag swbk --device-id 4A52829D-6427-599F-A166-4058881D2DF4 --wait 60 blocked on missing ASC API key; CoreDevice listed the physical iPhone as unavailable

Related

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved mobile terminal scroll prefetch, dynamically selecting a capped scrollback window based on scroll distance and correctly handling coalesced scroll actions.
    • Added smoother local scrollback scrolling to update the terminal immediately at the target cell position.
  • Performance / Reliability

    • Terminal scrolling now relies on authoritative render-grid updates from the server, reducing stale-frame delivery and improving replay/prefetch correctness.
  • Tests

    • Added unit tests for scroll delivery coalescing and scrollback prefetch window priming/refresh behavior.

@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 14, 2026 3:43am
cmux-staging Building Building Preview, Comment Jun 14, 2026 3:43am

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds mobile terminal scroll prefetch end-to-end: TerminalScrollDelivery and TerminalScrollbackPrefetchState track per-surface scroll distance to compute maxScrollbackRows hints; the shell encodes these hints into mobile.terminal.scroll RPC params and delivers the prefetched render-grid response authoritatively; the server builds a full render_grid payload from the scroll RPC; GhosttySurfaceView applies local scrollback directly on flush.

Changes

Mobile Terminal Scroll Prefetch

Layer / File(s) Summary
Scroll prefetch data model and TerminalScrollbackPrefetchState
Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift, Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
TerminalScrollDelivery gains an optional maxScrollbackRows field with max-merge coalescing. New TerminalScrollbackPrefetchState accumulates absolute scroll distance and emits windowRows once refreshDistanceRows is crossed. Two new unit tests cover max-row coalescing and the prime-then-refresh behavior.
Local scrollback feedback in GhosttySurfaceView
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift, Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
New UIKit-only applyLocalScrollbackScroll(lines:col:row:) computes scaled cell geometry, sets the ghostty mouse position, applies the scroll, and redraws. flushPendingScrollIfNeeded() calls this before the delegate callback.
Shell-side prefetch state tracking and render-grid delivery helper
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
MobileShellComposite adds terminalScrollbackPrefetchStatesBySurfaceID (declared, initialized, cleared on reset, removed on unregister). New deliverAuthoritativeTerminalRenderGrid(_:expectedSurfaceID:source:) helper centralizes surface/sink validation, stale-frame detection, and delivery; the live render_grid event path uses it with source: "event".
Shell-side scroll enqueue and prefetch RPC response handling
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
scrollTerminal consults per-surface TerminalScrollbackPrefetchState to populate maxScrollbackRows in TerminalScrollDelivery. performTerminalScroll conditionally adds max_scrollback_rows to the RPC dict, then decodes MobileTerminalReplayResponse and calls deliverAuthoritativeTerminalRenderGrid with source: "scroll_prefetch" when prefetch was requested and the client is unchanged.
Server-side scroll response payload construction
Sources/TerminalController+MobileScrollPrefetch.swift, Sources/TerminalController.swift, cmux.xcodeproj/project.pbxproj
New extension file defines scrollback line-budget constants (240 replay, 600 prefetch), adds mobileTerminalRenderGridFrame, mobileTerminalScrollResponsePayload, and mobileScrollPrefetchRows which build the full render-grid payload when max_scrollback_rows is positive, the screen is primary, and serialization succeeds. v2MobileTerminalScroll delegates to this helper; Xcode project registers the new file.

Sequence Diagram

sequenceDiagram
    participant GhosttySurfaceView
    participant MobileShellComposite
    participant TerminalScrollbackPrefetchState
    participant RemoteServer
    participant TerminalController

    GhosttySurfaceView->>GhosttySurfaceView: flushPendingScrollIfNeeded()
    GhosttySurfaceView->>GhosttySurfaceView: applyLocalScrollbackScroll(lines, col, row)
    GhosttySurfaceView->>MobileShellComposite: scrollTerminal(surfaceID, lines, col, row)
    MobileShellComposite->>TerminalScrollbackPrefetchState: rowsToPrefetch(forScrollLines: lines)
    TerminalScrollbackPrefetchState-->>MobileShellComposite: maxScrollbackRows (Int?)
    MobileShellComposite->>MobileShellComposite: enqueue TerminalScrollDelivery(maxScrollbackRows)
    MobileShellComposite->>RemoteServer: mobile.terminal.scroll RPC { max_scrollback_rows }
    RemoteServer->>TerminalController: v2MobileTerminalScroll(params)
    TerminalController->>TerminalController: mobileTerminalScrollResponsePayload(params)
    TerminalController-->>RemoteServer: { workspace_id, surface_id, render_grid, seq, ... }
    RemoteServer-->>MobileShellComposite: MobileTerminalReplayResponse (renderGrid)
    MobileShellComposite->>MobileShellComposite: deliverAuthoritativeTerminalRenderGrid(source: "scroll_prefetch")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • manaflow-ai/cmux#6035: Introduced the mobile.terminal.scroll RPC queueing and coalescing machinery in MobileShellComposite+TerminalScrollDelivery.swift and TerminalScrollDelivery.swift that this PR extends with prefetch-state-aware hints and response handling.

Poem

🐇 Hippity-hop, I scroll and scroll,
Prefetching rows to make the grid whole.
A distance threshold, a window of lines,
The render-grid arrives before the bunny whines.
With scroll_prefetch source stamped just right,
The terminal leaps at the speed of light! ✨


Important

Pre-merge checks failed

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

❌ Failed checks (8 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error JSON decoding in performTerminalScroll executes on MainActor without Task.detached in the scroll hot path, blocking frame rendering. Review comment flagged this actor isolation violation but was no... Wrap MobileTerminalReplayResponse.decode(data) with Task.detached(priority: .userInitiated) { }.value to move CPU-bound JSON parsing off the main actor.
Cmux Swift Blocking Runtime ❌ Error PR introduces blocking main-actor JSON decode (MobileTerminalReplayResponse.decode at line 92 of MobileShellComposite+TerminalScrollDelivery.swift) in scroll hot path; review comment explicitly fla... Detach JSON decode to background task: let payload = await Task.detached(priority: .userInitiated) { try? MobileTerminalReplayResponse.decode(data) }.value
Cmux Expensive Synchronous Load ❌ Error PR adds synchronous JSONDecoder().decode() of render grid on main actor in scroll gesture hot path (MobileShellComposite+TerminalScrollDelivery.swift:92); review comment flagged this must detach to... Move JSON decode to Task.detached(priority: .userInitiated) per review comment suggestion to avoid blocking frame rendering during scroll.
Cmux Swift Concurrency ❌ Error The performTerminalScroll method performs CPU-bound JSON decoding (MobileTerminalReplayResponse.decode) on the main actor within the scroll hot path, blocking frame rendering and gesture responsive... Move MobileTerminalReplayResponse.decode to a background Task.detached as suggested in the review comment to avoid main-actor blocking during scrollback prefetch JSON parsing.
Cmux Swift @Concurrent ❌ Error The PR violates the concurrent annotation rule by performing CPU-heavy JSON decoding (MobileTerminalReplayResponse.decode) on the MainActor without an explicit actor hop; the review comment suggest... Move MobileTerminalReplayResponse.decode(data) to a background task via Task.detached(priority: .userInitiated) to prevent JSON parsing from blocking frame rendering on the MainActor.
Cmux Swift File And Package Boundaries ❌ Error MobileShellComposite.swift (4938 lines, well over 800-line limit) adds scroll-prefetch state tracking and authoritative render-grid helper, contrary to explicit review comment flagging this as a vi... Extract terminalScrollbackPrefetchStatesBySurfaceID tracking and deliverAuthoritativeTerminalRenderGrid() method to a smaller scroll/output-focused extension or separate component, reducing MobileShellComposite.swift by 200+ lines.
Cmux Architecture Rethink ❌ Error PR performs CPU-bound JSON decoding (MobileTerminalReplayResponse.decode) on the main actor within the scroll hot path, causing potential rendering hitches. Review comment explicitly flags this sho... Move MobileTerminalReplayResponse.decode(data) to a Task.detached background task per review comment suggestion to avoid blocking scroll gesture responsiveness on the main actor.
Cmux Source Artifacts ❌ Error The PR adds .claude/scheduled_tasks.lock containing runtime state (session IDs, process IDs, timestamps), which is a clear artifact. Additionally, hidden .claude/ directory files (commands, ski... Remove .claude/scheduled_tasks.lock and .claude/ directory additions from the diff, or add /.claude/ (except specific documented exceptions) to .gitignore to exclude the hidden scratch directory.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Smooth iOS terminal scrollback with local prefetch' accurately summarizes the main change: enabling smooth local scrollback on iOS with prefetch optimization.
Description check ✅ Passed The description includes a clear Summary section explaining what changed and why, comprehensive Testing section with multiple test results, and provides concrete technical details.
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 Cache Substitution Correctness ✅ Passed Caches are transient, non-persisted heuristics/validators: prefetch state guides RPC requests but never substitutes for fresh Mac reads, and delivered-seq validates (not skips) fresh event data. Co...
Cmux No Hacky Sleeps ✅ Passed This PR changes only Swift source files, test files, and Xcode build metadata. The "cmux no hacky sleeps" rule explicitly covers TypeScript, JavaScript, shell, and non-Swift build/runtime scripts—n...
Cmux Algorithmic Complexity ✅ Passed PR introduces only O(1) dictionary lookups, bounded state machines, and arithmetic operations. No nested collection scans, filters, or sorts added to hot paths. Pre-existing nested loops not worsened.
Cmux Swift Logging ✅ Passed No logging violations found. The PR introduces no print, debugPrint, dump, NSLog statements; no ad hoc file/stdout logging; no MainActor-coupled Logger constants; and no unredacted sensitive data i...
Cmux User-Facing Error Privacy ✅ Passed All new production code passes user-facing error privacy check. No exposed vendor names, credentials, or sensitive details; only internal OSLog with privacy redaction.
Cmux Full Internationalization ✅ Passed PR contains only internal logic changes, protocol tokens, and debug-only identifiers with no user-facing strings added or localization infrastructure modified.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state layout violations detected. PR adds plain value types and extends existing @Observable root class with Dictionary of immutable snapshots—aligns with modern patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup instances are created or materially changed in this PR. All modifications focus on mobile terminal scroll deliv...
✨ 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 task-ios-scrollback-window-cache

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 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR achieves smooth iOS terminal scrollback by: (1) applying scroll deltas immediately to the local Ghostty mirror before the Mac RPC completes, and (2) piggybacking a bounded render-grid snapshot (up to 600 rows) on the first scroll and every 120 rows thereafter so the local mirror stays ahead of user deceleration. Both the prefetch RPC response and the live render-grid event now route through a shared deliverAuthoritativeTerminalRenderGrid helper.

  • Local scroll path: GhosttySurfaceView.flushPendingScrollIfNeeded calls applyLocalScrollbackScroll before the delegate RPC, giving native-feeling deceleration on primary screens; alt screens let libghostty convert the scroll to mouse-wheel bytes which the display-only mirror drops, keeping Mac output authoritative for TUIs.
  • Prefetch state machine: TerminalScrollbackPrefetchState primes on the first nonzero scroll and refreshes after every 120 rows of accumulated scroll distance (using abs so direction is irrelevant); coalesced deliveries correctly take the MAX of pending prefetch windows.
  • Mac-side cap: mobileScrollPrefetchRows clamps the client-requested rows to 600 and skips non-primary screens before serializing the render grid payload.

Confidence Score: 4/5

The dual-delivery path (scroll_prefetch RPC response + live render-grid event) introduced in this PR has a known open correctness gap in the stale-frame guard that can cause the same frame to be delivered twice; that gap is under active discussion in prior review threads but is not yet fixed in this diff.

The optimistic local-scroll and prefetch machinery is well-structured and the new tests cover the key state-machine behaviours. The main risk is the shared deliverAuthoritativeTerminalRenderGrid guard: because both delivery paths can arrive with the same stateSeq (the Mac emits a live event and returns the RPC response for the same scroll in rapid succession), the guard's current comparison allows both to pass through. This is an existing open thread, not newly introduced, but the new prefetch path is what makes the race reachable in practice.

MobileShellComposite.swift (deliverAuthoritativeTerminalRenderGrid stale-frame guard) and MobileShellComposite+TerminalScrollDelivery.swift (performTerminalScroll dual-path delivery)

Important Files Changed

Filename Overview
Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift Adds maxScrollbackRows to TerminalScrollDelivery with correct MAX-coalescing logic, and introduces TerminalScrollbackPrefetchState with distance-based thresholding. Logic is sound; state advances at enqueue time rather than confirmed delivery.
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift Adds prefetch-window computation per scroll, sends max_scrollback_rows in the RPC, and delivers the response through the shared deliverAuthoritativeTerminalRenderGrid guard. Stale-frame guard (strict >) and debug-log placement issues noted in prior review threads remain open.
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Extracts deliverAuthoritativeTerminalRenderGrid with shared stale-frame guard; both scroll_prefetch and live-event paths now route through it. The guard uses strict > allowing equal-stateSeq frames from both paths to both deliver (noted in prior review threads).
Sources/TerminalController+MobileScrollPrefetch.swift New Mac-side extension; correctly caps scrollback rows at 600, skips alt-screen surfaces, and builds the render-grid payload only when prefetch is requested. Clean extraction from TerminalController.swift.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift New file providing immediate local scrollback scroll via ghostty_surface_mouse_pos + ghostty_surface_mouse_scroll; correctly guards on lines != 0 and handles missing surface/window gracefully.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift Minimal one-line change in flushPendingScrollIfNeeded to call applyLocalScrollbackScroll before the delegate RPC call, achieving the optimistic local scroll goal.
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift Adds two tests covering MAX coalescing of prefetch windows and the prime-then-refresh-by-distance accumulation logic. Tests are correct and match the implementation.

Sequence Diagram

sequenceDiagram
    participant User as User Gesture
    participant GSV as GhosttySurfaceView (iOS)
    participant Mirror as Local Ghostty Mirror
    participant Shell as MobileShellComposite
    participant Mac as TerminalController (Mac)

    User->>GSV: scroll gesture (pan/decelerate)
    GSV->>Mirror: applyLocalScrollbackScroll(lines, col, row)
    Note over Mirror: Immediately moves viewport into prefetched scrollback
    GSV->>Shell: scrollTerminal(surfaceID, lines, col, row)
    Shell->>Shell: rowsToPrefetch() → maxScrollbackRows? advance hasPrimedWindow / accum
    Shell->>Mac: RPC mobile.terminal.scroll (+ max_scrollback_rows if prefetch window due)
    Mac->>Mac: mobileScroll(deltaLines)
    alt prefetch requested and primary screen
        Mac-->>Shell: "response {render_grid, seq}"
        Shell->>Shell: "deliverAuthoritativeTerminalRenderGrid source=scroll_prefetch (stale guard)"
        Shell->>Mirror: deliverTerminalRenderGrid (corrects local state)
    else no prefetch / alt screen
        Mac-->>Shell: "response {workspace_id, surface_id}"
    end
    Mac-)Shell: live render_grid event (async)
    Shell->>Shell: "deliverAuthoritativeTerminalRenderGrid source=event (stale guard)"
    Shell->>Mirror: deliverTerminalRenderGrid (authoritative)
Loading

Reviews (4): Last reviewed commit: "Refresh Swift file length budget" | Re-trigger Greptile

Comment on lines +4112 to +4113
if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID],
deliveredSeq > renderGrid.stateSeq {

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 The stale-frame guard uses strict >, so a render-grid frame with stateSeq equal to an already-delivered frame bypasses the guard and is delivered a second time. With this PR introducing a second delivery path (scroll prefetch response + live event), both paths can produce a frame at the same stateSeq when the Mac processes the scroll and emits the event before responding to the RPC. The guard should be >= to treat an equal seq as stale.

Suggested change
if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID],
deliveredSeq > renderGrid.stateSeq {
if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID],
deliveredSeq >= renderGrid.stateSeq {

Comment on lines 4344 to +4347
#if DEBUG
mobileShellLog.info("CMUX_REPLAY live render_grid surface=\(renderGrid.surfaceID, privacy: .public) full=\(renderGrid.full, privacy: .public) spans=\(renderGrid.rowSpans.count, privacy: .public) cleared=\(renderGrid.clearedRows.count, privacy: .public) seq=\(renderGrid.stateSeq, privacy: .public) hasSink=true")
#endif
deliverTerminalRenderGrid(renderGrid, surfaceID: renderGrid.surfaceID)
deliverAuthoritativeTerminalRenderGrid(renderGrid, source: "event")

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 The #if DEBUG log now fires before the stale-frame check inside deliverAuthoritativeTerminalRenderGrid, so frames that are subsequently rejected as stale will still emit CMUX_REPLAY live render_grid … hasSink=true. Moving the log inside deliverAuthoritativeTerminalRenderGrid (after the staleness guard) would keep the signal accurate for debugging.

Suggested change
#if DEBUG
mobileShellLog.info("CMUX_REPLAY live render_grid surface=\(renderGrid.surfaceID, privacy: .public) full=\(renderGrid.full, privacy: .public) spans=\(renderGrid.rowSpans.count, privacy: .public) cleared=\(renderGrid.clearedRows.count, privacy: .public) seq=\(renderGrid.stateSeq, privacy: .public) hasSink=true")
#endif
deliverTerminalRenderGrid(renderGrid, surfaceID: renderGrid.surfaceID)
deliverAuthoritativeTerminalRenderGrid(renderGrid, source: "event")
deliverAuthoritativeTerminalRenderGrid(renderGrid, source: "event")

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!

@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/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`:
- Around line 42-50: In the rowsToPrefetch method, when
accumulatedRowsSincePrefetch reaches or exceeds refreshDistanceRows and triggers
a prefetch, resetting accumulatedRowsSincePrefetch to 0 on Line 49 discards the
overflow distance. Instead of setting accumulatedRowsSincePrefetch to 0, set it
to the overflow amount by subtracting refreshDistanceRows from the current
accumulatedRowsSincePrefetch value. This preserves the extra traveled distance
beyond the threshold, ensuring consistent refresh timing across large scroll
deltas.

In `@Sources/TerminalController`+MobileScrollPrefetch.swift:
- Around line 59-63: The code at line 59 uses unsafe NSNumber.intValue coercion
on the max_scrollback_rows parameter, which silently accepts and coerces
non-integer JSON values and booleans. Replace the entire expression
`(params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0` with
`v2StrictInt(params, "max_scrollback_rows") ?? 0` to apply strict numeric
validation that rejects non-integers and boolean values explicitly, consistent
with the codebase pattern used for port numbers and other resource budgets in
TerminalController v2 handlers.
🪄 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: 058216f4-51bd-4fd8-bf25-374036fcd1cd

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2d87b and 6793c36.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Sources/TerminalController+MobileScrollPrefetch.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
👮 Files not reviewed due to content moderation or server errors (1)
  • Sources/TerminalController.swift

Comment on lines +42 to +50
mutating func rowsToPrefetch(forScrollLines lines: Double) -> Int? {
guard lines != 0, windowRows > 0 else { return nil }
accumulatedRowsSincePrefetch += abs(lines)
guard !hasPrimedWindow || accumulatedRowsSincePrefetch >= refreshDistanceRows else {
return nil
}
hasPrimedWindow = true
accumulatedRowsSincePrefetch = 0
return windowRows

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve overflow distance after each prefetch trigger.

On Line 49, resetting accumulatedRowsSincePrefetch to 0 drops extra traveled distance above refreshDistanceRows. With coalesced/large deltas, this under-fires refreshes and drifts below the intended “about every N rows” cadence.

💡 Proposed fix
     mutating func rowsToPrefetch(forScrollLines lines: Double) -> Int? {
         guard lines != 0, windowRows > 0 else { return nil }
         accumulatedRowsSincePrefetch += abs(lines)
         guard !hasPrimedWindow || accumulatedRowsSincePrefetch >= refreshDistanceRows else {
             return nil
         }
+        let wasPrimed = hasPrimedWindow
         hasPrimedWindow = true
-        accumulatedRowsSincePrefetch = 0
+        accumulatedRowsSincePrefetch = wasPrimed
+            ? accumulatedRowsSincePrefetch.truncatingRemainder(dividingBy: refreshDistanceRows)
+            : 0
         return windowRows
     }
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`
around lines 42 - 50, In the rowsToPrefetch method, when
accumulatedRowsSincePrefetch reaches or exceeds refreshDistanceRows and triggers
a prefetch, resetting accumulatedRowsSincePrefetch to 0 on Line 49 discards the
overflow distance. Instead of setting accumulatedRowsSincePrefetch to 0, set it
to the overflow amount by subtracting refreshDistanceRows from the current
accumulatedRowsSincePrefetch value. This preserves the extra traveled distance
beyond the threshold, ensuring consistent refresh timing across large scroll
deltas.

Comment on lines +59 to +63
let requestedRows = (params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0
return min(
max(0, requestedRows),
Self.mobileScrollPrefetchScrollbackLineBudget
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify existing TerminalController parsing convention and this callsite.
set -euo pipefail

echo "== v2 numeric helper definitions/usages in Sources/TerminalController* =="
rg -n --type=swift -C2 '\bv2(Int|StrictInt)\s*\(' Sources/TerminalController.swift Sources/TerminalController+*.swift

echo
echo "== current max_scrollback_rows parsing in mobile prefetch extension =="
rg -n --type=swift -C3 'max_scrollback_rows|NSNumber|intValue|mobileScrollPrefetchRows' Sources/TerminalController+MobileScrollPrefetch.swift

Repository: manaflow-ai/cmux

Length of output: 9676


🏁 Script executed:

rg -n --type=swift -B2 -A5 'func v2(Int|StrictInt)\(' Sources/TerminalController.swift | head -50

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

rg -n --type=swift 'func v2Int|func v2StrictInt' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

rg -n --type=swift 'v2Int|v2StrictInt' Sources/TerminalController.swift | grep 'func\|private' | head -20

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

fd --type f --extension swift . Sources | xargs rg -l 'func v2Int|func v2StrictInt' | head -5

Repository: manaflow-ai/cmux

Length of output: 114


🏁 Script executed:

rg -n --type=swift -A8 'func v2Int|func v2StrictInt' Sources/TerminalControllerV2ParamParsingSupport.swift

Repository: manaflow-ai/cmux

Length of output: 1088


🏁 Script executed:

cat -n Sources/TerminalControllerV2ParamParsingSupport.swift | sed -n '159,180p'

Repository: manaflow-ai/cmux

Length of output: 923


🏁 Script executed:

cat -n Sources/TerminalController+MobileScrollPrefetch.swift | sed -n '58,65p'

Repository: manaflow-ai/cmux

Length of output: 409


Use v2StrictInt for strict numeric validation of max_scrollback_rows instead of unsafe NSNumber.intValue coercion.

Line 59 uses NSNumber.intValue, which silently accepts and coerces non-integer JSON values and booleans. Replace with v2StrictInt(params, "max_scrollback_rows") ?? 0 to match the codebase's strict validation pattern (used for port numbers and other resource budgets). This rejects non-integers and boolean values explicitly rather than coercing them, ensuring request semantics are preserved consistently with other TerminalController v2 handlers.

🤖 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 `@Sources/TerminalController`+MobileScrollPrefetch.swift around lines 59 - 63,
The code at line 59 uses unsafe NSNumber.intValue coercion on the
max_scrollback_rows parameter, which silently accepts and coerces non-integer
JSON values and booleans. Replace the entire expression
`(params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0` with
`v2StrictInt(params, "max_scrollback_rows") ?? 0` to apply strict numeric
validation that rejects non-integers and boolean values explicitly, consistent
with the codebase pattern used for port numbers and other resource budgets in
TerminalController v2 handlers.

Source: Learnings

@azooz2003-bit azooz2003-bit force-pushed the task-ios-scrollback-window-cache branch from 6793c36 to 0fa6418 Compare June 14, 2026 01:46

@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

♻️ Duplicate comments (1)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift (1)

42-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve overflow distance after each refresh trigger.

Line 49 still zeroes accumulatedRowsSincePrefetch, which drops overflow from large/coalesced deltas and stretches the next refresh interval beyond refreshDistanceRows.

💡 Proposed fix
     mutating func rowsToPrefetch(forScrollLines lines: Double) -> Int? {
         guard lines != 0, windowRows > 0 else { return nil }
         accumulatedRowsSincePrefetch += abs(lines)
         guard !hasPrimedWindow || accumulatedRowsSincePrefetch >= refreshDistanceRows else {
             return nil
         }
+        let wasPrimed = hasPrimedWindow
         hasPrimedWindow = true
-        accumulatedRowsSincePrefetch = 0
+        accumulatedRowsSincePrefetch = wasPrimed
+            ? accumulatedRowsSincePrefetch.truncatingRemainder(dividingBy: refreshDistanceRows)
+            : 0
         return windowRows
     }
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`
around lines 42 - 50, The rowsToPrefetch(forScrollLines lines: Double) method
currently resets accumulatedRowsSincePrefetch to zero after triggering a
refresh, which discards any accumulated overflow that exceeded the
refreshDistanceRows threshold. Instead of setting accumulatedRowsSincePrefetch
to 0, preserve the overflow by setting it to the amount that exceeded the
threshold (the difference between the accumulated rows and refreshDistanceRows).
This ensures that overflow from large or coalesced scroll deltas carries forward
and does not artificially stretch the interval before the next refresh trigger.
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`:
- Around line 34-40: The initializer in TerminalScrollDelivery is clamping
refreshDistanceRows with max(1, refreshDistanceRows) on line 39, but this does
not guard against non-finite values like NaN or infinity. When NaN is passed,
max(1, NaN) still returns NaN, which causes the >= refreshDistanceRows
comparison check to always fail, leaving the refresh mechanism silently stuck.
Add a check to ensure refreshDistanceRows is finite (not NaN and not infinite)
before assigning it to self.refreshDistanceRows, either by rejecting invalid
input with a guard condition or by providing a safe default fallback value when
non-finite input is detected.

In
`@Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift`:
- Around line 70-78: Add a regression test case to the
terminalScrollbackPrefetchStatePrimesThenRefreshesByDistance test function that
verifies the prefetch cadence behavior when a single large scroll delta crosses
the refreshDistanceRows threshold by more than one unit. After the existing
`#expect` statements, add a case that demonstrates overflow-carry behavior by
scrolling a large distance (e.g., greater than twice the refreshDistanceRows
value of 10) and verify that the remainder is properly preserved between
prefetch triggers, ensuring the cadence logic correctly handles multi-threshold
crossings in a single operation.

---

Duplicate comments:
In
`@Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`:
- Around line 42-50: The rowsToPrefetch(forScrollLines lines: Double) method
currently resets accumulatedRowsSincePrefetch to zero after triggering a
refresh, which discards any accumulated overflow that exceeded the
refreshDistanceRows threshold. Instead of setting accumulatedRowsSincePrefetch
to 0, preserve the overflow by setting it to the amount that exceeded the
threshold (the difference between the accumulated rows and refreshDistanceRows).
This ensures that overflow from large or coalesced scroll deltas carries forward
and does not artificially stretch the interval before the next refresh trigger.
🪄 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: eb0b4967-9884-47c5-ae71-f297a7f2579c

📥 Commits

Reviewing files that changed from the base of the PR and between 6793c36 and 0fa6418.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Sources/TerminalController+MobileScrollPrefetch.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
👮 Files not reviewed due to content moderation or server errors (1)
  • Sources/TerminalController.swift

Comment on lines +34 to +40
init(
windowRows: Int = Self.defaultWindowRows,
refreshDistanceRows: Double = Self.defaultRefreshDistanceRows
) {
self.windowRows = max(0, windowRows)
self.refreshDistanceRows = max(1, refreshDistanceRows)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sanitize non-finite refreshDistanceRows in initializer.

Line 39 clamps lower bound, but NaN can still propagate and make the >= refreshDistanceRows check never pass after priming. Guarding finite input avoids a silent stuck-refresh state.

💡 Proposed fix
     init(
         windowRows: Int = Self.defaultWindowRows,
         refreshDistanceRows: Double = Self.defaultRefreshDistanceRows
     ) {
         self.windowRows = max(0, windowRows)
-        self.refreshDistanceRows = max(1, refreshDistanceRows)
+        let sanitizedRefresh = refreshDistanceRows.isFinite ? refreshDistanceRows : Self.defaultRefreshDistanceRows
+        self.refreshDistanceRows = max(1, sanitizedRefresh)
     }
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift`
around lines 34 - 40, The initializer in TerminalScrollDelivery is clamping
refreshDistanceRows with max(1, refreshDistanceRows) on line 39, but this does
not guard against non-finite values like NaN or infinity. When NaN is passed,
max(1, NaN) still returns NaN, which causes the >= refreshDistanceRows
comparison check to always fail, leaving the refresh mechanism silently stuck.
Add a check to ensure refreshDistanceRows is finite (not NaN and not infinite)
before assigning it to self.refreshDistanceRows, either by rejecting invalid
input with a guard condition or by providing a safe default fallback value when
non-finite input is detected.

Comment on lines +70 to +78
@Test func terminalScrollbackPrefetchStatePrimesThenRefreshesByDistance() {
var state = TerminalScrollbackPrefetchState(windowRows: 600, refreshDistanceRows: 10)

#expect(state.rowsToPrefetch(forScrollLines: 0) == nil)
#expect(state.rowsToPrefetch(forScrollLines: 1) == 600)
#expect(state.rowsToPrefetch(forScrollLines: 4) == nil)
#expect(state.rowsToPrefetch(forScrollLines: -5.5) == nil)
#expect(state.rowsToPrefetch(forScrollLines: 0.5) == 600)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an overflow-carry regression case for prefetch cadence.

This test covers prime + exact-threshold crossing, but it doesn’t lock behavior for a large single delta (e.g., crossing refreshDistanceRows by more than one threshold). Add one case that verifies remainder is preserved between triggers.

🤖 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/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift`
around lines 70 - 78, Add a regression test case to the
terminalScrollbackPrefetchStatePrimesThenRefreshesByDistance test function that
verifies the prefetch cadence behavior when a single large scroll delta crosses
the refreshDistanceRows threshold by more than one unit. After the existing
`#expect` statements, add a case that demonstrates overflow-carry behavior by
scrolling a large distance (e.g., greater than twice the refreshDistanceRows
value of 10) and verify that the remainder is properly preserved between
prefetch triggers, ensuring the cadence logic correctly handles multi-threshold
crossings in a single operation.

@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

♻️ Duplicate comments (1)
Sources/TerminalController+MobileScrollPrefetch.swift (1)

58-64: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use v2StrictInt for strict numeric validation of max_scrollback_rows instead of unsafe NSNumber.intValue coercion.

Line 59 uses NSNumber.intValue, which silently accepts and coerces non-integer JSON values and booleans. Replace with v2StrictInt(params, "max_scrollback_rows") ?? 0 to match the codebase's strict validation pattern and reject non-integers explicitly.

🔧 Proposed fix
 private func mobileScrollPrefetchRows(params: [String: Any]) -> Int {
-    let requestedRows = (params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0
+    let requestedRows = v2StrictInt(params, "max_scrollback_rows") ?? 0
     return min(
         max(0, requestedRows),
         Self.mobileScrollPrefetchScrollbackLineBudget
     )
 }

Based on learnings: "In Swift request/JSON handlers (e.g., v2 JSON-socket handlers) in Sources, prefer using the v2 helpers for numeric parsing—use v2Int(params, "<key>") for normal integer inputs and v2StrictInt(...) when strictness is required—rather than casting with as? Int."

🤖 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 `@Sources/TerminalController`+MobileScrollPrefetch.swift around lines 58 - 64,
The mobileScrollPrefetchRows function uses unsafe NSNumber.intValue coercion on
line 59 to extract the max_scrollback_rows parameter, which silently accepts and
converts non-integer JSON values and booleans. Replace the current extraction
logic `(params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0` with
`v2StrictInt(params, "max_scrollback_rows") ?? 0` to enforce strict numeric
validation that explicitly rejects non-integer inputs, aligning with the
codebase's standard validation pattern for JSON request handlers.

Source: Learnings

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1485-1679: Extract the presence sync subsystem from
MobileShellComposite into a dedicated collaborator class. Move the presenceMap
property, presenceTask property, and all presence-related methods
(evaluatePresenceSubscription, startPresenceSubscription, applyPresenceUpdate,
syncPushedRoutes with both variants, and applyPushedRoutes) into a new class
that owns the subscribe stream, backoff loop, and route-syncing logic. Keep
MobileShellComposite as an orchestration layer that instantiates and delegates
to this new collaborator, removing the presence implementation details and
keeping only the method calls that trigger the delegated operations. This
reduces MobileShellComposite's size and responsibility scope by separating the
presence pipeline from session state, paired-Mac persistence, transport, and
view-facing concerns.
- Around line 4319-4337: The freshness check in the
deliverAuthoritativeTerminalRenderGrid function only compares against the
delivered sequence from deliveredTerminalByteEndSeqBySurfaceID but should also
consider the pending sequence from pendingTerminalByteEndSeqBySurfaceID to
prevent stale frames from being applied after an input response has been
acknowledged. Update the condition that currently checks if deliveredSeq is
greater than renderGrid.stateSeq to use the maximum of both deliveredSeq and the
corresponding pendingTerminalByteEndSeqBySurfaceID value as the freshness floor,
rejecting the frame if stateSeq is older than this combined threshold.

---

Duplicate comments:
In `@Sources/TerminalController`+MobileScrollPrefetch.swift:
- Around line 58-64: The mobileScrollPrefetchRows function uses unsafe
NSNumber.intValue coercion on line 59 to extract the max_scrollback_rows
parameter, which silently accepts and converts non-integer JSON values and
booleans. Replace the current extraction logic `(params["max_scrollback_rows"]
as? NSNumber)?.intValue ?? 0` with `v2StrictInt(params, "max_scrollback_rows")
?? 0` to enforce strict numeric validation that explicitly rejects non-integer
inputs, aligning with the codebase's standard validation pattern for JSON
request handlers.
🪄 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: 7b1dbb9f-1818-4fd2-9aeb-84bcaafd301b

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa6418 and 472ce81.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Sources/TerminalController+MobileScrollPrefetch.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
👮 Files not reviewed due to content moderation or server errors (1)
  • Sources/TerminalController.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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (1)
Sources/TerminalController+MobileScrollPrefetch.swift (1)

58-64: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use v2StrictInt for strict numeric validation of max_scrollback_rows instead of unsafe NSNumber.intValue coercion.

Line 59 uses NSNumber.intValue, which silently accepts and coerces non-integer JSON values and booleans. Replace with v2StrictInt(params, "max_scrollback_rows") ?? 0 to match the codebase's strict validation pattern and reject non-integers explicitly.

🔧 Proposed fix
 private func mobileScrollPrefetchRows(params: [String: Any]) -> Int {
-    let requestedRows = (params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0
+    let requestedRows = v2StrictInt(params, "max_scrollback_rows") ?? 0
     return min(
         max(0, requestedRows),
         Self.mobileScrollPrefetchScrollbackLineBudget
     )
 }

Based on learnings: "In Swift request/JSON handlers (e.g., v2 JSON-socket handlers) in Sources, prefer using the v2 helpers for numeric parsing—use v2Int(params, "<key>") for normal integer inputs and v2StrictInt(...) when strictness is required—rather than casting with as? Int."

🤖 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 `@Sources/TerminalController`+MobileScrollPrefetch.swift around lines 58 - 64,
The mobileScrollPrefetchRows function uses unsafe NSNumber.intValue coercion on
line 59 to extract the max_scrollback_rows parameter, which silently accepts and
converts non-integer JSON values and booleans. Replace the current extraction
logic `(params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0` with
`v2StrictInt(params, "max_scrollback_rows") ?? 0` to enforce strict numeric
validation that explicitly rejects non-integer inputs, aligning with the
codebase's standard validation pattern for JSON request handlers.

Source: Learnings

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1485-1679: Extract the presence sync subsystem from
MobileShellComposite into a dedicated collaborator class. Move the presenceMap
property, presenceTask property, and all presence-related methods
(evaluatePresenceSubscription, startPresenceSubscription, applyPresenceUpdate,
syncPushedRoutes with both variants, and applyPushedRoutes) into a new class
that owns the subscribe stream, backoff loop, and route-syncing logic. Keep
MobileShellComposite as an orchestration layer that instantiates and delegates
to this new collaborator, removing the presence implementation details and
keeping only the method calls that trigger the delegated operations. This
reduces MobileShellComposite's size and responsibility scope by separating the
presence pipeline from session state, paired-Mac persistence, transport, and
view-facing concerns.
- Around line 4319-4337: The freshness check in the
deliverAuthoritativeTerminalRenderGrid function only compares against the
delivered sequence from deliveredTerminalByteEndSeqBySurfaceID but should also
consider the pending sequence from pendingTerminalByteEndSeqBySurfaceID to
prevent stale frames from being applied after an input response has been
acknowledged. Update the condition that currently checks if deliveredSeq is
greater than renderGrid.stateSeq to use the maximum of both deliveredSeq and the
corresponding pendingTerminalByteEndSeqBySurfaceID value as the freshness floor,
rejecting the frame if stateSeq is older than this combined threshold.

---

Duplicate comments:
In `@Sources/TerminalController`+MobileScrollPrefetch.swift:
- Around line 58-64: The mobileScrollPrefetchRows function uses unsafe
NSNumber.intValue coercion on line 59 to extract the max_scrollback_rows
parameter, which silently accepts and converts non-integer JSON values and
booleans. Replace the current extraction logic `(params["max_scrollback_rows"]
as? NSNumber)?.intValue ?? 0` with `v2StrictInt(params, "max_scrollback_rows")
?? 0` to enforce strict numeric validation that explicitly rejects non-integer
inputs, aligning with the codebase's standard validation pattern for JSON
request handlers.
🪄 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: 7b1dbb9f-1818-4fd2-9aeb-84bcaafd301b

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa6418 and 472ce81.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Sources/TerminalController+MobileScrollPrefetch.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
👮 Files not reviewed due to content moderation or server errors (1)
  • Sources/TerminalController.swift
🛑 Comments failed to post (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (2)

1485-1679: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the presence sync subsystem instead of extending the god-store.

Lines 1485-1679 add a full subscribe/backoff/store-sync/reconnect subsystem into MobileShellComposite, which is already far past the repo’s size/responsibility ceiling and already mixes session state, paired-Mac persistence, transport, terminal delivery, and view-facing draft state. Please move this presence pipeline behind a dedicated collaborator and keep MobileShellComposite as orchestration only.

As per coding guidelines, Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility and Flag files that mix UI rendering, state ownership, persistence, networking, parsing, subprocess/socket protocol, and platform bridge code in one place.

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 1485 - 1679, Extract the presence sync subsystem from
MobileShellComposite into a dedicated collaborator class. Move the presenceMap
property, presenceTask property, and all presence-related methods
(evaluatePresenceSubscription, startPresenceSubscription, applyPresenceUpdate,
syncPushedRoutes with both variants, and applyPushedRoutes) into a new class
that owns the subscribe stream, backoff loop, and route-syncing logic. Keep
MobileShellComposite as an orchestration layer that instantiates and delegates
to this new collaborator, removing the presence implementation details and
keeping only the method calls that trigger the delegated operations. This
reduces MobileShellComposite's size and responsibility scope by separating the
presence pipeline from session state, paired-Mac persistence, transport, and
view-facing concerns.

Source: Coding guidelines


4319-4337: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drop authoritative frames older than the pending input-ack floor.

Line 4328 only rejects frames older than deliveredTerminalByteEndSeqBySurfaceID. If handleTerminalInputResponse has already recorded a higher pendingTerminalByteEndSeqBySurfaceID, this shared helper can still apply a delayed live/scroll-prefetch frame whose stateSeq is behind an acknowledged host state and briefly roll the surface backward. Use max(deliveredSeq, pendingSeq) as the freshness floor here before marking delivery.

Suggested fix
     func deliverAuthoritativeTerminalRenderGrid(
         _ renderGrid: MobileTerminalRenderGridFrame,
         expectedSurfaceID: String? = nil,
         source: String
     ) {
         guard expectedSurfaceID == nil || renderGrid.surfaceID == expectedSurfaceID,
               hasTerminalOutputSink(surfaceID: renderGrid.surfaceID) else {
             return
         }
-        if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID],
-           deliveredSeq > renderGrid.stateSeq {
+        let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID] ?? 0
+        let pendingSeq = pendingTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID] ?? 0
+        let freshnessFloor = max(deliveredSeq, pendingSeq)
+        if freshnessFloor > renderGrid.stateSeq {
             MobileDebugLog.anchormux(
-                "sync.render_grid_stale source=\(source) surface=\(renderGrid.surfaceID) delivered=\(deliveredSeq) frame=\(renderGrid.stateSeq)"
+                "sync.render_grid_stale source=\(source) surface=\(renderGrid.surfaceID) floor=\(freshnessFloor) frame=\(renderGrid.stateSeq)"
             )
             return
         }
         markTerminalBytesDelivered(surfaceID: renderGrid.surfaceID, endSeq: renderGrid.stateSeq)
         deliverTerminalRenderGrid(renderGrid, surfaceID: renderGrid.surfaceID)
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 4319 - 4337, The freshness check in the
deliverAuthoritativeTerminalRenderGrid function only compares against the
delivered sequence from deliveredTerminalByteEndSeqBySurfaceID but should also
consider the pending sequence from pendingTerminalByteEndSeqBySurfaceID to
prevent stale frames from being applied after an input response has been
acknowledged. Update the condition that currently checks if deliveredSeq is
greater than renderGrid.stateSeq to use the maximum of both deliveredSeq and the
corresponding pendingTerminalByteEndSeqBySurfaceID value as the freshness floor,
rejecting the frame if stateSeq is older than this combined threshold.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmux.xcodeproj/project.pbxproj (1)

250-281: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add missing packageProductDependencies entries for new package products.

Line 1715 and Line 1726 add CmuxTerminal and CmuxWorkspaceWindow to the cmux Frameworks phase, and Lines 4672-4681 define their XCSwiftPackageProductDependency objects. But the PBXNativeTarget "cmux" packageProductDependencies list (Line 2660 onward) does not include these two product IDs, leaving target/package wiring incomplete.

Suggested pbxproj fix
 			packageProductDependencies = (
 				A5001231 /* Sparkle */,
 				A5001251 /* Sentry */,
 				A5001271 /* PostHog */,
@@
 				CC0DE30000000000000000A2 /* CmuxRemoteSession */,
 				C750300000000000000000A2 /* CmuxTerminalEngine */,
 				C750400000000000000000A2 /* CmuxTerminalServices */,
+				C750500000000000000000A2 /* CmuxTerminal */,
+				C750400000000000000000D2 /* CmuxWorkspaceWindow */,
 				A8BD195031FC4B82B4354297 /* StackAuth */,
 				EFB18E3B3099DFE2ECA3C263 /* CMUXMobileCore */,
 			);

As per coding guidelines, for cmux.xcodeproj/** we should flag project wiring changes that can bypass expected integration behavior; this is a target wiring completeness gap.

Also applies to: 1715-1726, 2888-2890, 4396-4403, 4672-4681

🤖 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 `@cmux.xcodeproj/project.pbxproj` around lines 250 - 281, The `PBXNativeTarget
"cmux"` configuration is missing `packageProductDependencies` entries for the
`CmuxTerminal` and `CmuxWorkspaceWindow` packages that were added to the
Frameworks build phase and defined as `XCSwiftPackageProductDependency` objects.
Locate the `packageProductDependencies` array in the `cmux` target definition
and add the product reference IDs for both `CmuxTerminal` and
`CmuxWorkspaceWindow` to complete the target/package wiring. Ensure that every
framework product added to the build files also has a corresponding entry in the
target's `packageProductDependencies` list to maintain project configuration
consistency.

Source: Coding guidelines

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Line 543: The scroll-prefetch bookkeeping additions (including the
terminalScrollbackPrefetchStatesBySurfaceID property and related functionality)
are causing MobileShellComposite.swift to exceed the file-size budget. Extract
all scroll-prefetch state management and authoritative render-grid delivery
helpers into a new, smaller scroll or output-focused component, then remove the
corresponding code from MobileShellComposite.swift. This will restore the file
to within budget and isolate the scroll-prefetch logic for better testability
and separation of concerns.

In
`@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalScrollDelivery.swift:
- Around line 86-100: The MobileTerminalReplayResponse.decode(data) call is
executing on the main actor within the scroll hot path, blocking frame rendering
and scroll gesture responsiveness. Move this CPU-bound JSON decoding operation
to a background task by wrapping the decode call and subsequent validation logic
in a detached Task or DispatchQueue.global() context, then dispatch the
deliverAuthoritativeTerminalRenderGrid call back to the main actor if needed.
This keeps the main actor free to handle frame rendering during scrolling.

---

Outside diff comments:
In `@cmux.xcodeproj/project.pbxproj`:
- Around line 250-281: The `PBXNativeTarget "cmux"` configuration is missing
`packageProductDependencies` entries for the `CmuxTerminal` and
`CmuxWorkspaceWindow` packages that were added to the Frameworks build phase and
defined as `XCSwiftPackageProductDependency` objects. Locate the
`packageProductDependencies` array in the `cmux` target definition and add the
product reference IDs for both `CmuxTerminal` and `CmuxWorkspaceWindow` to
complete the target/package wiring. Ensure that every framework product added to
the build files also has a corresponding entry in the target's
`packageProductDependencies` list to maintain project configuration consistency.
🪄 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: 8b221658-a68b-4f72-8a1c-13fd28f96978

📥 Commits

Reviewing files that changed from the base of the PR and between 472ce81 and 304a7fe.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Sources/TerminalController+MobileScrollPrefetch.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
👮 Files not reviewed due to content moderation or server errors (1)
  • Sources/TerminalController.swift

var terminalOutputQueuesBySurfaceID: [String: TerminalOutputDeliveryQueue]
var terminalScrollQueueTokensBySurfaceID: [String: UUID]
var terminalScrollQueuesBySurfaceID: [String: TerminalScrollDeliveryQueue]
var terminalScrollbackPrefetchStatesBySurfaceID: [String: TerminalScrollbackPrefetchState]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Keep new scroll-prefetch bookkeeping out of this over-budget store.

These additions further grow MobileShellComposite.swift, which is already well past the repo’s Swift file-size budget. Please move the scroll-prefetch state and authoritative render-grid delivery helper into a smaller scroll/output-focused component so this path stays isolated and testable.

As per coding guidelines, Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility.

Also applies to: 670-670, 3083-3083, 4319-4337, 4371-4371

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
at line 543, The scroll-prefetch bookkeeping additions (including the
terminalScrollbackPrefetchStatesBySurfaceID property and related functionality)
are causing MobileShellComposite.swift to exceed the file-size budget. Extract
all scroll-prefetch state management and authoritative render-grid delivery
helpers into a new, smaller scroll or output-focused component, then remove the
corresponding code from MobileShellComposite.swift. This will restore the file
to within budget and isolate the scroll-prefetch logic for better testability
and separation of concerns.

Source: Coding guidelines

Comment on lines +86 to +100
let data = try await client.sendRequest(request)
guard let maxScrollbackRows = delivery.maxScrollbackRows,
maxScrollbackRows > 0,
remoteClient === client else {
return
}
guard let payload = try? MobileTerminalReplayResponse.decode(data),
let renderGrid = payload.renderGrid,
renderGrid.surfaceID == delivery.surfaceID else {
return
}
deliverAuthoritativeTerminalRenderGrid(
renderGrid,
expectedSurfaceID: delivery.surfaceID,
source: "scroll_prefetch"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 '`@MainActor`|func performTerminalScroll|MobileTerminalReplayResponse\.decode' \
  Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift \
  Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift

echo

rg -n -C3 'max_scrollback_rows|render_grid|prefetch|scrollback' \
  Sources/TerminalController+MobileScrollPrefetch.swift

Repository: manaflow-ai/cmux

Length of output: 22542


Move scroll-prefetch JSON decode off the main actor.

MobileTerminalReplayResponse.decode(data) executes on the main actor inside performTerminalScroll(_:) within the scroll hot path. JSON decoding is CPU-bound work that can block frame rendering and scroll gesture responsiveness; detach it to a background task to avoid hitching.

♻️ Suggested change
             let data = try await client.sendRequest(request)
             guard let maxScrollbackRows = delivery.maxScrollbackRows,
                   maxScrollbackRows > 0,
                   remoteClient === client else {
                 return
             }
-            guard let payload = try? MobileTerminalReplayResponse.decode(data),
+            let payload = await Task.detached(priority: .userInitiated) {
+                try? MobileTerminalReplayResponse.decode(data)
+            }.value
+            guard remoteClient === client,
+                  let payload,
                   let renderGrid = payload.renderGrid,
                   renderGrid.surfaceID == delivery.surfaceID else {
                 return
             }
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalScrollDelivery.swift
around lines 86 - 100, The MobileTerminalReplayResponse.decode(data) call is
executing on the main actor within the scroll hot path, blocking frame rendering
and scroll gesture responsiveness. Move this CPU-bound JSON decoding operation
to a background task by wrapping the decode call and subsequent validation logic
in a detached Task or DispatchQueue.global() context, then dispatch the
deliverAuthoritativeTerminalRenderGrid call back to the main actor if needed.
This keeps the main actor free to handle frame rendering during scrolling.

@azooz2003-bit azooz2003-bit merged commit 675776a into main Jun 14, 2026
27 checks passed
@azooz2003-bit azooz2003-bit deleted the task-ios-scrollback-window-cache branch June 14, 2026 04:03
azooz2003-bit added a commit that referenced this pull request Jun 14, 2026
#6067 (iOS terminal scrollback) merged after the first re-sync. Only the
regenerable file-length budget conflicted; TerminalController.swift and
pbxproj auto-merged (new TerminalController+MobileScrollPrefetch.swift is
main's, wired by main). Budget regenerated; ghostty pinned to origin/main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant