Skip to content

Fix stuttery iOS workspace refresh animation - #8397

Closed
azooz2003-bit wants to merge 9 commits into
mainfrom
task-ios-refresh-animation
Closed

azooz2003-bit wants to merge 9 commits into
mainfrom
task-ios-refresh-animation

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Summary

  • Uses UITableView.refreshControl and UIRefreshControl for the pull gesture, spinner, hold, and collapse.
  • Removes the custom refresh header, gesture state machine, inset/offset writers, velocity interception, and manual collapse animation.
  • Ends native refresh from the diffable snapshot completion after refreshed SwiftUI state reaches the table.
  • Disables diffable row animations while the refresh control is active so table updates do not compete with UIKit’s inset collapse.

Root cause

The custom branch had multiple writers for UIScrollView geometry. The baseline native path also called endRefreshing before SwiftUI delivered the refreshed data, so an animated diffable snapshot could land during UIKit’s collapse.

Mechanism

UIRefreshControl owns all scroll geometry. The refresh action bumps a completion generation after the data round-trip. The coordinator applies that generation’s diffable snapshot without row animation, then calls endRefreshing from the snapshot completion.

Testing

  • Focused native geometry tests pass on isolated simulator cmux-rfrsh-019f.
  • Snapshot-ordering test verifies endRefreshing is not called before the post-refresh snapshot and is called once after it applies.
  • Tagged macOS and iOS simulator builds use tag rfrsh.
  • No user-facing strings were added.

Commits

@coderabbitai

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

The iOS workspace list now coordinates pull-to-refresh completion with authoritative diffable snapshots and collapse settling. It adds custom refresh geometry and presentation, lifecycle validation, teardown handling, deterministic tests, and a DEBUG preview refresh controlled by an environment-configured delay.

Changes

Workspace refresh coordination

Layer / File(s) Summary
Refresh lifecycle state machine
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListRefreshLifecycle.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshLifecycleTests.swift
Tracks refresh, snapshot application, collapsing, stale IDs, cancellation, and reset behavior while controlling snapshot animation suppression.
Refresh geometry, presentation, and gestures
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListRefresh*.swift
Adds pull geometry, spinner presentation, threshold detection, settling, pinned insets, adjusted-inset handling, and collapse animation behavior.
Coordinator, table, and snapshot integration
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTable*.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView*.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshGeometryTests.swift
Connects refresh generations, active tasks, container teardown, snapshot completion, and collapse scheduling; tests geometry, cancellation, refresh removal, and dismantling.
Configurable preview refresh behavior
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListLayoutPreviewView.swift, ios/cmuxUITests/cmuxUITests.swift
Adds an environment-configured delayed refresh that toggles the first workspace’s unread state and verifies two refresh cycles in the UI test.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WorkspaceListTableCoordinator
  participant WorkspaceListView
  participant DiffableDataSource
  User->>WorkspaceListTableCoordinator: pull workspace list
  WorkspaceListTableCoordinator->>WorkspaceListView: invoke refresh
  WorkspaceListView-->>WorkspaceListTableCoordinator: refresh completion generation
  WorkspaceListTableCoordinator->>DiffableDataSource: apply authoritative snapshot
  DiffableDataSource-->>WorkspaceListTableCoordinator: snapshot completion
  WorkspaceListTableCoordinator->>WorkspaceListTableCoordinator: collapse refresh presentation
Loading

Possibly related PRs

  • manaflow-ai/cmux#8186: Related iOS workspace list table and pull-to-refresh coordination changes.
  • manaflow-ai/cmux#8451: Related WorkspaceListTableCoordinator snapshot/update handling during refresh interactions.

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 WorkspaceListLayoutPreviewView.swift adds a DEBUG-only UI-test hook (CMUX_UITEST_WORKSPACE_LIST_REFRESH_DELAY_MS) in Sources, which is a test/debug seam. Move the preview/test hook out of production Sources into a dedicated debug/test-support file or the test target; keep Sources free of UI-test-only seams.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.49% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: New UI coordinators/views are @MainActor, the refresh state machine is a local value type used on-main, and no new sendable reference state or background store access appears.
Cmux Swift Blocking Runtime ✅ Passed Production refresh code uses async tasks, tokens, and animators only; no blocking waits/sleeps/locks. The only Task.sleep is DEBUG-only preview scaffolding.
Cmux Browser Automation Off-Main ✅ Passed PASS: The diff only changes iOS workspace refresh UI/tests; no browser.* socket commands, worker routing, or policy tests were touched.
Cmux Expensive Synchronous Load ✅ Passed Changed code only adds refresh-control geometry/lifecycle logic; no agent-history, JSON/JSONL, transcript, or filesystem load was introduced on MainActor/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed PASS: refreshCompletionGeneration is event-driven and freshness-checked; snapshot apply only proceeds on a matching generation, and the preview toggle is transient UI state.
Cmux No Hacky Sleeps ✅ Passed Only sleep is DEBUG-only preview scaffolding; production refresh code uses tokenized lifecycle and animations, with no new asyncAfter/timers/polling in runtime files.
Cmux Algorithmic Complexity ✅ Passed No new scalable collection scans were introduced; the added refresh code is state/geometry logic, and the existing snapshot diffing pipeline was unchanged.
Cmux Swift Concurrency ✅ Passed No new legacy async pattern was introduced; the new Task is lifecycle-bound and cancelled, and the remaining closures are UIKit/SwiftUI callback boundaries.
Cmux Swift @Concurrent ✅ Passed No changed async function is nonisolated or misannotated; the new delay/refresh paths are MainActor/UI-bound and add no invalid @concurrent usage.
Cmux Swift Package Boundaries ✅ Passed All new production logic lives in the iOS SwiftPM package Packages/iOS/CmuxMobileShellUI (plus tests/UI tests); nothing was kept in the app target root.
Cmux Swiftpm Lockfiles ✅ Passed Diff changes only source/test Swift files; no Package.swift, Package.resolved, .gitignore, workflow, or xcodeproj files were modified.
Cmux Swift Logging ✅ Passed The diff adds no print/debugPrint/dump/NSLog, no ad hoc stdout/file logging, and no new file-scoped Logger constants in the changed Swift runtime files.
Cmux User-Facing Error Privacy ✅ Passed PASS: The PR only adds internal accessibility IDs and storyboard fatalErrors; no user-visible error, alert, recovery copy, or vendor/internal details were introduced.
Cmux Full Internationalization ✅ Passed Touched files add only refresh logic and developer-only identifiers/fatalError strings; no new user-facing Swift text or locale assets were added.
Cmux Swiftui State Layout ✅ Passed No prohibited SwiftUI pattern appears: new preview state uses @Observable, no GeometryReader/@Published/store-in-row additions, and Task writes are event-driven.
Cmux Architecture Rethink ✅ Passed Refresh logic has a single coordinator-owned state machine with tokened transitions; no production sleep/delayed-dispatch patch is introduced, and the only sleep is DEBUG preview/test fixture.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: the diff only touches iOS table/refresh views and tests; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes appear.
Cmux Source Artifacts ✅ Passed All changed paths are Swift source/tests or review rules; no logs, screenshots, recordings, temp dirs, build output, or other artifacts appear in the diff.
Cmux No Ambient Global State ✅ Passed No new file-scope mutable state or singleton-style globals; added state is encapsulated in view/coordinator types and extensions.
Title check ✅ Passed The title clearly matches the main change: fixing the iOS workspace refresh animation stutter.
Description check ✅ Passed The description covers summary, root cause, mechanism, and testing, but omits demo video, review trigger, and checklist details.
✨ 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-refresh-animation

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

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift`:
- Around line 331-334: Update the mismatch branch in the refresh completion
handling to ignore stale completions instead of resetting refreshLifecycle or
clearing activeRefreshControl; only the currently authoritative refresh
generation/ID may mutate active refresh state, while invalidation continues
cleaning up the replaced control. Add a regression test covering refresh A being
replaced by refresh B before A completes, asserting A’s late completion leaves B
active and its lifecycle intact.
- Around line 361-363: Update the scroll-view delegate handling near
scrollViewDidScroll(_:) by adding scrollViewDidEndDragging(_:willDecelerate:)
and call settleRefreshCollapse(in:) with the ending scroll view there,
preserving the existing scrollViewDidScroll behavior.

In
`@Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift`:
- Around line 34-36: Replace the fixed ten-iteration Task.yield waits in the
affected tests with explicit completion signals: await the refreshDidComplete
probe and the RecordingRefreshControl.endRefreshing() probe before asserting
results. Ensure both refresh continuation and diffable snapshot completion are
observed, using bounded predicate polling only if necessary, and remove the
fixed scheduler-yield loops.
🪄 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: 30b643cc-30dc-424a-befe-e6981933f0d3

📥 Commits

Reviewing files that changed from the base of the PR and between 9687274 and dcf5ad0.

📒 Files selected for processing (8)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListLayoutPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListRefreshLifecycle.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTable.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView+Table.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshLifecycleTests.swift

Comment on lines +361 to +363
func scrollViewDidScroll(_ scrollView: UIScrollView) {
settleRefreshCollapse(in: scrollView)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift'

# Map the file structure, then inspect the relevant section.
ast-grep outline "$file" --view expanded || true
echo '---'
sed -n '1,520p' "$file" | cat -n | sed -n '300,430p'

Repository: manaflow-ai/cmux

Length of output: 7419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift'

# Inspect the implementation and nearby delegate methods.
grep -n "settleRefreshCollapse\|scrollViewDid\|endRefreshing\|isTracking\|refresh" "$file" || true
echo '---'
sed -n '1,260p' "$file" | cat -n | sed -n '1,260p'

Repository: manaflow-ai/cmux

Length of output: 15126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift'
nl -ba "$file" | sed -n '320,420p'

Repository: manaflow-ai/cmux

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift'

echo "[delegate methods]"
grep -nE 'func scrollViewDid|func settleRefreshCollapse|endRefreshing|isTracking' "$file" || true
echo
echo "[context]"
nl -ba "$file" | sed -n '340,390p'

Repository: manaflow-ai/cmux

Length of output: 573


Call settleRefreshCollapse(in:) on drag end too.
scrollViewDidScroll(_:) alone can miss the transition out of isTracking if endRefreshing() happens while the finger is still down and no further offset change occurs, leaving snapshot animation suppression stuck until the next scroll. Add scrollViewDidEndDragging(_:willDecelerate:) here as well.

🤖 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/WorkspaceListTableCoordinator.swift`
around lines 361 - 363, Update the scroll-view delegate handling near
scrollViewDidScroll(_:) by adding scrollViewDidEndDragging(_:willDecelerate:)
and call settleRefreshCollapse(in:) with the ending scroll view there,
preserving the existing scrollViewDidScroll behavior.

Source: Coding guidelines

@greptile-apps

greptile-apps Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces a custom pull-to-refresh gesture state machine with UIKit's standard UIRefreshControl, eliminating the stutter caused by multiple concurrent writers to UIScrollView geometry. A generation counter (refreshCompletionGeneration) threads through SwiftUI to ensure endRefreshing() is called only inside the diffable-datasource apply completion block, so the spinner collapses after the refreshed snapshot is committed to the table — not before.

  • Coordinator refresh path (WorkspaceListTableCoordinator): tracks the in-flight task by UUID, cancels it if refresh becomes nil, and delegates endRefreshing() to the datasource apply completion via completesRefresh; snapshot animation is suppressed while isRefreshing == true.
  • Generation counter (workspaceListRefreshCompletionGeneration in WorkspaceListView, wired in WorkspaceListView+Table): a @State var (internal access needed by the cross-file extension) that ticks when the async refresh action completes, driving the coordinator to close the refresh control after the data snapshot is applied.
  • Preview fixture (WorkspaceListLayoutPreviewView): adds a configurable CMUX_UITEST_WORKSPACE_LIST_REFRESH_DELAY_MS env-var path and a WorkspaceListRefreshPreviewState private class to allow the existing UITEST fixture to simulate a delayed refresh for screenshot and geometry validation.

Confidence Score: 5/5

Safe to merge. The change eliminates multiple concurrent geometry writers and delegates full visual ownership of the refresh lifecycle to UIKit.

The generation-counter handshake correctly sequences endRefreshing() inside the datasource apply completion, so the spinner collapses only after the refreshed snapshot is committed. Task cancellation on refresh-control removal, the UUID-based guard against stale task callbacks, and the isRefreshing animation suppression are all handled correctly. The two residual edge cases noted in prior review threads pre-date this PR and are not worsened by the change.

WorkspaceListTableCoordinator.swift carries the two pre-existing lifecycle edge cases noted in earlier review threads; the rest of the changed files are straightforward wiring or test scaffolding.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift Core of the fix: replaces fire-and-forget Task with UUID-tracked task; calls endRefreshing() inside the datasource apply completion when completesRefresh==true; suppresses snapshot animation while isRefreshing. Two residual edge cases were flagged in prior review threads.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift Adds @State var workspaceListRefreshCompletionGeneration at internal visibility needed by the cross-file extension.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView+Table.swift Wires refreshCompletionGeneration and a @mainactor refreshDidComplete closure into WorkspaceListTable.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTable.swift Adds refreshCompletionGeneration: UInt64 and refreshDidComplete: @mainactor () -> Void to the UIViewRepresentable configuration struct.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListLayoutPreviewView.swift Adds WorkspaceListRefreshPreviewState and CMUX_UITEST_WORKSPACE_LIST_REFRESH_DELAY_MS support to the existing UITEST-only preview fixture.
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift New test suite covering geometry invariants and the endRefreshing sequencing invariant using actor-based gates and a recording UIRefreshControl subclass.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant UIRefreshControl
    participant Coordinator as WorkspaceListTableCoordinator
    participant Task as Task[@MainActor]
    participant SwiftUI as WorkspaceListView (SwiftUI)
    participant DataSource as UITableViewDiffableDataSource

    User->>UIRefreshControl: pull-to-refresh gesture
    UIRefreshControl->>Coordinator: refreshRequested(_:) [valueChanged]
    Coordinator->>Task: "Task { await refresh() }"
    Note over Coordinator: refreshTask = (uuid, task)
    Task->>SwiftUI: await refresh() [async action]
    SwiftUI-->>Task: data model updated, returns
    Task->>SwiftUI: refreshDidComplete() increment generation
    Note over Task: refreshTask = nil
    SwiftUI->>Coordinator: "update(configuration:in:) completesRefresh=true"
    Coordinator->>DataSource: apply(snapshot, animatingDifferences: false)
    DataSource-->>UIRefreshControl: endRefreshing() in completion block
    UIRefreshControl-->>User: spinner collapses
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 User
    participant UIRefreshControl
    participant Coordinator as WorkspaceListTableCoordinator
    participant Task as Task[@MainActor]
    participant SwiftUI as WorkspaceListView (SwiftUI)
    participant DataSource as UITableViewDiffableDataSource

    User->>UIRefreshControl: pull-to-refresh gesture
    UIRefreshControl->>Coordinator: refreshRequested(_:) [valueChanged]
    Coordinator->>Task: "Task { await refresh() }"
    Note over Coordinator: refreshTask = (uuid, task)
    Task->>SwiftUI: await refresh() [async action]
    SwiftUI-->>Task: data model updated, returns
    Task->>SwiftUI: refreshDidComplete() increment generation
    Note over Task: refreshTask = nil
    SwiftUI->>Coordinator: "update(configuration:in:) completesRefresh=true"
    Coordinator->>DataSource: apply(snapshot, animatingDifferences: false)
    DataSource-->>UIRefreshControl: endRefreshing() in completion block
    UIRefreshControl-->>User: spinner collapses
Loading

Reviews (7): Last reviewed commit: "fix(ios): finish refresh after table upd..." | Re-trigger Greptile

Comment on lines +361 to +363
func scrollViewDidScroll(_ scrollView: UIScrollView) {
settleRefreshCollapse(in: scrollView)
}

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 collapsing → idle transition has no fallback for animation end

observeCollapse is only invoked from scrollViewDidScroll, which fires on every frame of the UIKit refresh-control collapse animation. If the animation is interrupted mid-flight by a concurrent programmatic setContentOffset (e.g., search-bar appearance, safe-area inset change, or a push navigation triggering a scroll restoration), scrollViewDidEndScrollingAnimation fires for the new scroll but scrollViewDidScroll may not subsequently reach within the 0.5 pt tolerance at the original restingTopY. The lifecycle stays stuck in .collapsing, keeping suppressesSnapshotAnimations = true for all subsequent diffable snapshots until the user manually scrolls. Implementing scrollViewDidEndScrollingAnimation (and scrollViewDidEndDecelerating) to also call settleRefreshCollapse(in:) would cover these interruption paths and make the settlement definitive.

Comment on lines +323 to +325
guard let refreshID = refreshLifecycle.begin(
currentGeneration: configuration.refreshCompletionGeneration
) else { 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 Second pull-to-refresh while collapsing is silently swallowed

If refreshLifecycle.begin() returns nil (phase is anything other than .idle), the method returns early without calling refreshControl.endRefreshing(). During the .collapsing phase isRefreshing is already false so UIKit won't normally let the user initiate a new pull, but if the refresh action is extremely fast (action completes and endRefreshing fires before the user releases their pull gesture), a second UIControlEvents.valueChanged can fire on the same control. In that case the refreshControl is left spinning with no lifecycle entry, no Task, and no future endRefreshing call. Adding refreshControl.endRefreshing() in the else { return } path would prevent the control from hanging.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift`:
- Around line 53-59: Replace the DispatchQueue.main.async hop in
WorkspaceListTableCoordinator.init’s scheduleRefreshCollapse closure with a
direct action() call from the authoritative snapshot completion. If a separate
phase is required, use an explicit UIKit completion callback instead, without
relying on delayed dispatch for refresh rendering.
- Around line 403-410: The failure branch of the queued closure in
scheduleRefreshCollapse must cancel the matching pending collapse before
returning. Preserve token validation by invoking
refreshLifecycle.cancelCollapse(collapseID) with the captured collapse ID, so
stale closures cannot affect a newer lifecycle or leave it in
.collapseScheduled.
🪄 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: cb2d807c-dd5b-4dc8-99b2-249dc92b6140

📥 Commits

Reviewing files that changed from the base of the PR and between dcf5ad0 and 95213b6.

📒 Files selected for processing (4)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListRefreshLifecycle.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshLifecycleTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift`:
- Line 57: Remove the production-only refreshTaskDidFinish callback seam from
the coordinator, including its property, initializer parameter, stored
assignment, and invocation. Update
staleCancelledTaskCannotReleaseReplacementTaskOwnership to observe task
ownership through `@testable` import instead, or use an existing genuine
production task-owner abstraction without adding test-only APIs under Sources.
🪄 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: 29133495-fd91-4b85-ac58-91aa24423c38

📥 Commits

Reviewing files that changed from the base of the PR and between 95213b6 and 617738c.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListRefreshLifecycle.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTable.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListTableCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshCoordinatorTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListRefreshLifecycleTests.swift

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants