Skip to content

Preserve iOS chat scroll momentum - #7109

Merged
azooz2003-bit merged 4 commits into
mainfrom
feat-ios-chat-scroll-deceleration
Jul 1, 2026
Merged

azooz2003-bit merged 4 commits into
mainfrom
feat-ios-chat-scroll-deceleration

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • treat UITableView tracking, dragging, and deceleration as active user scroll momentum
  • skip transcript inset restoration and live-tail auto-follow while that momentum is active
  • add an iOS UI evidence test that captures before/after screenshots around a fast transcript swipe

Verification

  • git diff --check
  • xcodebuild -workspace ios/cmux.xcworkspace -scheme cmux-ios -destination 'platform=iOS Simulator,id=C9D82663-886D-4EBD-92A0-96CF38A42FF3' -derivedDataPath /tmp/cmux-dcel-uitest -only-testing:cmuxUITests/cmuxUITests/testAgentChatTranscriptFastSwipeEvidence test\n- Screenshot evidence exported locally to /tmp/cmux-dcel-scroll-evidence/: before offset 767, after offset 1582, after distance from bottom 359px.\n\n## Dogfood\n- mac tag: dcel\n- iOS tag: dcel\n- simulator reload succeeded and auto-paired\n- iPhone reload unavailable because connected devices were unavailable to devicectl\n\nDo not merge until the iOS dogfood scroll behavior is approved.

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


Summary by cubic

Improves iOS chat scroll by preserving swipe momentum so fast swipes glide without snapping. Stops auto-follow and inset changes while the user is scrolling.

  • Bug Fixes
    • Added isUserScrollMomentumActive (tracking/dragging/decelerating) and gated bottom auto-follow, anchor restoration, and transcript inset application; preserves UIKit’s live inset compensation during momentum.
    • Added UI evidence test testAgentChatTranscriptFastSwipeEvidence; debug metrics now include scrollTracking/scrollDragging/scrollDecelerating; documented momentum-based inset handling.
    • Kept the chat transcript table within its length budget.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved chat transcript scrolling to better respect in-progress user momentum, preventing unexpected jumps and incorrect visible-position restoration.
    • Stabilized viewport and inset updates during active scroll momentum for more reliable anchoring.
  • Tests

    • Added a new UI regression test covering fast-swipe behavior in the agent chat transcript.
    • Extended transcript scroll metrics captured and asserted by the test to include additional scroll-state flags.

@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 Jul 1, 2026 12:58am
cmux-staging Building Building Preview, Comment Jul 1, 2026 12:58am

@coderabbitai

coderabbitai Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds isUserScrollMomentumActive to ChatTranscriptUITableView and uses it to guard viewport inset application, scroll-to-bottom, and anchor restoration in the coordinator. Exposes three scroll-state booleans in the debug accessibility string and extends ChatTranscriptMetrics parsing/serialization to match. Adds a UI regression test for fast-swipe behavior.

Changes

Momentum-aware scroll guard

Layer / File(s) Summary
isUserScrollMomentumActive property and inset guard
Packages/iOS/CmuxAgentChatUI/.../ChatTranscriptUITableView.swift
Adds computed isUserScrollMomentumActive (isTracking || isDragging || isDecelerating) and early-returns from applyTranscriptViewportInsets when momentum is active, recording current viewport and resetting isViewportInsetsExternallyDriven.
Coordinator scroll/anchor gates
Packages/iOS/CmuxAgentChatUI/.../ChatTranscriptTableView.swift
Coordinator.update scrolls to bottom only when shouldScrollToBottom is true or at-bottom with no momentum; handleLayoutChange early-exits when momentum is active, clearing pendingContentUpdateAnchor.
Debug scroll-state fields + UI regression test
Packages/iOS/CmuxAgentChatUI/.../ChatTranscriptUITableView.swift, ios/cmuxUITests/cmuxUITests.swift
Adds scrollTracking, scrollDragging, scrollDecelerating to the debug format string and argument list; extends ChatTranscriptMetrics description and parser for those fields; adds testAgentChatTranscriptFastSwipeEvidence asserting swipe increases offsetY and distanceFromBottom.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A bunny swiped up, fast and free,
No anchor could swallow its momentum spree!
isTracking, isDragging, isDecelerating too —
The scroll guard hops aside when the gesture flew.
🐇 Swipe on, little chat, swipe true!


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux No Test Or Debug Seam In Production Source ❌ Error ChatTranscriptUITableView.swift exposes scrollTracking/dragging/decelerating via DEBUG-only accessibilityValue/debugAccessibilityValue, and cmuxUITests reads table.value from it. Move the metric parsing into the test target via @testable import, or keep the raw state internal and drop the DEBUG accessibility seam from production source.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately captures the main change: preserving iOS chat scroll momentum.
Description check ✅ Passed The description covers the summary and verification well, but it omits the demo video, review-trigger block, and checklist sections from the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: Changes only touch UIKit/SwiftUI UI types and test-only structs; no new implicit MainActor model/service protocols or shared Sendable refs introduced.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits/sleeps/locks were added; changes only gate scroll restoration on momentum and add deterministic UI evidence waits.
Cmux Browser Automation Off-Main ✅ Passed Browser wait/callback commands are socket-worker routed with policy tests; I found no hunk moving them to mainActor or touching WebKit off-main incorrectly.
Cmux Expensive Synchronous Load ✅ Passed The PR only changes transcript scroll gating and UI test metrics; no new RestorableAgentSessionIndex.load()/file-scan/JSON-decode path was added on a main-actor interaction.
Cmux Cache Substitution Correctness ✅ Passed PASS: isUserScrollMomentumActive only gates transient scroll/inset restoration; no authoritative fresh read was swapped for a stale cache in persistence/history/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed PASS: The PR is Swift-only and adds no new sleeps/polling; the new UI test uses XCTest predicate waits, while existing Thread.sleep code is unchanged test scaffolding.
Cmux Algorithmic Complexity ✅ Passed PR adds O(1) scroll-momentum guards and test-only metrics parsing; no new scalable scans or hot-path collection rebuilds.
Cmux Swift Concurrency ✅ Passed The diff adds no new legacy async patterns in production Swift; the only DispatchQueue/completion usage is confined to XCTest/network callback boundaries.
Cmux Swift @Concurrent ✅ Passed No changed async/nonisolated work or @concurrent annotations appear in the diff; the new UI test and transcript changes are UI-bound sync/@mainactor only.
Cmux Swift File And Package Boundaries ✅ Passed PASS: The production changes are small, focused updates in coherent iOS package UI code (+5/+15 lines); the large 3.9k-line file is a test target file, which the rule allows.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore or SwiftPM lockfile/package-reference changes are present; the only Package.resolved ignore is in vendored vendor/bonsplit, which is allowed.
Cmux Swift Logging ✅ Passed No new print/debugPrint/dump/NSLog/Logger in the touched production Swift files; changes are scroll logic plus DEBUG/test diagnostics.
Cmux User-Facing Error Privacy ✅ Passed Diff only changes scroll-momentum gating and debug accessibility fields; no user-facing errors, alerts, or sensitive upstream details were added.
Cmux Full Internationalization ✅ Passed PASS: The PR only changes scroll behavior plus DEBUG/test code; no production user-facing text or locale assets were added or changed.
Cmux Swiftui State Layout ✅ Passed Touched code is a UIKit bridge/table view and XCTest only; no new ObservableObject/@Published/@StateObject, GeometryReader, row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed The change is a small UIKit bridge fix with a clear owner/invariant; it adds only computed scroll-state gating and test-only sync, with no sleeps, locks, observers, or split lifecycle ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes iOS transcript scrolling and a UI test; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers code was added or changed.
Cmux Source Artifacts ✅ Passed Changed paths are hand-written source/test files; no temp/log/screenshot/build artifacts or scratch dirs were added.
Cmux No Ambient Global State ✅ Passed Changes stay within existing types: the new momentum flag is an instance property, and the UITest metric additions are nested private types; no new file-scope globals or singletons.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-chat-scroll-deceleration

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/cmuxUITests/cmuxUITests.swift`:
- Around line 1228-1245: The transcript swipe test is asserting only an early
`offsetY` increase, which can pass before the coordinator later snaps back
during deceleration. Update the
`waitForTranscriptMetrics`/`captureKeyboardEvidenceFrame` flow in `cmuxUITests`
to key off the authoritative structured scroll-state fields (`scrollDragging`
and `scrollDecelerating`) instead of a single offset jump, and make the
`XCTAssertGreaterThan` checks verify the transcript remains away from the live
tail while momentum is active or after it fully settles.
🪄 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: 4069a8a7-8b19-49bb-a03d-240c95a14cc3

📥 Commits

Reviewing files that changed from the base of the PR and between 8513010 and 2940c80.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift
  • ios/cmuxUITests/cmuxUITests.swift

Comment on lines +1228 to +1245
let afterSwipe = try waitForTranscriptMetrics(table, timeout: 1.5) {
$0.offsetY > before.offsetY + 40
}
captureKeyboardEvidenceFrame(
prefix: "scroll-deceleration-after",
index: 0,
startedAt: Date(),
metrics: afterSwipe
)
XCTAssertGreaterThan(
afterSwipe.offsetY,
before.offsetY + 40,
"A fast transcript swipe should move through the chat history instead of being swallowed by parent gesture handling. before=\(before) after=\(afterSwipe)"
)
XCTAssertGreaterThan(
afterSwipe.distanceFromBottom,
80,
"A single fast swipe from the middle fixture must not snap to the live bottom. before=\(before) after=\(afterSwipe)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wait for momentum-state evidence, not just an early offset jump.

This goes green as soon as one post-swipe sample has a larger offsetY. That can still happen even if the coordinator later restores/snaps during deceleration, so the test does not actually pin the regression this PR is fixing. Please key the assertion off the new structured scroll-state fields (scrollDragging/scrollDecelerating) and verify the transcript stays away from the live tail while momentum is active, or after momentum fully settles.

Suggested direction
-        let afterSwipe = try waitForTranscriptMetrics(table, timeout: 1.5) {
-            $0.offsetY > before.offsetY + 40
-        }
+        let duringMomentum = try waitForTranscriptMetrics(table, timeout: 1.5) {
+            ($0.scrollDragging || $0.scrollDecelerating)
+                && $0.offsetY > before.offsetY + 40
+                && $0.distanceFromBottom > 80
+        }
+        let settled = try waitForTranscriptMetrics(table, timeout: 2.5) {
+            !$0.scrollDragging && !$0.scrollDecelerating
+        }
...
-            metrics: afterSwipe
+            metrics: duringMomentum
...
-            afterSwipe.offsetY,
+            duringMomentum.offsetY,
             before.offsetY + 40,
...
-            "A fast transcript swipe should move through the chat history instead of being swallowed by parent gesture handling. before=\(before) after=\(afterSwipe)"
+            "A fast transcript swipe should keep control while user momentum is active. before=\(before) duringMomentum=\(duringMomentum) settled=\(settled)"
         )
         XCTAssertGreaterThan(
-            afterSwipe.distanceFromBottom,
+            settled.distanceFromBottom,
             80,
-            "A single fast swipe from the middle fixture must not snap to the live bottom. before=\(before) after=\(afterSwipe)"
+            "A single fast swipe from the middle fixture must not snap back to the live bottom after momentum settles. before=\(before) duringMomentum=\(duringMomentum) settled=\(settled)"
         )

As per path instructions, "base logic on a single authoritative, structured source ... not on ... 'best effort' fallbacks." Based on learnings and the PR context, these new scroll-state fields are the authoritative momentum signal this regression test should assert against.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let afterSwipe = try waitForTranscriptMetrics(table, timeout: 1.5) {
$0.offsetY > before.offsetY + 40
}
captureKeyboardEvidenceFrame(
prefix: "scroll-deceleration-after",
index: 0,
startedAt: Date(),
metrics: afterSwipe
)
XCTAssertGreaterThan(
afterSwipe.offsetY,
before.offsetY + 40,
"A fast transcript swipe should move through the chat history instead of being swallowed by parent gesture handling. before=\(before) after=\(afterSwipe)"
)
XCTAssertGreaterThan(
afterSwipe.distanceFromBottom,
80,
"A single fast swipe from the middle fixture must not snap to the live bottom. before=\(before) after=\(afterSwipe)"
let duringMomentum = try waitForTranscriptMetrics(table, timeout: 1.5) {
($0.scrollDragging || $0.scrollDecelerating)
&& $0.offsetY > before.offsetY + 40
&& $0.distanceFromBottom > 80
}
let settled = try waitForTranscriptMetrics(table, timeout: 2.5) {
!$0.scrollDragging && !$0.scrollDecelerating
}
captureKeyboardEvidenceFrame(
prefix: "scroll-deceleration-after",
index: 0,
startedAt: Date(),
metrics: duringMomentum
)
XCTAssertGreaterThan(
duringMomentum.offsetY,
before.offsetY + 40,
"A fast transcript swipe should keep control while user momentum is active. before=\(before) duringMomentum=\(duringMomentum) settled=\(settled)"
)
XCTAssertGreaterThan(
settled.distanceFromBottom,
80,
"A single fast swipe from the middle fixture must not snap back to the live bottom after momentum settles. before=\(before) duringMomentum=\(duringMomentum) settled=\(settled)"
)
🤖 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 `@ios/cmuxUITests/cmuxUITests.swift` around lines 1228 - 1245, The transcript
swipe test is asserting only an early `offsetY` increase, which can pass before
the coordinator later snaps back during deceleration. Update the
`waitForTranscriptMetrics`/`captureKeyboardEvidenceFrame` flow in `cmuxUITests`
to key off the authoritative structured scroll-state fields (`scrollDragging`
and `scrollDecelerating`) instead of a single offset jump, and make the
`XCTAssertGreaterThan` checks verify the transcript remains away from the live
tail while momentum is active or after it fully settles.

Source: Path instructions

@greptile-apps

greptile-apps Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces isUserScrollMomentumActive on ChatTranscriptUITableView to detect when UIKit's tracking/dragging/deceleration is in progress, and gates the transcript's auto-follow and anchor-restoration logic behind this flag to avoid fighting the scroll physics.

  • ChatTranscriptUITableView: adds the isUserScrollMomentumActive computed property and an early-return path in applyTranscriptViewportInsets that records the current viewport and clears the externally-driven flag when momentum is active, deferring explicit offset restoration to UIKit.
  • ChatTranscriptTableView coordinator: skips wasAtBottom auto-scroll and anchor restoration in update() when momentum is active, and adds a dedicated early-return in handleLayoutChange that clears pendingContentUpdateAnchor and bails without touching the offset.
  • cmuxUITests: adds testAgentChatTranscriptFastSwipeEvidence to assert that a fast swipe advances the transcript and does not snap back to the live tail, and extends the TranscriptMetrics debug struct with scrollTracking/scrollDragging/scrollDecelerating fields parsed from the accessibility value.

Confidence Score: 5/5

Safe to merge once iOS dogfood approval is obtained per the PR description's own hold condition.

The changes are minimal and self-contained: isUserScrollMomentumActive is a read-only computed property over three existing UIKit booleans, the guards in update() and handleLayoutChange are straightforward early-returns deferring to UIKit physics, and explicit offset-restoration paths remain intact for non-momentum cases. No new mutable state is introduced. The UI test lives entirely in the test target and no custom rule violations were found.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift Adds isUserScrollMomentumActive computed property and a momentum early-return in applyTranscriptViewportInsets; debug metrics extended with scroll state fields inside #if DEBUG.
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift Momentum guard added to update() (auto-follow and anchor restore) and handleLayoutChange (early return with anchor nil-out); no new state, only read-only UIKit properties consumed.
ios/cmuxUITests/cmuxUITests.swift Adds testAgentChatTranscriptFastSwipeEvidence UI test and extends TranscriptMetrics with scroll state fields; lives entirely in the test target.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SwiftUI update / layout change fires] --> B{isUserScrollMomentumActive?}
    B -- Yes --> C[update: skip wasAtBottom auto-scroll and anchor restore]
    B -- Yes --> D[handleLayoutChange: clear pendingAnchor, update bottom state and return]
    B -- Yes --> E[applyTranscriptViewportInsets: apply inset values, recordCurrentViewport, clear isViewportInsetsExternallyDriven, return]
    B -- No --> F{shouldScrollToBottom or wasAtBottom?}
    F -- Yes --> G[scrollToBottom]
    F -- No --> H{anchor available?}
    H -- Yes --> I[restore anchor, set pendingContentUpdateAnchor]
    H -- No --> J[updateBottomState only]
    C --> J
    D --> J
    E --> K[UIKit handles offset compensation during momentum]
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"}}}%%
flowchart TD
    A[SwiftUI update / layout change fires] --> B{isUserScrollMomentumActive?}
    B -- Yes --> C[update: skip wasAtBottom auto-scroll and anchor restore]
    B -- Yes --> D[handleLayoutChange: clear pendingAnchor, update bottom state and return]
    B -- Yes --> E[applyTranscriptViewportInsets: apply inset values, recordCurrentViewport, clear isViewportInsetsExternallyDriven, return]
    B -- No --> F{shouldScrollToBottom or wasAtBottom?}
    F -- Yes --> G[scrollToBottom]
    F -- No --> H{anchor available?}
    H -- Yes --> I[restore anchor, set pendingContentUpdateAnchor]
    H -- No --> J[updateBottomState only]
    C --> J
    D --> J
    E --> K[UIKit handles offset compensation during momentum]
Loading

Reviews (2): Last reviewed commit: "Document transcript momentum inset handl..." | Re-trigger Greptile

Comment on lines +226 to +230
if tableView.isUserScrollMomentumActive {
pendingContentUpdateAnchor = nil
updateBottomState(from: tableView)
return
}

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 pendingContentUpdateAnchor = nil is redundant but may still be load-bearing for the deceleration-only window

scrollViewWillBeginDragging already clears pendingContentUpdateAnchor at drag start, and update() unconditionally sets it to nil on line 139 before checking momentum. In the common case this nil-out in handleLayoutChange is therefore a no-op. However, there is a narrow window: a data update that arrives mid-deceleration calls update(), clears the anchor (line 139), does not set it back (momentum active), and then a bounds-change layout fires before momentum settles — at that point this guard is the only thing preventing a stale anchor from leaking in. Worth a short comment to make the intent explicit so a future refactor doesn't drop it thinking it's purely defensive.

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.

Caution

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

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift (1)

129-132: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Re-run inset restoration when scroll momentum ends. applyTranscriptViewportInsets skips pin/restore/clamp while isUserScrollMomentumActive is true, and this file has no scrollViewDidEndDragging/scrollViewDidEndDecelerating path to apply the deferred inset change afterward. Queue that reconciliation and apply it once momentum stops, or the transcript can remain offset after the last mid-gesture inset update.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift`
around lines 129 - 132, `ChatTranscriptUITableView.isUserScrollMomentumActive`
currently blocks `applyTranscriptViewportInsets` during user momentum, but there
is no follow-up path to reconcile the deferred inset change after scrolling
ends. Add a deferred inset update flow in `ChatTranscriptUITableView` that
records the pending pin/restore/clamp work while `isTracking`, `isDragging`, or
`isDecelerating` is true, then applies it from the scroll-end callbacks such as
`scrollViewDidEndDragging` and `scrollViewDidEndDecelerating` so the transcript
is re-aligned once momentum stops.
🤖 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.

Outside diff comments:
In
`@Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift`:
- Around line 129-132: `ChatTranscriptUITableView.isUserScrollMomentumActive`
currently blocks `applyTranscriptViewportInsets` during user momentum, but there
is no follow-up path to reconcile the deferred inset change after scrolling
ends. Add a deferred inset update flow in `ChatTranscriptUITableView` that
records the pending pin/restore/clamp work while `isTracking`, `isDragging`, or
`isDecelerating` is true, then applies it from the scroll-end callbacks such as
`scrollViewDidEndDragging` and `scrollViewDidEndDecelerating` so the transcript
is re-aligned once momentum stops.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 60be1045-40e6-46b8-a5ea-8b824216e46a

📥 Commits

Reviewing files that changed from the base of the PR and between 2940c80 and 3a4b50a.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift

@azooz2003-bit
azooz2003-bit merged commit 2313855 into main Jul 1, 2026
39 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-ios-chat-scroll-deceleration branch July 1, 2026 01:18
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>

This branch was successfully deployed

1 active deployment
Preview – cmux — 3a4b50a3 Deployed Jul 1, 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.

1 participant