Skip to content

Fix sidebar Shift-click range anchor after no-op drag - #5601

Merged
lawrencecchen merged 3 commits into
mainfrom
task-shift-sidebar-selection-debug
Jun 8, 2026
Merged

lawrencecchen merged 3 commits into
mainfrom
task-shift-sidebar-selection-debug

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent sidebar Shift-click range selection bug where repeatedly Shift-selecting the next workspace could collapse or slide the selection after a tiny accidental drag.

The log-backed failure path was:

  • Shift-click selected the next workspace.
  • SwiftUI also started an .onDrag for the adjacent row.
  • SidebarTabDropDelegate.performDrop saw fromIndex == targetIndex and treated the no-op drop as a selection repair.
  • That reset lastSidebarSelectionIndex to the focused workspace, so the next Shift-click used the wrong anchor.

Changes

  • No-op sidebar drops now return without mutating sidebar selection or range anchor.
  • Real sidebar reorders preserve the Shift-click anchor by workspace identity, then derive the new anchor index after the reorder.
  • Shift-click resolves a stable anchor from the explicit anchor, single sidebar selection, or focused workspace, and keeps that anchor while focus moves.
  • Adds focused Swift Testing regression coverage for no-op/real reorder anchor handling and Shift-click anchor resolution.

Verification

  • git diff --check origin/main...HEAD
  • xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-shsel2-unit -only-testing:cmuxTests/SidebarWorkspaceSelectionSyncPolicyTests -only-testing:cmuxTests/SidebarWorkspaceSelectionAnchorPolicyTests test
    • ** TEST SUCCEEDED **
    • 2 XCTest tests and 6 Swift Testing tests passed
  • $autoreview: clean, including cmux policy
  • Tagged dogfood build: shsel2

Dogfood

Open shsel2. The tagged instance is pre-seeded with five workspaces: workspace:1 selected, then Shift Row 2 through Shift Row 5.

Starting from workspace:1, repeatedly Shift-click one row lower: row 2, row 3, row 4, row 5. Before this fix, the range could intermittently collapse or slide to only the last pair when a small accidental row drag/no-op drop happened while holding Shift. Expected behavior: the range stays anchored at workspace:1 and expands downward, even if a tiny accidental no-op drag occurs.


Note

Low Risk
Localized sidebar selection/drag-drop UI logic with dedicated unit tests; no auth, data, or network changes.

Overview
Fixes sidebar Shift-click range selection when a tiny accidental drag triggers a no-op drop that used to reset the range anchor to the focused row.

No-op drops (fromIndex == targetIndex) now return without calling syncSidebarSelection, so lastSidebarSelectionIndex is not overwritten by focus.

Shift-click resolves the range anchor via SidebarWorkspaceSelectionSyncPolicy.shiftClickAnchorIndex (existing anchor index, single selected workspace, or focused workspace) and updates the anchor with anchorIndexAfterWorkspaceClick so the anchor stays fixed while focus moves down the list.

Real reorders snapshot the anchor workspace ID before the move and restore it with anchorIndexAfterWorkspaceReorder instead of always anchoring on the focused tab after syncSidebarSelection(preserving:).

Adds SidebarWorkspaceSelectionAnchorPolicyTests for anchor identity, reorder, and shift-click behavior.

Reviewed by Cursor Bugbot for commit 9f4dab1. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved sidebar workspace selection behavior during shift-clicks and when reordering workspaces via drag-and-drop.
  • Tests

    • Added comprehensive test suite for workspace selection anchor logic.

@vercel

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 8, 2026 3:24am
cmux-staging Building Building Preview, Comment Jun 8, 2026 3:24am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors sidebar workspace selection anchor handling from inline logic into reusable SidebarWorkspaceSelectionSyncPolicy helper methods, integrates them into click and drag-drop workflows, and adds comprehensive test coverage to ensure anchors are preserved across workspace reordering.

Changes

Sidebar Anchor Preservation

Layer / File(s) Summary
Anchor resolution helpers
Sources/ContentView.swift
SidebarWorkspaceSelectionSyncPolicy gains four static methods: anchorWorkspaceId() to extract a stable workspace ID from an anchor index, shiftClickAnchorIndex() to derive anchor from shift-click context, anchorIndexAfterWorkspaceClick() to choose between shift-resolved or clicked index, and anchorIndexAfterWorkspaceReorder() to preserve or recompute anchor after reorder.
Click handler anchor integration
Sources/ContentView.swift
updateSelection() refactored to compute modifier flags upfront and delegate shift-click anchor resolution to shiftClickAnchorIndex(). After selection changes, lastSidebarSelectionIndex is updated via anchorIndexAfterWorkspaceClick() instead of direct assignment.
Drag-and-drop anchor preservation
Sources/ContentView.swift
Drop-commit path captures anchorWorkspaceIdBeforeReorder and calls syncSidebarSelection(preserving:preferredAnchorWorkspaceId:) to preserve the anchor across reorder. syncSidebarSelection() now accepts preferredAnchorWorkspaceId and computes lastSidebarSelectionIndex using anchorIndexAfterWorkspaceReorder().
Anchor policy test suite
cmuxTests/SidebarWorkspaceSelectionAnchorPolicyTests.swift
New @Suite of @MainActor test cases covering anchor workspace ID lookup, anchor preservation by identity across reorders, fallback when anchor is no longer selected, shift-click anchor behavior, and non-shift click anchor movement.
Project build configuration for tests
cmux.xcodeproj/project.pbxproj
Adds SidebarWorkspaceSelectionAnchorPolicyTests.swift to the project via PBXBuildFile, PBXFileReference, group children, and target build phase entries so the test file compiles as part of the unit test bundle.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4865: Modifies sidebar selection sync and anchor logic in SidebarWorkspaceSelectionSyncPolicy, directly related to the main PR's anchor-policy refactor and selection preservation after drag reorder.
  • manaflow-ai/cmux#4989: Touches the sidebar drag-drop reorder flow around tabManager.reorderSidebarWorkspace(...), with the main PR additionally preserving lastSidebarSelectionIndex/anchor workspace across the reorder.

Suggested reviewers

  • Ari4ka

Poem

🐰 With a thump and a hop through anchor-filled code,
We preserve what was lost down the workspace road,
Shift-click and drag-drop now dance in a line,
Anchors stay put, selection's divine! ✨

🚥 Pre-merge checks | ✅ 18 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (18 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: addressing a Shift-click range anchor bug triggered by no-op drag operations in the sidebar.
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 SidebarWorkspaceSelectionSyncPolicy enum marked @MainActor with only static methods using pure values; no problematic Codable/Sendable models or UI-bound stores improperly accessed.
Cmux Swift Blocking Runtime ✅ Passed Task.sleep(nanoseconds: 150_000_000) is a bounded, cancellable timeout for workspace handoff (allowed as genuine deadline). NSLock protects a metrics cache in non-latency-critical code.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift code and GitHub Actions workflows, which are out of scope. The rule covers TypeScript, JavaScript, shell, and build/runtime scripts only.
Cmux Algorithmic Complexity ✅ Passed New O(n) helper functions use sequential (not nested) firstIndex() calls; no-op drop fix removes unnecessary sync, improving perf. All scalable patterns maintain linear time for ~1000 workspaces.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New code uses pure sync functions and @MainActor. No Dispatch queues, Combine, completion handlers, or fire-and-forget Tasks added.
Cmux Swift @Concurrent ✅ Passed All new Swift functions in PR are synchronous pure helpers with no @concurrent/isolation issues; they run on @MainActor and operate only on value types without accessing isolated state.
Cmux Swift File And Package Boundaries ✅ Passed Adds 99 lines to oversized ContentView (under 250 threshold), pure state logic with no mixed responsibilities, focused bug fix with clear extraction path and tests.
Cmux Swift Logging ✅ Passed All logging in production Swift code is properly DEBUG-guarded; no unguarded print/debugPrint/dump/NSLog found; test files contain no logging violations.
Cmux User-Facing Error Privacy ✅ Passed No new user-facing errors, alerts, or strings violating the privacy policy. All changes are internal sidebar selection logic with debug logs properly gated in #if DEBUG blocks.
Cmux Full Internationalization ✅ Passed PR adds only debug-only code in #if DEBUG blocks and test cases. No new user-facing strings or string catalog changes requiring internationalization.
Cmux Swiftui State Layout ✅ Passed PR refactors sidebar selection anchor logic into pure helper functions with no new @Published, @Observable, @StateObject, GeometryReader, lazy row stores, or render-time mutations.
Cmux Architecture Rethink ✅ Passed Root cause fix with clear owner and invariant, no timing repairs or duplicate state owners; no-op drops guard early preventing anchor reset via unified SidebarWorkspaceSelectionSyncPolicy functions.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change any NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code. Changes are sidebar selection logic refactoring and unit tests only.
Cmux Source Artifacts ✅ Passed All changed files are legitimate hand-written source, tests, configs, and scripts—no generated artifacts, caches, logs, or scratch directories were added to source control.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering summary, changes, verification, and testing details.

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

✨ 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-shift-sidebar-selection-debug

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 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the intermittent sidebar Shift-click range selection bug where a tiny accidental drag could trigger a no-op drop that silently reset lastSidebarSelectionIndex to the focused workspace, causing the range anchor to slide or collapse on the next Shift-click.

  • No-op drops (fromIndex == targetIndex) now return immediately without calling syncSidebarSelection, so they can no longer overwrite the range anchor.
  • Real reorders capture the anchor workspace UUID before the move via anchorWorkspaceId, then map it back to its post-reorder index via anchorIndexAfterWorkspaceReorder, falling back to the focused workspace only when the anchor is no longer in the selection.
  • Shift-clicks resolve the anchor through shiftClickAnchorIndex (existing anchor index → single sidebar selection → focused workspace) and preserve it across clicks via anchorIndexAfterWorkspaceClick, rather than always advancing to the clicked row.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped to sidebar drag-drop and Shift-click anchor logic, backed by new regression tests that all pass.

All three changed paths (no-op drop, real reorder, Shift-click) are well-reasoned and consistent with one another. The no-op early-return is the simplest correct fix; the UUID-based anchor snapshot for real reorders is correctly captured before the list mutates and correctly resolved after; the shiftClickAnchorIndex fallback chain (existing index → single selection → focus) handles all nil cases without crashing. New unit tests exercise each path deterministically, and the xcodebuild run confirms 8/8 pass.

No files require special attention.

Important Files Changed

Filename Overview
Sources/ContentView.swift Core bug fix: no-op drops no longer call syncSidebarSelection; adds four new SidebarWorkspaceSelectionSyncPolicy helpers (anchorWorkspaceId, shiftClickAnchorIndex, anchorIndexAfterWorkspaceClick, anchorIndexAfterWorkspaceReorder) to preserve the Shift-click anchor by UUID through reorders and focus changes. Logic is correct; all edge cases are handled.
cmuxTests/SidebarWorkspaceSelectionAnchorPolicyTests.swift New Swift Testing suite (5 tests) covering anchor identity reads, reorder identity preservation, reorder fallback to focus, shift-click single-selection fallback, and shift-click anchor stability across focus moves. Tests are well-targeted and deterministic.
cmux.xcodeproj/project.pbxproj Registers SidebarWorkspaceSelectionAnchorPolicyTests.swift in both the file references and the cmux-unit test target build sources sections. Mechanical Xcode project plumbing only.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User action in sidebar] --> B{Action type?}

    B -->|Drag drop| C{fromIndex == targetIndex?}
    C -->|Yes — no-op| D[return true\nno selection mutation]
    C -->|No — real reorder| E[Snapshot anchorWorkspaceId\nbefore reorder]
    E --> F[tabManager.reorderSidebarWorkspace]
    F --> G[syncSidebarSelection\npreferredAnchorWorkspaceId]
    G --> H{anchor UUID still\nin new selection?}
    H -->|Yes| I[anchorIndex = new position\nof anchor UUID]
    H -->|No| J[anchorIndex = focused\nworkspace index]

    B -->|Click| K{isShift?}
    K -->|Yes| L[shiftClickAnchorIndex\nexistingAnchorIndex → singleSelection → focus]
    L --> M{anchor resolved?}
    M -->|Yes| N[Select range\nlower…upper]
    M -->|No| O[Fall through to\nnormal selection]
    N --> P[anchorIndexAfterWorkspaceClick\nkeeps resolved anchor]
    O --> P
    K -->|No| Q[Regular / Cmd selection]
    Q --> R[anchorIndexAfterWorkspaceClick\nsets anchor = clickedIndex]
Loading

Reviews (2): Last reviewed commit: "Move sidebar anchor tests to Swift Testi..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines +17661 to +17680
static func shiftClickAnchorIndex(
existingAnchorIndex: Int?,
selectedWorkspaceIds: Set<UUID>,
focusedWorkspaceId: UUID?,
liveWorkspaceIds: [UUID]
) -> Int? {
if let existingAnchorIndex,
liveWorkspaceIds.indices.contains(existingAnchorIndex) {
return existingAnchorIndex
}
if selectedWorkspaceIds.count == 1,
let selectedWorkspaceId = selectedWorkspaceIds.first,
let selectedIndex = liveWorkspaceIds.firstIndex(of: selectedWorkspaceId) {
return selectedIndex
}
if let focusedWorkspaceId {
return liveWorkspaceIds.firstIndex(of: focusedWorkspaceId)
}
return nil
}

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 Anchor still tracked by index, not workspace identity, for non-drag mutations

shiftClickAnchorIndex returns existingAnchorIndex whenever it falls in-bounds, without verifying the workspace at that position is the one originally anchored. The drag-reorder path is now correctly protected by the pre-reorder UUID snapshot, but any mutation that shifts the list without going through reorderSidebarWorkspace — workspace close, workspace create, or group collapse that renumbers rows — can silently move the anchor to a different workspace. If workspace B at index 0 is closed while the anchor sits at index 2, the next Shift-click anchors to whatever is now at index 2 rather than the original anchor workspace. Converting lastSidebarSelectionIndex from Int? to UUID? would make the anchor invariant to all mutations without special-casing each mutating path.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@lawrencecchen
lawrencecchen merged commit 1c20831 into main Jun 8, 2026
21 checks passed
@lawrencecchen
lawrencecchen deleted the task-shift-sidebar-selection-debug branch June 8, 2026 03:41

This branch was successfully deployed

1 active deployment
Preview – cmux — 9f4dab15 Deployed Jun 8, 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