Skip to content

Fix iOS chat transcript expansion anchoring - #7057

Merged
azooz2003-bit merged 6 commits into
mainfrom
feat-ios-chat-scroll-anchor
Jun 30, 2026
Merged

azooz2003-bit merged 6 commits into
mainfrom
feat-ios-chat-scroll-anchor

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Summary

  • add a focused iOS regression test for expanding tool and terminal rows away from the live tail
  • capture the first visible transcript row before UIKit layout and restore it when hosted SwiftUI rows resize
  • use the table geometry, not stale SwiftUI bottom state, to decide when reloads should stay pinned to bottom

Verification

  • swift test in Packages/iOS/CmuxAgentChatUI
  • xcodebuild build-for-testing -workspace ios/cmux.xcworkspace -scheme cmux-ios -configuration Debug -destination platform=iOS Simulator,id=C9D82663-886D-4EBD-92A0-96CF38A42FF3 -derivedDataPath /tmp/cmux-scroll-ios
  • xcodebuild test-without-building -workspace ios/cmux.xcworkspace -scheme cmux-ios -configuration Debug -destination platform=iOS Simulator,id=C9D82663-886D-4EBD-92A0-96CF38A42FF3 -derivedDataPath /tmp/cmux-scroll-ios -only-testing:cmuxUITests/cmuxUITests/testAgentChatExpansionControlsPreserveTranscriptScrollPosition
  • ./scripts/reload-cloud.sh --tag scrl, cloud unavailable, local fallback ./scripts/reload.sh --tag scrl succeeded
  • ./ios/scripts/reload-cloud.sh --tag scrl --device-id C9D82663-886D-4EBD-92A0-96CF38A42FF3 failed because ASC API key is unavailable; fallback ./ios/scripts/reload.sh --tag scrl --simulator "iPhone 17" succeeded

Notes

  • regression test is in the first commit, fix is in the second commit
  • no new visible strings; added accessibility identifiers only

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


Summary by cubic

Fixes iOS chat transcript snapping when expanding tool, terminal, or per‑command blocks by anchoring the first visible row and restoring it when content grows. Auto-scroll only triggers if the previous viewport was at the bottom, and viewport snapshots stay accurate after programmatic scrolls.

  • Bug Fixes

    • Capture the first visible row before layout and restore it after resizes/content updates; added anchorBeforeLayout, extended afterLayout with the old anchor, and manage a pending content-update anchor (cleared on user drag and ignored during data updates).
    • Decide “at bottom” from table geometry and prior viewport; only pin when the previous viewport was at bottom.
    • Refresh the transcript viewport snapshot after programmatic scrolls and anchor restores; record viewport on any content offset change.
    • Add deterministic accessibility identifiers for tool-use, terminal card, and per-command toggles; switch command-block expand/collapse to an explicit button and add UI tests that assert expansion preserves the visible region.
  • Refactors

    • Refresh Swift file length budget to match the current main branch.

Written for commit 17122ec. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a DEBUG-only terminal log preview option (environment-variable controlled).
    • Added deterministic accessibility identifiers for chat transcript expand/collapse controls (tool-use, terminal, and command-block rows/buttons).
  • Bug Fixes

    • Improved transcript scroll restoration by capturing and reusing stable “anchors” across reloads/layout updates, preventing snapping toward the live tail.
    • Enhanced viewport snapshot recording to better support keyboard/viewport restoration.
  • Tests

    • Added UI tests to ensure expanding tool-use and terminal command blocks preserves the transcript’s visible region/scroll position.

@vercel

vercel Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 30, 2026 4:30am
cmux-staging Building Building Preview, Comment Jun 30, 2026 4:30am

@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds pre-layout transcript anchor handling, a debug-only terminal log preview path, stable accessibility identifiers for expansion controls, and UI tests that verify transcript scroll position stays stable after expansion.

Changes

Transcript scroll restoration and preview wiring

Layer / File(s) Summary
Transcript anchor and layout callback contract
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift, Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift
ChatTranscriptTableAnchor is widened from private to internal. ChatTranscriptUITableView gains anchorBeforeLayout and extends afterLayout to include ChatTranscriptTableAnchor?; layoutSubviews captures and passes the anchor. The table also centralizes viewport recording and restores keyboard viewport through that helper.
Coordinator scroll-restoration logic
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
Coordinator gains isApplyingDataUpdate, captures the first visible anchor before layout, removes the isAtBottom binding from wasAtBottom computation, brackets reloadData and layout with the flag, extends handleLayoutChange to accept oldAnchor, and adds early-return plus anchor-based restore branching.
Terminal log preview entry point
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/UITestConfig.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLogDemoScreen.swift
UITestConfig adds a DEBUG-only terminal log preview flag. CMUXMobileRootView adds a terminal log preview branch, and TerminalLogDemoScreen expands the terminal output fixtures for the standalone preview.
Accessibility identifiers on expansion toggles
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatTerminalCardView.swift, Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatToolUseRowView.swift, Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/TerminalCommandBlockView.swift
ChatTerminalCardView, ChatToolUseRowView, and TerminalCommandBlockView each add a deterministic accessibilityIdentifier to their expand/collapse toggle buttons, parameterized by rowID or block.id.
UI regression tests
ios/cmuxUITests/cmuxUITests.swift
New XCTest coverage launches the agent chat inline preview and terminal log preview, taps expansion controls, and asserts the transcript visible region stays stable after expansion. Helper methods scroll to hittable controls, capture metrics, and verify the transcript does not jump to the live tail.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 Hop-hop, the transcript holds its place,
A preview blooms in debug-space.
Toggles can bounce, yet scroll stays neat,
With tiny test paws and a terminal treat.


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error CMUXMobileRootView wires a new DEBUG-only TerminalLogDemoScreen through shipped rootContent, adding a duplicate preview entrypoint in production bootstrap code. Move the terminal-log demo into a test/demo-only host or preview shell; keep CMUXMobileRootView’s shipped rootContent free of new debug seams.
Cmux No Test Or Debug Seam In Production Source ❌ Error CMUXMobileRootView adds a new DEBUG-only terminalLogPreview branch and UITestConfig.terminalLogPreviewEnabled in production Sources/, creating a test/demo seam. Move the preview/demo wiring into a test-only harness or a dedicated debug-only file/folder; keep production root code free of new UI-test/debug entrypoints.
Cmux No Ambient Global State ❌ Error Adds a new public static env flag in global config (UITestConfig.swift:87-93) instead of threading it through the injectable UITestEnvironmentConfig/app seam. Move terminalLogPreviewEnabled onto UITestEnvironmentConfig and inject it from the composition root into CMUXMobileRootView; avoid adding new static config seams.
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.
Description check ⚠️ Warning The PR description has Summary and Testing, but it omits the required Demo Video, Review Trigger, and Checklist sections for this UI behavior change. Add a demo video link, include the review trigger block, and complete the checklist to match the repository template.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main iOS chat transcript anchoring fix.
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 Changes are UI/test/debug-only and add no new MainActor, Sendable, or background-access isolation mistakes in production Swift.
Cmux Swift Blocking Runtime ✅ Passed Production Swift changes add layout/viewport anchoring only; no semaphores, sleeps, sync waits, asyncAfter, polling, or locks appear in changed app files.
Cmux Browser Automation Off-Main ✅ Passed PR only touches iOS transcript/UI-test files; the browser automation rule targets macOS ControlCommandExecutionPolicy, and no touched file contains browser/socket routing code.
Cmux Expensive Synchronous Load ✅ Passed Modified production Swift only adds viewport/layout restoration and debug preview toggles; no new sync agent-history/load/JSONL work or main-actor hot-path parsing found.
Cmux Cache Substitution Correctness ✅ Passed Only transient scroll anchors/viewport snapshots were added, with refreshes after programmatic scrolls; no persisted/history/undo read was swapped for a stale cache.
Cmux No Hacky Sleeps ✅ Passed PR only changes Swift app/test files; no TypeScript, JavaScript, shell, or build/runtime delay hacks were introduced.
Cmux Algorithmic Complexity ✅ Passed PASS: The new production work is O(1) viewport/anchor bookkeeping and small debug-preview toggles; no nested rescans or hot-path repeated filters were introduced.
Cmux Swift Concurrency ✅ Passed The diff adds only SwiftUI/UIKit/XCTest callback boundaries and async tests; no new DispatchQueue, Combine state, completion-handler APIs, or fire-and-forget Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed No changed Swift code adds or misuses @concurrent; the new async/UI test work is MainActor-bound, and no nonisolated async helper needing @concurrent was introduced.
Cmux Swift File And Package Boundaries ✅ Passed Touched production files are existing package targets; added code is small and focused, while preview and regression-test logic stays in debug/test code.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, .gitignore, or .xcodeproj changes appear in the PR diff, so the SwiftPM lockfile rule is not triggered.
Cmux Swift Logging ✅ Passed No added print/debugPrint/dump/NSLog, stdout/stderr logging, or Logger misuse appeared in the touched Swift diff; patch searches found none.
Cmux User-Facing Error Privacy ✅ Passed New output text is confined to a DEBUG-only terminal-log preview; shipped rows only add accessibility IDs and layout logic, not user-facing error copy.
Cmux Full Internationalization ✅ Passed Only accessibility IDs and debug-only demo copy were added; user-facing production text remains localized or unchanged, and no locale assets were touched.
Cmux Swiftui State Layout ✅ Passed Touched SwiftUI files add UIKit bridge/view plumbing, accessibility IDs, and a debug demo with ordinary @State; no new ObservableObject/@published, GeometryReader, or render-time state mutation.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: the PR only changes transcript views, tests, and debug demo screens; no NSWindow/NSPanel/WindowGroup/NSWindowController or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed Changed paths are hand-written Swift source/test files under Sources/ and ios/cmuxUITests; none are logs, build output, scratch dirs, or other artifacts.
✨ 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-anchor

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.

@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-chat-scroll-anchor branch from e852950 to 328d7f7 Compare June 29, 2026 20:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift`:
- Around line 217-222: Refresh the viewport snapshot before using
oldViewport?.wasAtBottom in ChatTranscriptTableView’s scroll/state restoration
flow. In the branch that calls restoreKeyboardViewport, scrollToBottom, or
restore, make sure the current viewport state is captured from the table view
first so lastViewport reflects the latest scroll position before deciding
whether to snap to bottom or restore the anchor. Use the existing
restoreKeyboardViewport(_:in:), scrollToBottom(in:animated:), and restore(_:in:)
paths as the reference points for where to update the snapshot.
🪄 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: 051e5740-2080-4000-b219-efecda9d5479

📥 Commits

Reviewing files that changed from the base of the PR and between b196623 and e852950.

📒 Files selected for processing (6)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatTerminalCardView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatToolUseRowView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/TerminalCommandBlockView.swift
  • ios/cmuxUITests/cmuxUITests.swift

@greptile-apps

greptile-apps Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes iOS transcript viewport jumping when tool-use, terminal-card, or command-block rows are expanded. It does so by capturing the first-visible-row anchor before each UIKit layout pass and restoring it when SwiftUI self-sizing rows resize after a data reload, and by computing "at-bottom" from live table geometry rather than the previously-stale isAtBottom binding.

  • ChatTranscriptUITableView: overrides contentOffset.didSet to keep lastViewport always current (closing the race where scrollToBottom + deferred layoutSubviews produced a stale wasAtBottom=false snapshot); adds anchorBeforeLayout closure called immediately before super.layoutSubviews().
  • ChatTranscriptTableView.Coordinator: introduces pendingContentUpdateAnchor (set after a data reload, cleared on drag or at-bottom scroll) so post-reload SwiftUI layout passes re-anchor to the pre-reload first visible row; guards all in-flight layout callbacks with isApplyingDataUpdate to prevent re-entrancy.
  • Rows / UI tests: adds deterministic accessibilityIdentifier values to expansion controls and a focused UI test that asserts visible-region drift ≤ 120 pt after tapping each toggle.

Confidence Score: 5/5

Safe to merge. The anchoring logic is carefully layered, the race condition identified in the prior review thread is closed by the synchronous contentOffset didSet, and the new UI test covers the primary regression scenario.

All changes are confined to the iOS chat transcript scroll layer and DEBUG-gated demo fixtures. The core mechanism — pendingContentUpdateAnchor + oldAnchor fallback + wasAtBottom derived from geometry — correctly handles data reloads, post-reload SwiftUI self-sizing passes, keyboard viewport restores, and user drags without conflicts. The two style nits in TerminalCommandBlockView have no runtime impact.

No files require special attention. TerminalCommandBlockView.swift has a dead computed property (accessibilityLabel) and an unreachable ternary branch worth cleaning up before future work touches that file.

Important Files Changed

Filename Overview
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift Core scroll-anchoring fix: adds pendingContentUpdateAnchor to survive SwiftUI self-sizing layout passes after a data reload, switches wasAtBottom check to table geometry instead of stale isAtBottom binding, and records viewport synchronously after each programmatic setContentOffset.
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift Adds contentOffset didSet to keep lastViewport always current; adds anchorBeforeLayout closure called before each super.layoutSubviews; promotes ChatTranscriptTableAnchor to internal struct so both files in the module can use it.
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/TerminalCommandBlockView.swift Replaces .accessibilityElement(children:.combine) custom-action pattern with a proper Button in collapsedOutput; leaves private var accessibilityLabel unreferenced and an always-false ternary branch in the new label modifier.
ios/cmuxUITests/cmuxUITests.swift Adds UI test that verifies visible-region drift ≤120 pt after tapping tool-use and terminal toggle controls.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SwiftUI as SwiftUI (updateUIView)
    participant Coord as Coordinator
    participant Table as ChatTranscriptUITableView
    participant Layout as UIKit layoutSubviews

    SwiftUI->>Coord: update(configuration, scrollToBottomRequest)
    Coord->>Coord: "wasAtBottom = distanceFromBottom ≤ 40"
    Coord->>Coord: "anchor = firstVisibleAnchor()"
    Coord->>Coord: "isApplyingDataUpdate = true"
    Coord->>Table: reloadData()
    Coord->>Table: layoutIfNeeded()
    Table->>Layout: layoutSubviews()
    Layout->>Coord: anchorBeforeLayout() → ignored (isApplyingDataUpdate)
    Layout->>Coord: afterLayout() → early-return (isApplyingDataUpdate)
    Coord->>Table: restore(anchor) OR scrollToBottom()
    Table-->>Table: "setContentOffset → didSet → recordViewport() wasAtBottom=true"
    Table->>Table: recordCurrentViewport()
    Coord->>Coord: "pendingContentUpdateAnchor = anchor"
    Coord->>Coord: "isApplyingDataUpdate = false (defer)"

    Note over Table,Layout: SwiftUI row self-sizing triggers next layout pass
    Table->>Layout: layoutSubviews()
    Layout->>Coord: anchorBeforeLayout() → oldAnchor
    Layout->>Coord: afterLayout(oldViewport, oldAnchor)
    alt pendingContentUpdateAnchor set
        Coord->>Table: restore(pendingContentUpdateAnchor)
    else wasAtBottom
        Coord->>Table: scrollToBottom()
    else contentChanged
        Coord->>Table: restore(oldAnchor)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SwiftUI as SwiftUI (updateUIView)
    participant Coord as Coordinator
    participant Table as ChatTranscriptUITableView
    participant Layout as UIKit layoutSubviews

    SwiftUI->>Coord: update(configuration, scrollToBottomRequest)
    Coord->>Coord: "wasAtBottom = distanceFromBottom ≤ 40"
    Coord->>Coord: "anchor = firstVisibleAnchor()"
    Coord->>Coord: "isApplyingDataUpdate = true"
    Coord->>Table: reloadData()
    Coord->>Table: layoutIfNeeded()
    Table->>Layout: layoutSubviews()
    Layout->>Coord: anchorBeforeLayout() → ignored (isApplyingDataUpdate)
    Layout->>Coord: afterLayout() → early-return (isApplyingDataUpdate)
    Coord->>Table: restore(anchor) OR scrollToBottom()
    Table-->>Table: "setContentOffset → didSet → recordViewport() wasAtBottom=true"
    Table->>Table: recordCurrentViewport()
    Coord->>Coord: "pendingContentUpdateAnchor = anchor"
    Coord->>Coord: "isApplyingDataUpdate = false (defer)"

    Note over Table,Layout: SwiftUI row self-sizing triggers next layout pass
    Table->>Layout: layoutSubviews()
    Layout->>Coord: anchorBeforeLayout() → oldAnchor
    Layout->>Coord: afterLayout(oldViewport, oldAnchor)
    alt pendingContentUpdateAnchor set
        Coord->>Table: restore(pendingContentUpdateAnchor)
    else wasAtBottom
        Coord->>Table: scrollToBottom()
    else contentChanged
        Coord->>Table: restore(oldAnchor)
    end
Loading

Reviews (5): Last reviewed commit: "Stabilize iOS chat scroll anchor tests" | Re-trigger Greptile

if boundsChanged, let oldViewport {
restoreKeyboardViewport(snapshot: oldViewport, in: tableView)
} else if isAtBottom.wrappedValue {
} else if oldViewport?.wasAtBottom == true {

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 Potential gap between scrollToBottom() and lastViewport update

When update() calls scrollToBottom() (because wasAtBottom was true), it calls tableView.setContentOffset(…, animated: false). UIKit marks the table as needing layout (setNeedsLayout) but does not call layoutSubviews synchronously — recordViewport() is therefore not called until the next run-loop pass. If a SwiftUI self-sizing layout fires in between (before that deferred layout), oldViewport passed here still holds the pre-scrollToBottom snapshot with wasAtBottom == false, and the branch falls through to restore(oldAnchor) instead. The anchor captured at the bottom will keep the first-visible row in place, but newly added rows that landed below the viewport remain hidden — the "stay pinned to bottom during live output" invariant is silently broken.

The previous code used isAtBottom.wrappedValue, which is written synchronously by setAtBottom(true) inside scrollToBottom(), so it was always up-to-date for this path. One option is to call recordViewport() (or an equivalent snapshot update on lastViewport) immediately after setContentOffset inside scrollToBottom() so that lastViewport.wasAtBottom is authoritative before control returns to any callers that may trigger further layouts.

@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-chat-scroll-anchor branch from 328d7f7 to d83b81e Compare June 29, 2026 20:24

@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/ChatTranscriptTableView.swift (1)

101-103: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Capture the anchor from the pre-update transcript.

Because Line 140 swaps items before reload, the callback from Lines 101-103 reaches items[indexPath.row] in Lines 227-235 against the new transcript while the visible rows still describe the old table. Any insertion/removal above the viewport will therefore anchor to the wrong id and restore the wrong row. Snapshot the visible row’s id before replacing items, or derive it from the currently visible cell/model instead of the mutated array. As per path instructions, transcript correctness should not depend on “more than one disagreeing source of truth for the same fact.”

Also applies to: 140-147, 227-235

🤖 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/ChatTranscriptTableView.swift`
around lines 101 - 103, The transcript anchor lookup is reading from the updated
items array instead of the pre-update transcript, so the visible row can map to
the wrong id after inserts/removals. Update ChatTranscriptTableView so the
anchor is captured before items is swapped, or derive it from the currently
visible cell/model rather than items inside firstVisibleAnchor(in:) and the
anchorBeforeLayout callback. Make sure the reload path that replaces items
preserves the old visible row identity across the update so restoration uses the
pre-change transcript state.

Source: Path instructions

🤖 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/ChatTranscriptTableView.swift`:
- Around line 101-103: The transcript anchor lookup is reading from the updated
items array instead of the pre-update transcript, so the visible row can map to
the wrong id after inserts/removals. Update ChatTranscriptTableView so the
anchor is captured before items is swapped, or derive it from the currently
visible cell/model rather than items inside firstVisibleAnchor(in:) and the
anchorBeforeLayout callback. Make sure the reload path that replaces items
preserves the old visible row identity across the update so restoration uses the
pre-change transcript state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1d72a04-e0e1-49c5-ba20-21561addb5b9

📥 Commits

Reviewing files that changed from the base of the PR and between d83b81e and db96ce5.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptUITableView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@ios/cmuxUITests/cmuxUITests.swift`:
- Around line 2569-2572: The tap helper is still using a hard-coded coordinate
offset instead of the toggle’s accessibility identifier, which makes the
cmuxUITests regression brittle. Update the helper around the block tap logic to
target the control via the TerminalCommandBlockToggle-\(block.id) accessibility
identifier rather than computing an offset from block.frame. Use the existing
block.id and the relevant tap helper in cmuxUITests.swift so the test locates
the toggle reliably across layout and device changes.

In
`@Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift`:
- Around line 154-156: The `pendingContentUpdateAnchor` in
`ChatTranscriptTableView` is being retained beyond the reload that created it,
so it can be reused by unrelated later resizes. Update the reload path around
`restore(_:in:)` and the later `contentChanged` handling so the anchor is scoped
to a single reload cycle and cleared once that reload finishes or is superseded;
keep the fix centered on `pendingContentUpdateAnchor` and the restore logic that
consumes it.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`:
- Around line 65-71: The DEBUG-only terminal log preview is being exposed
through the production root view via
CMUXMobileRootView.shouldShowTerminalLogPreview and rootContent, which adds a
test/demo seam to shipped Sources code. Remove this conditional bootstrap path
from CMUXMobileRootView and move the preview entrypoint into a test/demo-only
harness or UI-test-specific wrapper, keeping production rootContent free of
UITestConfig-driven debug routing.
🪄 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: ef955a57-ea38-47f7-997f-30b73b1277ca

📥 Commits

Reviewing files that changed from the base of the PR and between db96ce5 and 48a3c46.

📒 Files selected for processing (6)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/TerminalCommandBlockView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLogDemoScreen.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/UITestConfig.swift
  • ios/cmuxUITests/cmuxUITests.swift

Comment thread ios/cmuxUITests/cmuxUITests.swift Outdated
Comment on lines +2569 to +2572
let blockFrame = block.frame
app.coordinate(withNormalizedOffset: .zero)
.withOffset(CGVector(dx: blockFrame.minX + 84, dy: blockFrame.minY + 118))
.tap()

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 | 🟡 Minor | ⚡ Quick win

Use the toggle's accessibility identifier instead of a hard-coded tap offset.

This helper ignores the new TerminalCommandBlockToggle-\(block.id) contract and taps blockFrame.minX + 84 / minY + 118 instead. Any row-layout or device-geometry change can miss the control and make the regression test flaky.

🤖 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 2569 - 2572, The tap helper
is still using a hard-coded coordinate offset instead of the toggle’s
accessibility identifier, which makes the cmuxUITests regression brittle. Update
the helper around the block tap logic to target the control via the
TerminalCommandBlockToggle-\(block.id) accessibility identifier rather than
computing an offset from block.frame. Use the existing block.id and the relevant
tap helper in cmuxUITests.swift so the test locates the toggle reliably across
layout and device changes.

Comment on lines +65 to +71
private var shouldShowTerminalLogPreview: Bool {
#if os(iOS) && DEBUG
return UITestConfig.terminalLogPreviewEnabled
#else
return false
#endif
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Don't route a new DEBUG-only fixture through the production root view.

This adds a UI-test/demo-only screen to rootContent via UITestConfig.terminalLogPreviewEnabled, which expands the app's production Sources/ surface with another debug seam. Move this preview entrypoint into a test/demo-only harness instead of wiring it through shipped bootstrap code. As per coding guidelines, **/Sources/**/*.swift: Do not add test-only or debug-only seams in production Swift source files. As per path instructions, **/Sources/**/*.swift: flag added test-only or debug-only seams in production source.

Also applies to: 105-112, 221-222

🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`
around lines 65 - 71, The DEBUG-only terminal log preview is being exposed
through the production root view via
CMUXMobileRootView.shouldShowTerminalLogPreview and rootContent, which adds a
test/demo seam to shipped Sources code. Remove this conditional bootstrap path
from CMUXMobileRootView and move the preview entrypoint into a test/demo-only
harness or UI-test-specific wrapper, keeping production rootContent free of
UITestConfig-driven debug routing.

Sources: Coding guidelines, Path instructions

@azooz2003-bit
azooz2003-bit merged commit aee4ba2 into main Jun 30, 2026
35 of 36 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-ios-chat-scroll-anchor branch June 30, 2026 04:35

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant