Skip to content

Reduce iOS workspace row swipe contention#6064

Merged
azooz2003-bit merged 4 commits into
mainfrom
feat-ios-swipe-stutter
Jun 14, 2026
Merged

Reduce iOS workspace row swipe contention#6064
azooz2003-bit merged 4 commits into
mainfrom
feat-ios-swipe-stutter

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Contributor

Summary

  • remove the live TimelineView from mobile workspace rows so swipe tracking is not invalidated by timer-driven row updates
  • use Button/NavigationLink based row activation instead of bare onTapGesture, with compact rows now using NavigationLink(value:)
  • keep native swipe actions and full-swipe behavior intact

Verification

  • ./scripts/reload-cloud.sh --tag swst (fell back to local reload, passed)
  • ios/scripts/reload.sh --tag swst
  • ios/scripts/reload.sh --tag swst --device-only --device-id 4A52829D-6427-599F-A166-4058881D2DF4 --team 7WLXT3NR37

Note: the remaining physical-device-only slow-swipe hitch is not claimed fixed by this PR; follow-up profiling continues separately.

@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

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

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

@coderabbitai

coderabbitai Bot commented Jun 13, 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

This PR removes per-row minute-driven timestamp updates and simplifies the timestamp API by eliminating the injected now parameter. It introduces static activity timestamp formatting, removes TimelineView per-row refresh, and switches workspace row selection from onTapGesture to Button action.

Changes

Workspace row timestamp and interaction simplification

Layer / File(s) Summary
Static timestamp API and formatting
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspacePreview+Display.swift
Remove now parameter from timestampOrStatus(connectionStatus:) and remove relative/bucketed clock labeling. Introduce activityTimestampLabel(referenceDate:calendar:) to format a static connected-state timestamp (same-day hour:minute, otherwise month/day, or empty when no real timestamp). Add Foundation import for date formatting.
WorkspaceRow label rendering simplification
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceRow.swift
Remove TimelineView(.everyMinute) wrapper from label and per-minute re-evaluation via context.date. Replace with direct Text rendering using the new timestampOrStatus(connectionStatus:) API.
WorkspaceNavigationRow selection interaction
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift
Replace .onTapGesture selection handler with a .plain Button wrapper whose action calls selectWorkspace(workspace.id).
DeviceTreeView multi-line formatting
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
Reformat instanceRows method call to multi-line argument layout; no functional changes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • manaflow-ai/cmux#5726: Also modifies workspace-row timestamp rendering by changing timestampOrStatus logic in MobileWorkspacePreview+Display.swift and minute-updating mechanism in WorkspaceRow.swift.
  • manaflow-ai/cmux#5544: Refactors the same timestampOrStatus helper and related label formatting logic in the workspace timestamp/status rendering path.

Poem

🐰 The timeline ticks no more per row,
One button tap replaces gesture's flow,
Timestamps stand still, no minute-chime,
Static beauty marks the time! 🕐✨

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of replacing .onTapGesture with a native Button to reduce gesture contention during swipes.
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 Touched SwiftUI/value-helper code adds sync timestamp formatting and wraps row taps in Button; background access uses await on @MainActor MobileShellComposite store (DeviceTreeView Task). No ne...
Cmux Swift Blocking Runtime ✅ Passed Reviewed the PR’s Swift touchpoints for forbidden blocking/timing primitives (semaphores/waits/sleeps/asyncAfter/main sync/locks/polling); none found.
Cmux Expensive Synchronous Load ✅ Passed Checked WorkspaceNavigationRow/WorkspaceRow/MobileWorkspacePreview+Display/DeviceTreeView and repo-wide for RestorableAgentSessionIndex.load/sysctl/SharedLiveAgentIndex.shared; no expensive sync lo...
Cmux Cache Substitution Correctness ✅ Passed Inspected the PR’s Swift files (workspace row/button + timestamp label changes) and found no swaps of fresh reads with cached/opportunistic values in persistence/history/undo/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed PR #6064 changes only Swift files (workspace row/timestamp/clock); no TS/JS/shell/build/runtime script diffs, so no hacky sleeps/polling delays were introduced.
Cmux Algorithmic Complexity ✅ Passed Inspection of changed Swift files shows no added sort/filter/map loops; WorkspaceRow removed TimelineView/everyMinute and gesture tap host, and timestamp label is O(1) formatting.
Cmux Swift Concurrency ✅ Passed PR diff adds WorkspaceRelativeTimestampClock using async/await + SwiftUI .task (Task.sleep, cancellation); searches show no DispatchQueue/Combine/@Published/completion-handler APIs/Task.detached in...
Cmux Swift @Concurrent ✅ Passed Scanned the PR’s Swift UI files: no @concurrent or nonisolated async present, and no new async func/heavy async callsites were added.
Cmux Swift File And Package Boundaries ✅ Passed Touched Swift files are all <400 LOC (152/103/94/295) and changes are UI glue (Button-based selection, static timestamp label). No added persistence/network/protocol logic or oversized-file growth.
Cmux Swift Logging ✅ Passed Checked added Swift lines under Packages/CmuxMobileShellUI/Sources: no added print/debugPrint/dump/NSLog outside #if DEBUG; only an NSLog in WorkspaceDetailView.swift within #if DEBUG.
Cmux User-Facing Error Privacy ✅ Passed Scanned the changed Swift UI files for alert/error copy and L10n values used in confirmations & status labels; only generic text like “Delete Workspace?”, “Cancel”, and “Connected/Mac offline”—no u...
Cmux Full Internationalization ✅ Passed Updated Swift files use L10n.string or Date.formatted for user-facing text (no new hardcoded Text/String literals). Localizable.xcstrings translations for referenced keys are complete for all local...
Cmux Swiftui State Layout ✅ Passed Inspected the modified SwiftUI files: no new @Published/ObservableObject/@Observable/@StateObject/@EnvironmentObject or GeometryReader; list-row subviews receive value snapshots, and @State updates...
Cmux Architecture Rethink ✅ Passed PR #6064 only wraps WorkspaceRow in a plain SwiftUI Button and removes the row .onTapGesture; no sleeps, polling, locks/observers, side channels, or split lifecycle ownership introduced per fetched...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Scanned the PR’s Swift files (WorkspaceNavigationRow/Row, MobileWorkspacePreview+Display, DeviceTreeView): no NSWindow/NSPanel/Window/WindowGroup code or cmux.* identifier assignments. Repo close-o...
Cmux Source Artifacts ✅ Passed PR commit shows only a single Swift source file (WorkspaceNavigationRow.swift) changed; no generated logs/build/DerivedData or other artifact paths appear.
Description check ✅ Passed The PR description covers the main changes (Button replacement, timestamp removal) and provides testing/verification details, but is missing a demo video, bot review request copy-paste block, and the structured checklist completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-swipe-stutter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces iOS swipe-gesture contention by removing the per-row TimelineView(.everyMinute) that was causing timer-driven list invalidations, and replaces bare onTapGesture with Button (sidebar) or NavigationLink(value:) (compact push) for better UIKit gesture integration. The activity label switches from live relative buckets ("now", "2m", "1h") to a static wall-clock or month/day format computed once per body evaluation.

  • WorkspaceRow: TimelineView(.everyMinute) wrapper dropped; timestamp is now a plain Text computed at body evaluation time.
  • MobileWorkspacePreview+Display: relativeActivityLabel(now:) replaced by activityTimestampLabel() using date.formatted with locale-aware FormatStyle; timestampOrStatus no longer takes a now: parameter.
  • WorkspaceNavigationRow: row activation split into a Button (sidebar) and NavigationLink(value:) + simultaneousGesture (push); DeviceTreeView switched from .push to .sidebar since its NavigationStack has no registered navigationDestination for workspace IDs.

Confidence Score: 4/5

Safe to merge with one concern worth resolving: the compact-stack push case could double-push the workspace detail view if gesture ordering between the SwiftUI TapGesture and UIKit's NavigationLink activation is not deterministic.

The WorkspaceRow and timestamp changes are correct and well-scoped. The DeviceTreeView .push→.sidebar switch is a necessary fix. The WorkspaceNavigationRow push case attaches a simultaneousGesture(TapGesture) alongside a NavigationLink(value:). selectWorkspace guards with compactNavigationPath.last != id before setting the path, but that guard only protects against redundancy when the NavigationLink has already mutated the path first — if the SwiftUI gesture fires first (path still []), selectWorkspace sets [id] and the NavigationLink subsequently appends a second id, leaving [id, id] in the stack. Whether this ordering occurs in practice was tested on-device, but the ordering is not guaranteed by the SwiftUI/UIKit contract.

WorkspaceNavigationRow.swift — specifically the .push branch of rowTarget where NavigationLink and simultaneousGesture(TapGesture) both mutate compactNavigationPath on the same tap.

Important Files Changed

Filename Overview
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift Replaces onTapGesture with Button (sidebar) or NavigationLink+simultaneousGesture (push); the push case has a potential double-push race between the SwiftUI TapGesture and UIKit's NavigationLink activation.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceRow.swift Removes TimelineView(.everyMinute) wrapper, replacing the timer-driven row invalidation with a static Text; straightforward and correct within the PR's stated goals.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspacePreview+Display.swift Replaces relative bucket labels (now/Nm/Nh/Nd) with wall-clock time (same day) or month/day (older); logic is sound, FormatStyle handles locale natively, and the distantPast guard is correct.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift Switches workspace rows from .push to .sidebar navigation style, necessary because DeviceTreeView's own NavigationStack has no navigationDestination for workspace IDs. Change is correct.

Sequence Diagram

sequenceDiagram
    participant User
    participant UIKit as UIKit (List)
    participant NL as NavigationLink
    participant TG as simultaneousGesture(TapGesture)
    participant SW as selectWorkspace
    participant Path as compactNavigationPath

    User->>UIKit: tap row
    par NavigationLink activation
        UIKit->>NL: didSelectRow
        NL->>Path: append(workspace.id) → [id]
    and SwiftUI TapGesture
        UIKit->>TG: tap ended
        TG->>SW: selectWorkspace(id)
        SW->>Path: "if last != id → set [id]"
    end
    Note over Path: Race: if TG fires before NL,<br/>Path becomes [id] then [id,id]
Loading

Reviews (9): Last reviewed commit: "Keep device tree workspace rows as butto..." | Re-trigger Greptile

@azooz2003-bit azooz2003-bit force-pushed the feat-ios-swipe-stutter branch from 8e596ec to a9f0510 Compare June 13, 2026 21:58
/// Passive timestamp reference for the relative activity label. This avoids a
/// per-row `TimelineView` invalidating list rows while UIKit is tracking a
/// native swipe gesture.
var timestampReferenceDate: Date = .now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Relative timestamps will go stale without a parent-level timer

timestampReferenceDate defaults to Date.now at WorkspaceRow init time, but neither WorkspaceNavigationRow nor either of its callers (WorkspaceListView.workspaceRow(), DeviceTreeView) passes a live date. Without any periodic update mechanism, the label freezes at its initial value and only refreshes if some unrelated state change causes the parent to rebuild the row. In a quiet session — where no new workspace activity arrives — a row initialized at T+0 that shows "now" will still show "now" minutes later. The old TimelineView(.everyMinute) was the only guarantee of a minute-by-minute tick. The intended fix is for the list owner to host a single TimelineView(.everyMinute) and thread context.date into every WorkspaceNavigationRow as timestampReferenceDate, but that wiring is not present in this PR.

Comment on lines +94 to +100
case .push:
NavigationLink(value: workspace.id) {
rowLabel
}
.simultaneousGesture(TapGesture().onEnded {
selectWorkspace(workspace.id)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 NavigationLink + simultaneousGesture may double-push in the compact stack

NavigationLink(value: workspace.id) and simultaneousGesture(TapGesture().onEnded { selectWorkspace(workspace.id) }) both fire on the same tap. selectWorkspace (line 182–184 of WorkspaceShellView) guards with compactNavigationPath.last != id before setting the path to [id] — but that guard is only effective if the NavigationLink has already appended id to the path first. If the SwiftUI TapGesture fires before UIKit/NavigationStack processes the link activation (making the path still []), selectWorkspace sets compactNavigationPath = [id] and then the NavigationLink appends id again, yielding [id, id]. The NavigationStack would then push the workspace detail view twice, leaving a stale intermediate destination in the back-stack. Since simultaneousGesture makes the ordering undefined, this race is present even though it may not reproduce consistently on any given device.

@azooz2003-bit azooz2003-bit merged commit 26a96ac into main Jun 14, 2026
26 of 27 checks passed
@azooz2003-bit azooz2003-bit deleted the feat-ios-swipe-stutter branch June 14, 2026 05:07
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